@renzrollon/interlock 1.0.0
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/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +58 -0
- package/CHANGELOG.md +380 -0
- package/LICENSE +21 -0
- package/README.md +358 -0
- package/agents/ping.md +8 -0
- package/agents/worker.md +8 -0
- package/bin/interlock +2655 -0
- package/bin/interlock-graph +6 -0
- package/bin/interlock-run +508 -0
- package/bin/interlock-ship-acp +39 -0
- package/hooks/_shared.mjs +162 -0
- package/hooks/guard-commit.mjs +77 -0
- package/hooks/guard-tasks.mjs +107 -0
- package/hooks/guard-tests.mjs +63 -0
- package/hooks/preflight.mjs +107 -0
- package/lib/artifacts.mjs +316 -0
- package/lib/autonomy.mjs +149 -0
- package/lib/conformance.mjs +132 -0
- package/lib/doctor.mjs +1176 -0
- package/lib/drift.mjs +409 -0
- package/lib/eval-history.mjs +304 -0
- package/lib/evals-calibrate.mjs +317 -0
- package/lib/evals-capture.mjs +437 -0
- package/lib/evals-promote.mjs +257 -0
- package/lib/evals-triage.mjs +216 -0
- package/lib/findings.mjs +340 -0
- package/lib/graph/cli.mjs +266 -0
- package/lib/graph/extract/config-json.mjs +74 -0
- package/lib/graph/extract/create-extractor.mjs +35 -0
- package/lib/graph/extract/memory.mjs +99 -0
- package/lib/graph/extract/openspec.mjs +80 -0
- package/lib/graph/extract/python.mjs +137 -0
- package/lib/graph/extract/regex-fallback.mjs +187 -0
- package/lib/graph/extract/shell.mjs +71 -0
- package/lib/graph/extract/typescript.mjs +217 -0
- package/lib/graph/extract/walk.mjs +272 -0
- package/lib/graph/format.mjs +51 -0
- package/lib/graph/graph/build.mjs +374 -0
- package/lib/graph/graph/query.mjs +249 -0
- package/lib/graph/graph/report.mjs +122 -0
- package/lib/graph/graph/update.mjs +47 -0
- package/lib/graph/paths.mjs +66 -0
- package/lib/graph/retrieve/context.mjs +86 -0
- package/lib/graph/retrieve/docs-index.mjs +116 -0
- package/lib/graph/retrieve/markdown.mjs +246 -0
- package/lib/host/acp.mjs +551 -0
- package/lib/host/claude-cli.mjs +285 -0
- package/lib/host/codex.mjs +231 -0
- package/lib/host/model-map.mjs +129 -0
- package/lib/host/qwen.mjs +176 -0
- package/lib/host/registry.mjs +213 -0
- package/lib/host.mjs +393 -0
- package/lib/ledger.mjs +444 -0
- package/lib/limits.mjs +525 -0
- package/lib/locators.mjs +99 -0
- package/lib/merge-lanes.mjs +148 -0
- package/lib/metrics.mjs +99 -0
- package/lib/notify.mjs +229 -0
- package/lib/outcomes.mjs +560 -0
- package/lib/plan-fingerprint.mjs +570 -0
- package/lib/project-slug.mjs +18 -0
- package/lib/prompts/commit.mjs +26 -0
- package/lib/prompts/dimensions.mjs +70 -0
- package/lib/prompts/handoff.mjs +86 -0
- package/lib/prompts/implementer.mjs +150 -0
- package/lib/prompts/index.mjs +250 -0
- package/lib/prompts/planner.mjs +70 -0
- package/lib/prompts/remediate.mjs +137 -0
- package/lib/prompts/replan.mjs +23 -0
- package/lib/prompts/review.mjs +107 -0
- package/lib/prompts/schemas.mjs +206 -0
- package/lib/prompts/stage.mjs +48 -0
- package/lib/prompts/verify.mjs +88 -0
- package/lib/ready.mjs +812 -0
- package/lib/receipt.mjs +434 -0
- package/lib/remediate.mjs +275 -0
- package/lib/report-html.mjs +623 -0
- package/lib/report.mjs +1124 -0
- package/lib/review-core.mjs +713 -0
- package/lib/risk.mjs +370 -0
- package/lib/run-log.mjs +1004 -0
- package/lib/run-paths.mjs +239 -0
- package/lib/run.mjs +2921 -0
- package/lib/ship-stage.mjs +223 -0
- package/lib/spill.mjs +150 -0
- package/lib/surface.mjs +195 -0
- package/lib/verify.mjs +1074 -0
- package/lib/waves.mjs +2661 -0
- package/package.json +40 -0
- package/shared/CONTEXT-HYGIENE.md +84 -0
- package/shared/DECISION-LEDGER.md +141 -0
- package/shared/EXPLORE-BRIEF.md +96 -0
- package/shared/INVARIANT-SWEEP.md +57 -0
- package/shared/SESSION-SHAPES.md +261 -0
- package/shared/SKILL-CANDIDATE-BRIEF.md +106 -0
- package/shared/TEST-PROFILE.md +204 -0
- package/shared/TOOL-ECONOMY.md +155 -0
- package/skills/bootstrap/SKILL.md +226 -0
- package/skills/commit/SKILL.md +99 -0
- package/skills/dispatch/SKILL.md +78 -0
- package/skills/docs-digest/SKILL.md +109 -0
- package/skills/evals/SKILL.md +160 -0
- package/skills/explain-code/SKILL.md +42 -0
- package/skills/explain-code/commit-teachin.md +136 -0
- package/skills/explain-code/module-walkthrough.md +358 -0
- package/skills/explore/SKILL.md +159 -0
- package/skills/fix-tests/SKILL.md +120 -0
- package/skills/graph/SKILL.md +136 -0
- package/skills/manual-test-plan/SKILL.md +121 -0
- package/skills/mr/SKILL.md +170 -0
- package/skills/report/SKILL.md +121 -0
- package/skills/review-artifacts/SKILL.md +127 -0
- package/skills/review-code/REVIEW.template.md +49 -0
- package/skills/review-code/SKILL.md +156 -0
- package/skills/review-code/dimensions/architecture.md +10 -0
- package/skills/review-code/dimensions/devops.md +12 -0
- package/skills/review-code/dimensions/language.md +12 -0
- package/skills/review-code/dimensions/qa.md +12 -0
- package/skills/review-code/dimensions/security.md +12 -0
- package/skills/review-code/dimensions/technical-lead.md +19 -0
- package/skills/ship/SKILL.md +67 -0
- package/skills/spec/SKILL.md +264 -0
- package/skills/spec/continuity.md +66 -0
- package/workflows/ship.js +565 -0
package/lib/limits.mjs
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
// Every cap the ship loop obeys, in one place.
|
|
2
|
+
//
|
|
3
|
+
// These numbers used to live in `skills/ship/SKILL.md` as prose — "cap two
|
|
4
|
+
// attempts", "cap five root-cause iterations", "more than two task failures".
|
|
5
|
+
// Prose caps drift: a number written in one heading gets restated differently
|
|
6
|
+
// three headings later, and nothing catches it. Worse, a model reading a cap in
|
|
7
|
+
// prose treats it as guidance, and the whole point of a cap is that it is not.
|
|
8
|
+
//
|
|
9
|
+
// So the loop engines read them from here, the CLI prints them, and the skills
|
|
10
|
+
// cite the CLI rather than restating the number. When a cap changes it changes
|
|
11
|
+
// once.
|
|
12
|
+
//
|
|
13
|
+
// Pure: no fs, no agent, no I/O.
|
|
14
|
+
// Exposed to skills as `interlock limits [--json]`.
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Ceilings that come from the dynamic-workflow runtime rather than from
|
|
18
|
+
* Interlock policy. The runtime caps concurrency at 16 and total agents at 1000
|
|
19
|
+
* per run; exceeding either is the runtime's error, not ours, so the planner
|
|
20
|
+
* clamps below them rather than discovering them the hard way.
|
|
21
|
+
* @see https://code.claude.com/docs/en/workflows
|
|
22
|
+
*/
|
|
23
|
+
export const RUNTIME = {
|
|
24
|
+
maxConcurrentAgents: 16,
|
|
25
|
+
maxAgentsPerRun: 1000
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const LIMITS = {
|
|
29
|
+
/** Agents spawned per batch. Waves wider than this are split. */
|
|
30
|
+
maxParallel: 8,
|
|
31
|
+
|
|
32
|
+
// `maxTasksPerAgent` — the single scalar lane cap — was REMOVED here rather
|
|
33
|
+
// than aliased to the tier table that replaced it (`LANE_CAPS.byTier` below).
|
|
34
|
+
// It had six readers: the planner, `makeWave`, `createRunState`, the plan
|
|
35
|
+
// fingerprint's canonical text, `formatFingerprint` and `formatLimits`. A
|
|
36
|
+
// deprecated alias would have let any one of them keep reading a scalar while
|
|
37
|
+
// the rest read the table, and the two would disagree silently about how long
|
|
38
|
+
// a lane may get — which is the drift this module exists to prevent. Removing
|
|
39
|
+
// it makes the sweep enforceable instead: a stale reader fails at import.
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Targeted fix attempts after a failed inter-wave check, per wave. After
|
|
43
|
+
* this the errors are logged and the run either halts (if they block the
|
|
44
|
+
* next wave) or continues with a warning.
|
|
45
|
+
*/
|
|
46
|
+
interWaveFixAttempts: 2,
|
|
47
|
+
|
|
48
|
+
/** Revisions of not-yet-executed wave groups, per run. */
|
|
49
|
+
replansPerRun: 2,
|
|
50
|
+
|
|
51
|
+
/** Review → fix → re-review cycles. Surviving blockers after this halt. */
|
|
52
|
+
remediationRounds: 2,
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Root-cause repair iterations against a red unit suite, across the whole
|
|
56
|
+
* run. Repairing by root cause is slow by design; this bounds it before the
|
|
57
|
+
* run turns into an open-ended debugging session.
|
|
58
|
+
*/
|
|
59
|
+
rootCauseIterations: 5,
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Task failures tolerated across all waves. Strictly more than this halts —
|
|
63
|
+
* a run losing three tasks is not producing a coherent change.
|
|
64
|
+
*/
|
|
65
|
+
taskFailureHalt: 2,
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Steps one run of the ship program may take before it halts. A step is one
|
|
69
|
+
* CLI call that yields a step record; a run that has taken this many without
|
|
70
|
+
* reaching a terminal state is not converging.
|
|
71
|
+
*
|
|
72
|
+
* This was `MAX_LOOP_STEPS`, a literal restated in both drivers — a loop bound
|
|
73
|
+
* the cap-authority spec forbids stating anywhere but here. It is enforced by
|
|
74
|
+
* `lib/run.mjs`, which counts on the run manifest and returns a `halt` step
|
|
75
|
+
* naming this cap once it is exceeded. A driver's own `RUNAWAY_BACKSTOP` is a
|
|
76
|
+
* different thing: the host runtime's agent ceiling, not a policy cap.
|
|
77
|
+
*/
|
|
78
|
+
maxRunSteps: 200,
|
|
79
|
+
|
|
80
|
+
// `memoryEntriesPerRun` was removed here rather than wired. It had no reader
|
|
81
|
+
// anywhere outside this file and its own value-pinning test, and memory
|
|
82
|
+
// writing is not implemented as a counted operation — so wiring it would have
|
|
83
|
+
// meant inventing an enforcement point to justify a number. A cap this module
|
|
84
|
+
// prints but nothing obeys is the same failure as a cap written in prose,
|
|
85
|
+
// which is the failure this module exists to end.
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Soft budget for inter-wave verification. Past this, drop to typecheck only
|
|
89
|
+
* rather than letting the checks outweigh the work they guard.
|
|
90
|
+
*/
|
|
91
|
+
interWaveVerifyBudgetMs: 60_000,
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Combined stdout/stderr byte threshold past which a verify step's output is
|
|
95
|
+
* spilled to disk (`lib/spill.mjs`) instead of returned inline. See
|
|
96
|
+
* add-ship-run-inspectability design.md §3.
|
|
97
|
+
*/
|
|
98
|
+
verifySpillBytes: 8192,
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Character budget for a spilled step's head-and-tail preview, and the
|
|
102
|
+
* ceiling every other verify-result text field must stay under. A result
|
|
103
|
+
* field larger than this is rejected as an oversized-result leak rather than
|
|
104
|
+
* judged.
|
|
105
|
+
*/
|
|
106
|
+
verifyPreviewChars: 4096,
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Inter-wave verification checkpoints allowed per run. A wave boundary is
|
|
110
|
+
* ordering; a checkpoint is an agent. Ordering is free, so the cap is on
|
|
111
|
+
* checkpoints, not on waves. Docs-only waves skip without consuming a slot.
|
|
112
|
+
*/
|
|
113
|
+
interWaveVerifications: 3,
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Character budget for one task's wave handoff — `summary`, `next`,
|
|
117
|
+
* `blocker` and the joined `evidence` locators added together. A packet over
|
|
118
|
+
* this fails its task rather than being truncated: silent truncation is the
|
|
119
|
+
* degradation the schema exists to forbid, and a next wave reading half a
|
|
120
|
+
* sentence is worse off than one told the report was rejected.
|
|
121
|
+
* See add-wave-handoff-and-prompt-snapshots design.md §2.
|
|
122
|
+
*/
|
|
123
|
+
maxHandoffChars: 2000,
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Byte ceiling for the repo-root `REVIEW.md` policy file. A file larger than
|
|
127
|
+
* this is not scanned: it is reported as a problem and the run proceeds under
|
|
128
|
+
* default policy (fail-open, add-interlock-review-policy design.md D3). The
|
|
129
|
+
* bound exists so a pathological or accidental (a committed build log named
|
|
130
|
+
* `REVIEW.md`) file cannot make the policy reader do unbounded work — the same
|
|
131
|
+
* reason every other scan in this loop is bounded rather than trusting.
|
|
132
|
+
* Read by `readReviewPolicy` in `lib/review-core.mjs`.
|
|
133
|
+
*/
|
|
134
|
+
maxReviewPolicyBytes: 65_536,
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Milliseconds a close's push to the ntfy relay may take before it is
|
|
138
|
+
* abandoned as a failed push. A hanging network call must not stall the
|
|
139
|
+
* close itself (design D5, add-harden-unattended-ship-runs). Read by
|
|
140
|
+
* `postNtfy` in `lib/notify.mjs` via `AbortController`.
|
|
141
|
+
*/
|
|
142
|
+
notifyTimeoutMs: 5000
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Caps the eval suite obeys, published here so nothing restates them in
|
|
147
|
+
* `.github/workflows/evals.yml` — a cost written in workflow YAML and echoed in
|
|
148
|
+
* a skill drifts exactly like a prose cap does. The CI job reads them from
|
|
149
|
+
* `interlock limits` (add-interlock-evals design.md D8), following the same
|
|
150
|
+
* precedent the implementer prompt sets at `workflows/ship.js:194-195`.
|
|
151
|
+
*
|
|
152
|
+
* These are deliberately a separate object from LIMITS rather than more entries
|
|
153
|
+
* in it: LIMITS counts loop iterations, whereas a cost ceiling is a dollar
|
|
154
|
+
* amount. They are held to the same reader invariant all the same — the
|
|
155
|
+
* cap-authority test walks `.github/workflows/` alongside `lib/`, `bin/` and
|
|
156
|
+
* `workflows/`, and a CI reader counts when it names the published field
|
|
157
|
+
* (`evals.<cap>`) rather than the export. There is no exemption: a cap here with
|
|
158
|
+
* no reader fails the same test a cap in LIMITS would.
|
|
159
|
+
*
|
|
160
|
+
* `reportingThreshold` was removed rather than wired, following the
|
|
161
|
+
* `memoryEntriesPerRun` precedent above. It described a score at or above which
|
|
162
|
+
* a case reported as passing, but `lib/evals-triage.mjs` reads no score at all —
|
|
163
|
+
* `classifyCase` branches on each grader's `passed` boolean — so there was no
|
|
164
|
+
* path it could govern without inventing one. Publishing it told a contributor
|
|
165
|
+
* that a scoring rule existed which did not.
|
|
166
|
+
*
|
|
167
|
+
* The two cost ceilings are first guesses to be tuned from the `cost_usd` the
|
|
168
|
+
* first runs actually report (design.md D15), not numbers the specs depend on.
|
|
169
|
+
*/
|
|
170
|
+
export const EVAL_CAPS = {
|
|
171
|
+
/** US dollars. Ceiling for the per-pull-request smoke subset. */
|
|
172
|
+
smokeCostUsd: 2,
|
|
173
|
+
|
|
174
|
+
/** US dollars. Ceiling for the scheduled full-suite run. */
|
|
175
|
+
fullRunCostUsd: 15,
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* US dollars. Ceiling for the scheduled **outcome** eval — the one that ships
|
|
179
|
+
* a committed fixture through the real loop and grades what is on disk
|
|
180
|
+
* (`evals/ship/run.mjs`), not the transcript-grading case suite above.
|
|
181
|
+
*
|
|
182
|
+
* Its two readers exist on the day it lands, which is the whole reason it may
|
|
183
|
+
* be published at all: `evals/ship/run.mjs` reads it from `interlock limits
|
|
184
|
+
* --json` before it starts a fixture, and `.github/workflows/ship-outcome-
|
|
185
|
+
* eval.yml` reads the same field to bound the job. The number appears nowhere
|
|
186
|
+
* else — not in the workflow YAML, not in the eval's README.
|
|
187
|
+
*
|
|
188
|
+
* Enforced at fixture and arm boundaries only. It stops the runner from
|
|
189
|
+
* STARTING the next arm; it never kills a run in flight, because a
|
|
190
|
+
* half-killed run cannot be graded and would be recorded as a failure it did
|
|
191
|
+
* not earn (design D6). A sweep that reaches it is reported partial.
|
|
192
|
+
*
|
|
193
|
+
* A first guess, to be tuned from the spend the first rows actually report —
|
|
194
|
+
* the same posture as the two ceilings above.
|
|
195
|
+
*/
|
|
196
|
+
shipEvalCostUsd: 20,
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Agent runs per case. Passed as `--runs` by the scheduled full-suite job,
|
|
200
|
+
* which overrides each case's own value: those were chosen for the fast
|
|
201
|
+
* per-change subset, and the scheduled arm is the one that can afford
|
|
202
|
+
* repetition. The smoke job's `--runs 1` is that subset's single-run posture,
|
|
203
|
+
* not a restatement of this cap.
|
|
204
|
+
*/
|
|
205
|
+
runsPerCase: 3,
|
|
206
|
+
|
|
207
|
+
// --- the promotion rule (spec: evals/promotion) --------------------------
|
|
208
|
+
//
|
|
209
|
+
// These three are the whole rule that decides whether an eval case may fail a
|
|
210
|
+
// build. They live here, with readers, for the reason the module header gives:
|
|
211
|
+
// a promotion threshold written in a specification is a sentence a reviewer
|
|
212
|
+
// re-argues once per proposal. `lib/evals-promote.mjs` reads all three by
|
|
213
|
+
// name, and nothing restates them in a skill, a spec, a doc, or the workflow
|
|
214
|
+
// YAML.
|
|
215
|
+
//
|
|
216
|
+
// Landing them WITH readers is deliberate: `runsPerCase` sat here printed and
|
|
217
|
+
// unread, and `reportingThreshold` was removed for the same reason. A fourth
|
|
218
|
+
// unread cap is the one thing this group must not gain.
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Consecutive qualifying runs a case must pass across before it may be
|
|
222
|
+
* promoted from advisory to blocking. A run triage classified no-signal or
|
|
223
|
+
* configuration does not qualify, is named, and does not break the chain.
|
|
224
|
+
* Read by `promote()` in `lib/evals-promote.mjs`.
|
|
225
|
+
*/
|
|
226
|
+
promotionRuns: 3,
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Trials of a case a run must carry to count as evidence for promotion. Below
|
|
230
|
+
* this the run is real but too thin to prove repeatability, and the case is
|
|
231
|
+
* refused naming the run rather than promoted on it.
|
|
232
|
+
* Read by `promote()` in `lib/evals-promote.mjs`.
|
|
233
|
+
*/
|
|
234
|
+
promotionTrialsPerRun: 3,
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Minimum measured judge/human agreement, as a fraction, for each judged
|
|
238
|
+
* grader of a case being promoted. `interlock evals calibrate` measures;
|
|
239
|
+
* this is the floor it is measured against, applied only here — the
|
|
240
|
+
* calibration report itself issues no verdict.
|
|
241
|
+
*
|
|
242
|
+
* The one non-integer in this group, which is why the positive-integer
|
|
243
|
+
* invariant is over LIMITS rather than over every cap group.
|
|
244
|
+
* Read by `promote()` in `lib/evals-promote.mjs`.
|
|
245
|
+
*/
|
|
246
|
+
judgeAgreementFloor: 0.8
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The price table the outcome eval converts measured tokens into dollars with,
|
|
251
|
+
* so `EVAL_CAPS.shipEvalCostUsd` can be a ceiling on money rather than on a
|
|
252
|
+
* token count nobody can compare across models (design D6).
|
|
253
|
+
*
|
|
254
|
+
* A separate export from EVAL_CAPS for the reason EFFORT and LANE_CAPS are: the
|
|
255
|
+
* cap groups hold scalars a code path obeys, and this is a table. It lives here
|
|
256
|
+
* all the same, because a price written in the runner and echoed in a workflow
|
|
257
|
+
* would drift exactly like a prose cap.
|
|
258
|
+
*
|
|
259
|
+
* `id` IS THE LOAD-BEARING FIELD. It travels on every recorded result row, so a
|
|
260
|
+
* later price revision cannot silently reinterpret rows written under the old
|
|
261
|
+
* one. Revising a price therefore means a NEW id, never an edit to a number
|
|
262
|
+
* under the existing one — old rows keep meaning what they meant, and a reader
|
|
263
|
+
* comparing across a revision can see that it happened.
|
|
264
|
+
*
|
|
265
|
+
* The figures are the published list prices for each tier, in US dollars per
|
|
266
|
+
* million tokens, as of the date in the id. They are an input to a ceiling and
|
|
267
|
+
* to a recorded estimate — never to a bill, and never to a gate.
|
|
268
|
+
*
|
|
269
|
+
* A model that is not in the table yields NO cost figure: the runner records the
|
|
270
|
+
* spend as absent with that reason rather than pricing it at zero or guessing a
|
|
271
|
+
* tier from the name. An unpriced run still runs; it just cannot be counted
|
|
272
|
+
* against the ceiling, and the sweep says so.
|
|
273
|
+
*
|
|
274
|
+
* Read by `evals/ship/run.mjs` through `interlock limits --json`; printed by
|
|
275
|
+
* `formatLimits()` below.
|
|
276
|
+
*/
|
|
277
|
+
export const MODEL_PRICES = {
|
|
278
|
+
// A NEW id, not an edit under `anthropic-list-2026-09`. The base rates below
|
|
279
|
+
// are unchanged, but the table now answers a question the old one could not —
|
|
280
|
+
// what a cached prefix costs — and a row keyed to the old id was priced by a
|
|
281
|
+
// table with no cache structure at all. The `b` suffix rather than a new month
|
|
282
|
+
// because this is a revision of the same published list, not a later one.
|
|
283
|
+
id: 'anthropic-list-2026-09b',
|
|
284
|
+
perMillionTokens: {
|
|
285
|
+
'claude-opus-5': { input: 15, output: 75 },
|
|
286
|
+
'claude-sonnet-5': { input: 3, output: 15 },
|
|
287
|
+
'claude-haiku-4-5': { input: 1, output: 5 }
|
|
288
|
+
},
|
|
289
|
+
/**
|
|
290
|
+
* What a cached prefix costs, as multipliers on the model's own `input` rate
|
|
291
|
+
* — the same shape for every model, which is why they are multipliers rather
|
|
292
|
+
* than a fourth and fifth column per model.
|
|
293
|
+
*
|
|
294
|
+
* `write` is keyed by LIFETIME TIER and is never flattened: a prefix written
|
|
295
|
+
* for five minutes and one written for an hour are different prices, and a
|
|
296
|
+
* total that added them could not be priced back apart. The keys are the tier
|
|
297
|
+
* names the host's usage envelope reports them under, so a recorded figure
|
|
298
|
+
* and its multiplier are looked up by the same string.
|
|
299
|
+
*
|
|
300
|
+
* A read is a tenth of base, which is the whole economic argument: a prefix
|
|
301
|
+
* that survives to be read costs 10% of what re-sending it costs, and a write
|
|
302
|
+
* that is never read cost 25% (or 100%) more than not caching at all.
|
|
303
|
+
*
|
|
304
|
+
* NO READER IN THIS CHANGE, deliberately (design D9): the figures are recorded
|
|
305
|
+
* so a dollar number becomes computable, and whether to compute and present one
|
|
306
|
+
* is a later decision. They live in the table rather than beside it so the two
|
|
307
|
+
* cannot drift — a run priced against one id and multiplied by a differently
|
|
308
|
+
* versioned constant is attributable to nothing.
|
|
309
|
+
*/
|
|
310
|
+
cacheMultipliers: {
|
|
311
|
+
write: { ephemeral_5m: 1.25, ephemeral_1h: 2 },
|
|
312
|
+
read: 0.1
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Reasoning-effort defaults for the wave planner and the two adversarial steps.
|
|
318
|
+
*
|
|
319
|
+
* Deliberately a separate object from LIMITS, exactly like EVAL_CAPS/REPORT_CAPS:
|
|
320
|
+
* LIMITS holds positive-integer iteration counts (its invariant test asserts
|
|
321
|
+
* every entry is one, and every entry must be read as `LIMITS.<cap>` by a
|
|
322
|
+
* code path). These are a tier→effort table and two fixed effort strings —
|
|
323
|
+
* neither an integer nor a loop count. Keeping them here still satisfies the one
|
|
324
|
+
* property that matters: the mapping is read from a single source and surfaced
|
|
325
|
+
* by `interlock limits`, never restated in code that can drift.
|
|
326
|
+
*
|
|
327
|
+
* `byTier` maps a lane's hardest tier to a workflow-runtime effort. `null` means
|
|
328
|
+
* "inherit the session default — do not force": tiers 3–4 inherit by policy so
|
|
329
|
+
* the run keeps whatever the session/model default is (xhigh for coding since
|
|
330
|
+
* w16); an untiered lane inherits by fallback. `verify` and `skeptic` pin the
|
|
331
|
+
* inter-wave verify and the review skeptics regardless of any lane tier — the
|
|
332
|
+
* two steps whose whole job is catching what an implementer missed.
|
|
333
|
+
*/
|
|
334
|
+
export const EFFORT = {
|
|
335
|
+
byTier: { 1: 'low', 2: 'low', 3: null, 4: null, 5: 'xhigh' },
|
|
336
|
+
verify: 'xhigh',
|
|
337
|
+
skeptic: 'xhigh'
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* How long a lane may get, per tier, and which tiers may be packed together.
|
|
342
|
+
*
|
|
343
|
+
* A lane is an ordered task list one agent executes start to finish. Until this
|
|
344
|
+
* table existed the bound was one scalar for every lane in the run, which forced
|
|
345
|
+
* one number to answer two different questions: how much sequential trivia one
|
|
346
|
+
* agent may hold, and how much judgement-heavy work it may hold. Those have
|
|
347
|
+
* different answers, so this is a table.
|
|
348
|
+
*
|
|
349
|
+
* `byTier` is keyed by the lane's HARDEST task tier (`laneTier`), because that is
|
|
350
|
+
* the tier the lane's agent is dispatched at. An untiered lane (tier 0) uses the
|
|
351
|
+
* tier-1 entry. Tier 4 keeps the old scalar's 4 — cross-file pattern-following is
|
|
352
|
+
* where a fresh context per task still pays. Tier 5 is raised to 8 because a
|
|
353
|
+
* tier-5 lane is the only opus lane in waves mode, and the point of an opus
|
|
354
|
+
* worker is that it can hold more of one design in its head.
|
|
355
|
+
*
|
|
356
|
+
* `cohesionMaxTier` is the ceiling for COHESION: path-disjoint components in one
|
|
357
|
+
* dependency layer whose hardest tier is at or below it may be packed into one
|
|
358
|
+
* lane. Above it a component stays a lane of its own, joined only by a real path
|
|
359
|
+
* collision. Set it to 0 to disable cohesion while keeping the table.
|
|
360
|
+
*
|
|
361
|
+
* A separate export from LIMITS for the same reason `EFFORT` is: the LIMITS
|
|
362
|
+
* invariant asserts every entry is a positive integer, and `byTier` is a table.
|
|
363
|
+
* The cap-authority check covers this group all the same — every entry here must
|
|
364
|
+
* have a reader in `lib/`, `bin/` or `workflows/`.
|
|
365
|
+
*
|
|
366
|
+
* Read by `lib/waves.mjs` (lane construction, cohesion packing) and
|
|
367
|
+
* `lib/plan-fingerprint.mjs`; printed by `interlock limits` and emitted as
|
|
368
|
+
* `laneCaps` by `interlock limits --json`.
|
|
369
|
+
*/
|
|
370
|
+
export const LANE_CAPS = {
|
|
371
|
+
byTier: { 1: 8, 2: 8, 3: 6, 4: 4, 5: 8 },
|
|
372
|
+
cohesionMaxTier: 3
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* The envelope inside which a change may be planned as ONE lane — every task,
|
|
377
|
+
* in order, on one opus agent (`--solo`, or the classifier's recommendation).
|
|
378
|
+
*
|
|
379
|
+
* One bound, task count, on purpose. The classifier supplies the shape
|
|
380
|
+
* judgement ("this change is one coherent edit"); this supplies the ceiling.
|
|
381
|
+
* Neither restates the other, which is why the `plan-waves` prompt is forbidden
|
|
382
|
+
* from naming this number: a model that can read the bound can argue with it.
|
|
383
|
+
*
|
|
384
|
+
* A recommendation above the envelope is refused and named. An explicit `--solo`
|
|
385
|
+
* flag may exceed it — a human flag is the one input allowed to, the same way
|
|
386
|
+
* `--max-parallel` may ask for anything under the runtime ceiling — and the plan
|
|
387
|
+
* warns that it did.
|
|
388
|
+
*
|
|
389
|
+
* Read by `lib/waves.mjs` (mode decision) and `lib/plan-fingerprint.mjs`;
|
|
390
|
+
* printed by `interlock limits` and emitted as `solo` by `--json`.
|
|
391
|
+
*/
|
|
392
|
+
export const SOLO = {
|
|
393
|
+
maxTasks: 20
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Caps for the corpus reader (`interlock report`).
|
|
398
|
+
*
|
|
399
|
+
* Not a policy ceiling like the ones above — nothing halts on it. It bounds how
|
|
400
|
+
* much of an unbounded, ever-growing corpus one invocation opens, so a
|
|
401
|
+
* repository with fifty thousand trajectories still gets a report. Reaching it
|
|
402
|
+
* is stated in the output rather than sampled silently: a truncated scan that
|
|
403
|
+
* looked complete would be worse than no report at all.
|
|
404
|
+
*/
|
|
405
|
+
export const REPORT_CAPS = {
|
|
406
|
+
/** Trajectory files opened in one `interlock report` invocation. */
|
|
407
|
+
maxRunsScanned: 2000
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Clamp a requested parallelism to something both Interlock and the runtime
|
|
412
|
+
* will honour. A caller asking for 40 gets the runtime ceiling, not an error:
|
|
413
|
+
* over-asking is a preference, not a mistake worth failing a run over.
|
|
414
|
+
* @param {number} [requested]
|
|
415
|
+
* @returns {{value: number, clamped: boolean, reason: string|null}}
|
|
416
|
+
*/
|
|
417
|
+
export function clampParallel(requested) {
|
|
418
|
+
if (!Number.isInteger(requested) || requested <= 0) {
|
|
419
|
+
return { value: LIMITS.maxParallel, clamped: false, reason: null }
|
|
420
|
+
}
|
|
421
|
+
if (requested > RUNTIME.maxConcurrentAgents) {
|
|
422
|
+
return {
|
|
423
|
+
value: RUNTIME.maxConcurrentAgents,
|
|
424
|
+
clamped: true,
|
|
425
|
+
reason: `workflow runtime allows at most ${RUNTIME.maxConcurrentAgents} concurrent agents`
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return { value: requested, clamped: false, reason: null }
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Human-readable dump, so a skill can show the caps without hardcoding them. */
|
|
432
|
+
export function formatLimits() {
|
|
433
|
+
const rows = [
|
|
434
|
+
['max parallel agents per batch', LIMITS.maxParallel],
|
|
435
|
+
['inter-wave fix attempts (per wave)', LIMITS.interWaveFixAttempts],
|
|
436
|
+
['replans (per run)', LIMITS.replansPerRun],
|
|
437
|
+
['remediation rounds', LIMITS.remediationRounds],
|
|
438
|
+
['root-cause iterations (per run)', LIMITS.rootCauseIterations],
|
|
439
|
+
['task failures tolerated', LIMITS.taskFailureHalt],
|
|
440
|
+
['run steps (per run)', LIMITS.maxRunSteps],
|
|
441
|
+
['inter-wave verify budget', `${LIMITS.interWaveVerifyBudgetMs / 1000}s`],
|
|
442
|
+
['inter-wave verifications (per run)', LIMITS.interWaveVerifications],
|
|
443
|
+
['verify spill threshold (bytes)', LIMITS.verifySpillBytes],
|
|
444
|
+
['verify preview budget (chars)', LIMITS.verifyPreviewChars],
|
|
445
|
+
['wave handoff budget (chars, per task)', LIMITS.maxHandoffChars],
|
|
446
|
+
['review policy scan cap (bytes)', LIMITS.maxReviewPolicyBytes],
|
|
447
|
+
['push timeout (ms)', LIMITS.notifyTimeoutMs]
|
|
448
|
+
]
|
|
449
|
+
const evalRows = [
|
|
450
|
+
['eval smoke cost ceiling (per PR)', `$${EVAL_CAPS.smokeCostUsd}`],
|
|
451
|
+
['eval full-run cost ceiling (scheduled)', `$${EVAL_CAPS.fullRunCostUsd}`],
|
|
452
|
+
['eval outcome-run cost ceiling (scheduled)', `$${EVAL_CAPS.shipEvalCostUsd}`],
|
|
453
|
+
['eval runs per case', EVAL_CAPS.runsPerCase],
|
|
454
|
+
['eval promotion: consecutive qualifying runs', EVAL_CAPS.promotionRuns],
|
|
455
|
+
['eval promotion: trials per qualifying run', EVAL_CAPS.promotionTrialsPerRun],
|
|
456
|
+
['eval promotion: judge agreement floor', `${Math.round(EVAL_CAPS.judgeAgreementFloor * 100)}%`]
|
|
457
|
+
]
|
|
458
|
+
// The price table prints beside the ceiling it serves: an operator reading a
|
|
459
|
+
// dollar cap needs to see which prices turned tokens into those dollars, and
|
|
460
|
+
// the id is what a recorded row is keyed against.
|
|
461
|
+
const priceRows = [
|
|
462
|
+
['price table id', MODEL_PRICES.id],
|
|
463
|
+
...Object.keys(MODEL_PRICES.perMillionTokens)
|
|
464
|
+
.sort()
|
|
465
|
+
.map(model => [
|
|
466
|
+
`price: ${model} ($/Mtok)`,
|
|
467
|
+
`in ${MODEL_PRICES.perMillionTokens[model].input}, out ${MODEL_PRICES.perMillionTokens[model].output}`
|
|
468
|
+
]),
|
|
469
|
+
// The cache multipliers print beside the base rates for the same reason the
|
|
470
|
+
// base rates print beside the ceiling: they are part of what turns a token
|
|
471
|
+
// count into a dollar figure, and a multiplier nobody can see is a price
|
|
472
|
+
// written in prose. One row per lifetime tier, because they are never summed.
|
|
473
|
+
...Object.keys(MODEL_PRICES.cacheMultipliers.write)
|
|
474
|
+
.sort()
|
|
475
|
+
.map(tier => [`cache write multiplier: ${tier} (x input)`, MODEL_PRICES.cacheMultipliers.write[tier]]),
|
|
476
|
+
['cache read multiplier (x input)', MODEL_PRICES.cacheMultipliers.read]
|
|
477
|
+
]
|
|
478
|
+
const reportRows = [['report trajectory scan cap', REPORT_CAPS.maxRunsScanned]]
|
|
479
|
+
// One row per tier, rather than a rendered object: an operator reading this
|
|
480
|
+
// wants to know what bounds the lane in front of them, and a tier is how they
|
|
481
|
+
// find it. The cohesion ceiling and the solo envelope print beside them because
|
|
482
|
+
// all three answer one question — how many tasks may one agent be handed.
|
|
483
|
+
const laneRows = [
|
|
484
|
+
['lane cap: tier 1 lane (also untiered)', LANE_CAPS.byTier[1]],
|
|
485
|
+
['lane cap: tier 2 lane', LANE_CAPS.byTier[2]],
|
|
486
|
+
['lane cap: tier 3 lane', LANE_CAPS.byTier[3]],
|
|
487
|
+
['lane cap: tier 4 lane', LANE_CAPS.byTier[4]],
|
|
488
|
+
['lane cap: tier 5 lane', LANE_CAPS.byTier[5]],
|
|
489
|
+
['cohesion tier ceiling (packs at or below)', LANE_CAPS.cohesionMaxTier],
|
|
490
|
+
['solo envelope (max tasks in one lane)', SOLO.maxTasks]
|
|
491
|
+
]
|
|
492
|
+
// `null` in the tier table means inherit the session default; print it as
|
|
493
|
+
// "inherit" so an operator reads the policy, not an empty cell.
|
|
494
|
+
const inherit = v => (v === null || v === undefined ? 'inherit (session default)' : v)
|
|
495
|
+
const effortRows = [
|
|
496
|
+
['effort: tier 1 lane', inherit(EFFORT.byTier[1])],
|
|
497
|
+
['effort: tier 2 lane', inherit(EFFORT.byTier[2])],
|
|
498
|
+
['effort: tier 3 lane', inherit(EFFORT.byTier[3])],
|
|
499
|
+
['effort: tier 4 lane', inherit(EFFORT.byTier[4])],
|
|
500
|
+
['effort: tier 5 lane', inherit(EFFORT.byTier[5])],
|
|
501
|
+
['effort: inter-wave verify step', EFFORT.verify],
|
|
502
|
+
['effort: review skeptic step', EFFORT.skeptic]
|
|
503
|
+
]
|
|
504
|
+
const width = Math.max(
|
|
505
|
+
...[...rows, ...laneRows, ...evalRows, ...priceRows, ...reportRows, ...effortRows].map(
|
|
506
|
+
([label]) => label.length
|
|
507
|
+
)
|
|
508
|
+
)
|
|
509
|
+
const fmt = ([label, value]) => ` ${String(label).padEnd(width)} ${value}`
|
|
510
|
+
return (
|
|
511
|
+
rows.map(fmt).join('\n') +
|
|
512
|
+
'\n\n' +
|
|
513
|
+
laneRows.map(fmt).join('\n') +
|
|
514
|
+
'\n\n' +
|
|
515
|
+
evalRows.map(fmt).join('\n') +
|
|
516
|
+
'\n\n' +
|
|
517
|
+
priceRows.map(fmt).join('\n') +
|
|
518
|
+
'\n\n' +
|
|
519
|
+
reportRows.map(fmt).join('\n') +
|
|
520
|
+
'\n\n' +
|
|
521
|
+
effortRows.map(fmt).join('\n') +
|
|
522
|
+
`\n\nruntime ceilings: ${RUNTIME.maxConcurrentAgents} concurrent, ` +
|
|
523
|
+
`${RUNTIME.maxAgentsPerRun} agents per run\n`
|
|
524
|
+
)
|
|
525
|
+
}
|
package/lib/locators.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Evidence locators — one definition of "what a citation looks like".
|
|
2
|
+
//
|
|
3
|
+
// Two surfaces cite work by pointing at it: a review skeptic's `evidence`
|
|
4
|
+
// prose (`lib/review-core.mjs`) and a wave handoff packet's `evidence` array
|
|
5
|
+
// (`lib/waves.mjs`). Both audit a citation the same way, on two conditions,
|
|
6
|
+
// because either alone is defeated:
|
|
7
|
+
//
|
|
8
|
+
// 1. Shape — `path`, `path:line` or `path:start-end`. Non-emptiness alone was
|
|
9
|
+
// satisfied by the string "👍".
|
|
10
|
+
// 2. Membership — the path canonicalizes to one present in the changed-path
|
|
11
|
+
// set. Shape alone is satisfied by inventing `lib/nowhere.ts:1`.
|
|
12
|
+
//
|
|
13
|
+
// The line is never required to exist in the file. A citation may legitimately
|
|
14
|
+
// name a line the work itself later moved or deleted, so path membership is the
|
|
15
|
+
// strongest condition that cannot produce a false rejection.
|
|
16
|
+
//
|
|
17
|
+
// WHY THIS IS ONE MODULE AND NOT TWO REGEXES. The second surface arrived after
|
|
18
|
+
// the first, and the cheap version of adding it is a second regex next to the
|
|
19
|
+
// new caller. Two definitions of "locator" drift, and the drift is silent —
|
|
20
|
+
// each surface keeps passing its own tests while they diverge. So the
|
|
21
|
+
// canonicalizer and the membership index live here and are imported.
|
|
22
|
+
//
|
|
23
|
+
// WHAT IS DELIBERATELY *NOT* SHARED: the anchoring. `review-core` scans free
|
|
24
|
+
// prose, so `CITATION_TOKEN` matches within a sentence and tolerates a trailing
|
|
25
|
+
// `:column`. `validateHandoff` receives an array of discrete locators, so it
|
|
26
|
+
// anchors the whole string and forbids a colon in the path. Both are correct
|
|
27
|
+
// for their input, and unifying them would loosen handoff validation. Any
|
|
28
|
+
// future "cleanup" that merges them is a regression.
|
|
29
|
+
//
|
|
30
|
+
// Pure: no fs, no clock, no I/O.
|
|
31
|
+
|
|
32
|
+
import { canonicalizePath } from './risk.mjs'
|
|
33
|
+
|
|
34
|
+
// Re-exported so a caller needs one import for the whole vocabulary, and so a
|
|
35
|
+
// test can assert on the canonicalizer the audit actually uses rather than on a
|
|
36
|
+
// same-named function it hopes is the same one.
|
|
37
|
+
export { canonicalizePath }
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* `path:line`, `path:start-end`, and a trailing `:column` a tool may have
|
|
41
|
+
* added. The path is a non-whitespace run, so this half cannot see a path
|
|
42
|
+
* containing spaces — that case is handled by `citesLineIn` against a known
|
|
43
|
+
* spelling.
|
|
44
|
+
*
|
|
45
|
+
* Global, so `matchAll` finds every citation in a block of prose. Callers must
|
|
46
|
+
* not hold state on it beyond one `matchAll` (it carries `lastIndex`).
|
|
47
|
+
*/
|
|
48
|
+
export const CITATION_TOKEN = /(\S+?):(\d+)(?:-\d+)?(?::\d+)?(?![\w.\-/])/g
|
|
49
|
+
|
|
50
|
+
/** Does `text` cite `path` with a line number after it? */
|
|
51
|
+
export function citesLineIn(text, path) {
|
|
52
|
+
for (let from = 0; ; ) {
|
|
53
|
+
const at = text.indexOf(path, from)
|
|
54
|
+
if (at === -1) return false
|
|
55
|
+
if (/^:\d+/.test(text.slice(at + path.length))) return true
|
|
56
|
+
from = at + 1
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The changed-path set as canonical identities plus the spellings the caller
|
|
62
|
+
* used, so a citation can be matched either way.
|
|
63
|
+
*
|
|
64
|
+
* `null` — not an empty Set — when there is nothing to match against, so a
|
|
65
|
+
* caller can tell "no path set was available" from "the path set is empty",
|
|
66
|
+
* which are different facts about a citation that failed.
|
|
67
|
+
*
|
|
68
|
+
* @param {unknown} changedPaths
|
|
69
|
+
* @returns {Set<string>|null}
|
|
70
|
+
*/
|
|
71
|
+
export function diffIndex(changedPaths) {
|
|
72
|
+
if (!Array.isArray(changedPaths) || !changedPaths.length) return null
|
|
73
|
+
const out = new Set()
|
|
74
|
+
for (const entry of changedPaths) {
|
|
75
|
+
if (typeof entry !== 'string' || !entry.trim()) continue
|
|
76
|
+
out.add(entry.trim())
|
|
77
|
+
const canonical = canonicalizePath(entry)
|
|
78
|
+
if (canonical) out.add(canonical)
|
|
79
|
+
}
|
|
80
|
+
return out.size ? out : null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The path half of one DISCRETE locator — `lib/a.mjs:12-40` → `lib/a.mjs`.
|
|
85
|
+
*
|
|
86
|
+
* For an array of locators, not for prose: it takes the whole string as the
|
|
87
|
+
* citation and only strips a trailing `:line` / `:start-end`. `null` when the
|
|
88
|
+
* remainder cannot be placed inside the repository, because an identity that
|
|
89
|
+
* could not be computed must not be treated as one that matches.
|
|
90
|
+
*
|
|
91
|
+
* @param {unknown} entry
|
|
92
|
+
* @returns {string|null}
|
|
93
|
+
*/
|
|
94
|
+
export function locatorPath(entry) {
|
|
95
|
+
if (typeof entry !== 'string') return null
|
|
96
|
+
const trimmed = entry.trim()
|
|
97
|
+
if (!trimmed) return null
|
|
98
|
+
return canonicalizePath(trimmed.replace(/:\d+(-\d+)?$/, ''))
|
|
99
|
+
}
|