@alecsibilia/luca 14.0.0-alpha.1 → 14.0.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/board/package.json +1 -1
- package/board/server/board-vocabulary.ts +12 -1
- package/board/server/luca-version.ts +1 -1
- package/board/server/make-rows.ts +24 -2
- package/board/server/reduce-board.ts +41 -1
- package/engine/agents/role-instructions.ts +3 -1
- package/engine/agents/role-results.ts +39 -5
- package/engine/config/engine-config.ts +25 -0
- package/engine/core/already-done.ts +139 -0
- package/engine/core/decide-build.ts +83 -18
- package/engine/core/decide-usage.ts +5 -3
- package/engine/core/decide.ts +10 -4
- package/engine/core/execute-already-done.ts +65 -0
- package/engine/core/execute-build.ts +111 -26
- package/engine/core/execute-changeset.ts +5 -1
- package/engine/core/execute.ts +13 -1
- package/engine/core/learner-digest.ts +3 -1
- package/engine/core/not-in-run.ts +29 -0
- package/engine/core/pull-request-text.ts +45 -4
- package/engine/core/session-close.ts +10 -3
- package/engine/core/stuck-text.ts +4 -3
- package/engine/gates/gate-runner.ts +52 -7
- package/engine/gates/gate-schemas.ts +14 -0
- package/engine/gates/leftover-scan.ts +7 -2
- package/engine/git/git-adapter.ts +69 -4
- package/engine/git/path-text.ts +34 -0
- package/engine/guards/worktree-state.ts +14 -2
- package/engine/journal/journal-record.ts +62 -1
- package/engine/journal/replay.ts +41 -0
- package/engine/shell/run-command.ts +109 -7
- package/engine/testing/build-fixtures.ts +66 -0
- package/engine/tracker/github-tracker.ts +5 -0
- package/engine/tracker/in-memory-tracker.ts +3 -0
- package/engine/tracker/tracker.ts +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/board/package.json
CHANGED
|
@@ -128,7 +128,10 @@ export const BOARD_VOCABULARY = {
|
|
|
128
128
|
})
|
|
129
129
|
),
|
|
130
130
|
}),
|
|
131
|
-
|
|
131
|
+
/** The run ends with nothing to do; `already_done` is empty at intake (#484). */
|
|
132
|
+
nothing_to_do: z.looseObject({
|
|
133
|
+
already_done: z.array(z.number().int()).catch([]).default([]),
|
|
134
|
+
}),
|
|
132
135
|
spec_snapshot: z.looseObject({
|
|
133
136
|
spec: z.looseObject({ number: z.number(), title: z.string() }),
|
|
134
137
|
ticket_order: z.array(z.number()).catch([]),
|
|
@@ -313,6 +316,12 @@ export const BOARD_VOCABULARY = {
|
|
|
313
316
|
ticket_skipped: z.looseObject({
|
|
314
317
|
because: z.number().int().nullable().catch(null),
|
|
315
318
|
}),
|
|
319
|
+
/** A ticket whose work was already on the base branch (#484), done by `shas`. */
|
|
320
|
+
ticket_already_done: z.looseObject({
|
|
321
|
+
shas: z.array(z.string()).catch([]),
|
|
322
|
+
}),
|
|
323
|
+
/** The engine closed an already-done ticket, as the run opens no PR. */
|
|
324
|
+
ticket_closed: z.looseObject({}),
|
|
316
325
|
|
|
317
326
|
// The final review (#367). Its agent, gate, scan, commit, and push
|
|
318
327
|
// records are the usual kinds with `ticket: null`.
|
|
@@ -428,6 +437,8 @@ const BoardEntrySchema = z.discriminatedUnion('kind', [
|
|
|
428
437
|
entry({ kind: 'reply_ignored' }),
|
|
429
438
|
entry({ kind: 'ticket_retried' }),
|
|
430
439
|
entry({ kind: 'ticket_skipped' }),
|
|
440
|
+
entry({ kind: 'ticket_already_done' }),
|
|
441
|
+
entry({ kind: 'ticket_closed' }),
|
|
431
442
|
entry({ kind: 'final_review_started' }),
|
|
432
443
|
entry({ kind: 'lens_started' }),
|
|
433
444
|
entry({ kind: 'lens_finished' }),
|
|
@@ -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.2"
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
runReasonText,
|
|
12
12
|
reviewCountsText,
|
|
13
13
|
roleWords,
|
|
14
|
+
shortSha,
|
|
14
15
|
startKind,
|
|
15
16
|
} from './reduce-board'
|
|
16
17
|
|
|
@@ -216,11 +217,16 @@ export const describeRecord = ({
|
|
|
216
217
|
text: `Intake refused the run. ${after.run.refusal.join('; ')}`,
|
|
217
218
|
tone: 'danger',
|
|
218
219
|
})
|
|
219
|
-
case 'nothing_to_do':
|
|
220
|
+
case 'nothing_to_do': {
|
|
221
|
+
const done = record.content.already_done
|
|
220
222
|
return event({
|
|
221
|
-
text:
|
|
223
|
+
text:
|
|
224
|
+
done.length === 0
|
|
225
|
+
? 'Nothing to do: the spec has no open tickets.'
|
|
226
|
+
: `Nothing to do: the work of ${done.map((number) => `#${number}`).join(', ')} was already on the base branch.`,
|
|
222
227
|
tone: 'info',
|
|
223
228
|
})
|
|
229
|
+
}
|
|
224
230
|
case 'spec_snapshot':
|
|
225
231
|
return event({
|
|
226
232
|
text: `Intake passed: spec #${record.content.spec.number}, ${record.content.ticket_order.length} tickets.`,
|
|
@@ -389,6 +395,16 @@ export const describeRecord = ({
|
|
|
389
395
|
tone: 'info',
|
|
390
396
|
})
|
|
391
397
|
}
|
|
398
|
+
case 'ticket_already_done':
|
|
399
|
+
return event({
|
|
400
|
+
text: `${at}already done on the base branch, by ${record.content.shas.map((sha) => shortSha({ sha })).join(', ')}. Nothing to build.`,
|
|
401
|
+
tone: 'success',
|
|
402
|
+
})
|
|
403
|
+
case 'ticket_closed':
|
|
404
|
+
return event({
|
|
405
|
+
text: `${at}closed, since its work was already done.`,
|
|
406
|
+
tone: 'info',
|
|
407
|
+
})
|
|
392
408
|
case 'ticket_skipped': {
|
|
393
409
|
const { because } = record.content
|
|
394
410
|
return event({
|
|
@@ -689,6 +705,12 @@ const finishedText = ({
|
|
|
689
705
|
})
|
|
690
706
|
}
|
|
691
707
|
if (role === 'test-writer') {
|
|
708
|
+
if (result.outcome === 'already_done') {
|
|
709
|
+
return event({
|
|
710
|
+
text: `${at}the test-writer found the work already done.`,
|
|
711
|
+
tone: 'info',
|
|
712
|
+
})
|
|
713
|
+
}
|
|
692
714
|
return result.outcome === 'nothing_new_to_test'
|
|
693
715
|
? event({ text: `${at}nothing new to test.`, tone: 'info' })
|
|
694
716
|
: event({ text: `${at}tests written.`, tone: 'info' })
|
|
@@ -40,7 +40,7 @@ const STUCK_REASONS: Record<string, string> = {
|
|
|
40
40
|
agent_failed: 'An agent failed on its last try.',
|
|
41
41
|
red_check_failed: 'The red check failed.',
|
|
42
42
|
nothing_new_to_test:
|
|
43
|
-
'The test-writer
|
|
43
|
+
'The test-writer says the ticket changes no behavior, so there is nothing new to test. Label it refactor and retry it.',
|
|
44
44
|
leftovers_found:
|
|
45
45
|
'The leftover scan found files that must not be committed.',
|
|
46
46
|
gates_failed: 'The checks failed.',
|
|
@@ -55,6 +55,24 @@ const STUCK_REASONS: Record<string, string> = {
|
|
|
55
55
|
crashed: 'The engine crashed in the same step again and again.',
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* A commit's short sha, as git and GitHub show it.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* shortSha({ sha: '3559c25f5a1b' }) // '3559c25'
|
|
63
|
+
*/
|
|
64
|
+
export const shortSha = ({ sha }: { sha: string }): string => sha.slice(0, 7)
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* An already-done ticket's activity (#484): calm, with the commits that
|
|
68
|
+
* did its work.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* alreadyDoneText({ shas: ['3559c25f5a1b'] }) // 'already done · 3559c25'
|
|
72
|
+
*/
|
|
73
|
+
export const alreadyDoneText = ({ shas }: { shas: string[] }): string =>
|
|
74
|
+
['already done', ...shas.map((sha) => shortSha({ sha }))].join(' · ')
|
|
75
|
+
|
|
58
76
|
/** Why a retry of an edited ticket was refused, in words. */
|
|
59
77
|
export const RETRY_REFUSED_TEXT =
|
|
60
78
|
"Its new text or labels aren't ready to build."
|
|
@@ -376,6 +394,8 @@ const STEP_WORDS: Record<string, string> = {
|
|
|
376
394
|
push_final_fixes: 'pushing the run branch',
|
|
377
395
|
write_changeset: 'writing the changeset',
|
|
378
396
|
open_pull_request: 'opening the pull request',
|
|
397
|
+
mark_already_done: 'noting the work is already done',
|
|
398
|
+
close_ticket: 'closing the ticket',
|
|
379
399
|
}
|
|
380
400
|
|
|
381
401
|
/**
|
|
@@ -1275,6 +1295,23 @@ const applyKind = ({
|
|
|
1275
1295
|
ticket,
|
|
1276
1296
|
because: record.content.because,
|
|
1277
1297
|
})
|
|
1298
|
+
// Done, not stuck (#484): its work was already on the base branch.
|
|
1299
|
+
case 'ticket_already_done':
|
|
1300
|
+
return updateTicket({
|
|
1301
|
+
state,
|
|
1302
|
+
number: ticket,
|
|
1303
|
+
update: (card) => ({
|
|
1304
|
+
...card,
|
|
1305
|
+
stage: 'done',
|
|
1306
|
+
step: ALL_STEPS_DONE,
|
|
1307
|
+
role: null,
|
|
1308
|
+
open_check: null,
|
|
1309
|
+
activity: alreadyDoneText({ shas: record.content.shas }),
|
|
1310
|
+
}),
|
|
1311
|
+
})
|
|
1312
|
+
// Its closing changes nothing the board shows.
|
|
1313
|
+
case 'ticket_closed':
|
|
1314
|
+
return state
|
|
1278
1315
|
case 'ticket_retried':
|
|
1279
1316
|
return ticketRetried({ state, record })
|
|
1280
1317
|
// An ignored reply is only a chat row; the engine answered it.
|
|
@@ -1577,6 +1614,9 @@ const agentFinished = ({
|
|
|
1577
1614
|
if (card.open_check === 'review') {
|
|
1578
1615
|
return { ...done, activity: 'test findings answered' }
|
|
1579
1616
|
}
|
|
1617
|
+
if (result.outcome === 'already_done') {
|
|
1618
|
+
return { ...done, activity: 'already done' }
|
|
1619
|
+
}
|
|
1580
1620
|
return result.outcome === 'nothing_new_to_test'
|
|
1581
1621
|
? { ...done, step: 2, activity: 'nothing new to test' }
|
|
1582
1622
|
: { ...done, step: 1, activity: 'red check' }
|
|
@@ -82,8 +82,10 @@ You write the failing tests for ONE ticket, before any code exists.
|
|
|
82
82
|
- 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.
|
|
83
83
|
|
|
84
84
|
Your result (structured output):
|
|
85
|
-
- outcome: "tests_written", or "nothing_new_to_test" if the ticket truly changes no behavior (a refactor), or "needs_setup_change" if the tests can't work without a change to a test setup file${setupFiles({ config })} (you may never change one): say which file and why in "setup_change", and the user makes the change.
|
|
85
|
+
- outcome: "tests_written", or "nothing_new_to_test" if the ticket truly changes no behavior (a refactor), or "already_done" if the ticket's work is already on the base branch, or "needs_setup_change" if the tests can't work without a change to a test setup file${setupFiles({ config })} (you may never change one): say which file and why in "setup_change", and the user makes the change.
|
|
86
|
+
- Answer "already_done" only when EVERY acceptance criterion is already met on the base branch and already has a test that checks it. Look with \`git log\` and \`git show\`, and read the code and tests. Then write no tests, and give your evidence: in "done_by", the commits that did the work, as { sha, title }; in "criteria", the existing tests that cover each criterion. If even one criterion is not met, or not tested, write the tests instead.
|
|
86
87
|
- criteria: for EACH criterion id (AC1, AC2, ...), the tests that check it, as { file, name }. "name" is the full name bun prints: describe names and the test name joined by " > ".
|
|
88
|
+
- done_by: empty, unless the outcome is "already_done".
|
|
87
89
|
- finding_responses: empty, unless your prompt gives you ticket review findings (see below).
|
|
88
90
|
- summary, assumptions, run_notes.
|
|
89
91
|
|
|
@@ -126,21 +126,39 @@ const SHARED_FIELDS = {
|
|
|
126
126
|
run_notes: z.array(z.string()).default([]),
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/** A commit on the base branch that did a ticket's work (#484). */
|
|
130
|
+
export const DoneCommitSchema = z.object({
|
|
131
|
+
sha: z.string().min(1),
|
|
132
|
+
/** Its subject line, if the agent read it. */
|
|
133
|
+
title: z.string().default(''),
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
export type DoneCommit = z.infer<typeof DoneCommitSchema>
|
|
137
|
+
|
|
129
138
|
/**
|
|
130
139
|
* The test-writer's result. `nothing_new_to_test` is an honest answer for a
|
|
131
140
|
* ticket that changes no behavior; the engine then makes the ticket stuck,
|
|
132
|
-
* with a hint to label it `refactor`. `
|
|
133
|
-
*
|
|
134
|
-
*
|
|
141
|
+
* with a hint to label it `refactor`. `already_done` says the ticket's work
|
|
142
|
+
* is already on the base branch (#484): `done_by` names the commits that did
|
|
143
|
+
* it and `criteria` the existing tests that cover each criterion; the engine
|
|
144
|
+
* counts the ticket as done. `needs_setup_change` says the tests can't be
|
|
145
|
+
* written without a change to a test setup file (`setup_change`), which
|
|
146
|
+
* only the user may make.
|
|
135
147
|
*/
|
|
136
148
|
export const TestWriterResultSchema = z.object({
|
|
137
149
|
outcome: z.enum([
|
|
138
150
|
'tests_written',
|
|
139
151
|
'nothing_new_to_test',
|
|
152
|
+
'already_done',
|
|
140
153
|
'needs_setup_change',
|
|
141
154
|
]),
|
|
142
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* For each criterion id (AC1, AC2, ...), the tests that check it: new
|
|
157
|
+
* ones, or with `already_done`, the ones already on the base branch.
|
|
158
|
+
*/
|
|
143
159
|
criteria: z.array(CriterionTestsSchema).default([]),
|
|
160
|
+
/** With `already_done`: the base branch's commits that did the work. */
|
|
161
|
+
done_by: z.array(DoneCommitSchema).default([]),
|
|
144
162
|
...SETUP_FIELDS,
|
|
145
163
|
...SHARED_FIELDS,
|
|
146
164
|
...FIXER_FIELDS,
|
|
@@ -317,8 +335,24 @@ export const parseRoleResult = ({
|
|
|
317
335
|
error: `The ${role}'s result does not fit its schema:\n${z.prettifyError(parsed.error)}`,
|
|
318
336
|
}
|
|
319
337
|
}
|
|
320
|
-
// Every reviewer (the ticket reviewer and each lens) gives a verdict.
|
|
321
338
|
const { result } = parsed.data
|
|
339
|
+
// "Already done" is only an answer with its evidence (#484).
|
|
340
|
+
if (parsed.data.role === 'test-writer') {
|
|
341
|
+
const { outcome, done_by, criteria } = parsed.data.result
|
|
342
|
+
if (outcome === 'already_done' && done_by.length === 0) {
|
|
343
|
+
return {
|
|
344
|
+
ok: false,
|
|
345
|
+
error: 'The test-writer\'s "already_done" names no commit: list in "done_by" the base branch\'s commits that did the work.',
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (outcome === 'already_done' && criteria.length === 0) {
|
|
349
|
+
return {
|
|
350
|
+
ok: false,
|
|
351
|
+
error: 'The test-writer\'s "already_done" names no tests: list in "criteria" the existing tests that cover each criterion.',
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Every reviewer (the ticket reviewer and each lens) gives a verdict.
|
|
322
356
|
if ('verdict' in result) {
|
|
323
357
|
const { verdict, findings } = result
|
|
324
358
|
const expected = findings.some(isBlocking)
|
|
@@ -60,6 +60,11 @@ export const EngineConfigSchema = z.object({
|
|
|
60
60
|
* that are gitignored, such as `bun run build:rom`. Agents never run it.
|
|
61
61
|
*/
|
|
62
62
|
prepare: z.string().min(1).optional(),
|
|
63
|
+
/**
|
|
64
|
+
* How long the prepare command may run, in milliseconds; left out, the
|
|
65
|
+
* engine's default (`DEFAULT_PREPARE_TIMEOUT_MS`, 30 minutes).
|
|
66
|
+
*/
|
|
67
|
+
prepare_timeout_ms: z.number().int().positive().optional(),
|
|
63
68
|
test_file_patterns: z.array(z.string()).default(['**/*.test.ts']),
|
|
64
69
|
test_setup_files: z.array(z.string()).default([]),
|
|
65
70
|
rule_files: z.array(z.string()).default([]),
|
|
@@ -73,6 +78,26 @@ export const EngineConfigSchema = z.object({
|
|
|
73
78
|
|
|
74
79
|
export type EngineConfig = z.infer<typeof EngineConfigSchema>
|
|
75
80
|
|
|
81
|
+
/**
|
|
82
|
+
* How long the prepare command may run when the config doesn't say: 30
|
|
83
|
+
* minutes. A first build in a fresh worktree can take many minutes
|
|
84
|
+
* (HeartGold's takes about 8), far past the 5 minutes other commands get.
|
|
85
|
+
*/
|
|
86
|
+
export const DEFAULT_PREPARE_TIMEOUT_MS = 30 * 60 * 1000
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The prepare command's time limit: the config's `prepare_timeout_ms`, else
|
|
90
|
+
* the default.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* prepareTimeoutOf({ config: { ...config, prepare_timeout_ms: 3_600_000 } }) // 3_600_000
|
|
94
|
+
*/
|
|
95
|
+
export const prepareTimeoutOf = ({
|
|
96
|
+
config,
|
|
97
|
+
}: {
|
|
98
|
+
config: EngineConfig
|
|
99
|
+
}): number => config.prepare_timeout_ms ?? DEFAULT_PREPARE_TIMEOUT_MS
|
|
100
|
+
|
|
76
101
|
/** One test command the engine runs, and how it reads the results. */
|
|
77
102
|
export type TestCommand = { run: string; results: TestResults }
|
|
78
103
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CriterionTests,
|
|
3
|
+
DoneCommit,
|
|
4
|
+
TestWriterResult,
|
|
5
|
+
} from '../agents/role-results'
|
|
6
|
+
import type { TicketSnapshot } from '../intake/intake-schemas'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A ticket whose work is already on the base branch (#484): its
|
|
10
|
+
* test-writer answered `already_done`, with the commits that did the work
|
|
11
|
+
* and the tests that already cover each criterion. The ticket is done, not
|
|
12
|
+
* stuck, and nobody has to reply. The run's PR closes it; a run with no PR
|
|
13
|
+
* closes it itself, and a run where every ticket was already done ends with
|
|
14
|
+
* nothing to do.
|
|
15
|
+
*/
|
|
16
|
+
export type AlreadyDoneAction =
|
|
17
|
+
/**
|
|
18
|
+
* Tell the spec issue (`body`) that the ticket's work is already done by
|
|
19
|
+
* `shas`, then journal the ticket as done.
|
|
20
|
+
*/
|
|
21
|
+
| {
|
|
22
|
+
type: 'mark_already_done'
|
|
23
|
+
ticket: number
|
|
24
|
+
spec_number: number
|
|
25
|
+
shas: string[]
|
|
26
|
+
body: string
|
|
27
|
+
}
|
|
28
|
+
/** Close an already-done ticket, with `body` as a comment on it. */
|
|
29
|
+
| { type: 'close_ticket'; ticket: number; body: string }
|
|
30
|
+
/**
|
|
31
|
+
* End the run with nothing to do: at intake (`already_done` is empty),
|
|
32
|
+
* or once every ticket's work was already done and those are closed.
|
|
33
|
+
*/
|
|
34
|
+
| {
|
|
35
|
+
type: 'finish_nothing_to_do'
|
|
36
|
+
closed_tickets: number[]
|
|
37
|
+
already_done: number[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** How many characters of a sha the engine shows. */
|
|
41
|
+
const SHORT_SHA = 7
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A commit's short sha, as git and GitHub show it.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* shortSha({ sha: '3559c25f5a1b2c3d' }) // '3559c25'
|
|
48
|
+
*/
|
|
49
|
+
export const shortSha = ({ sha }: { sha: string }): string =>
|
|
50
|
+
sha.slice(0, SHORT_SHA)
|
|
51
|
+
|
|
52
|
+
/** The commits in words: `3559c25`, or `3559c25 (feat: add sum)`. */
|
|
53
|
+
const commitsText = ({ done_by }: { done_by: DoneCommit[] }): string =>
|
|
54
|
+
done_by
|
|
55
|
+
.map(({ sha, title }) =>
|
|
56
|
+
title === '' ? shortSha({ sha }) : `${shortSha({ sha })} (${title})`
|
|
57
|
+
)
|
|
58
|
+
.join(', ')
|
|
59
|
+
|
|
60
|
+
const testsText = ({ criteria }: { criteria: CriterionTests[] }): string[] =>
|
|
61
|
+
criteria.flatMap(({ criterion_id, tests }) =>
|
|
62
|
+
tests.map(({ file, name }) => `- ${criterion_id}: ${file} > ${name}`)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The step that marks a ticket already done, from its test-writer's
|
|
67
|
+
* `already_done`: the comment on the spec issue names the commits that
|
|
68
|
+
* did the work and the tests that cover each criterion, and says nobody
|
|
69
|
+
* has to reply.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* alreadyDoneStep({ spec_number: 10, base_branch: 'main', ticket, result })
|
|
73
|
+
* // { type: 'mark_already_done', ticket: 11, spec_number: 10, shas: ['3559c25f5...'], body: '**#11 is already done: Add sum** ...' }
|
|
74
|
+
*/
|
|
75
|
+
export const alreadyDoneStep = ({
|
|
76
|
+
spec_number,
|
|
77
|
+
base_branch,
|
|
78
|
+
ticket,
|
|
79
|
+
result,
|
|
80
|
+
}: {
|
|
81
|
+
spec_number: number
|
|
82
|
+
base_branch: string
|
|
83
|
+
ticket: TicketSnapshot
|
|
84
|
+
result: TestWriterResult
|
|
85
|
+
}): AlreadyDoneAction => {
|
|
86
|
+
const tests = testsText({ criteria: result.criteria })
|
|
87
|
+
const body = [
|
|
88
|
+
`**#${ticket.number} is already done: ${ticket.title}**`,
|
|
89
|
+
`The test-writer found this ticket's work already on \`${base_branch}\`, so this run builds nothing for it. It is done, not stuck: you don't need to reply.`,
|
|
90
|
+
`Done by: ${commitsText({ done_by: result.done_by })}`,
|
|
91
|
+
tests.length === 0
|
|
92
|
+
? ''
|
|
93
|
+
: `Tests that already cover it:\n\n${tests.join('\n')}`,
|
|
94
|
+
result.summary === '' ? '' : `The test-writer says: ${result.summary}`,
|
|
95
|
+
"This run's PR closes it. If the run opens no PR, the engine closes it.",
|
|
96
|
+
]
|
|
97
|
+
return {
|
|
98
|
+
type: 'mark_already_done',
|
|
99
|
+
ticket: ticket.number,
|
|
100
|
+
spec_number,
|
|
101
|
+
shas: result.done_by.map(({ sha }) => sha),
|
|
102
|
+
body: body.filter((part) => part !== '').join('\n\n'),
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The step that closes an already-done ticket in a run with no PR, with a
|
|
108
|
+
* comment saying which commits did its work.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* closeTicketStep({ spec_number: 10, base_branch: 'main', ticket: 11, shas })
|
|
112
|
+
* // { type: 'close_ticket', ticket: 11, body: "Closing: this ticket's work is already on `main` ..." }
|
|
113
|
+
*/
|
|
114
|
+
export const closeTicketStep = ({
|
|
115
|
+
spec_number,
|
|
116
|
+
base_branch,
|
|
117
|
+
ticket,
|
|
118
|
+
shas,
|
|
119
|
+
}: {
|
|
120
|
+
spec_number: number
|
|
121
|
+
base_branch: string
|
|
122
|
+
ticket: number
|
|
123
|
+
shas: string[]
|
|
124
|
+
}): AlreadyDoneAction => ({
|
|
125
|
+
type: 'close_ticket',
|
|
126
|
+
ticket,
|
|
127
|
+
body:
|
|
128
|
+
`Closing: this ticket's work is already on \`${base_branch}\`, done by ${shas.map((sha) => shortSha({ sha })).join(', ')}. ` +
|
|
129
|
+
`Luca's run for spec #${spec_number} found nothing left to build for it, so it opens no PR for it.`,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* An already-done ticket's line in the PR's ticket list, after its title.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* alreadyDoneNote({ shas: ['3559c25f5a1b'] }) // ' (already done before this run, by 3559c25)'
|
|
137
|
+
*/
|
|
138
|
+
export const alreadyDoneNote = ({ shas }: { shas: string[] }): string =>
|
|
139
|
+
` (already done before this run, by ${shas.map((sha) => shortSha({ sha })).join(', ')})`
|