@alecsibilia/luca 14.0.0-alpha.2 → 14.0.0-alpha.21
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 +76 -0
- package/README.md +7 -2
- package/board/client/board-look.ts +31 -1
- package/board/client/board-panel-parts.tsx +150 -0
- package/board/client/board-panel-stages.ts +99 -0
- package/board/client/board-panel-styles.ts +223 -0
- package/board/client/board-panel.tsx +77 -1275
- package/board/client/engine-life.ts +48 -0
- package/board/client/final-review-stack.tsx +144 -0
- package/board/client/heartbeat.tsx +61 -0
- package/board/client/helper-lines.tsx +69 -0
- package/board/client/live-feed.tsx +86 -0
- package/board/client/live-line.ts +73 -0
- package/board/client/model-line.tsx +178 -0
- package/board/client/needs-you-section.tsx +133 -0
- package/board/client/one-line.tsx +193 -0
- package/board/client/repo-wait-banner.tsx +69 -0
- package/board/client/run-footer.tsx +68 -0
- package/board/client/run-header.tsx +189 -0
- package/board/client/run-picker.tsx +98 -0
- package/board/client/status-bar.tsx +131 -0
- package/board/client/step-dots.tsx +127 -0
- package/board/client/stuck-actions.tsx +269 -0
- package/board/client/ticket-card-view.tsx +238 -0
- package/board/client/ticket-detail.tsx +87 -0
- package/board/client/ticket-stack.tsx +119 -0
- package/board/client/ticket-wait.ts +188 -0
- package/board/client/timeline-rows.tsx +41 -14
- package/board/client/usage-line.tsx +51 -0
- package/board/client/wait-line.tsx +39 -0
- package/board/index.client.tsx +1 -1
- package/board/index.server.ts +7 -1
- package/board/package.json +1 -1
- package/board/server/board-panel-state-helpers.ts +67 -0
- package/board/server/board-server-check.ts +294 -0
- package/board/server/board-server-engine.ts +129 -0
- package/board/server/board-server-events.ts +134 -0
- package/board/server/board-server-read.ts +190 -0
- package/board/server/board-server-resume.ts +164 -0
- package/board/server/board-server-start.ts +174 -0
- package/board/server/board-server-store.ts +281 -0
- package/board/server/board-server-types.ts +114 -0
- package/board/server/board-server-util.ts +4 -0
- package/board/server/board-server.ts +52 -943
- package/board/server/board-vocabulary-engine.ts +144 -0
- package/board/server/board-vocabulary-review.ts +115 -0
- package/board/server/board-vocabulary-run.ts +145 -0
- package/board/server/board-vocabulary-shapes.ts +153 -0
- package/board/server/board-vocabulary.ts +20 -375
- package/board/server/current-step-panel-helpers.ts +108 -0
- package/board/server/describe-record.ts +403 -0
- package/board/server/engine-launch.ts +20 -3
- package/board/server/engine-restart-helpers.ts +105 -0
- package/board/server/engine-watch.ts +45 -5
- package/board/server/journal-rebuild-helpers.ts +169 -0
- package/board/server/live-file.ts +130 -0
- package/board/server/luca-version.ts +1 -1
- package/board/server/make-rows.ts +14 -629
- package/board/server/record-texts.ts +350 -0
- package/board/server/reduce-board-agent-start.ts +224 -0
- package/board/server/reduce-board-agents.ts +160 -0
- package/board/server/reduce-board-counters.ts +138 -0
- package/board/server/reduce-board-create.ts +148 -0
- package/board/server/reduce-board-final-review.ts +174 -0
- package/board/server/reduce-board-needs-you.ts +283 -0
- package/board/server/reduce-board-reasons.ts +59 -0
- package/board/server/reduce-board-run.ts +255 -0
- package/board/server/reduce-board-settle.ts +88 -0
- package/board/server/reduce-board-steps.ts +226 -0
- package/board/server/reduce-board-text.ts +139 -0
- package/board/server/reduce-board-ticket-checks.ts +168 -0
- package/board/server/reduce-board-ticket-join.ts +144 -0
- package/board/server/reduce-board-ticket-start.ts +132 -0
- package/board/server/reduce-board-update.ts +117 -0
- package/board/server/reduce-board-usage.ts +247 -0
- package/board/server/reduce-board.ts +100 -1917
- package/board/server/reply-post.ts +407 -0
- package/board/server/run-registry.ts +8 -4
- package/board/server/ticket-stages-panel-helpers.ts +296 -0
- package/board/shared/board-rows.ts +9 -0
- package/board/shared/board-rpc.ts +42 -0
- package/board/shared/board-state-counts.ts +77 -0
- package/board/shared/board-state-live.ts +99 -0
- package/board/shared/board-state-review.ts +59 -0
- package/board/shared/board-state-run.ts +236 -0
- package/board/shared/board-state-ticket.ts +257 -0
- package/board/shared/board-state-usage.ts +107 -0
- package/board/shared/board-state.ts +97 -567
- package/board/shared/reply-actions.ts +207 -0
- package/engine/agents/agent-launcher.ts +27 -4
- package/engine/agents/claude-launcher-helpers.ts +233 -0
- package/engine/agents/claude-launcher.ts +28 -669
- package/engine/agents/claude-messages.ts +212 -0
- package/engine/agents/claude-options.ts +43 -8
- package/engine/agents/common-instructions.ts +138 -0
- package/engine/agents/input-channel.ts +36 -0
- package/engine/agents/launch-paths.ts +29 -0
- package/engine/agents/launch-session.ts +227 -0
- package/engine/agents/learner-instructions.ts +31 -0
- package/engine/agents/open-session.ts +210 -0
- package/engine/agents/prompt-sections.ts +95 -0
- package/engine/agents/rejoin-prompts.ts +135 -0
- package/engine/agents/reviewer-instructions.ts +99 -0
- package/engine/agents/role-instructions.ts +31 -244
- package/engine/agents/role-prompts.ts +39 -109
- package/engine/agents/role-results.ts +106 -7
- package/engine/agents/scripted-launcher.ts +21 -6
- package/engine/agents/session-pool.ts +124 -0
- package/engine/agents/turn-progress.ts +283 -0
- package/engine/agents/writer-instructions.ts +151 -0
- package/engine/cli/computer-adapters-real.ts +15 -3
- package/engine/cli/computer-adapters.ts +2 -0
- package/engine/cli/computer-checks.ts +87 -4
- package/engine/cli/config-reload-text.ts +39 -0
- package/engine/cli/decision-model-setup.ts +70 -0
- package/engine/cli/doctor-command.ts +1 -0
- package/engine/cli/doctor.ts +21 -3
- package/engine/cli/going-runs.ts +192 -36
- package/engine/cli/init-command.ts +1 -0
- package/engine/cli/init.ts +23 -1
- package/engine/cli/live-runs.ts +73 -0
- package/engine/cli/luca-run.ts +42 -7
- package/engine/cli/luca-skills.ts +183 -0
- package/engine/cli/luca.ts +8 -1
- package/engine/cli/models-command.ts +119 -0
- package/engine/cli/read-run.ts +81 -0
- package/engine/cli/run-args.ts +33 -6
- package/engine/cli/run-demo.ts +143 -0
- package/engine/cli/run-end.ts +184 -0
- package/engine/cli/run-modes-helpers.ts +81 -0
- package/engine/cli/run-modes.ts +62 -439
- package/engine/cli/setup.ts +5 -48
- package/engine/cli/unfinished-runs.ts +122 -0
- package/engine/cli/upgrade-command.ts +212 -30
- package/engine/cli/upgrade-finish.ts +149 -0
- package/engine/cli/upgrade.ts +112 -71
- package/engine/config/config-fields.ts +127 -0
- package/engine/config/config-reload.ts +250 -0
- package/engine/config/engine-config-schema.ts +235 -0
- package/engine/config/engine-config.ts +72 -142
- package/engine/config/test-commands.ts +102 -0
- package/engine/core/agent-guards-helpers.ts +84 -0
- package/engine/core/agent-turn.ts +395 -0
- package/engine/core/already-done.ts +44 -1
- package/engine/core/build-action.ts +214 -0
- package/engine/core/build-context.ts +135 -0
- package/engine/core/build-launch.ts +259 -0
- package/engine/core/commit-in.ts +282 -0
- package/engine/core/decide-build-early.ts +108 -0
- package/engine/core/decide-build-ending.ts +145 -0
- package/engine/core/decide-build-helpers.ts +87 -0
- package/engine/core/decide-build-tickets.ts +207 -0
- package/engine/core/decide-build-view.ts +103 -0
- package/engine/core/decide-build.ts +101 -1368
- package/engine/core/decide-final-fix.ts +203 -0
- package/engine/core/decide-final-review.ts +55 -310
- package/engine/core/decide-red-green.ts +398 -0
- package/engine/core/decide-rejoin.ts +259 -0
- package/engine/core/decide-repo-wait.ts +129 -0
- package/engine/core/decide-run-budget.ts +86 -12
- package/engine/core/decide-stuck-helpers.ts +127 -0
- package/engine/core/decide-stuck.ts +60 -8
- package/engine/core/decide-ticket-review.ts +95 -0
- package/engine/core/decide-ticket.ts +267 -0
- package/engine/core/decide-waits.ts +47 -0
- package/engine/core/decide.ts +74 -23
- package/engine/core/engine-chores.ts +163 -0
- package/engine/core/environment-rerun.ts +83 -0
- package/engine/core/evidence-text.ts +84 -0
- package/engine/core/execute-action.ts +303 -0
- package/engine/core/execute-already-done-check.ts +133 -0
- package/engine/core/execute-build.ts +68 -944
- package/engine/core/execute-changeset.ts +5 -7
- package/engine/core/execute-final-review.ts +125 -13
- package/engine/core/execute-gates.ts +182 -0
- package/engine/core/execute-intake.ts +130 -0
- package/engine/core/execute-join.ts +276 -0
- package/engine/core/execute-memory.ts +12 -0
- package/engine/core/execute-pull-request.ts +84 -0
- package/engine/core/execute-red-check.ts +219 -0
- package/engine/core/execute-repo-wait.ts +129 -0
- package/engine/core/execute-stuck.ts +25 -10
- package/engine/core/execute-waits.ts +158 -0
- package/engine/core/execute-with-jev.ts +118 -0
- package/engine/core/execute.ts +124 -831
- package/engine/core/final-failure-cap.ts +38 -0
- package/engine/core/final-review-action.ts +125 -0
- package/engine/core/final-review-text.ts +63 -17
- package/engine/core/final-stuck-text.ts +86 -0
- package/engine/core/fix-loop-text.ts +49 -2
- package/engine/core/left-out.ts +120 -0
- package/engine/core/live-file.ts +372 -0
- package/engine/core/loop-caps.ts +25 -2
- package/engine/core/not-in-run.ts +7 -3
- package/engine/core/pull-request-text.ts +255 -6
- package/engine/core/repo-wait-text.ts +81 -0
- package/engine/core/review-text.ts +55 -1
- package/engine/core/run-crash-recovery-helpers.ts +16 -0
- package/engine/core/run-session-close-helpers.ts +96 -0
- package/engine/core/run-start.ts +148 -0
- package/engine/core/run-stuck-text.ts +86 -0
- package/engine/core/schedule-rules.ts +193 -0
- package/engine/core/stuck-detail-text.ts +89 -0
- package/engine/core/stuck-format.ts +22 -0
- package/engine/core/stuck-text.ts +29 -332
- package/engine/core/ticket-stuck-text.ts +192 -0
- package/engine/core/worktree-changes.ts +106 -0
- package/engine/gates/already-done-check.ts +120 -0
- package/engine/gates/feature-map.ts +96 -0
- package/engine/gates/gate-runner.ts +108 -108
- package/engine/gates/gate-schemas.ts +61 -2
- package/engine/gates/leftover-scan.ts +154 -31
- package/engine/gates/new-tests.ts +45 -0
- package/engine/gates/own-files-check.ts +98 -0
- package/engine/gates/prepare-check.ts +60 -0
- package/engine/gates/prepare-slots.ts +99 -0
- package/engine/gates/red-check.ts +184 -16
- package/engine/gates/shell-check.ts +64 -0
- package/engine/gates/slot-waits.ts +55 -0
- package/engine/gates/slow-test-runner.ts +59 -0
- package/engine/gates/test-checks.ts +161 -0
- package/engine/gates/test-runner.ts +164 -5
- package/engine/gates/test-slots.ts +48 -0
- package/engine/git/git-adapter.ts +55 -396
- package/engine/git/git-commits.ts +132 -0
- package/engine/git/git-join.ts +139 -0
- package/engine/git/git-read.ts +123 -0
- package/engine/git/git-run.ts +32 -0
- package/engine/git/git-worktrees.ts +143 -0
- package/engine/git/outside-links.ts +55 -0
- package/engine/guards/after-turn-check.ts +31 -5
- package/engine/guards/guard-hook.ts +60 -12
- package/engine/guards/guard-roles.ts +68 -0
- package/engine/guards/helper-rules.ts +92 -0
- package/engine/guards/permission-rules.ts +161 -0
- package/engine/guards/role-rules-helpers.ts +48 -0
- package/engine/guards/role-rules.ts +34 -606
- package/engine/guards/shell-rules.ts +344 -0
- package/engine/guards/worktree-git.ts +208 -0
- package/engine/guards/worktree-snapshot.ts +412 -0
- package/engine/guards/worktree-state.ts +28 -825
- package/engine/guards/worktree-undo.ts +260 -0
- package/engine/guards/write-rules.ts +149 -0
- package/engine/index.ts +30 -9
- package/engine/intake/criteria-checks.ts +114 -0
- package/engine/intake/intake-checks.ts +164 -30
- package/engine/intake/intake-schemas.ts +37 -2
- package/engine/jev/clef-client.ts +259 -0
- package/engine/jev/decision-model-credentials.ts +212 -0
- package/engine/jev/jev-jobs.ts +144 -60
- package/engine/jev/jev-schemas.ts +86 -5
- package/engine/jev/jev-shadow-helpers.ts +218 -0
- package/engine/jev/jev-shadow.ts +109 -64
- package/engine/jev/model-pick.ts +275 -0
- package/engine/journal/agent-entries.ts +167 -0
- package/engine/journal/agent-message-entries.ts +72 -0
- package/engine/journal/agent-turn-replay.ts +203 -0
- package/engine/journal/already-done-entries.ts +55 -0
- package/engine/journal/commit-entries.ts +56 -0
- package/engine/journal/cross-ticket-replay.ts +222 -0
- package/engine/journal/decision-model-entries.ts +86 -0
- package/engine/journal/entry-fields.ts +10 -0
- package/engine/journal/final-review-entries.ts +128 -0
- package/engine/journal/final-review-replay.ts +324 -0
- package/engine/journal/final-review-round.ts +320 -0
- package/engine/journal/final-review-state.ts +180 -0
- package/engine/journal/gate-entries.ts +81 -0
- package/engine/journal/intake-entries.ts +87 -0
- package/engine/journal/join-entries.ts +126 -0
- package/engine/journal/journal-entry-schema.ts +218 -0
- package/engine/journal/journal-kinds.ts +97 -0
- package/engine/journal/journal-record-schema.ts +218 -0
- package/engine/journal/journal-record.ts +43 -1400
- package/engine/journal/journal.ts +6 -1
- package/engine/journal/limit-entries.ts +90 -0
- package/engine/journal/memory-entries.ts +124 -0
- package/engine/journal/memory-replay.ts +157 -0
- package/engine/journal/plan-replay.ts +139 -0
- package/engine/journal/pull-request-entries.ts +41 -0
- package/engine/journal/replay.ts +157 -2144
- package/engine/journal/reply-replay.ts +144 -0
- package/engine/journal/repo-wait-entries.ts +53 -0
- package/engine/journal/run-entries.ts +172 -0
- package/engine/journal/run-state.ts +264 -0
- package/engine/journal/shared-failure-replay.ts +133 -0
- package/engine/journal/stuck-entries.ts +193 -0
- package/engine/journal/ticket-progress.ts +392 -0
- package/engine/journal/ticket-replay.ts +411 -0
- package/engine/journal/ticket-result-replay.ts +364 -0
- package/engine/journal/ticket-retry-replay.ts +142 -0
- package/engine/journal/worktree-entries.ts +122 -0
- package/engine/models/model-score.ts +512 -0
- package/engine/shell/run-command.ts +187 -18
- package/engine/testing/agent-turn-fixtures.ts +287 -0
- package/engine/testing/already-done-fixtures.ts +97 -0
- package/engine/testing/build-fixtures.ts +68 -973
- package/engine/testing/checkouts-fixtures.ts +48 -0
- package/engine/testing/final-review-fixtures.ts +62 -1
- package/engine/testing/final-review-steps.ts +143 -0
- package/engine/testing/limit-fixtures.ts +85 -0
- package/engine/testing/live-file-fixtures.ts +57 -0
- package/engine/testing/practice-repo.ts +10 -4
- package/engine/testing/practice-run.ts +1 -1
- package/engine/testing/reply-fixtures.ts +88 -0
- package/engine/testing/run-budget-fixtures.ts +165 -0
- package/engine/testing/run-fixtures.ts +219 -0
- package/engine/testing/sdk-play.ts +177 -0
- package/engine/testing/setup-repo.ts +194 -0
- package/engine/testing/step-record-fixtures.ts +55 -0
- package/engine/testing/ticket-step-fixtures.ts +272 -0
- package/engine/testing/wait-scenarios.ts +186 -0
- package/engine/tracker/gh-retry.ts +217 -0
- package/engine/tracker/github-tracker.ts +310 -90
- package/engine/tracker/in-memory-tracker.ts +8 -4
- package/engine/tracker/tracker.ts +131 -6
- package/package.json +2 -1
- package/skills/luca-retro/SKILL.md +74 -0
- package/skills/luca-retro/scripts/retro-summary.ts +1356 -0
- package/skills/luca-unstick/SKILL.md +153 -0
- package/skills/luca-unstick/scripts/stuck-summary.ts +1063 -0
- package/engine/jev/jev-client.ts +0 -173
|
@@ -3,10 +3,16 @@ import flatMap from 'lodash/flatMap'
|
|
|
3
3
|
import uniq from 'lodash/uniq'
|
|
4
4
|
|
|
5
5
|
import { alreadyDoneNote } from './already-done'
|
|
6
|
-
import {
|
|
6
|
+
import { evidenceSection } from './evidence-text'
|
|
7
|
+
import {
|
|
8
|
+
freshFindingsSection,
|
|
9
|
+
shippedFindingsSection,
|
|
10
|
+
} from './final-review-text'
|
|
11
|
+
import { leftOutSections } from './left-out'
|
|
7
12
|
import { newMemoriesSection } from './memory-text'
|
|
8
13
|
import { reasonLine } from './stuck-text'
|
|
9
14
|
|
|
15
|
+
import type { AgentRole, MergeDanger } from '../agents/role-results'
|
|
10
16
|
import {
|
|
11
17
|
EMPTY_FINAL_REVIEW,
|
|
12
18
|
type FinalReviewState,
|
|
@@ -24,15 +30,95 @@ const SEVERITY_TEXT = {
|
|
|
24
30
|
const fileText = (file: string | null): string =>
|
|
25
31
|
file === null ? '' : ` (${file})`
|
|
26
32
|
|
|
33
|
+
/**
|
|
34
|
+
* The longest Summary picture the PR body shows (#513); a longer one is
|
|
35
|
+
* clipped, so a runaway picture can't blow the body's budget.
|
|
36
|
+
*/
|
|
37
|
+
export const MAX_SUMMARY_PICTURE = 3_000
|
|
38
|
+
|
|
39
|
+
const FENCE = '```'
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The Summary section (#513): the integration lens's picture, clipped to
|
|
43
|
+
* `MAX_SUMMARY_PICTURE` (a fence the clip leaves open is closed), or a
|
|
44
|
+
* note that there is none.
|
|
45
|
+
*/
|
|
46
|
+
const summarySection = (picture: string | null): string => {
|
|
47
|
+
if (picture === null) {
|
|
48
|
+
return '## Summary\n\nNot available: the final review gave no picture.'
|
|
49
|
+
}
|
|
50
|
+
if (picture.length <= MAX_SUMMARY_PICTURE) return `## Summary\n\n${picture}`
|
|
51
|
+
const clipped = picture.slice(0, MAX_SUMMARY_PICTURE)
|
|
52
|
+
const open = clipped.split(FENCE).length % 2 === 0
|
|
53
|
+
return `## Summary\n\n${clipped}${open ? `\n${FENCE}` : ''}\n\n… (clipped: the picture is over ${MAX_SUMMARY_PICTURE.toLocaleString('en-US')} characters)`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const DOOR_TEXT = { one_way: 'one-way', two_way: 'two-way' } as const
|
|
57
|
+
|
|
58
|
+
/** The Merge danger section (#513), or a note that there is none. */
|
|
59
|
+
const mergeDangerSection = (danger: MergeDanger | null): string =>
|
|
60
|
+
danger === null
|
|
61
|
+
? '## Merge danger\n\nNot available: the final review gave no merge danger.'
|
|
62
|
+
: `## Merge danger\n\n- **Door:** ${DOOR_TEXT[danger.door]}: ${danger.door_reason}\n- **Blast radius:** ${danger.blast_radius}: ${danger.blast_radius_reason}`
|
|
63
|
+
|
|
64
|
+
/** One line per role that ran out of turns, under `where`. */
|
|
65
|
+
const outOfTurnsLines = ({
|
|
66
|
+
where,
|
|
67
|
+
counts,
|
|
68
|
+
}: {
|
|
69
|
+
where: string
|
|
70
|
+
counts: Partial<Record<AgentRole, number>>
|
|
71
|
+
}): string[] =>
|
|
72
|
+
Object.entries(counts).map(
|
|
73
|
+
([role, times]) =>
|
|
74
|
+
`- ${where}: ${role}, ${times} ${times === 1 ? 'time' : 'times'}`
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The Out of turns section (#543): each ticket's roles whose agents ran out
|
|
79
|
+
* of turns, and how often, then the final review's. Empty with none.
|
|
80
|
+
*/
|
|
81
|
+
const outOfTurnsSection = ({
|
|
82
|
+
ticket_order,
|
|
83
|
+
tickets,
|
|
84
|
+
review,
|
|
85
|
+
}: {
|
|
86
|
+
ticket_order: number[]
|
|
87
|
+
tickets: Record<number, TicketProgress>
|
|
88
|
+
review: FinalReviewState
|
|
89
|
+
}): string => {
|
|
90
|
+
const lines = [
|
|
91
|
+
...flatMap(ticket_order, (number) =>
|
|
92
|
+
outOfTurnsLines({
|
|
93
|
+
where: `#${number}`,
|
|
94
|
+
counts: tickets[number]?.out_of_turns ?? {},
|
|
95
|
+
})
|
|
96
|
+
),
|
|
97
|
+
...outOfTurnsLines({
|
|
98
|
+
where: 'the final review',
|
|
99
|
+
counts: review.out_of_turns,
|
|
100
|
+
}),
|
|
101
|
+
]
|
|
102
|
+
return lines.length === 0
|
|
103
|
+
? ''
|
|
104
|
+
: `## Out of turns\n\nThese agents ran out of turns: a sign the ticket may have been too big for one go.\n\n${lines.join('\n')}`
|
|
105
|
+
}
|
|
106
|
+
|
|
27
107
|
/**
|
|
28
108
|
* The run's pull request title and body, from the snapshot, each ticket's
|
|
29
109
|
* progress, and the final review: which tickets it closes (an already-done
|
|
30
|
-
* one with the commits that did its work, #484), the tickets left
|
|
110
|
+
* one with the commits that did its work, #484), the tickets left for a
|
|
111
|
+
* person and the ones waiting on them (#499), the tickets left
|
|
31
112
|
* out (skipped, and why), the assumptions agents made (from every round of
|
|
32
|
-
* every agent on each ticket, and in the final review), the
|
|
113
|
+
* every agent on each ticket, and in the final review), the tickets whose
|
|
114
|
+
* agents ran out of turns (#543), the reviews' nits,
|
|
33
115
|
* the findings declined through "won't fix", and the memories the learner
|
|
34
116
|
* added or updated (#370). A final review shipped
|
|
35
|
-
* while stuck puts its open findings at the very top.
|
|
117
|
+
* while stuck puts its open findings at the very top. Right after the
|
|
118
|
+
* "Built by" line come the Summary picture and the Merge Danger (from the
|
|
119
|
+
* final review's integration lens, or "Not available" without them) and
|
|
120
|
+
* the Evidence that each ticket's new tests failed first and pass now
|
|
121
|
+
* (#513), then the tickets.
|
|
36
122
|
*
|
|
37
123
|
* @example
|
|
38
124
|
* const { title, body } = pullRequestText({ snapshot, tickets, final_review })
|
|
@@ -127,23 +213,36 @@ export const pullRequestText = ({
|
|
|
127
213
|
})
|
|
128
214
|
),
|
|
129
215
|
]
|
|
216
|
+
const stillOpen = review.fix?.findings ?? []
|
|
130
217
|
const shipped =
|
|
131
218
|
review.shipped && review.stuck !== null
|
|
132
219
|
? shippedFindingsSection({
|
|
133
|
-
findings:
|
|
220
|
+
findings: stillOpen,
|
|
134
221
|
stuck: review.stuck,
|
|
222
|
+
edits: review.shipped_edits,
|
|
135
223
|
})
|
|
136
|
-
:
|
|
224
|
+
: review.fresh_started && stillOpen.length > 0
|
|
225
|
+
? freshFindingsSection({ findings: stillOpen })
|
|
226
|
+
: ''
|
|
137
227
|
const body = [
|
|
138
228
|
shipped,
|
|
139
229
|
`Built by the Luca engine from spec #${spec.number}.`,
|
|
230
|
+
summarySection(review.summary_picture),
|
|
231
|
+
mergeDangerSection(review.merge_danger),
|
|
232
|
+
evidenceSection({ snapshot, tickets }),
|
|
140
233
|
`## Tickets\n\n${closes.join('\n')}`,
|
|
234
|
+
...leftOutSections({ left_out: snapshot.left_out }),
|
|
141
235
|
skipped.length === 0
|
|
142
236
|
? ''
|
|
143
237
|
: `## Skipped tickets\n\nLeft out of this run. They stay open for a later run.\n\n${skipped.join('\n')}`,
|
|
144
238
|
assumptions.length + finalAssumptions.length === 0
|
|
145
239
|
? ''
|
|
146
240
|
: `## Assumptions\n\nCalls agents made by themselves. Please check them.\n\n${[...assumptions, ...finalAssumptions].join('\n')}`,
|
|
241
|
+
outOfTurnsSection({
|
|
242
|
+
ticket_order: snapshot.ticket_order,
|
|
243
|
+
tickets,
|
|
244
|
+
review,
|
|
245
|
+
}),
|
|
147
246
|
nits.length === 0
|
|
148
247
|
? ''
|
|
149
248
|
: `## Nits\n\nSmall things the ticket reviews and the final review noted but did not send back.\n\n${nits.join('\n')}`,
|
|
@@ -158,6 +257,156 @@ export const pullRequestText = ({
|
|
|
158
257
|
}
|
|
159
258
|
}
|
|
160
259
|
|
|
260
|
+
/**
|
|
261
|
+
* What the engine keeps the PR body, and each of its PR comments, under
|
|
262
|
+
* (#508): a margin below GitHub's limit of 65,536 characters.
|
|
263
|
+
*/
|
|
264
|
+
export const PULL_REQUEST_BUDGET = 60_000
|
|
265
|
+
|
|
266
|
+
/** The file in the run folder the PR's full text is saved to first (#508). */
|
|
267
|
+
export const PULL_REQUEST_FILE = 'pull-request.md'
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The body's long parts, which move into PR comments when the body is over
|
|
271
|
+
* its budget. The rest (open findings, the Summary and Merge danger, the
|
|
272
|
+
* tickets it closes, and the ones left out) always stays in the body.
|
|
273
|
+
*/
|
|
274
|
+
const MOVABLE_HEADINGS = [
|
|
275
|
+
'## Evidence',
|
|
276
|
+
'## Assumptions',
|
|
277
|
+
'## Out of turns',
|
|
278
|
+
'## Nits',
|
|
279
|
+
'## Declined findings',
|
|
280
|
+
'## New memories',
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
const CLIPPED = `\n\n… (clipped: the full text is in the run folder's \`${PULL_REQUEST_FILE}\`)`
|
|
284
|
+
|
|
285
|
+
/** Room kept in each comment for its heading and the engine's marker. */
|
|
286
|
+
const COMMENT_ROOM = 200
|
|
287
|
+
|
|
288
|
+
/** `text`, clipped to stay under `limit` characters. */
|
|
289
|
+
const clip = ({ text, limit }: { text: string; limit: number }): string =>
|
|
290
|
+
text.length < limit
|
|
291
|
+
? text
|
|
292
|
+
: `${text.slice(0, limit - CLIPPED.length - 1)}${CLIPPED}`
|
|
293
|
+
|
|
294
|
+
const isMovable = (section: string): boolean =>
|
|
295
|
+
MOVABLE_HEADINGS.some(
|
|
296
|
+
(heading) => section === heading || section.startsWith(`${heading}\n`)
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* A section cut into pieces under `limit`, between its list items; each
|
|
301
|
+
* piece after the first repeats its heading. An item over the limit alone
|
|
302
|
+
* is clipped.
|
|
303
|
+
*/
|
|
304
|
+
const sectionPieces = ({
|
|
305
|
+
section,
|
|
306
|
+
limit,
|
|
307
|
+
}: {
|
|
308
|
+
section: string
|
|
309
|
+
limit: number
|
|
310
|
+
}): string[] => {
|
|
311
|
+
if (section.length < limit) return [section]
|
|
312
|
+
const heading = section.split('\n', 1)[0] ?? ''
|
|
313
|
+
const pieces: string[] = []
|
|
314
|
+
let current = ''
|
|
315
|
+
for (const item of section.split(/\n(?=- )/)) {
|
|
316
|
+
const joined = current === '' ? item : `${current}\n${item}`
|
|
317
|
+
if (joined.length < limit) {
|
|
318
|
+
current = joined
|
|
319
|
+
continue
|
|
320
|
+
}
|
|
321
|
+
if (current !== '') pieces.push(current)
|
|
322
|
+
current = clip({ text: `${heading} (continued)\n\n${item}`, limit })
|
|
323
|
+
}
|
|
324
|
+
return current === '' ? pieces : [...pieces, current]
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Pieces packed into as few texts under `limit` as they fit in. */
|
|
328
|
+
const packed = ({
|
|
329
|
+
pieces,
|
|
330
|
+
limit,
|
|
331
|
+
}: {
|
|
332
|
+
pieces: string[]
|
|
333
|
+
limit: number
|
|
334
|
+
}): string[] =>
|
|
335
|
+
pieces.reduce<string[]>((texts, piece) => {
|
|
336
|
+
const last = texts.at(-1)
|
|
337
|
+
if (last !== undefined && `${last}\n\n${piece}`.length < limit) {
|
|
338
|
+
return [...texts.slice(0, -1), `${last}\n\n${piece}`]
|
|
339
|
+
}
|
|
340
|
+
return [...texts, piece]
|
|
341
|
+
}, [])
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The PR body as sent to GitHub, and the PR comments posted after it, from
|
|
345
|
+
* its full text (#508). A body under `budget` goes as it is. A longer one
|
|
346
|
+
* keeps its essentials (open findings, the Summary and Merge danger, the
|
|
347
|
+
* tickets it closes, and the tickets left out), and its long parts (the
|
|
348
|
+
* Evidence, assumptions, out of turns, nits, declined findings, new
|
|
349
|
+
* memories) as long as
|
|
350
|
+
* they fit beside every essential (#513); the rest moves into PR
|
|
351
|
+
* comments, each under the budget, cut between list items, and the body
|
|
352
|
+
* says so. Whatever is still too long is clipped, pointing at the full
|
|
353
|
+
* text in the run folder.
|
|
354
|
+
*
|
|
355
|
+
* @example
|
|
356
|
+
* fitPullRequestBody({ body: full })
|
|
357
|
+
* // { body: '...## More in the PR comments\n\n...', comments: ['## Assumptions\n\n...'] }
|
|
358
|
+
*/
|
|
359
|
+
export const fitPullRequestBody = ({
|
|
360
|
+
body,
|
|
361
|
+
budget = PULL_REQUEST_BUDGET,
|
|
362
|
+
}: {
|
|
363
|
+
body: string
|
|
364
|
+
budget?: number
|
|
365
|
+
}): { body: string; comments: string[] } => {
|
|
366
|
+
if (body.length < budget) return { body, comments: [] }
|
|
367
|
+
const note = (moved: string[]) =>
|
|
368
|
+
`## More in the PR comments\n\nThis PR's text is too long for its body, so its ${moved.map((section) => section.split('\n', 1)[0]?.replace('## ', '').toLowerCase()).join(', ')} are in the PR comments below. The full text is in the run folder's \`${PULL_REQUEST_FILE}\`.`
|
|
369
|
+
const sections = body.split(/\n\n(?=## )/)
|
|
370
|
+
// Essentials after a long part still need their room (#513).
|
|
371
|
+
const essentials = sections.filter((section) => !isMovable(section))
|
|
372
|
+
const kept: string[] = []
|
|
373
|
+
const kept_long: string[] = []
|
|
374
|
+
const moved: string[] = []
|
|
375
|
+
for (const section of sections) {
|
|
376
|
+
if (!isMovable(section)) {
|
|
377
|
+
kept.push(section)
|
|
378
|
+
continue
|
|
379
|
+
}
|
|
380
|
+
const fits =
|
|
381
|
+
moved.length === 0 &&
|
|
382
|
+
[...essentials, ...kept_long, section, note([section])].join('\n\n')
|
|
383
|
+
.length < budget
|
|
384
|
+
if (fits) {
|
|
385
|
+
kept.push(section)
|
|
386
|
+
kept_long.push(section)
|
|
387
|
+
} else moved.push(section)
|
|
388
|
+
}
|
|
389
|
+
const comments = packed({
|
|
390
|
+
pieces: moved.flatMap((section) =>
|
|
391
|
+
sectionPieces({ section, limit: budget - COMMENT_ROOM })
|
|
392
|
+
),
|
|
393
|
+
limit: budget - COMMENT_ROOM,
|
|
394
|
+
})
|
|
395
|
+
return {
|
|
396
|
+
body: clip({
|
|
397
|
+
text: [...kept, ...(moved.length === 0 ? [] : [note(moved)])].join(
|
|
398
|
+
'\n\n'
|
|
399
|
+
),
|
|
400
|
+
limit: budget,
|
|
401
|
+
}),
|
|
402
|
+
comments: comments.map((text, index) =>
|
|
403
|
+
comments.length === 1
|
|
404
|
+
? text
|
|
405
|
+
: `**More of this PR's notes (${index + 1} of ${comments.length})**\n\n${text}`
|
|
406
|
+
),
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
161
410
|
/** One open ticket of the spec, as the tracker lists it. */
|
|
162
411
|
type OpenTicket = { number: number; title: string }
|
|
163
412
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The texts of the repo wait (#615): the spec issue's comment when the run
|
|
3
|
+
* starts waiting for earlier runs on its repo, and when it is stuck on them.
|
|
4
|
+
*/
|
|
5
|
+
import type { RepoWaitRun } from './decide-repo-wait'
|
|
6
|
+
|
|
7
|
+
/** The runs, as a list of lines. */
|
|
8
|
+
const runLines = (runs: RepoWaitRun[]): string =>
|
|
9
|
+
runs
|
|
10
|
+
.map(
|
|
11
|
+
({ run_id, spec }) =>
|
|
12
|
+
`- run \`${run_id}\`${spec === null ? ' (a demo)' : ` (spec #${spec})`}`
|
|
13
|
+
)
|
|
14
|
+
.join('\n')
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The comment on the spec issue when the run waits for earlier runs on its
|
|
18
|
+
* repo: which ones, and that it goes on by itself.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* repoWaitComment({ runs: [{ run_id: 'run-a', spec: 7 }] })
|
|
22
|
+
* // '**The run waits for another run on this repo**\n\n- run `run-a` (spec #7)\n\n...'
|
|
23
|
+
*/
|
|
24
|
+
export const repoWaitComment = ({ runs }: { runs: RepoWaitRun[] }): string =>
|
|
25
|
+
[
|
|
26
|
+
'**The run waits for another run on this repo**',
|
|
27
|
+
runLines(runs),
|
|
28
|
+
'Two runs on one repo slow each other down, so this one starts once none of these is going. It checks every minute and goes on by itself. After 24 hours it stops and asks you. To run two at once on purpose, start a run with `luca-run --alongside`.',
|
|
29
|
+
].join('\n\n')
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Why the run is stuck on its repo wait, in one line, naming the `ps`
|
|
33
|
+
* failure when the wait couldn't check whether those runs are going (#624).
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* repoBusyDetail({ runs: [{ run_id: 'run-a', spec: 7 }], ps_error: null })
|
|
37
|
+
* // 'The run waited 24 hours for an earlier run on this repo: run-a (spec #7).'
|
|
38
|
+
*/
|
|
39
|
+
export const repoBusyDetail = ({
|
|
40
|
+
runs,
|
|
41
|
+
ps_error = null,
|
|
42
|
+
}: {
|
|
43
|
+
runs: RepoWaitRun[]
|
|
44
|
+
ps_error?: string | null
|
|
45
|
+
}): string => {
|
|
46
|
+
const named = runs
|
|
47
|
+
.map(
|
|
48
|
+
({ run_id, spec }) =>
|
|
49
|
+
`${run_id}${spec === null ? ' (a demo)' : ` (spec #${spec})`}`
|
|
50
|
+
)
|
|
51
|
+
.join(', ')
|
|
52
|
+
const ps =
|
|
53
|
+
ps_error === null
|
|
54
|
+
? ''
|
|
55
|
+
: ` Luca couldn't list the processes (${ps_error}), so it couldn't tell whether those runs are still going.`
|
|
56
|
+
return `The run waited 24 hours for an earlier run on this repo: ${named}.${ps}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The comment on the spec issue when the run is stuck after 24 hours of
|
|
61
|
+
* its repo wait (`repo_busy`): why, and the replies `retry` and `stop`.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* repoBusyStuckComment({ detail })
|
|
65
|
+
* // '**The run is stuck: another run on this repo is still going**\n\nWhy: ...'
|
|
66
|
+
*/
|
|
67
|
+
export const repoBusyStuckComment = ({ detail }: { detail: string }): string =>
|
|
68
|
+
[
|
|
69
|
+
'**The run is stuck: another run on this repo is still going**',
|
|
70
|
+
[
|
|
71
|
+
`Why: ${detail}`,
|
|
72
|
+
'Nothing has started yet: no ticket was read and no worktree made.',
|
|
73
|
+
].join('\n'),
|
|
74
|
+
[
|
|
75
|
+
'Reply with one word on this issue:',
|
|
76
|
+
'- `retry`: wait up to 24 hours more, then go on once no earlier run on the repo is going.',
|
|
77
|
+
'- `stop`: end the run. Start it again later, or with `luca-run --alongside` to run both at once.',
|
|
78
|
+
'',
|
|
79
|
+
'Only the spec owner counts.',
|
|
80
|
+
].join('\n'),
|
|
81
|
+
].join('\n\n')
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ReplayedGates,
|
|
7
7
|
ReviewFix,
|
|
8
8
|
TicketProgress,
|
|
9
|
+
WriterRole,
|
|
9
10
|
} from '../journal/replay'
|
|
10
11
|
import { clipOutput } from '../shell/run-command'
|
|
11
12
|
|
|
@@ -59,6 +60,7 @@ export const responseText = ({
|
|
|
59
60
|
}): string => {
|
|
60
61
|
const answer = responses.find(({ finding_id }) => finding_id === finding.id)
|
|
61
62
|
if (answer === undefined) return ' Fixer: no answer.'
|
|
63
|
+
// An `other_role` the other role never answered stands as "won't fix".
|
|
62
64
|
return answer.response === 'fixed'
|
|
63
65
|
? ` Fixer: fixed. ${answer.reason}`.trimEnd()
|
|
64
66
|
: ` Fixer: WON'T FIX. Reason: ${answer.reason || 'none given'}`
|
|
@@ -161,10 +163,62 @@ export const reviewSections = ({
|
|
|
161
163
|
|
|
162
164
|
/** How a review fixer answers each finding it got. */
|
|
163
165
|
export const FIX_RULES =
|
|
164
|
-
'For EACH finding, answer in "finding_responses": "fixed", or "wont_fix" with your reason if the finding is wrong
|
|
166
|
+
'For EACH finding, answer in "finding_responses": "fixed", or "wont_fix" with your reason if the finding is wrong, ' +
|
|
167
|
+
'or "other_role" with your reason if the other writer role must fix it, or must fix it too ' +
|
|
168
|
+
'(a code fix that needs a test, or a test that needs a non-test helper the test-writer may not write). ' +
|
|
169
|
+
'Fix your part first; the engine then sends an "other_role" finding to a fresh agent of the other role, before the gates. ' +
|
|
165
170
|
'A fresh reviewer rules on every "won\'t fix", so give a reason that stands on its own. ' +
|
|
166
171
|
'The engine runs the gates again and then a fresh reviewer checks your changes.'
|
|
167
172
|
|
|
173
|
+
/** How the other role answers the findings handed to it (#616). */
|
|
174
|
+
const HANDOFF_RULES =
|
|
175
|
+
'For EACH finding, answer in "finding_responses": "fixed", or "wont_fix" with your reason. ' +
|
|
176
|
+
'Your answer is final: another "other_role" counts as "wont_fix", and a fresh reviewer rules on it. ' +
|
|
177
|
+
'The engine runs the gates again and then a fresh reviewer checks the changes.'
|
|
178
|
+
|
|
179
|
+
const OTHER_WRITER: Record<WriterRole, WriterRole> = {
|
|
180
|
+
'test-writer': 'implementer',
|
|
181
|
+
implementer: 'test-writer',
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The findings the other writer role answered `other_role`, handed to
|
|
186
|
+
* `role` (#616), each with that fixer's reason, and how to answer them.
|
|
187
|
+
* `heading` names the fix round.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* const section = handoffSection({ fix, role: 'test-writer', heading: 'Review fix round 1' })
|
|
191
|
+
*/
|
|
192
|
+
export const handoffSection = ({
|
|
193
|
+
fix,
|
|
194
|
+
role,
|
|
195
|
+
heading,
|
|
196
|
+
}: {
|
|
197
|
+
fix: Pick<ReviewFix, 'findings' | 'responses' | 'handed_off'>
|
|
198
|
+
role: WriterRole
|
|
199
|
+
heading: string
|
|
200
|
+
}): string => {
|
|
201
|
+
const ids = fix.handed_off[role] ?? []
|
|
202
|
+
const other = OTHER_WRITER[role]
|
|
203
|
+
const findings = fix.findings
|
|
204
|
+
.filter(({ id }) => ids.includes(id))
|
|
205
|
+
.map((finding) => {
|
|
206
|
+
const answer = fix.responses.find(
|
|
207
|
+
({ finding_id, response }) =>
|
|
208
|
+
finding_id === finding.id && response === 'other_role'
|
|
209
|
+
)
|
|
210
|
+
return `${findingLine(finding)}\n The ${other} passed it on: ${answer?.reason || 'no reason given'}`
|
|
211
|
+
})
|
|
212
|
+
.join('\n')
|
|
213
|
+
return [
|
|
214
|
+
`## ${heading}: findings the ${other} passed to you`,
|
|
215
|
+
`The ${other} answered these findings "other_role": they need the ${role}'s work, instead of or as well as its own. ` +
|
|
216
|
+
`Its own fixes are in the worktree, uncommitted. Do your part for these findings only. Keep to your role's rules.`,
|
|
217
|
+
findings,
|
|
218
|
+
HANDOFF_RULES,
|
|
219
|
+
].join('\n\n')
|
|
220
|
+
}
|
|
221
|
+
|
|
168
222
|
/**
|
|
169
223
|
* The findings a review fixer gets: the test findings for a fresh
|
|
170
224
|
* test-writer, or the code findings for the implementer.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { JournalRecord } from '../journal/journal-record'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper shared by the crash-recovery end-to-end tests
|
|
5
|
+
* (`run-crash-recovery.e2e.test.ts` and
|
|
6
|
+
* `run-crash-recovery-git.e2e.test.ts`).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const ofKind = <Kind extends JournalRecord['kind']>(
|
|
10
|
+
records: JournalRecord[],
|
|
11
|
+
kind: Kind
|
|
12
|
+
): Extract<JournalRecord, { kind: Kind }>[] =>
|
|
13
|
+
records.filter(
|
|
14
|
+
(record): record is Extract<JournalRecord, { kind: Kind }> =>
|
|
15
|
+
record.kind === kind
|
|
16
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { expect } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createScriptedLauncher,
|
|
5
|
+
type ScriptedLauncher,
|
|
6
|
+
type ScriptedTurn,
|
|
7
|
+
} from '../agents/scripted-launcher'
|
|
8
|
+
import type { JournalRecord } from '../journal/journal-record'
|
|
9
|
+
import {
|
|
10
|
+
CLEAN_LENS_TURNS,
|
|
11
|
+
EMPTY_LEARNER_TURN,
|
|
12
|
+
IMPLEMENTER_RESULT,
|
|
13
|
+
SUM_TEST,
|
|
14
|
+
} from '../testing/practice-repo'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Helpers shared by the end-to-end tests for closing finished agents'
|
|
18
|
+
* sessions (`run-session-close.e2e.test.ts`,
|
|
19
|
+
* `run-session-close-ticket-ends.e2e.test.ts`, and
|
|
20
|
+
* `run-session-close-interruptions.e2e.test.ts`).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A scripted launcher with these turns, then a clean final review of spec
|
|
25
|
+
* #10 and an empty learner to fall back on.
|
|
26
|
+
*/
|
|
27
|
+
export const scriptedWith = (turns: ScriptedTurn[]): ScriptedLauncher =>
|
|
28
|
+
createScriptedLauncher({
|
|
29
|
+
turns: [...turns, ...CLEAN_LENS_TURNS(10), EMPTY_LEARNER_TURN(10)],
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
type SessionOf = { role: string | null; session_id: string }
|
|
33
|
+
|
|
34
|
+
export const byId = (a: SessionOf, b: SessionOf) =>
|
|
35
|
+
a.session_id.localeCompare(b.session_id)
|
|
36
|
+
|
|
37
|
+
/** Every session a scripted launcher started, with its agent's role. */
|
|
38
|
+
export const launchedBy = (launcher: ScriptedLauncher): SessionOf[] =>
|
|
39
|
+
launcher
|
|
40
|
+
.launches()
|
|
41
|
+
.filter(({ kind }) => kind === 'launch')
|
|
42
|
+
.map(({ role, session_id }) => ({ role, session_id }))
|
|
43
|
+
|
|
44
|
+
/** Every session close the journal records, with its agent's role. */
|
|
45
|
+
export const closedIn = (records: JournalRecord[]): SessionOf[] =>
|
|
46
|
+
records
|
|
47
|
+
.flatMap((record) =>
|
|
48
|
+
record.kind === 'agent_session_closed'
|
|
49
|
+
? [{ role: record.role, session_id: record.content.session_id }]
|
|
50
|
+
: []
|
|
51
|
+
)
|
|
52
|
+
.toSorted(byId)
|
|
53
|
+
|
|
54
|
+
/** The journal's close of `session_id`, if any. */
|
|
55
|
+
export const closeOf = (records: JournalRecord[], session_id: string) =>
|
|
56
|
+
records.find(
|
|
57
|
+
(record) =>
|
|
58
|
+
record.kind === 'agent_session_closed' &&
|
|
59
|
+
record.content.session_id === session_id
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* No session the launcher started is still open, and the journal records
|
|
64
|
+
* each one's close exactly once.
|
|
65
|
+
*/
|
|
66
|
+
export const expectEveryoneClosed = ({
|
|
67
|
+
launcher,
|
|
68
|
+
records,
|
|
69
|
+
}: {
|
|
70
|
+
launcher: ScriptedLauncher
|
|
71
|
+
records: JournalRecord[]
|
|
72
|
+
}) => {
|
|
73
|
+
expect(launcher.openSessions()).toEqual([])
|
|
74
|
+
expect(closedIn(records)).toEqual(launchedBy(launcher).toSorted(byId))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A ticket's launches and follow-ups, in order. */
|
|
78
|
+
export const callsOf = (launcher: ScriptedLauncher, ticket: number) =>
|
|
79
|
+
launcher.launches().filter((call) => call.ticket === ticket)
|
|
80
|
+
|
|
81
|
+
/** A sum test that defines sum itself, so it passes before any code exists. */
|
|
82
|
+
export const PASSING_SUM_TEST = SUM_TEST.replace(
|
|
83
|
+
"import { sum } from './sum'",
|
|
84
|
+
'const sum = ({ numbers }: { numbers: number[] }) =>\n numbers.reduce((a, b) => a + b, 0)'
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
/** An implementer of `ticket` whose code fails the tests. */
|
|
88
|
+
export const wrongImplementer = (ticket: number): ScriptedTurn => ({
|
|
89
|
+
role: 'implementer',
|
|
90
|
+
ticket,
|
|
91
|
+
files: {
|
|
92
|
+
'src/sum.ts': 'export const sum = (): number => 42\n',
|
|
93
|
+
'src/index.ts': "export { sum } from './sum'\n",
|
|
94
|
+
},
|
|
95
|
+
result: IMPLEMENTER_RESULT,
|
|
96
|
+
})
|