@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,207 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The reply buttons on stuck work (#503): which one-word replies the engine
|
|
5
|
+
* takes for each kind of stuck item, the words of the confirm step, the
|
|
6
|
+
* `/luca-unstick` command for "Help me", and what each button shows. Plain
|
|
7
|
+
* values only, so the panel, the chat rows, and the server share them.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** The key of a ticket's "Needs you" item. */
|
|
11
|
+
export const ticketKey = ({ ticket }: { ticket: number }): string =>
|
|
12
|
+
`ticket-${ticket}`
|
|
13
|
+
|
|
14
|
+
/** The key of the final review's "Needs you" item. */
|
|
15
|
+
export const FINAL_KEY = 'final'
|
|
16
|
+
|
|
17
|
+
/** The key of the "Needs you" item for the whole run, stuck on its budget. */
|
|
18
|
+
export const RUN_KEY = 'run'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The replies the engine takes for one stuck item, as the owner would type
|
|
22
|
+
* them. A stuck ticket always gets its number (`retry #12`), since a bare
|
|
23
|
+
* word is only taken while one ticket is stuck. The final review takes a
|
|
24
|
+
* bare `retry`, `stop`, or `ship`; the run stuck on its budget a bare
|
|
25
|
+
* `retry` or `stop` (see the engine's README, "Replies"). An item this
|
|
26
|
+
* doesn't know gets none, so no button can post a word the engine refuses.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* allowedReplies({ key: 'ticket-12', ticket: 12 }) // ['retry #12', 'skip #12', 'stop']
|
|
30
|
+
* allowedReplies({ key: 'final', ticket: null }) // ['retry', 'stop', 'ship']
|
|
31
|
+
*/
|
|
32
|
+
export const allowedReplies = ({
|
|
33
|
+
key,
|
|
34
|
+
ticket,
|
|
35
|
+
}: {
|
|
36
|
+
key: string
|
|
37
|
+
ticket: number | null
|
|
38
|
+
}): string[] => {
|
|
39
|
+
if (ticket !== null) {
|
|
40
|
+
return key === ticketKey({ ticket })
|
|
41
|
+
? [`retry #${ticket}`, `skip #${ticket}`, 'stop']
|
|
42
|
+
: []
|
|
43
|
+
}
|
|
44
|
+
if (key === FINAL_KEY) return ['retry', 'stop', 'ship']
|
|
45
|
+
if (key === RUN_KEY) return ['retry', 'stop']
|
|
46
|
+
return []
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A reply word and the ticket it names, as the engine journals it. */
|
|
50
|
+
export type ReplyParts = {
|
|
51
|
+
word: 'retry' | 'skip' | 'stop' | 'ship'
|
|
52
|
+
ticket: number | null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const REPLY_PATTERN = /^(retry|skip|stop|ship)(?: #(\d+))?$/
|
|
56
|
+
|
|
57
|
+
/** `retry` with an amount for the run stuck on its budget (#543). */
|
|
58
|
+
const AMOUNT_REPLY_PATTERN = /^retry \+\S+$/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Splits one of `allowedReplies`, or a `retry +<amount>` the owner typed,
|
|
62
|
+
* into its word and ticket, to match it with the engine's `reply_received`.
|
|
63
|
+
* `null` for anything else.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* replyParts({ reply: 'retry #12' }) // { word: 'retry', ticket: 12 }
|
|
67
|
+
* replyParts({ reply: 'retry +3M' }) // { word: 'retry', ticket: null }
|
|
68
|
+
*/
|
|
69
|
+
export const replyParts = ({ reply }: { reply: string }): ReplyParts | null => {
|
|
70
|
+
if (AMOUNT_REPLY_PATTERN.test(reply)) return { word: 'retry', ticket: null }
|
|
71
|
+
const match = REPLY_PATTERN.exec(reply)
|
|
72
|
+
const word = z.enum(['retry', 'skip', 'stop', 'ship']).safeParse(match?.[1])
|
|
73
|
+
if (!match || !word.success) return null
|
|
74
|
+
return {
|
|
75
|
+
word: word.data,
|
|
76
|
+
ticket: match[2] === undefined ? null : Number(match[2]),
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const ISSUE_URL_PATTERN =
|
|
81
|
+
/^https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/issues\/\d+/
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The repo of a GitHub issue URL, as `owner/repo`; `null` for any other URL.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* repoOfIssueUrl({ url: 'https://github.com/o/r/issues/10' }) // 'o/r'
|
|
88
|
+
*/
|
|
89
|
+
export const repoOfIssueUrl = ({ url }: { url: string }): string | null => {
|
|
90
|
+
const match = ISSUE_URL_PATTERN.exec(url)
|
|
91
|
+
return match ? `${match[1]}/${match[2]}` : null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The question before a reply is posted.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* confirmReplyText({ reply: 'retry #134', spec_number: 133 })
|
|
99
|
+
* // 'Post `retry #134` on spec #133?'
|
|
100
|
+
*/
|
|
101
|
+
export const confirmReplyText = ({
|
|
102
|
+
reply,
|
|
103
|
+
spec_number,
|
|
104
|
+
}: {
|
|
105
|
+
reply: string
|
|
106
|
+
spec_number: number | null
|
|
107
|
+
}): string =>
|
|
108
|
+
`Post \`${reply}\` on ${spec_number === null ? 'the spec issue' : `spec #${spec_number}`}?`
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The `/luca-unstick` command "Help me" puts in the chat: the run id, and
|
|
112
|
+
* the ticket for a stuck ticket (none for the final review or the budget).
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* unstickCommand({ run_id: 'luca-20260928-101500-abcd', ticket: 134 })
|
|
116
|
+
* // '/luca-unstick luca-20260928-101500-abcd #134'
|
|
117
|
+
*/
|
|
118
|
+
export const unstickCommand = ({
|
|
119
|
+
run_id,
|
|
120
|
+
ticket,
|
|
121
|
+
}: {
|
|
122
|
+
run_id: string
|
|
123
|
+
ticket: number | null
|
|
124
|
+
}): string =>
|
|
125
|
+
ticket === null
|
|
126
|
+
? `/luca-unstick ${run_id}`
|
|
127
|
+
: `/luca-unstick ${run_id} #${ticket}`
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* API Response part: a reply the board posted on the spec issue, for one
|
|
131
|
+
* stuck item (its key and `since`), and whether the engine took it
|
|
132
|
+
* (`reply_received`). Kept in the plugin's memory only.
|
|
133
|
+
*/
|
|
134
|
+
export const PostedReplySchema = z.object({
|
|
135
|
+
key: z.string(),
|
|
136
|
+
/** The stuck item's `since`: the same ticket stuck again is a new item. */
|
|
137
|
+
since: z.string(),
|
|
138
|
+
reply: z.string(),
|
|
139
|
+
posted_at: z.string(),
|
|
140
|
+
/** The comment's URL, as `gh issue comment` printed it. */
|
|
141
|
+
comment_url: z.string().nullable(),
|
|
142
|
+
/** Set once the engine journaled `reply_received` for it. */
|
|
143
|
+
taken_at: z.string().nullable(),
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
export type PostedReply = z.infer<typeof PostedReplySchema>
|
|
147
|
+
|
|
148
|
+
/** One reply button: its reply, and whether and why it can't be tapped. */
|
|
149
|
+
export type ReplyButton = {
|
|
150
|
+
reply: string
|
|
151
|
+
/** `ready`, `sending` (this one is in flight), `posted`, or `locked`. */
|
|
152
|
+
state: 'ready' | 'sending' | 'posted' | 'locked'
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The buttons of one stuck item. Once a reply is posted (or while one is in
|
|
157
|
+
* flight) for this item, that button shows it and the others lock, so a
|
|
158
|
+
* second tap can't post again or post a second word the engine would
|
|
159
|
+
* refuse.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* replyButtons({ item, posted: [], sending: null })
|
|
163
|
+
* // [{ reply: 'retry #12', state: 'ready' }, ...]
|
|
164
|
+
*/
|
|
165
|
+
export const replyButtons = ({
|
|
166
|
+
item,
|
|
167
|
+
posted,
|
|
168
|
+
sending,
|
|
169
|
+
}: {
|
|
170
|
+
item: { key: string; ticket: number | null; since: string }
|
|
171
|
+
posted: PostedReply[]
|
|
172
|
+
/** The reply being posted for this item now, if any. */
|
|
173
|
+
sending: string | null
|
|
174
|
+
}): ReplyButton[] => {
|
|
175
|
+
const done = postedFor({ item, posted })
|
|
176
|
+
return allowedReplies(item).map((reply) => {
|
|
177
|
+
if (done)
|
|
178
|
+
return { reply, state: done.reply === reply ? 'posted' : 'locked' }
|
|
179
|
+
if (sending !== null) {
|
|
180
|
+
return { reply, state: sending === reply ? 'sending' : 'locked' }
|
|
181
|
+
}
|
|
182
|
+
return { reply, state: 'ready' }
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The reply posted for this stuck item, if any. */
|
|
187
|
+
export const postedFor = ({
|
|
188
|
+
item,
|
|
189
|
+
posted,
|
|
190
|
+
}: {
|
|
191
|
+
item: { key: string; since: string }
|
|
192
|
+
posted: PostedReply[]
|
|
193
|
+
}): PostedReply | null =>
|
|
194
|
+
posted.find(
|
|
195
|
+
(entry) => entry.key === item.key && entry.since === item.since
|
|
196
|
+
) ?? null
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* What a posted reply says under the buttons.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* postedText({ posted }) // 'Posted `retry #12`. Waiting for the engine to read it.'
|
|
203
|
+
*/
|
|
204
|
+
export const postedText = ({ posted }: { posted: PostedReply }): string =>
|
|
205
|
+
posted.taken_at === null
|
|
206
|
+
? `Posted \`${posted.reply}\`. Waiting for the engine to read it (it checks about once a minute).`
|
|
207
|
+
: `Posted \`${posted.reply}\`. The engine took it.`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
3
|
import type { AgentRole } from './role-results'
|
|
4
|
+
import type { ProgressListener } from './turn-progress'
|
|
4
5
|
|
|
5
6
|
import type { EngineConfig } from '../config/engine-config'
|
|
6
7
|
|
|
@@ -66,6 +67,11 @@ export const AgentSessionSchema = z.object({
|
|
|
66
67
|
rate_limit_events: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
67
68
|
/** An assistant message came back with `error: "billing_error"`. */
|
|
68
69
|
billing_error: z.boolean().default(false),
|
|
70
|
+
/**
|
|
71
|
+
* How many helpers (#627) the turn started. Their tokens are in
|
|
72
|
+
* `model_usage`. Older journals have none.
|
|
73
|
+
*/
|
|
74
|
+
helpers_started: z.number().int().min(0).default(0),
|
|
69
75
|
})
|
|
70
76
|
|
|
71
77
|
export type AgentSession = z.infer<typeof AgentSessionSchema>
|
|
@@ -94,7 +100,10 @@ export type LauncherFailure = z.infer<typeof LauncherFailureSchema>
|
|
|
94
100
|
* the same way; a success with no structured output is a failed try.
|
|
95
101
|
* `session_id` names the session the turn ran in, so the engine can send that
|
|
96
102
|
* same agent a follow-up in a fix loop. `session` is the launcher's summary
|
|
97
|
-
* of the model session, for the journal.
|
|
103
|
+
* of the model session, for the journal. `session_gone` marks a follow-up
|
|
104
|
+
* that found its session closed or unknown (#521): no agent ran, and the
|
|
105
|
+
* engine hands the follow-up's message to a fresh agent. `out_of_turns`
|
|
106
|
+
* marks a turn that reached its role's turn limit (#543).
|
|
98
107
|
*/
|
|
99
108
|
export type AgentTurn =
|
|
100
109
|
| {
|
|
@@ -109,6 +118,8 @@ export type AgentTurn =
|
|
|
109
118
|
error: string
|
|
110
119
|
session_id?: string
|
|
111
120
|
session?: AgentSession
|
|
121
|
+
session_gone?: boolean
|
|
122
|
+
out_of_turns?: boolean
|
|
112
123
|
}
|
|
113
124
|
|
|
114
125
|
/**
|
|
@@ -157,13 +168,23 @@ export type AgentLauncher = {
|
|
|
157
168
|
* for its follow-ups.
|
|
158
169
|
*/
|
|
159
170
|
messaging: AgentMessaging | null
|
|
171
|
+
/**
|
|
172
|
+
* The model's short name: `opus`, `sonnet` or `haiku`. The session's
|
|
173
|
+
* follow-ups run on it too.
|
|
174
|
+
*/
|
|
175
|
+
model: string
|
|
176
|
+
/**
|
|
177
|
+
* Hears the turn's live progress (#628) each time it changes, before
|
|
178
|
+
* the turn ends, for the engine's live file. Left out, nothing hears it.
|
|
179
|
+
*/
|
|
180
|
+
on_progress?: ProgressListener
|
|
160
181
|
}) => Promise<AgentTurn>
|
|
161
182
|
/**
|
|
162
183
|
* Sends a follow-up message to an agent session that is still open, such
|
|
163
184
|
* as a failed gate's output in a fix loop, and returns the session's new
|
|
164
|
-
* result. The agent keeps everything it knew from its earlier turns,
|
|
165
|
-
*
|
|
166
|
-
* or one that has closed, is an engine failure
|
|
185
|
+
* result. The agent keeps everything it knew from its earlier turns, the
|
|
186
|
+
* guards it was launched with, and its model. A session the launcher doesn't know,
|
|
187
|
+
* or one that has closed, is an engine failure with `session_gone`.
|
|
167
188
|
*
|
|
168
189
|
* @example
|
|
169
190
|
* const turn = await launcher.followUp({ session_id, role: 'implementer', ticket: 11, message: 'lint failed: ...', cwd, config })
|
|
@@ -175,6 +196,8 @@ export type AgentLauncher = {
|
|
|
175
196
|
message: string
|
|
176
197
|
cwd: string
|
|
177
198
|
config: EngineConfig
|
|
199
|
+
/** Hears the follow-up turn's live progress, as for `launch`. */
|
|
200
|
+
on_progress?: ProgressListener
|
|
178
201
|
}) => Promise<AgentTurn>
|
|
179
202
|
/**
|
|
180
203
|
* Closes one agent session once nothing can send it a follow-up anymore,
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { $ } from 'bun'
|
|
6
|
+
|
|
7
|
+
import type { Options, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk'
|
|
8
|
+
import { afterEach, beforeEach } from 'bun:test'
|
|
9
|
+
|
|
10
|
+
import type { AgentMessaging, AgentTurn } from './agent-launcher'
|
|
11
|
+
import { createClaudeLauncher, type AgentQuery } from './claude-launcher'
|
|
12
|
+
import { CLAUDE_MODEL } from './claude-options'
|
|
13
|
+
import type { AgentRole } from './role-results'
|
|
14
|
+
|
|
15
|
+
import type { EngineConfig } from '../config/engine-config'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Helpers shared by the launcher tests (`claude-launcher.test.ts` and the
|
|
19
|
+
* `claude-launcher-*.test.ts` files beside it).
|
|
20
|
+
*
|
|
21
|
+
* The launcher seam: `createClaudeLauncher` with a fake `query` that plays
|
|
22
|
+
* back SDK-shaped messages, and a fake `claude` path. No model, no process.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export const CONFIG: EngineConfig = {
|
|
26
|
+
checks: { test: 'bun test', types: 'bunx --bun tsc --noEmit' },
|
|
27
|
+
test_file_patterns: ['src/**/*.test.ts'],
|
|
28
|
+
test_setup_files: ['src/test-setup.ts'],
|
|
29
|
+
rule_files: [],
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const CLAUDE_PATH = '/opt/fake/bin/claude'
|
|
33
|
+
|
|
34
|
+
export const MAX_ACCOUNT = {
|
|
35
|
+
email: 'me@example.com',
|
|
36
|
+
subscriptionType: 'max',
|
|
37
|
+
apiKeySource: 'none',
|
|
38
|
+
apiProvider: 'firstParty',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const INIT = {
|
|
42
|
+
type: 'system',
|
|
43
|
+
subtype: 'init',
|
|
44
|
+
apiKeySource: 'none',
|
|
45
|
+
model: 'claude-opus-5-5',
|
|
46
|
+
claude_code_version: '2.1.280',
|
|
47
|
+
session_id: 'session-1',
|
|
48
|
+
cwd: '/somewhere',
|
|
49
|
+
tools: ['Read', 'Grep', 'Glob', 'Bash', 'StructuredOutput'],
|
|
50
|
+
mcp_servers: [],
|
|
51
|
+
permissionMode: 'dontAsk',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const APPROVE = {
|
|
55
|
+
verdict: 'approve',
|
|
56
|
+
findings: [],
|
|
57
|
+
summary: 'Fine.',
|
|
58
|
+
assumptions: [],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const result = (fields: Record<string, unknown>) => ({
|
|
62
|
+
type: 'result',
|
|
63
|
+
subtype: 'success',
|
|
64
|
+
is_error: false,
|
|
65
|
+
num_turns: 3,
|
|
66
|
+
duration_ms: 1200,
|
|
67
|
+
total_cost_usd: 0.25,
|
|
68
|
+
usage: {
|
|
69
|
+
input_tokens: 10,
|
|
70
|
+
output_tokens: 20,
|
|
71
|
+
cache_read_input_tokens: 30,
|
|
72
|
+
cache_creation_input_tokens: 40,
|
|
73
|
+
},
|
|
74
|
+
permission_denials: [],
|
|
75
|
+
session_id: 'session-1',
|
|
76
|
+
...fields,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
type FakeCall = {
|
|
80
|
+
options: Options
|
|
81
|
+
prompts: SDKUserMessage[]
|
|
82
|
+
account_checked_before_prompt: boolean
|
|
83
|
+
interrupted: boolean
|
|
84
|
+
closed: boolean
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A fake `query`: it answers `accountInfo()` with `account`, waits for the
|
|
89
|
+
* first prompt, then plays `messages` (a function throws where it sits).
|
|
90
|
+
* With `hang`, it runs until it is closed. With `turns`, it plays one list
|
|
91
|
+
* per prompt instead, like a session in streaming-input mode, and runs until
|
|
92
|
+
* it is closed. `during` runs after each prompt arrives and before that
|
|
93
|
+
* turn's messages play, the way the agent's tool calls happen mid-turn.
|
|
94
|
+
*/
|
|
95
|
+
export const fakeQuery = ({
|
|
96
|
+
account,
|
|
97
|
+
messages,
|
|
98
|
+
hang,
|
|
99
|
+
turns,
|
|
100
|
+
during,
|
|
101
|
+
}: {
|
|
102
|
+
account?: unknown
|
|
103
|
+
messages?: (unknown | (() => never))[]
|
|
104
|
+
hang?: boolean
|
|
105
|
+
turns?: unknown[][]
|
|
106
|
+
during?: (call: FakeCall, turn: number) => Promise<void>
|
|
107
|
+
}) => {
|
|
108
|
+
const calls: FakeCall[] = []
|
|
109
|
+
const query: AgentQuery = ({ prompt, options }) => {
|
|
110
|
+
let accountChecked = false
|
|
111
|
+
const call: FakeCall = {
|
|
112
|
+
options,
|
|
113
|
+
prompts: [],
|
|
114
|
+
account_checked_before_prompt: false,
|
|
115
|
+
interrupted: false,
|
|
116
|
+
closed: false,
|
|
117
|
+
}
|
|
118
|
+
calls.push(call)
|
|
119
|
+
let wake = () => {}
|
|
120
|
+
const woken = new Promise<void>((resolve) => {
|
|
121
|
+
wake = resolve
|
|
122
|
+
})
|
|
123
|
+
let end = () => {}
|
|
124
|
+
const ended = new Promise<void>((resolve) => {
|
|
125
|
+
end = resolve
|
|
126
|
+
})
|
|
127
|
+
let notify = () => {}
|
|
128
|
+
const prompted = (count: number) =>
|
|
129
|
+
new Promise<void>((resolve) => {
|
|
130
|
+
notify = () => {
|
|
131
|
+
if (call.prompts.length >= count || call.closed) resolve()
|
|
132
|
+
}
|
|
133
|
+
notify()
|
|
134
|
+
})
|
|
135
|
+
void (async () => {
|
|
136
|
+
for await (const message of prompt) {
|
|
137
|
+
if (call.prompts.length === 0) {
|
|
138
|
+
call.account_checked_before_prompt = accountChecked
|
|
139
|
+
}
|
|
140
|
+
call.prompts.push(message)
|
|
141
|
+
wake()
|
|
142
|
+
notify()
|
|
143
|
+
}
|
|
144
|
+
})()
|
|
145
|
+
return {
|
|
146
|
+
[Symbol.asyncIterator]: async function* () {
|
|
147
|
+
if (turns !== undefined) {
|
|
148
|
+
for (const [index, batch] of turns.entries()) {
|
|
149
|
+
await prompted(index + 1)
|
|
150
|
+
if (call.closed) return
|
|
151
|
+
await during?.(call, index)
|
|
152
|
+
for (const message of batch) yield message
|
|
153
|
+
}
|
|
154
|
+
await ended
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
await woken
|
|
158
|
+
if (call.closed) return
|
|
159
|
+
await during?.(call, 0)
|
|
160
|
+
for (const message of messages ?? []) {
|
|
161
|
+
if (typeof message === 'function') message()
|
|
162
|
+
yield message
|
|
163
|
+
}
|
|
164
|
+
if (hang) await ended
|
|
165
|
+
},
|
|
166
|
+
accountInfo: async () => {
|
|
167
|
+
accountChecked = true
|
|
168
|
+
return account ?? MAX_ACCOUNT
|
|
169
|
+
},
|
|
170
|
+
interrupt: async () => {
|
|
171
|
+
call.interrupted = true
|
|
172
|
+
},
|
|
173
|
+
close: () => {
|
|
174
|
+
call.closed = true
|
|
175
|
+
wake()
|
|
176
|
+
end()
|
|
177
|
+
notify()
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { query, calls }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The git repo the current test runs in: a fresh one for each test. */
|
|
185
|
+
export let repo = ''
|
|
186
|
+
|
|
187
|
+
/** Gives each test of the file that calls it a fresh git repo in `repo`. */
|
|
188
|
+
export const repoPerTest = () => {
|
|
189
|
+
beforeEach(async () => {
|
|
190
|
+
repo = await mkdtemp(join(tmpdir(), 'luca-launcher-'))
|
|
191
|
+
await $`git init -q -b main ${repo}`.quiet()
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
afterEach(async () => {
|
|
195
|
+
await rm(repo, { recursive: true, force: true })
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export const launch = ({
|
|
200
|
+
query,
|
|
201
|
+
model,
|
|
202
|
+
role,
|
|
203
|
+
turn_timeout_ms,
|
|
204
|
+
messaging,
|
|
205
|
+
}: {
|
|
206
|
+
query: AgentQuery
|
|
207
|
+
model?: string
|
|
208
|
+
role?: AgentRole
|
|
209
|
+
turn_timeout_ms?: number
|
|
210
|
+
messaging?: AgentMessaging
|
|
211
|
+
}): Promise<AgentTurn> =>
|
|
212
|
+
createClaudeLauncher({
|
|
213
|
+
query,
|
|
214
|
+
claude_path: CLAUDE_PATH,
|
|
215
|
+
turn_timeout_ms,
|
|
216
|
+
}).launch({
|
|
217
|
+
role: role ?? 'ticket-reviewer',
|
|
218
|
+
ticket: 11,
|
|
219
|
+
prompt: 'Review ticket #11.',
|
|
220
|
+
cwd: repo,
|
|
221
|
+
may_edit_tests: role === 'test-writer',
|
|
222
|
+
config: CONFIG,
|
|
223
|
+
messaging: messaging ?? null,
|
|
224
|
+
model: model ?? CLAUDE_MODEL,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
export const IMPLEMENTER_DONE = {
|
|
228
|
+
outcome: 'done',
|
|
229
|
+
bad_test: null,
|
|
230
|
+
summary: 'Done.',
|
|
231
|
+
assumptions: [],
|
|
232
|
+
run_notes: [],
|
|
233
|
+
}
|