@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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a build step's executor works with (`BuildContext`, `buildContext`),
|
|
3
|
+
* and the small lookups every step shares: a journaled value, a ticket's
|
|
4
|
+
* worktree, a report file, and the test files in a worktree.
|
|
5
|
+
*/
|
|
6
|
+
import { mkdir } from 'node:fs/promises'
|
|
7
|
+
import { basename, dirname, join } from 'node:path'
|
|
8
|
+
|
|
9
|
+
import type { AgentLauncher } from '../agents/agent-launcher'
|
|
10
|
+
import type { EngineConfig } from '../config/engine-config'
|
|
11
|
+
import { testFilesAmong } from '../gates/test-runner'
|
|
12
|
+
import type { GitAdapter } from '../git/git-adapter'
|
|
13
|
+
import type { Journal } from '../journal/journal'
|
|
14
|
+
import {
|
|
15
|
+
replayRun,
|
|
16
|
+
type ReplayedWorktree,
|
|
17
|
+
type RunState,
|
|
18
|
+
} from '../journal/replay'
|
|
19
|
+
import type { CommentStep } from '../tracker/post-comment-once'
|
|
20
|
+
import type { Tracker } from '../tracker/tracker'
|
|
21
|
+
|
|
22
|
+
/** What the engine needs, beyond the journal and tracker, to build tickets. */
|
|
23
|
+
export type BuildDeps = {
|
|
24
|
+
git: GitAdapter
|
|
25
|
+
launcher: AgentLauncher
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** What a build step's executor works with. */
|
|
29
|
+
export type BuildContext = BuildDeps & {
|
|
30
|
+
journal: Journal
|
|
31
|
+
tracker: Tracker
|
|
32
|
+
state: RunState
|
|
33
|
+
config: EngineConfig
|
|
34
|
+
/** The run's folder, next to its journal and outside git. */
|
|
35
|
+
run_dir: string
|
|
36
|
+
/**
|
|
37
|
+
* Which try of its step this is: a redo after a crash adopts what its
|
|
38
|
+
* first try left behind.
|
|
39
|
+
*/
|
|
40
|
+
step: CommentStep
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A journaled value, or a clear error that the journal has none yet. */
|
|
44
|
+
export const need = <T>({
|
|
45
|
+
value,
|
|
46
|
+
what,
|
|
47
|
+
}: {
|
|
48
|
+
value: T | null | undefined
|
|
49
|
+
what: string
|
|
50
|
+
}): T => {
|
|
51
|
+
if (value === null || value === undefined) {
|
|
52
|
+
throw new Error(`The journal has no ${what} yet.`)
|
|
53
|
+
}
|
|
54
|
+
return value
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const ticketWorktree = ({
|
|
58
|
+
state,
|
|
59
|
+
ticket,
|
|
60
|
+
}: {
|
|
61
|
+
state: RunState
|
|
62
|
+
ticket: number
|
|
63
|
+
}): ReplayedWorktree =>
|
|
64
|
+
need({
|
|
65
|
+
value: state.tickets[ticket]?.worktree,
|
|
66
|
+
what: `worktree for #${ticket}`,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
/** A fresh report file outside the worktree, so it is never a leftover. */
|
|
70
|
+
export const reportFile = async ({
|
|
71
|
+
context,
|
|
72
|
+
ticket,
|
|
73
|
+
label,
|
|
74
|
+
}: {
|
|
75
|
+
context: BuildContext
|
|
76
|
+
/** `null` for the final review. */
|
|
77
|
+
ticket: number | null
|
|
78
|
+
label: string
|
|
79
|
+
}): Promise<string> => {
|
|
80
|
+
const folder = join(context.run_dir, 'reports')
|
|
81
|
+
await mkdir(folder, { recursive: true })
|
|
82
|
+
return join(
|
|
83
|
+
folder,
|
|
84
|
+
`${context.state.last_seq + 1}-${ticket ?? 'final'}-${label}.xml`
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const testFilesIn = async ({
|
|
89
|
+
context,
|
|
90
|
+
cwd,
|
|
91
|
+
}: {
|
|
92
|
+
context: BuildContext
|
|
93
|
+
cwd: string
|
|
94
|
+
}): Promise<string[]> =>
|
|
95
|
+
testFilesAmong({
|
|
96
|
+
files: await context.git.listFiles({ cwd }),
|
|
97
|
+
test_file_patterns: context.config.test_file_patterns,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* What a build step's executor works with, from the journal as it is now.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* const context = buildContext({ journal, tracker, git, launcher })
|
|
105
|
+
*/
|
|
106
|
+
export const buildContext = ({
|
|
107
|
+
journal,
|
|
108
|
+
tracker,
|
|
109
|
+
git,
|
|
110
|
+
launcher,
|
|
111
|
+
step,
|
|
112
|
+
}: BuildDeps & {
|
|
113
|
+
journal: Journal
|
|
114
|
+
tracker: Tracker
|
|
115
|
+
/** Left out, a first try, numbered after the journal's last record. */
|
|
116
|
+
step?: CommentStep
|
|
117
|
+
}): BuildContext => {
|
|
118
|
+
const records = journal.read()
|
|
119
|
+
const state = replayRun({ records })
|
|
120
|
+
const run_dir = dirname(journal.file)
|
|
121
|
+
return {
|
|
122
|
+
git,
|
|
123
|
+
launcher,
|
|
124
|
+
journal,
|
|
125
|
+
tracker,
|
|
126
|
+
state,
|
|
127
|
+
config: need({ value: state.config, what: 'engine config' }),
|
|
128
|
+
run_dir,
|
|
129
|
+
step: step ?? {
|
|
130
|
+
run_id: basename(run_dir),
|
|
131
|
+
first_seq: (records.at(-1)?.seq ?? 0) + 1,
|
|
132
|
+
redo: false,
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fresh agent's launch in the build: the run notes it gets, whether it may
|
|
3
|
+
* edit tests, and the sections its prompt adds.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
isRefactorTicket,
|
|
8
|
+
type BuildAction,
|
|
9
|
+
type StepArgs,
|
|
10
|
+
} from './build-action'
|
|
11
|
+
import {
|
|
12
|
+
CRASH_SECTION,
|
|
13
|
+
followUpSection,
|
|
14
|
+
testsUpdatedMessage,
|
|
15
|
+
} from './fix-loop-text'
|
|
16
|
+
import { handoffSection, reviewFixSection, reviewSections } from './review-text'
|
|
17
|
+
import { retrySection } from './stuck-text'
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
CHANGESETS_LENS_SECTION,
|
|
21
|
+
CHANGESETS_SECTION,
|
|
22
|
+
rejoinSection,
|
|
23
|
+
rolePrompt,
|
|
24
|
+
testUpdateSection,
|
|
25
|
+
} from '../agents/role-prompts'
|
|
26
|
+
import type { AgentRole } from '../agents/role-results'
|
|
27
|
+
import type { TicketSnapshot } from '../intake/intake-schemas'
|
|
28
|
+
import {
|
|
29
|
+
handoffRole,
|
|
30
|
+
type ReplayedRunNote,
|
|
31
|
+
type TicketProgress,
|
|
32
|
+
} from '../journal/replay'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* How many run notes a fresh agent is handed: the newest ones in the run,
|
|
36
|
+
* whatever ticket or role wrote them.
|
|
37
|
+
*/
|
|
38
|
+
export const MAX_RUN_NOTES = 10
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The run notes a fresh agent gets: the `MAX_RUN_NOTES` newest, oldest
|
|
42
|
+
* first. The same text written twice counts once, where it was last written.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* newestRunNotes({ run_notes: state.run_notes }) // at most 10, oldest first
|
|
46
|
+
*/
|
|
47
|
+
export const newestRunNotes = ({
|
|
48
|
+
run_notes,
|
|
49
|
+
}: {
|
|
50
|
+
run_notes: ReplayedRunNote[]
|
|
51
|
+
}): ReplayedRunNote[] => {
|
|
52
|
+
const seen = new Set<string>()
|
|
53
|
+
const newest: ReplayedRunNote[] = []
|
|
54
|
+
for (const entry of [...run_notes].reverse()) {
|
|
55
|
+
if (newest.length === MAX_RUN_NOTES) break
|
|
56
|
+
if (seen.has(entry.note)) continue
|
|
57
|
+
seen.add(entry.note)
|
|
58
|
+
newest.push(entry)
|
|
59
|
+
}
|
|
60
|
+
return newest.reverse()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether an agent of this role may edit test files on this ticket: only
|
|
65
|
+
* the test-writer, and a refactor ticket's implementer (who may follow
|
|
66
|
+
* renames into tests). The engine hands it to the launcher's guards and
|
|
67
|
+
* uses it for its own after-turn check, on launches and follow-ups alike.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* mayEditTests({ role: 'implementer', ticket }) // true only on a refactor ticket
|
|
71
|
+
*/
|
|
72
|
+
export const mayEditTests = ({
|
|
73
|
+
role,
|
|
74
|
+
ticket,
|
|
75
|
+
}: {
|
|
76
|
+
role: AgentRole
|
|
77
|
+
ticket: TicketSnapshot
|
|
78
|
+
}): boolean =>
|
|
79
|
+
role === 'test-writer' ||
|
|
80
|
+
(role === 'implementer' && isRefactorTicket({ ticket }))
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The sections a launch adds: the review's (see `reviewPromptSections`),
|
|
84
|
+
* and, for a test-writer or implementer fixing a ticket on top of the run
|
|
85
|
+
* branch, the files that clashed. The reviewer's re-review after a rebase
|
|
86
|
+
* is the review's own. The first agent after a `retry` resumed a stuck
|
|
87
|
+
* ticket is also told why it got stuck, and the agent taking a turn a
|
|
88
|
+
* crash cut off is told of the crash.
|
|
89
|
+
*/
|
|
90
|
+
const promptSections = ({
|
|
91
|
+
role,
|
|
92
|
+
progress,
|
|
93
|
+
}: {
|
|
94
|
+
role: AgentRole
|
|
95
|
+
progress: TicketProgress
|
|
96
|
+
}): string[] => {
|
|
97
|
+
const sections = buildPromptSections({ role, progress })
|
|
98
|
+
const { retried, crashed_turn } = progress
|
|
99
|
+
return [
|
|
100
|
+
...sections,
|
|
101
|
+
...(retried === null ? [] : [retrySection({ retried })]),
|
|
102
|
+
...(crashed_turn === role ? [CRASH_SECTION] : []),
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const buildPromptSections = ({
|
|
107
|
+
role,
|
|
108
|
+
progress,
|
|
109
|
+
}: {
|
|
110
|
+
role: AgentRole
|
|
111
|
+
progress: TicketProgress
|
|
112
|
+
}): string[] => {
|
|
113
|
+
const review = reviewPromptSections({ role, progress })
|
|
114
|
+
const { rejoin } = progress
|
|
115
|
+
if (rejoin === null || role === 'ticket-reviewer') return review
|
|
116
|
+
// A review fix round after the rejoin has its own findings.
|
|
117
|
+
if (progress.commits.green !== null) return review
|
|
118
|
+
const section = rejoinSection({ role, rejoin })
|
|
119
|
+
return [
|
|
120
|
+
...review,
|
|
121
|
+
...(section === null ? [] : [section]),
|
|
122
|
+
...testUpdateSections({ role, progress }),
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The sections a launch adds while a test update is open (#489): the
|
|
128
|
+
* test-writer's task, then, once it answered, the implementer's news.
|
|
129
|
+
*/
|
|
130
|
+
const testUpdateSections = ({
|
|
131
|
+
role,
|
|
132
|
+
progress,
|
|
133
|
+
}: {
|
|
134
|
+
role: AgentRole
|
|
135
|
+
progress: TicketProgress
|
|
136
|
+
}): string[] => {
|
|
137
|
+
const update = progress.test_update
|
|
138
|
+
if (update === null) return []
|
|
139
|
+
if (role === 'test-writer' && update.result === null) {
|
|
140
|
+
return [testUpdateSection({ since: update, bad_test: update.bad_test })]
|
|
141
|
+
}
|
|
142
|
+
if (role === 'implementer' && update.result !== null) {
|
|
143
|
+
return [
|
|
144
|
+
testsUpdatedMessage({
|
|
145
|
+
bad_test: update.bad_test,
|
|
146
|
+
summary: update.result.summary,
|
|
147
|
+
}),
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
return []
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The sections a launch adds for the ticket review: the reviewer's diff and
|
|
155
|
+
* gate results (a re-review's new changes and earlier findings), or a
|
|
156
|
+
* review fixer's findings while a review fix round is open.
|
|
157
|
+
*/
|
|
158
|
+
const reviewPromptSections = ({
|
|
159
|
+
role,
|
|
160
|
+
progress,
|
|
161
|
+
}: {
|
|
162
|
+
role: AgentRole
|
|
163
|
+
progress: TicketProgress
|
|
164
|
+
}): string[] => {
|
|
165
|
+
const fix = progress.review_fix
|
|
166
|
+
if (role === 'ticket-reviewer') {
|
|
167
|
+
return reviewSections({
|
|
168
|
+
progress,
|
|
169
|
+
base_sha: progress.worktree?.base_sha ?? 'HEAD',
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
if (fix === null || progress.commits.green === null) return []
|
|
173
|
+
if (role === 'test-writer' && !fix.tests_answered) {
|
|
174
|
+
return [reviewFixSection({ fix, kind: 'test' })]
|
|
175
|
+
}
|
|
176
|
+
if (role === 'implementer' && !fix.code_answered) {
|
|
177
|
+
return [reviewFixSection({ fix, kind: 'code' })]
|
|
178
|
+
}
|
|
179
|
+
if (role === handoffRole({ fix })) {
|
|
180
|
+
return [
|
|
181
|
+
handoffSection({
|
|
182
|
+
fix,
|
|
183
|
+
role,
|
|
184
|
+
heading: `Review fix round ${fix.round}`,
|
|
185
|
+
}),
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
return []
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A fresh agent session, handed the run's newest notes. With `follow_up`,
|
|
193
|
+
* it stands in for a follow-up whose session is gone (a crash cut it off),
|
|
194
|
+
* and its prompt carries the follow-up message.
|
|
195
|
+
*/
|
|
196
|
+
export const launch = ({
|
|
197
|
+
role,
|
|
198
|
+
snapshot,
|
|
199
|
+
ticket,
|
|
200
|
+
progress,
|
|
201
|
+
run_notes,
|
|
202
|
+
follow_up,
|
|
203
|
+
}: StepArgs & { role: AgentRole; follow_up?: string }): BuildAction => ({
|
|
204
|
+
type: 'launch_agent',
|
|
205
|
+
ticket: ticket.number,
|
|
206
|
+
role,
|
|
207
|
+
prompt: rolePrompt({
|
|
208
|
+
role,
|
|
209
|
+
spec: snapshot.spec,
|
|
210
|
+
ticket,
|
|
211
|
+
refactor: isRefactorTicket({ ticket }),
|
|
212
|
+
// A test update names the bad test in its own section.
|
|
213
|
+
bad_test: progress.test_update === null ? progress.bad_test : null,
|
|
214
|
+
bad_test_by: progress.bad_test_source ?? undefined,
|
|
215
|
+
sections: [
|
|
216
|
+
...promptSections({ role, progress }),
|
|
217
|
+
...(follow_up === undefined
|
|
218
|
+
? []
|
|
219
|
+
: [followUpSection({ message: follow_up })]),
|
|
220
|
+
],
|
|
221
|
+
run_notes,
|
|
222
|
+
}),
|
|
223
|
+
may_edit_tests: mayEditTests({ role, ticket }),
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* A fresh test-writer's or implementer's launch (a ticket's or the final
|
|
228
|
+
* review's fixers), with `section` added to its prompt, such as the repo's
|
|
229
|
+
* prepare command. Any other step as it is. With `only`, just that role's
|
|
230
|
+
* launches get it (the feature map is the test-writer's, #583).
|
|
231
|
+
*/
|
|
232
|
+
export const withWriterSection = ({
|
|
233
|
+
step,
|
|
234
|
+
section,
|
|
235
|
+
only,
|
|
236
|
+
}: {
|
|
237
|
+
step: BuildAction
|
|
238
|
+
section: string
|
|
239
|
+
only?: 'test-writer' | 'implementer'
|
|
240
|
+
}): BuildAction => {
|
|
241
|
+
const writesCode =
|
|
242
|
+
(step.type === 'launch_agent' || step.type === 'launch_final_fixer') &&
|
|
243
|
+
(step.role === 'test-writer' || step.role === 'implementer') &&
|
|
244
|
+
(only === undefined || step.role === only)
|
|
245
|
+
return writesCode
|
|
246
|
+
? { ...step, prompt: `${step.prompt}\n\n${section}` }
|
|
247
|
+
: step
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* In a repo with changesets, the writers are told not to write one (#507),
|
|
252
|
+
* and every reviewer (the ticket-reviewer and each lens) that the engine adds
|
|
253
|
+
* the run's one at PR time (#507, #545).
|
|
254
|
+
*/
|
|
255
|
+
export const withChangesetsSections = (step: BuildAction): BuildAction =>
|
|
256
|
+
step.type === 'launch_lens' ||
|
|
257
|
+
(step.type === 'launch_agent' && step.role === 'ticket-reviewer')
|
|
258
|
+
? { ...step, prompt: `${step.prompt}\n\n${CHANGESETS_LENS_SECTION}` }
|
|
259
|
+
: withWriterSection({ step, section: CHANGESETS_SECTION })
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine's commits: the leftover scan, then a commit of a worktree's
|
|
3
|
+
* changes (`commitIn`), with agents' changesets dropped first and a crashed
|
|
4
|
+
* try's commit adopted. Serves the `commit_ticket` step and the final review.
|
|
5
|
+
*/
|
|
6
|
+
import { rm } from 'node:fs/promises'
|
|
7
|
+
import { basename, join } from 'node:path'
|
|
8
|
+
|
|
9
|
+
import { need, ticketWorktree, type BuildContext } from './build-context'
|
|
10
|
+
import type { BuildAction } from './decide-build'
|
|
11
|
+
import { leaveOutIn, ticketChanges } from './worktree-changes'
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
importStem,
|
|
15
|
+
isAgentChangeset,
|
|
16
|
+
leftoverPasses,
|
|
17
|
+
nameCheckedFiles,
|
|
18
|
+
newCodeFiles,
|
|
19
|
+
scanLeftovers,
|
|
20
|
+
} from '../gates/leftover-scan'
|
|
21
|
+
import { testFilesAmong } from '../gates/test-runner'
|
|
22
|
+
import type { FileChange } from '../git/git-adapter'
|
|
23
|
+
import { pathText } from '../git/path-text'
|
|
24
|
+
import { isTestHelper } from '../guards/role-rules'
|
|
25
|
+
import type { CommitStage } from '../journal/journal-record'
|
|
26
|
+
|
|
27
|
+
/** Whether anything besides the file itself mentions a new code file. */
|
|
28
|
+
const isUsed = async ({
|
|
29
|
+
context,
|
|
30
|
+
cwd,
|
|
31
|
+
path,
|
|
32
|
+
added_texts,
|
|
33
|
+
}: {
|
|
34
|
+
context: BuildContext
|
|
35
|
+
cwd: string
|
|
36
|
+
path: string
|
|
37
|
+
added_texts: Record<string, string>
|
|
38
|
+
}): Promise<boolean> => {
|
|
39
|
+
const stem = importStem(path)
|
|
40
|
+
const tracked = await context.git.filesMentioning({ cwd, text: stem })
|
|
41
|
+
if (tracked.some((file) => file !== path)) return true
|
|
42
|
+
return Object.entries(added_texts).some(
|
|
43
|
+
([file, text]) => file !== path && text.includes(stem)
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether a file other than itself and the tests names this scratch-named
|
|
49
|
+
* file or new markdown (#520) by its basename, as a TOC, `package.json`, a
|
|
50
|
+
* doc index, or a config does: a
|
|
51
|
+
* tracked file, or one this change adds (a new addon's TOC names its new
|
|
52
|
+
* modules). A mention in a test doesn't make a file the repo's.
|
|
53
|
+
*/
|
|
54
|
+
const isNamed = async ({
|
|
55
|
+
context,
|
|
56
|
+
cwd,
|
|
57
|
+
path,
|
|
58
|
+
added_texts,
|
|
59
|
+
}: {
|
|
60
|
+
context: BuildContext
|
|
61
|
+
cwd: string
|
|
62
|
+
path: string
|
|
63
|
+
added_texts: Record<string, string>
|
|
64
|
+
}): Promise<boolean> => {
|
|
65
|
+
const name = basename(path)
|
|
66
|
+
const tracked = await context.git.filesMentioning({ cwd, text: name })
|
|
67
|
+
const added = Object.entries(added_texts)
|
|
68
|
+
.filter(([, text]) => text.includes(name))
|
|
69
|
+
.map(([file]) => file)
|
|
70
|
+
const files = [...tracked, ...added].filter((file) => file !== path)
|
|
71
|
+
const tests = testFilesAmong({
|
|
72
|
+
files,
|
|
73
|
+
test_file_patterns: context.config.test_file_patterns,
|
|
74
|
+
})
|
|
75
|
+
return files.some((file) => !tests.includes(file))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* In a repo with changesets, removes the changesets an agent wrote (see
|
|
80
|
+
* `isAgentChangeset`) from the worktree and journals `changeset_dropped`:
|
|
81
|
+
* the engine writes the run's one itself. Returns the other changes.
|
|
82
|
+
*/
|
|
83
|
+
export const dropAgentChangesets = async ({
|
|
84
|
+
context,
|
|
85
|
+
cwd,
|
|
86
|
+
ticket,
|
|
87
|
+
changes,
|
|
88
|
+
}: {
|
|
89
|
+
context: BuildContext
|
|
90
|
+
cwd: string
|
|
91
|
+
ticket: number | null
|
|
92
|
+
changes: FileChange[]
|
|
93
|
+
}): Promise<FileChange[]> => {
|
|
94
|
+
if (!context.state.changesets) return changes
|
|
95
|
+
const paths = changes.filter(isAgentChangeset).map(({ path }) => path)
|
|
96
|
+
if (paths.length === 0) return changes
|
|
97
|
+
for (const path of paths) await rm(join(cwd, path), { force: true })
|
|
98
|
+
context.journal.append({
|
|
99
|
+
kind: 'changeset_dropped',
|
|
100
|
+
ticket,
|
|
101
|
+
role: null,
|
|
102
|
+
content: { paths },
|
|
103
|
+
})
|
|
104
|
+
return changes.filter(({ path }) => !paths.includes(path))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The commit a crash left behind at `cwd`: HEAD carries `message` and no
|
|
109
|
+
* record names it yet, so the step's first try made it and died before
|
|
110
|
+
* journaling it. `null` when HEAD is no such commit.
|
|
111
|
+
*/
|
|
112
|
+
const leftCommit = async ({
|
|
113
|
+
context,
|
|
114
|
+
cwd,
|
|
115
|
+
message,
|
|
116
|
+
}: {
|
|
117
|
+
context: BuildContext
|
|
118
|
+
cwd: string
|
|
119
|
+
message: string
|
|
120
|
+
}): Promise<{ sha: string; message: string; files: string[] } | null> => {
|
|
121
|
+
const last = await context.git.lastCommit({ cwd })
|
|
122
|
+
if (last.message !== message.trim()) return null
|
|
123
|
+
const journaled = context.journal
|
|
124
|
+
.read()
|
|
125
|
+
.some(
|
|
126
|
+
(record) =>
|
|
127
|
+
(record.kind === 'commit_made' &&
|
|
128
|
+
record.content.sha === last.sha) ||
|
|
129
|
+
(record.kind === 'ticket_joined' &&
|
|
130
|
+
record.content.ok &&
|
|
131
|
+
record.content.shas.includes(last.sha))
|
|
132
|
+
)
|
|
133
|
+
return journaled ? null : { sha: last.sha, message, files: last.files }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The leftover scan, then an engine commit of everything in `cwd`, both
|
|
138
|
+
* journaled. What the prepare command made (`prepare_made`) and untracked
|
|
139
|
+
* links pointing outside the checkout (see `leaveOutIn`) are neither
|
|
140
|
+
* scanned nor committed. An agent's changeset is dropped before the scan
|
|
141
|
+
* (see `dropAgentChangesets`). A hit blocks the commit. A redo after a crash that
|
|
142
|
+
* already committed (nothing left to commit, and HEAD is an unjournaled
|
|
143
|
+
* commit with this message) journals that commit instead of making
|
|
144
|
+
* another; its scan, of a clean worktree, finds nothing, as the first try's
|
|
145
|
+
* did before it committed.
|
|
146
|
+
*
|
|
147
|
+
* Nothing to commit never runs `git commit`, which would fail (#494): the
|
|
148
|
+
* current commit is journaled with no files instead. That is a `fix` round
|
|
149
|
+
* whose every finding was a "won't fix", a green stage whose change a
|
|
150
|
+
* rebase found already on the run branch, or one whose every change came
|
|
151
|
+
* from prepare. (A red stage with nothing to commit fails its red check
|
|
152
|
+
* first, so it gets here only if the worktree changed after that check.)
|
|
153
|
+
*/
|
|
154
|
+
export const commitIn = async ({
|
|
155
|
+
context,
|
|
156
|
+
cwd,
|
|
157
|
+
ticket,
|
|
158
|
+
stage,
|
|
159
|
+
message,
|
|
160
|
+
mention_text,
|
|
161
|
+
}: {
|
|
162
|
+
context: BuildContext
|
|
163
|
+
cwd: string
|
|
164
|
+
/** `null` for the final review. */
|
|
165
|
+
ticket: number | null
|
|
166
|
+
stage: CommitStage
|
|
167
|
+
message: string
|
|
168
|
+
/** The spec and ticket text a new markdown file may be named in. */
|
|
169
|
+
mention_text: string
|
|
170
|
+
}) => {
|
|
171
|
+
const leave_out = await leaveOutIn({ context, cwd, ticket })
|
|
172
|
+
const changes = await dropAgentChangesets({
|
|
173
|
+
context,
|
|
174
|
+
cwd,
|
|
175
|
+
ticket,
|
|
176
|
+
changes: await ticketChanges({ context, cwd, leave_out }),
|
|
177
|
+
})
|
|
178
|
+
const test_files = testFilesAmong({
|
|
179
|
+
files: changes.map(({ path }) => path),
|
|
180
|
+
test_file_patterns: context.config.test_file_patterns,
|
|
181
|
+
})
|
|
182
|
+
const added = changes.filter(({ change }) => change === 'added')
|
|
183
|
+
const added_texts: Record<string, string> = {}
|
|
184
|
+
for (const { path } of added) {
|
|
185
|
+
added_texts[path] = (await pathText({ path: join(cwd, path) })) ?? ''
|
|
186
|
+
}
|
|
187
|
+
const used_code: Record<string, boolean> = {}
|
|
188
|
+
for (const path of newCodeFiles({ changes, test_files })) {
|
|
189
|
+
used_code[path] = await isUsed({ context, cwd, path, added_texts })
|
|
190
|
+
}
|
|
191
|
+
const named: string[] = []
|
|
192
|
+
for (const path of nameCheckedFiles({ changes })) {
|
|
193
|
+
// A test helper (#619) is the tests', so a test that uses it names it.
|
|
194
|
+
const helper = isTestHelper({ path, config: context.config })
|
|
195
|
+
if (
|
|
196
|
+
helper
|
|
197
|
+
? await isUsed({ context, cwd, path, added_texts })
|
|
198
|
+
: await isNamed({ context, cwd, path, added_texts })
|
|
199
|
+
) {
|
|
200
|
+
named.push(path)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const inputs = {
|
|
204
|
+
changes,
|
|
205
|
+
test_files,
|
|
206
|
+
mention_text,
|
|
207
|
+
used_code,
|
|
208
|
+
named,
|
|
209
|
+
leftover_allow: context.config.leftover_allow ?? [],
|
|
210
|
+
}
|
|
211
|
+
const hits = scanLeftovers(inputs)
|
|
212
|
+
const passed = leftoverPasses(inputs)
|
|
213
|
+
context.journal.append({
|
|
214
|
+
kind: 'leftover_scan',
|
|
215
|
+
ticket,
|
|
216
|
+
role: null,
|
|
217
|
+
content: { stage, hits, ...(passed.length > 0 ? { passed } : {}) },
|
|
218
|
+
})
|
|
219
|
+
if (hits.length > 0) return
|
|
220
|
+
const adopted =
|
|
221
|
+
changes.length === 0 && context.step.redo
|
|
222
|
+
? await leftCommit({ context, cwd, message })
|
|
223
|
+
: null
|
|
224
|
+
if (adopted !== null) {
|
|
225
|
+
context.journal.append({
|
|
226
|
+
kind: 'commit_made',
|
|
227
|
+
ticket,
|
|
228
|
+
role: null,
|
|
229
|
+
content: { stage, ...adopted },
|
|
230
|
+
})
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
if (changes.length === 0) {
|
|
234
|
+
// Nothing to commit: `git commit` would fail. For a fix round, the
|
|
235
|
+
// re-review's new changes are then empty.
|
|
236
|
+
context.journal.append({
|
|
237
|
+
kind: 'commit_made',
|
|
238
|
+
ticket,
|
|
239
|
+
role: null,
|
|
240
|
+
content: {
|
|
241
|
+
stage,
|
|
242
|
+
sha: await context.git.head({ cwd }),
|
|
243
|
+
message,
|
|
244
|
+
files: [],
|
|
245
|
+
},
|
|
246
|
+
})
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
const commit = await context.git.commitAll({ cwd, message, leave_out })
|
|
250
|
+
context.journal.append({
|
|
251
|
+
kind: 'commit_made',
|
|
252
|
+
ticket,
|
|
253
|
+
role: null,
|
|
254
|
+
content: { stage, sha: commit.sha, message, files: commit.files },
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export const commitTicket = async ({
|
|
259
|
+
context,
|
|
260
|
+
action,
|
|
261
|
+
}: {
|
|
262
|
+
context: BuildContext
|
|
263
|
+
action: Extract<BuildAction, { type: 'commit_ticket' }>
|
|
264
|
+
}) => {
|
|
265
|
+
const { path: cwd } = ticketWorktree({
|
|
266
|
+
state: context.state,
|
|
267
|
+
ticket: action.ticket,
|
|
268
|
+
})
|
|
269
|
+
const snapshot = need({
|
|
270
|
+
value: context.state.snapshot,
|
|
271
|
+
what: 'spec snapshot',
|
|
272
|
+
})
|
|
273
|
+
const ticket = snapshot.tickets[action.ticket]
|
|
274
|
+
await commitIn({
|
|
275
|
+
context,
|
|
276
|
+
cwd,
|
|
277
|
+
ticket: action.ticket,
|
|
278
|
+
stage: action.stage,
|
|
279
|
+
message: action.message,
|
|
280
|
+
mention_text: [snapshot.spec.body, ticket?.body ?? ''].join('\n'),
|
|
281
|
+
})
|
|
282
|
+
}
|