@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,512 @@
|
|
|
1
|
+
import type { FindingSeverity } from '../agents/role-results'
|
|
2
|
+
import { ModelNameSchema, type ModelName } from '../config/engine-config'
|
|
3
|
+
import type { JournalRecord } from '../journal/journal-record'
|
|
4
|
+
import { countedTokens } from '../limits/plan-usage'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The model score report (#605): how good each routed role's work was on
|
|
8
|
+
* each model it ran on, from run journals, so the owner knows when to lower
|
|
9
|
+
* a model floor or raise one back. Pure: it reads records and never writes.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** How much each ticket review finding weighs. */
|
|
13
|
+
export const FINDING_WEIGHTS: Record<FindingSeverity, number> = {
|
|
14
|
+
blocker: 3,
|
|
15
|
+
should_fix: 1,
|
|
16
|
+
nit: 0,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The fewest tickets on each side before a verdict is given. */
|
|
20
|
+
export const MIN_TICKETS = 10
|
|
21
|
+
|
|
22
|
+
/** How far above the baseline a rate may be before it is "worse". */
|
|
23
|
+
export const WORSE_BY = 0.2
|
|
24
|
+
|
|
25
|
+
/** Rounding slack when a rate is compared with the baseline's. */
|
|
26
|
+
const EPSILON = 1e-9
|
|
27
|
+
|
|
28
|
+
/** The roles scored by the ticket review's findings. */
|
|
29
|
+
export const SCORED_ROLES = ['test-writer', 'implementer'] as const
|
|
30
|
+
|
|
31
|
+
export type ScoredRole = (typeof SCORED_ROLES)[number]
|
|
32
|
+
|
|
33
|
+
export type Verdict = 'not enough data' | 'worse' | 'about the same'
|
|
34
|
+
|
|
35
|
+
/** The rates a model is judged by, for its tickets or its baseline's. */
|
|
36
|
+
export type ScoreSide = {
|
|
37
|
+
tickets: number
|
|
38
|
+
/** Weighted first-review findings per 100 changed lines; `null` with no lines. */
|
|
39
|
+
findings_per_100_lines: number | null
|
|
40
|
+
/** `null` with no tickets. */
|
|
41
|
+
fix_rounds_per_ticket: number | null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One routed role on one model. */
|
|
45
|
+
export type ModelScore = ScoreSide & {
|
|
46
|
+
role: ScoredRole
|
|
47
|
+
model: ModelName
|
|
48
|
+
stuck_tickets: number
|
|
49
|
+
/** `null` when no finished ticket has its usage journaled. */
|
|
50
|
+
tokens_per_finished_ticket: number | null
|
|
51
|
+
/** Clef's median confidence in its model pick; `null` with none. */
|
|
52
|
+
median_confidence: number | null
|
|
53
|
+
/** Its tickets whose other build role ran on a cheaper model than Opus. */
|
|
54
|
+
other_role_cheaper: number
|
|
55
|
+
/**
|
|
56
|
+
* The tickets where Clef picked this model in shadow mode but the role
|
|
57
|
+
* ran Opus. Empty for Opus.
|
|
58
|
+
*/
|
|
59
|
+
baseline: ScoreSide
|
|
60
|
+
/** `null` for Opus, which is the baseline the others are judged by. */
|
|
61
|
+
verdict: Verdict | null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The final review's findings over a set of runs. */
|
|
65
|
+
export type FinalReviewSide = {
|
|
66
|
+
runs: number
|
|
67
|
+
findings_per_100_lines: number | null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type ModelReport = {
|
|
71
|
+
runs: number
|
|
72
|
+
scores: ModelScore[]
|
|
73
|
+
/** Fresh learner agents per model; the learner isn't scored. */
|
|
74
|
+
learner: { model: ModelName; agents: number }[]
|
|
75
|
+
/** Runs with any agent on a cheaper model, and runs without. */
|
|
76
|
+
final_review: { routed: FinalReviewSide; not_routed: FinalReviewSide }
|
|
77
|
+
/** Joined tickets with no `lines_changed`, left out of the rates. */
|
|
78
|
+
without_lines_changed: number
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Clef's first model pick for a role on a ticket. */
|
|
82
|
+
type Pick = {
|
|
83
|
+
model: ModelName | null
|
|
84
|
+
confidence: number | null
|
|
85
|
+
/** Asked in shadow mode: `acted: false`, or an older `ticket_model` ask. */
|
|
86
|
+
shadow: boolean
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** What one run's journal says about one ticket. */
|
|
90
|
+
type TicketFacts = {
|
|
91
|
+
/** The model of each role's first agent. */
|
|
92
|
+
models: Partial<Record<ScoredRole, ModelName>>
|
|
93
|
+
picks: Partial<Record<ScoredRole, Pick>>
|
|
94
|
+
reviews: number
|
|
95
|
+
/** The first ticket review's weighted findings. */
|
|
96
|
+
findings: number
|
|
97
|
+
fix_rounds: number
|
|
98
|
+
joined: boolean
|
|
99
|
+
lines: number | null
|
|
100
|
+
stuck: boolean
|
|
101
|
+
tokens: number | null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** What one run's journal says. */
|
|
105
|
+
type RunFacts = {
|
|
106
|
+
tickets: TicketFacts[]
|
|
107
|
+
routed: boolean
|
|
108
|
+
learner: ModelName[]
|
|
109
|
+
/** The first final review round's weighted findings, or `null` with none. */
|
|
110
|
+
final_findings: number | null
|
|
111
|
+
/** The joined tickets' lines, or `null` when one has none. */
|
|
112
|
+
lines: number | null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The model family a short name or full model id (as older `ticket_model`
|
|
117
|
+
* asks named them) belongs to, or `null` for none.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* familyOf('sonnet') // 'sonnet'
|
|
121
|
+
*/
|
|
122
|
+
const familyOf = (name: unknown): ModelName | null => {
|
|
123
|
+
if (typeof name !== 'string') return null
|
|
124
|
+
const lower = name.toLowerCase()
|
|
125
|
+
return (
|
|
126
|
+
ModelNameSchema.options.find((model) => lower.includes(model)) ?? null
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const isScored = (role: string | null): role is ScoredRole =>
|
|
131
|
+
SCORED_ROLES.some((scored) => scored === role)
|
|
132
|
+
|
|
133
|
+
const sum = (values: number[]): number =>
|
|
134
|
+
values.reduce((total, value) => total + value, 0)
|
|
135
|
+
|
|
136
|
+
const median = (values: number[]): number | null => {
|
|
137
|
+
if (values.length === 0) return null
|
|
138
|
+
const sorted = values.toSorted((a, b) => a - b)
|
|
139
|
+
const middle = Math.floor(sorted.length / 2)
|
|
140
|
+
return sorted.length % 2 === 1
|
|
141
|
+
? (sorted[middle] ?? null)
|
|
142
|
+
: ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const weigh = (counts: Record<FindingSeverity, number>): number =>
|
|
146
|
+
counts.blocker * FINDING_WEIGHTS.blocker +
|
|
147
|
+
counts.should_fix * FINDING_WEIGHTS.should_fix +
|
|
148
|
+
counts.nit * FINDING_WEIGHTS.nit
|
|
149
|
+
|
|
150
|
+
const newTicket = (): TicketFacts => ({
|
|
151
|
+
models: {},
|
|
152
|
+
picks: {},
|
|
153
|
+
reviews: 0,
|
|
154
|
+
findings: 0,
|
|
155
|
+
fix_rounds: 0,
|
|
156
|
+
joined: false,
|
|
157
|
+
lines: null,
|
|
158
|
+
stuck: false,
|
|
159
|
+
tokens: null,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
/** What one run's records say, ticket by ticket. */
|
|
163
|
+
const factsOf = (records: JournalRecord[]): RunFacts => {
|
|
164
|
+
const tickets = new Map<number, TicketFacts>()
|
|
165
|
+
const ticketOf = (number: number): TicketFacts => {
|
|
166
|
+
const found = tickets.get(number) ?? newTicket()
|
|
167
|
+
tickets.set(number, found)
|
|
168
|
+
return found
|
|
169
|
+
}
|
|
170
|
+
const asks = new Map<number, { role: string | null; shadow: boolean }>()
|
|
171
|
+
const learner: ModelName[] = []
|
|
172
|
+
let routed = false
|
|
173
|
+
let final_rounds = 0
|
|
174
|
+
let final_findings = 0
|
|
175
|
+
for (const record of records) {
|
|
176
|
+
switch (record.kind) {
|
|
177
|
+
case 'jev_asked':
|
|
178
|
+
if (
|
|
179
|
+
record.content.job === 'agent_model' ||
|
|
180
|
+
record.content.job === 'ticket_model'
|
|
181
|
+
) {
|
|
182
|
+
asks.set(record.seq, {
|
|
183
|
+
role: record.role,
|
|
184
|
+
shadow:
|
|
185
|
+
record.content.job === 'ticket_model' ||
|
|
186
|
+
!record.content.acted,
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
break
|
|
190
|
+
case 'jev_answered': {
|
|
191
|
+
const ask = asks.get(record.content.asked_seq)
|
|
192
|
+
if (ask === undefined || record.ticket === null) break
|
|
193
|
+
const answer = record.content.answers.model
|
|
194
|
+
const pick: Pick = {
|
|
195
|
+
model: familyOf(answer?.value),
|
|
196
|
+
confidence: answer?.confidence ?? null,
|
|
197
|
+
shadow: ask.shadow,
|
|
198
|
+
}
|
|
199
|
+
const roles =
|
|
200
|
+
record.content.job === 'ticket_model'
|
|
201
|
+
? SCORED_ROLES
|
|
202
|
+
: SCORED_ROLES.filter((role) => role === ask.role)
|
|
203
|
+
const facts = ticketOf(record.ticket)
|
|
204
|
+
for (const role of roles) facts.picks[role] ??= pick
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
case 'agent_started': {
|
|
208
|
+
const { role, model, follow_up_of } = record.content
|
|
209
|
+
const family = familyOf(model) ?? 'opus'
|
|
210
|
+
if (follow_up_of !== null) break
|
|
211
|
+
if (role === 'learner') learner.push(family)
|
|
212
|
+
if (isScored(role) || role === 'learner') {
|
|
213
|
+
routed ||= family !== 'opus'
|
|
214
|
+
}
|
|
215
|
+
if (isScored(role) && record.ticket !== null) {
|
|
216
|
+
ticketOf(record.ticket).models[role] ??= family
|
|
217
|
+
}
|
|
218
|
+
break
|
|
219
|
+
}
|
|
220
|
+
case 'agent_finished': {
|
|
221
|
+
const { content } = record
|
|
222
|
+
if (
|
|
223
|
+
content.role !== 'ticket-reviewer' ||
|
|
224
|
+
record.ticket === null
|
|
225
|
+
) {
|
|
226
|
+
break
|
|
227
|
+
}
|
|
228
|
+
const facts = ticketOf(record.ticket)
|
|
229
|
+
if (facts.reviews === 0) {
|
|
230
|
+
facts.findings = sum(
|
|
231
|
+
content.result.findings.map(
|
|
232
|
+
({ severity }) => FINDING_WEIGHTS[severity]
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
facts.reviews += 1
|
|
237
|
+
if (content.result.verdict === 'changes_requested') {
|
|
238
|
+
facts.fix_rounds += 1
|
|
239
|
+
}
|
|
240
|
+
break
|
|
241
|
+
}
|
|
242
|
+
case 'ticket_joined':
|
|
243
|
+
if (record.ticket !== null && record.content.ok) {
|
|
244
|
+
const facts = ticketOf(record.ticket)
|
|
245
|
+
facts.joined = true
|
|
246
|
+
facts.lines = record.content.lines_changed
|
|
247
|
+
}
|
|
248
|
+
break
|
|
249
|
+
case 'ticket_stuck':
|
|
250
|
+
if (record.ticket !== null) ticketOf(record.ticket).stuck = true
|
|
251
|
+
break
|
|
252
|
+
case 'usage_recorded': {
|
|
253
|
+
const { scope, ticket, tokens } = record.content
|
|
254
|
+
if (scope === 'ticket' && ticket !== null) {
|
|
255
|
+
ticketOf(ticket).tokens = countedTokens({ tokens })
|
|
256
|
+
}
|
|
257
|
+
break
|
|
258
|
+
}
|
|
259
|
+
case 'final_review_started':
|
|
260
|
+
final_rounds += 1
|
|
261
|
+
break
|
|
262
|
+
case 'lens_finished':
|
|
263
|
+
if (final_rounds === 1) {
|
|
264
|
+
final_findings += weigh(record.content.findings)
|
|
265
|
+
}
|
|
266
|
+
break
|
|
267
|
+
default:
|
|
268
|
+
break
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const joined = [...tickets.values()].filter(({ joined }) => joined)
|
|
272
|
+
return {
|
|
273
|
+
tickets: [...tickets.values()],
|
|
274
|
+
routed,
|
|
275
|
+
learner,
|
|
276
|
+
final_findings: final_rounds > 0 ? final_findings : null,
|
|
277
|
+
lines: joined.some(({ lines }) => lines === null)
|
|
278
|
+
? null
|
|
279
|
+
: sum(joined.map(({ lines }) => lines ?? 0)),
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Findings per 100 lines over `tickets`, those with no lines left out. */
|
|
284
|
+
const findingsRate = (tickets: TicketFacts[]): number | null => {
|
|
285
|
+
const rated = tickets.filter(({ lines }) => lines !== null)
|
|
286
|
+
const lines = sum(rated.map(({ lines }) => lines ?? 0))
|
|
287
|
+
return lines === 0
|
|
288
|
+
? null
|
|
289
|
+
: (sum(rated.map(({ findings }) => findings)) * 100) / lines
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const sideOf = (tickets: TicketFacts[]): ScoreSide => ({
|
|
293
|
+
tickets: tickets.length,
|
|
294
|
+
findings_per_100_lines: findingsRate(tickets),
|
|
295
|
+
fix_rounds_per_ticket:
|
|
296
|
+
tickets.length === 0
|
|
297
|
+
? null
|
|
298
|
+
: sum(tickets.map(({ fix_rounds }) => fix_rounds)) / tickets.length,
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
/** Whether `value` is more than `WORSE_BY` above `base`. */
|
|
302
|
+
const above = (value: number | null, base: number | null): boolean =>
|
|
303
|
+
value !== null && base !== null && value > base * (1 + WORSE_BY) + EPSILON
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The verdict on a cheaper model against its baseline: "not enough data"
|
|
307
|
+
* while either side has fewer than `MIN_TICKETS` tickets, "worse" when its
|
|
308
|
+
* findings rate or fix rounds per ticket is more than `WORSE_BY` above the
|
|
309
|
+
* baseline's, else "about the same". Pure.
|
|
310
|
+
*/
|
|
311
|
+
export const verdictOf = ({
|
|
312
|
+
side,
|
|
313
|
+
baseline,
|
|
314
|
+
}: {
|
|
315
|
+
side: ScoreSide
|
|
316
|
+
baseline: ScoreSide
|
|
317
|
+
}): Verdict => {
|
|
318
|
+
if (side.tickets < MIN_TICKETS || baseline.tickets < MIN_TICKETS) {
|
|
319
|
+
return 'not enough data'
|
|
320
|
+
}
|
|
321
|
+
const worse =
|
|
322
|
+
above(side.findings_per_100_lines, baseline.findings_per_100_lines) ||
|
|
323
|
+
above(side.fix_rounds_per_ticket, baseline.fix_rounds_per_ticket)
|
|
324
|
+
return worse ? 'worse' : 'about the same'
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** One routed role on one model, over every run's tickets. */
|
|
328
|
+
const scoreOf = ({
|
|
329
|
+
tickets,
|
|
330
|
+
role,
|
|
331
|
+
model,
|
|
332
|
+
}: {
|
|
333
|
+
tickets: TicketFacts[]
|
|
334
|
+
role: ScoredRole
|
|
335
|
+
model: ModelName
|
|
336
|
+
}): ModelScore => {
|
|
337
|
+
const other = SCORED_ROLES.find((scored) => scored !== role) ?? role
|
|
338
|
+
const group = tickets.filter((ticket) => ticket.models[role] === model)
|
|
339
|
+
const finished = group.flatMap(({ joined, tokens }) =>
|
|
340
|
+
joined && tokens !== null ? [tokens] : []
|
|
341
|
+
)
|
|
342
|
+
const baseline =
|
|
343
|
+
model === 'opus'
|
|
344
|
+
? []
|
|
345
|
+
: tickets.filter(
|
|
346
|
+
({ models, picks }) =>
|
|
347
|
+
models[role] === 'opus' &&
|
|
348
|
+
picks[role]?.shadow === true &&
|
|
349
|
+
picks[role]?.model === model
|
|
350
|
+
)
|
|
351
|
+
const side = sideOf(group)
|
|
352
|
+
const baseline_side = sideOf(baseline)
|
|
353
|
+
return {
|
|
354
|
+
role,
|
|
355
|
+
model,
|
|
356
|
+
...side,
|
|
357
|
+
stuck_tickets: group.filter(({ stuck }) => stuck).length,
|
|
358
|
+
tokens_per_finished_ticket:
|
|
359
|
+
finished.length === 0 ? null : sum(finished) / finished.length,
|
|
360
|
+
median_confidence: median(
|
|
361
|
+
group.flatMap(({ picks }) => {
|
|
362
|
+
const confidence = picks[role]?.confidence ?? null
|
|
363
|
+
return confidence === null ? [] : [confidence]
|
|
364
|
+
})
|
|
365
|
+
),
|
|
366
|
+
other_role_cheaper: group.filter(({ models }) => {
|
|
367
|
+
const ran = models[other]
|
|
368
|
+
return ran !== undefined && ran !== 'opus'
|
|
369
|
+
}).length,
|
|
370
|
+
baseline: baseline_side,
|
|
371
|
+
verdict:
|
|
372
|
+
model === 'opus'
|
|
373
|
+
? null
|
|
374
|
+
: verdictOf({ side, baseline: baseline_side }),
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** The final review's findings per 100 changed lines over `runs`. */
|
|
379
|
+
const finalReviewOf = (runs: RunFacts[]): FinalReviewSide => {
|
|
380
|
+
const reviewed = runs.filter(
|
|
381
|
+
({ final_findings }) => final_findings !== null
|
|
382
|
+
)
|
|
383
|
+
const rated = reviewed.filter(({ lines }) => lines !== null)
|
|
384
|
+
const lines = sum(rated.map(({ lines }) => lines ?? 0))
|
|
385
|
+
return {
|
|
386
|
+
runs: reviewed.length,
|
|
387
|
+
findings_per_100_lines:
|
|
388
|
+
lines === 0
|
|
389
|
+
? null
|
|
390
|
+
: (sum(rated.map(({ final_findings }) => final_findings ?? 0)) *
|
|
391
|
+
100) /
|
|
392
|
+
lines,
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* The model score report from run journals, each run's records oldest
|
|
398
|
+
* first. A ticket counts under the model of each role's first agent on it
|
|
399
|
+
* (an agent from before #600, with no model, ran Opus). Pure.
|
|
400
|
+
*
|
|
401
|
+
* @example
|
|
402
|
+
* const report = scoreModels({ runs: [createJournal({ file }).read()] })
|
|
403
|
+
* console.log(formatModelReport({ report }))
|
|
404
|
+
*/
|
|
405
|
+
export const scoreModels = ({
|
|
406
|
+
runs,
|
|
407
|
+
}: {
|
|
408
|
+
runs: JournalRecord[][]
|
|
409
|
+
}): ModelReport => {
|
|
410
|
+
const facts = runs.map(factsOf)
|
|
411
|
+
const tickets = facts
|
|
412
|
+
.flatMap((run) => run.tickets)
|
|
413
|
+
.filter(({ models }) => Object.keys(models).length > 0)
|
|
414
|
+
const scores = SCORED_ROLES.flatMap((role) =>
|
|
415
|
+
ModelNameSchema.options.map((model) =>
|
|
416
|
+
scoreOf({ tickets, role, model })
|
|
417
|
+
)
|
|
418
|
+
).filter(({ tickets, baseline }) => tickets > 0 || baseline.tickets > 0)
|
|
419
|
+
const learner = facts.flatMap((run) => run.learner)
|
|
420
|
+
return {
|
|
421
|
+
runs: runs.length,
|
|
422
|
+
scores,
|
|
423
|
+
learner: ModelNameSchema.options
|
|
424
|
+
.map((model) => ({
|
|
425
|
+
model,
|
|
426
|
+
agents: learner.filter((ran) => ran === model).length,
|
|
427
|
+
}))
|
|
428
|
+
.filter(({ agents }) => agents > 0),
|
|
429
|
+
final_review: {
|
|
430
|
+
routed: finalReviewOf(facts.filter(({ routed }) => routed)),
|
|
431
|
+
not_routed: finalReviewOf(facts.filter(({ routed }) => !routed)),
|
|
432
|
+
},
|
|
433
|
+
without_lines_changed: tickets.filter(
|
|
434
|
+
({ joined, lines }) => joined && lines === null
|
|
435
|
+
).length,
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const plural = (count: number, word: string): string =>
|
|
440
|
+
`${count} ${word}${count === 1 ? '' : 's'}`
|
|
441
|
+
|
|
442
|
+
const decimal = (value: number | null): string =>
|
|
443
|
+
value === null ? '-' : value.toFixed(2)
|
|
444
|
+
|
|
445
|
+
const sideText = (side: ScoreSide): string =>
|
|
446
|
+
[
|
|
447
|
+
plural(side.tickets, 'ticket'),
|
|
448
|
+
`${decimal(side.findings_per_100_lines)} findings per 100 lines`,
|
|
449
|
+
`${decimal(side.fix_rounds_per_ticket)} fix rounds per ticket`,
|
|
450
|
+
].join(', ')
|
|
451
|
+
|
|
452
|
+
const scoreText = (score: ModelScore): string[] => {
|
|
453
|
+
const other = SCORED_ROLES.find((role) => role !== score.role)
|
|
454
|
+
const tokens = score.tokens_per_finished_ticket
|
|
455
|
+
return [
|
|
456
|
+
` ${score.model}: ${[
|
|
457
|
+
sideText(score),
|
|
458
|
+
`${score.stuck_tickets} stuck`,
|
|
459
|
+
`${tokens === null ? '-' : Math.round(tokens)} tokens per finished ticket`,
|
|
460
|
+
`median confidence ${decimal(score.median_confidence)}`,
|
|
461
|
+
`${other} cheaper on ${score.other_role_cheaper}`,
|
|
462
|
+
].join(', ')}`,
|
|
463
|
+
...(score.verdict === null
|
|
464
|
+
? []
|
|
465
|
+
: [
|
|
466
|
+
` baseline (Clef picked ${score.model} in shadow mode, ran opus): ${sideText(score.baseline)}`,
|
|
467
|
+
` verdict: ${score.verdict}`,
|
|
468
|
+
]),
|
|
469
|
+
]
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const finalText = (label: string, side: FinalReviewSide): string =>
|
|
473
|
+
`${decimal(side.findings_per_100_lines)} ${label} (${plural(side.runs, 'run')})`
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* The report as `luca models` prints it: each routed role's models with
|
|
477
|
+
* their verdicts, the learner, the final review's line, and the tickets
|
|
478
|
+
* left out of the rates. Pure.
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* console.log(formatModelReport({ report: scoreModels({ runs }) }))
|
|
482
|
+
*/
|
|
483
|
+
export const formatModelReport = ({
|
|
484
|
+
report,
|
|
485
|
+
}: {
|
|
486
|
+
report: ModelReport
|
|
487
|
+
}): string => {
|
|
488
|
+
const weights = `blocker ${FINDING_WEIGHTS.blocker}, should-fix ${FINDING_WEIGHTS.should_fix}, nit ${FINDING_WEIGHTS.nit}`
|
|
489
|
+
const roles = SCORED_ROLES.flatMap((role) => {
|
|
490
|
+
const scores = report.scores.filter((score) => score.role === role)
|
|
491
|
+
return [
|
|
492
|
+
role,
|
|
493
|
+
...(scores.length === 0
|
|
494
|
+
? [' no tickets yet']
|
|
495
|
+
: scores.flatMap(scoreText)),
|
|
496
|
+
]
|
|
497
|
+
})
|
|
498
|
+
const learner =
|
|
499
|
+
report.learner.length === 0
|
|
500
|
+
? ''
|
|
501
|
+
: ` (${report.learner.map(({ model, agents }) => `${plural(agents, 'agent')} on ${model}`).join(', ')})`
|
|
502
|
+
const { routed, not_routed } = report.final_review
|
|
503
|
+
return [
|
|
504
|
+
`Model scores from ${plural(report.runs, 'run')}: the first ticket review's findings weighted ${weights}, per 100 changed lines.`,
|
|
505
|
+
'',
|
|
506
|
+
...roles,
|
|
507
|
+
`learner: not measured by findings${learner}`,
|
|
508
|
+
'',
|
|
509
|
+
`Final review findings per 100 changed lines: ${finalText('with routed agents', routed)}, ${finalText('without', not_routed)}.`,
|
|
510
|
+
`${plural(report.without_lines_changed, 'joined ticket')} with no lines_changed, left out of the rates.`,
|
|
511
|
+
].join('\n')
|
|
512
|
+
}
|