@joshuafolkken/kit 1.1373.0 → 1.1383.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.
Files changed (92) hide show
  1. package/.claude/skills/dependency-update/SKILL.md +6 -3
  2. package/.claude/skills/workflow-commands/backlogrun-progress.md +2 -3
  3. package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -4
  4. package/.claude/skills/workflow-commands/retrospective.md +1 -2
  5. package/dist/hooks/batch-guard.js +6 -6
  6. package/dist/hooks/{chunk-UTHMB4O2.js → chunk-2X2ART5G.js} +4 -33
  7. package/dist/hooks/{chunk-NZN2UFI6.js → chunk-4MGHBTOM.js} +162 -233
  8. package/dist/hooks/{chunk-YLBBSSTF.js → chunk-6AC4C2IO.js} +91 -67
  9. package/dist/hooks/{chunk-AKGCP4LX.js → chunk-6MAW7BCM.js} +12 -12
  10. package/dist/hooks/{chunk-JCRDBQXB.js → chunk-7VSZ2N4B.js} +3 -3
  11. package/dist/hooks/{chunk-GG72ANKI.js → chunk-KC6LUZZ2.js} +1 -1
  12. package/dist/hooks/{chunk-BZMPVFDY.js → chunk-KCF2YE7O.js} +5 -5
  13. package/dist/hooks/{chunk-7WKYHEGU.js → chunk-LC56R53A.js} +3 -3
  14. package/dist/hooks/{chunk-V4TKZHER.js → chunk-LOJTBU5V.js} +3 -3
  15. package/dist/hooks/{chunk-JGW24KKM.js → chunk-RYP3TSUJ.js} +3 -3
  16. package/dist/hooks/{chunk-SLSFIWTX.js → chunk-TT742SIO.js} +8 -5
  17. package/dist/hooks/{chunk-Z5TMJIPW.js → chunk-Y5R47QHU.js} +1 -1
  18. package/dist/hooks/{chunk-7UBFU3CL.js → chunk-YKUITFT7.js} +2 -2
  19. package/dist/hooks/codex-hook-adapter.js +13 -13
  20. package/dist/hooks/duplicate-read-guard.js +7 -7
  21. package/dist/hooks/format-edited.js +3 -3
  22. package/dist/hooks/inputs.json +190 -191
  23. package/dist/hooks/investigation-guard.js +7 -7
  24. package/dist/hooks/pretool-guard.js +12 -12
  25. package/dist/hooks/session-lang.js +3 -3
  26. package/dist/hooks/stop-guard.js +19 -15
  27. package/dist/josh.js +40 -5
  28. package/package.json +4 -4
  29. package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
  30. package/scripts/adopt/adopt-logic.ts +4 -1
  31. package/scripts/adopt/adopt.ts +7 -4
  32. package/scripts/backlog/backlog-budget-cli.ts +2 -20
  33. package/scripts/backlog/backlog-budget.ts +72 -109
  34. package/scripts/backlog/backlog-drive-cli.ts +10 -3
  35. package/scripts/backlog/backlog-drive.ts +5 -2
  36. package/scripts/backlog/backlog-offer-cli.ts +1 -15
  37. package/scripts/cost-runtime/context-cut-payback.ts +9 -4
  38. package/scripts/cost-runtime/cost-pricing.ts +94 -33
  39. package/scripts/document/document-byte-budget.ts +1 -1
  40. package/scripts/document/read-set-pricing.ts +7 -5
  41. package/scripts/epic/epic-candidate-confirm.ts +1 -1
  42. package/scripts/epic/epic-cross-repo.ts +4 -4
  43. package/scripts/epic/epic-next.ts +1 -1
  44. package/scripts/git/git-spawn-sync.ts +9 -1
  45. package/scripts/hooks/stop-guard.ts +20 -9
  46. package/scripts/init/init-ai-copy.ts +10 -3
  47. package/scripts/init/init-copy-content.ts +8 -1
  48. package/scripts/init/security-policy.ts +79 -0
  49. package/scripts/josh/josh-commands-ai.ts +1 -1
  50. package/scripts/josh/josh-commands-maintenance.ts +3 -0
  51. package/scripts/josh/josh-commands-project.ts +1 -1
  52. package/scripts/josh/josh-commands-versioning.ts +2 -1
  53. package/scripts/lane/lane-cli.ts +1 -3
  54. package/scripts/propagate/propagate-run.ts +17 -1
  55. package/scripts/propagate/propagate-steps.ts +9 -1
  56. package/scripts/run/board/run-board-header.ts +107 -84
  57. package/scripts/run/board/run-board-labels.ts +18 -17
  58. package/scripts/run/board/run-board-machine.ts +21 -8
  59. package/scripts/run/board/run-board-render.ts +1 -5
  60. package/scripts/run/board/run-board-span.ts +0 -1
  61. package/scripts/run/board/run-board-status.ts +0 -17
  62. package/scripts/run/board/run-board-tick.ts +1 -0
  63. package/scripts/run/carry/run-carry-cli.ts +8 -62
  64. package/scripts/run/carry/run-carry-ended.ts +1 -1
  65. package/scripts/run/carry/run-carry.ts +22 -68
  66. package/scripts/run/cut/run-cut.ts +4 -4
  67. package/scripts/run/event/run-event-render.ts +0 -1
  68. package/scripts/run/event/run-event-scope.ts +4 -8
  69. package/scripts/run/event/run-event-stream-emit.ts +0 -25
  70. package/scripts/run/event/run-event-stream.ts +23 -31
  71. package/scripts/run/merge/run-merge-collect.ts +1 -1
  72. package/scripts/run/merge/run-merge-steps.ts +1 -1
  73. package/scripts/run/progress/run-progress-read.ts +1 -1
  74. package/scripts/run/run-headless.ts +2 -2
  75. package/scripts/run/run-status-cli.ts +1 -5
  76. package/scripts/run/run-step.ts +2 -9
  77. package/scripts/run/run-stop-notify.ts +1 -1
  78. package/scripts/run/run-stranded-detect.ts +3 -3
  79. package/scripts/run/run-stranded.ts +25 -17
  80. package/scripts/run/wake/run-wake-cli.ts +7 -9
  81. package/scripts/run/wake/run-wake-describe.ts +3 -5
  82. package/scripts/run/wake/run-wake-driver.ts +3 -3
  83. package/scripts/run/wake/run-wake-loop.ts +4 -21
  84. package/scripts/run/wake/run-wake-restart.ts +1 -1
  85. package/scripts/run/wake/run-wake.ts +6 -7
  86. package/scripts/sync/sync-ai-files.ts +2 -0
  87. package/scripts/version/latest-update.ts +20 -6
  88. package/scripts/version/peer-floor.ts +99 -0
  89. package/scripts/version/playwright-browsers.ts +109 -0
  90. package/scripts/version/publishable-range-check.ts +41 -4
  91. package/templates/SECURITY.md +23 -0
  92. package/scripts/backlog/backlog-idle.ts +0 -88
@@ -13,10 +13,12 @@ import { run_carry, type RunCarry } from '#scripts/run/carry/run-carry'
13
13
  import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
14
14
  import { run_merge_cli, type MergeResult } from '#scripts/run/merge/run-merge-cli'
15
15
  import { z } from 'zod'
16
+ import { backlog_budget } from './backlog-budget'
16
17
  import {
17
18
  backlog_drive,
18
19
  type DriveEnd,
19
20
  type DriveState,
21
+ type LoopConfig,
20
22
  type LoopPorts,
21
23
  type OfferRead,
22
24
  } from './backlog-drive'
@@ -46,7 +48,6 @@ const SUCCESS_EXIT_CODE = 0
46
48
  const FAILURE_EXIT_CODE = 1
47
49
  const should_forward_stderr = true
48
50
  const POLL_MS = 5000
49
- const OFFER_MS = 60_000
50
51
  const FIRST_LINE = 0
51
52
 
52
53
  // The `--json` object `backlog:offer` prints, keyed under its `JSON_KEY`.
@@ -71,7 +72,7 @@ async function read_record(): Promise<RunCarry | undefined> {
71
72
 
72
73
  const read = run_carry.read_carry(run_carry.carry_path(target))
73
74
 
74
- return read.kind === 'carried' || read.kind === 'expired' ? read.carry : undefined
75
+ return read.kind === 'carried' ? read.carry : undefined
75
76
  }
76
77
 
77
78
  async function read_offer(
@@ -204,6 +205,11 @@ async function seeded_lanes(context: DriveContext): Promise<ReadonlyArray<string
204
205
  return backlog_drive_restore.restore(lanes, events, carry.merged_issues ?? [])
205
206
  }
206
207
 
208
+ // The interval between two asks is `backlog:budget`'s own, so the driver keeps the one its reason prints.
209
+ function loop_config(window_ms: number | undefined): LoopConfig {
210
+ return { poll_ms: POLL_MS, offer_ms: backlog_budget.OFFER_INTERVAL_MS, window_ms }
211
+ }
212
+
207
213
  async function drive(context: DriveContext): Promise<number> {
208
214
  const seeded = await seeded_lanes(context)
209
215
 
@@ -212,7 +218,7 @@ async function drive(context: DriveContext): Promise<number> {
212
218
  const initial =
213
219
  context.stopped === undefined ? fresh : backlog_drive.merge_stopped(context.stopped, fresh)
214
220
  const last = { state: { ...initial, exclude: [...context.exclude] } }
215
- const config = { poll_ms: POLL_MS, offer_ms: OFFER_MS, window_ms: context.window_ms }
221
+ const config = loop_config(context.window_ms)
216
222
  const end = await backlog_drive.run_loop(last.state, config, ports_of(context, seeded, last))
217
223
 
218
224
  console.info(end_line(end))
@@ -258,6 +264,7 @@ async function main(argv: ReadonlyArray<string>): Promise<void> {
258
264
  const backlog_drive_cli = {
259
265
  end_line,
260
266
  finish: backlog_drive_finish.finish,
267
+ loop_config,
261
268
  merge,
262
269
  merge_token,
263
270
  offer_argv: backlog_drive_offer_argv.offer_argv,
@@ -140,9 +140,12 @@ function ended(reason: string, state: DriveState, token?: string, issue?: string
140
140
  }
141
141
  }
142
142
 
143
+ // Only an empty backlog drains: `backlog:offer` marks the drain on `exhausted` alone, so a watch over a
144
+ // parked or unreadable backlog has no retrospective for `run:step` to fire and stays the driver's.
143
145
  function is_drain(read: OfferRead, state: DriveState): boolean {
144
146
  return (
145
147
  read.verdict === WATCH_VERDICT &&
148
+ read.answer === 'exhausted' &&
146
149
  state.in_flight.length === 0 &&
147
150
  read.is_retrospective_owed === true
148
151
  )
@@ -352,8 +355,8 @@ async function run_pass(
352
355
 
353
356
  interface LoopConfig {
354
357
  poll_ms: number
355
- // The floor between two `backlog:offer` asks while nothing was collected — the same one-minute
356
- // cadence the wake supervisor polls the backlog on, so an idle stretch is not a network hammer.
358
+ // The floor between two `backlog:offer` asks while nothing was collected, running or idle alike —
359
+ // `backlog_budget.OFFER_INTERVAL_MS`, the interval the wake supervisor polls on too.
357
360
  offer_ms: number
358
361
  // How long one invocation may wait before returning `window`, or `undefined` for no bound. A headless
359
362
  // parent's foreground call is capped by its shell's timeout, so it passes one under that cap.
@@ -125,27 +125,19 @@ const budget_read_schema = z.object({
125
125
  [BUDGET_JSON_KEY]: z.string(),
126
126
  reason: z.string(),
127
127
  is_finish: z.boolean(),
128
- idle: z.string().optional(),
129
128
  })
130
129
 
131
130
  interface BudgetRead {
132
131
  verdict: string
133
132
  reason: string
134
133
  is_finish: boolean
135
- // The idle watch's window, as `backlog_idle.text_of` wrote it, when the budget opened one.
136
- idle?: string | undefined
137
134
  }
138
135
 
139
136
  function budget_of(out: string): BudgetRead | undefined {
140
137
  const read = json_value.parse_with(out, budget_read_schema)
141
138
  if (read === undefined) return undefined
142
139
 
143
- return {
144
- verdict: read[BUDGET_JSON_KEY],
145
- reason: read.reason,
146
- is_finish: read.is_finish,
147
- idle: read.idle,
148
- }
140
+ return { verdict: read[BUDGET_JSON_KEY], reason: read.reason, is_finish: read.is_finish }
149
141
  }
150
142
 
151
143
  function to_tokens(out: string): ReadonlyArray<string> {
@@ -225,12 +217,6 @@ async function decide(values: ParsedValues, counts: OfferCounts): Promise<number
225
217
 
226
218
  await mark_drain(decision.verdict, offer.answer, counts.running)
227
219
 
228
- // The idle window with the ask that read it, once per watching poll, so `run:board` counts the next
229
- // check from the poll that ran rather than guessing one.
230
- if (decision.idle !== undefined) {
231
- await run_event_stream_emit.emit_changed(run_event_stream.EVENT_KIND.IDLE, decision.idle)
232
- }
233
-
234
220
  return emit(decision, offer, values.json === true)
235
221
  }
236
222
 
@@ -26,11 +26,16 @@ const POST_CUT_CONTEXT = 60_000
26
26
  const EXPECTED_REMAINING_REQUESTS = 10
27
27
 
28
28
  // The price of a dropped token relative to a rewritten one, from `cost-pricing.ts`. A cache read bills
29
- // `CACHE_READ_MULTIPLIER` of base input and a 5-minute cache write `CACHE_WRITE_5M_MULTIPLIER`, so a
30
- // preamble rewrite costs this many per-request cache reads. The base input rate is common to both and
31
- // cancels, which is why only the ratio appears.
29
+ // `DEFAULT_CACHE_READ_MULTIPLIER` of base input and a 5-minute cache write `CACHE_WRITE_5M_MULTIPLIER`,
30
+ // so a preamble rewrite costs this many per-request cache reads. The base input rate is common to both
31
+ // and cancels, which is why only the ratio appears.
32
+ //
33
+ // **The standard cache-read rate, deliberately, not the running model's.** Opus 5.5 reads cache at
34
+ // 0.05x and Fable 5.1 at 0.025x, which would move the threshold from 135,000 to 210,000 and 360,000 —
35
+ // outside the 100k–200k band the cut was validated in. That changes when a cut fires, a decision that
36
+ // needs a lane measurement of its own rather than arriving as a side effect of a corrected price list.
32
37
  function write_over_read(): number {
33
- return cost_pricing.CACHE_WRITE_5M_MULTIPLIER / cost_pricing.CACHE_READ_MULTIPLIER
38
+ return cost_pricing.CACHE_WRITE_5M_MULTIPLIER / cost_pricing.DEFAULT_CACHE_READ_MULTIPLIER
34
39
  }
35
40
 
36
41
  // The current per-request context at which a cut breaks even over exactly `remaining` requests. The
@@ -1,68 +1,114 @@
1
1
  import { cost_usage, type UsageTotals } from './cost-usage'
2
2
 
3
- // The current Claude price list, and the three multipliers that make a token's price depend on how
4
- // it was sent.
3
+ // The current Claude price list, and the multipliers that make a token's price depend on how it was
4
+ // sent.
5
5
  //
6
- // A run's cost is not "tokens x one rate". A cache read costs a tenth of base input, a 5-minute
7
- // cache write 1.25x, a 1-hour cache write 2x, and output 5x. On a measured request with
8
- // `cache_read_input_tokens` 97,190 against `input_tokens` 2, an estimate that ignored the
9
- // multipliers would be wrong by more than an order of magnitude in either direction depending on
6
+ // A run's cost is not "tokens x one rate". A cache read costs a fraction of base input that depends
7
+ // on the model, a 5-minute cache write 1.25x, a 1-hour cache write 2x, and output 5x. On a measured
8
+ // request with `cache_read_input_tokens` 97,190 against `input_tokens` 2, an estimate that ignored
9
+ // the multipliers would be wrong by more than an order of magnitude in either direction depending on
10
10
  // which single rate it picked.
11
11
 
12
12
  const CACHE_WRITE_5M_MULTIPLIER = 1.25
13
13
  const CACHE_WRITE_1H_MULTIPLIER = 2
14
- const CACHE_READ_MULTIPLIER = 0.1
14
+ // The standard cache-read rate, a tenth of base input — what a model bills unless its row says
15
+ // otherwise. Named the default because it is not every model's rate: pricing a model reads the row's
16
+ // multiplier through `estimate_cost`, never this constant.
17
+ const DEFAULT_CACHE_READ_MULTIPLIER = 0.1
18
+ // The discounted cache-read rates of the 5.5 generation (platform.claude.com "Model pricing", read
19
+ // 2026-10-12): Opus 5.5 and Sonnet 5.5 bill a cache read at 0.05x base input, Fable 5.1 and Mythos
20
+ // 5.1 at 0.025x.
21
+ const HALVED_CACHE_READ_MULTIPLIER = 0.05
22
+ const QUARTERED_CACHE_READ_MULTIPLIER = 0.025
15
23
  const TOKENS_PER_MILLION = 1_000_000
16
24
 
17
25
  interface ModelPrice {
18
26
  // USD per million tokens.
19
27
  input: number
20
28
  output: number
29
+ // The cache-read rate as a multiple of `input`. Absent on a model billed at the standard
30
+ // `DEFAULT_CACHE_READ_MULTIPLIER`, so a row states a multiplier only where it differs.
31
+ cache_read_multiplier?: number
21
32
  }
22
33
 
34
+ // A model priced by prompt length carries a second row under its id plus this suffix — the rates a
35
+ // request over `LONG_PROMPT_THRESHOLD` pays. The suffixed name is not an id the API reports; it is
36
+ // what `cost_by_model` bills such a request under, so the higher-priced requests show as their own
37
+ // row and resolve to their own price like any other.
38
+ const LONG_PROMPT_SUFFIX = ':long-prompt'
39
+ const LONG_PROMPT_THRESHOLD = 100_000
40
+
23
41
  // Base input/output rates per million tokens. Cache rates are derived from `input` by the
24
42
  // multipliers above rather than listed, because they are defined as multiples of it — writing them
25
43
  // out would let the two drift.
26
44
  /* eslint-disable @typescript-eslint/naming-convention */
27
45
  const MODEL_PRICES: Record<string, ModelPrice> = {
46
+ 'claude-fable-5-1': {
47
+ input: 10,
48
+ output: 50,
49
+ cache_read_multiplier: QUARTERED_CACHE_READ_MULTIPLIER,
50
+ },
51
+ 'claude-mythos-5-1': {
52
+ input: 10,
53
+ output: 50,
54
+ cache_read_multiplier: QUARTERED_CACHE_READ_MULTIPLIER,
55
+ },
28
56
  'claude-fable-5': { input: 10, output: 50 },
29
57
  'claude-mythos-5': { input: 10, output: 50 },
58
+ 'claude-opus-5-5': { input: 4, output: 20, cache_read_multiplier: HALVED_CACHE_READ_MULTIPLIER },
30
59
  'claude-opus-5': { input: 5, output: 25 },
31
60
  'claude-opus-4-8': { input: 5, output: 25 },
32
61
  'claude-opus-4-7': { input: 5, output: 25 },
33
62
  'claude-opus-4-6': { input: 5, output: 25 },
63
+ 'claude-sonnet-5-5': {
64
+ input: 2,
65
+ output: 10,
66
+ cache_read_multiplier: HALVED_CACHE_READ_MULTIPLIER,
67
+ },
34
68
  'claude-sonnet-5': { input: 2, output: 10 },
35
69
  'claude-sonnet-4-6': { input: 3, output: 15 },
70
+ 'claude-haiku-5-5': { input: 0.1, output: 0.5 },
71
+ [`claude-haiku-5-5${LONG_PROMPT_SUFFIX}`]: { input: 0.5, output: 2.5 },
36
72
  'claude-haiku-4-5': { input: 1, output: 5 },
37
73
  }
38
74
  /* eslint-enable @typescript-eslint/naming-convention */
39
75
 
40
76
  const CONTEXT_MARKER = '['
77
+ // A deployment's date suffix — `claude-haiku-4-5-20251001`. Eight digits and nothing else, so a
78
+ // version segment (the last `-5` of `claude-opus-5-5`) is never read as one.
79
+ const DATE_SUFFIX = /-\d{8}$/u
41
80
 
42
81
  // Claude Code appends a context-window marker to the model id it records — `claude-opus-5[1m]` —
43
82
  // and a deployment may carry a date suffix. Both name the same priced model, so the id is reduced
44
- // to its longest prefix that the table knows rather than reported as unknown.
83
+ // to the table's id by removing exactly those two decorations.
84
+ //
85
+ // **Nothing else is removed.** The reduction was once "the longest prefix the table knows", which
86
+ // priced `claude-opus-5-5` at `claude-opus-5`'s rates and `claude-fable-5-1` at `claude-fable-5`'s —
87
+ // another model's price list, reported with no warning. A version the table does not carry is
88
+ // unpriced, which `total_cost` names, rather than billed as the closest id it resembles.
45
89
  //
46
90
  // The `[1m]` marker is the 1M-context beta, and stripping it to the base rate is confirmed correct,
47
- // not an approximation. The published price list — platform.claude.com
48
- // "Long context pricing", read 2026-09 — states that Claude 4.6 and later models "include the full
49
- // 1M token context window at standard pricing (a 900k-token request is billed at the same per-token
50
- // rate as a 9k-token request)". Every id in `MODEL_PRICES` is 4.6-or-later, so no request has an
51
- // input-size surcharge band to account for, whatever its size. (Fast mode is a separate premium, but
52
- // it is signalled by `speed: "fast"` rather than by the context marker, so it is not this marker's
53
- // concern.) Were a future model to reintroduce a >200K band, the marker could no longer be stripped
54
- // blindly and this function would branch on the input size instead.
55
- function resolve_price(model: string): ModelPrice | undefined {
91
+ // not an approximation. The published price list — platform.claude.com "Long context pricing", read
92
+ // 2026-10-12 — states that Claude 4.6 and later models other than Haiku 5.5 "include the full 1M
93
+ // token context window at standard pricing (a 900k-token request is billed at the same per-token
94
+ // rate as a 9k-token request)". The one model with an input-size band is priced from the request's
95
+ // own size by `billed_model`, not from the marker. (Fast mode is a separate premium, but it is
96
+ // signalled by `speed: "fast"` rather than by the context marker, so it is not this marker's
97
+ // concern.)
98
+ function known_id(model: string): string | undefined {
56
99
  const base = model.split(CONTEXT_MARKER)[0] ?? model
57
- const direct = MODEL_PRICES[base]
58
100
 
59
- if (direct !== undefined) return direct
101
+ return [base, base.replace(DATE_SUFFIX, '')].find((id) => Object.hasOwn(MODEL_PRICES, id))
102
+ }
103
+
104
+ function resolve_price(model: string): ModelPrice | undefined {
105
+ const id = known_id(model)
60
106
 
61
- const [matched] = Object.keys(MODEL_PRICES)
62
- .filter((id) => base.startsWith(id))
63
- .toSorted((left, right) => right.length - left.length)
107
+ return id === undefined ? undefined : MODEL_PRICES[id]
108
+ }
64
109
 
65
- return matched === undefined ? undefined : MODEL_PRICES[matched]
110
+ function cache_read_multiplier(price: ModelPrice): number {
111
+ return price.cache_read_multiplier ?? DEFAULT_CACHE_READ_MULTIPLIER
66
112
  }
67
113
 
68
114
  function estimate_cost(totals: UsageTotals, price: ModelPrice): number {
@@ -70,16 +116,16 @@ function estimate_cost(totals: UsageTotals, price: ModelPrice): number {
70
116
  totals.input_tokens +
71
117
  totals.cache_write_5m_tokens * CACHE_WRITE_5M_MULTIPLIER +
72
118
  totals.cache_write_1h_tokens * CACHE_WRITE_1H_MULTIPLIER +
73
- totals.cache_read_tokens * CACHE_READ_MULTIPLIER
119
+ totals.cache_read_tokens * cache_read_multiplier(price)
74
120
 
75
121
  return (weighted_input * price.input + totals.output_tokens * price.output) / TOKENS_PER_MILLION
76
122
  }
77
123
 
78
124
  // The five dollar terms `estimate_cost` sums, kept apart so a report can say what a run's dollars
79
- // were spent on rather than only their total. Each is priced by the same
80
- // multipliers `estimate_cost` uses — the exported constants above — so the five always sum to
81
- // `estimate_cost(totals, price)`. It is a decomposition of the same arithmetic, not a second price
82
- // list, which is why the weights live in the constants both read rather than in either function.
125
+ // were spent on rather than only their total. Each is priced by the same multipliers `estimate_cost`
126
+ // uses — the write constants above and the price's own cache-read multiplier — so the five always
127
+ // sum to `estimate_cost(totals, price)`. It is a decomposition of the same arithmetic, not a second
128
+ // price list, which is why the weights live where both read them rather than in either function.
83
129
  interface CostComposition {
84
130
  input_usd: number
85
131
  cache_write_5m_usd: number
@@ -103,7 +149,7 @@ function estimate_composition(totals: UsageTotals, price: ModelPrice): CostCompo
103
149
  totals.cache_write_1h_tokens * CACHE_WRITE_1H_MULTIPLIER,
104
150
  price.input,
105
151
  ),
106
- cache_read_usd: priced(totals.cache_read_tokens * CACHE_READ_MULTIPLIER, price.input),
152
+ cache_read_usd: priced(totals.cache_read_tokens * cache_read_multiplier(price), price.input),
107
153
  output_usd: priced(totals.output_tokens, price.output),
108
154
  }
109
155
  }
@@ -129,15 +175,29 @@ interface ModelUsage {
129
175
  totals: UsageTotals
130
176
  }
131
177
 
178
+ // The id a request is billed under: its own, unless the model is priced by prompt length and this
179
+ // request is over the threshold, in which case the model's long-prompt row. The prompt length is
180
+ // everything the request sent — cache reads and writes included — and each request is priced on its
181
+ // own (platform.claude.com "Long context pricing", read 2026-10-12), which is why this reads one
182
+ // request's totals and cannot be asked of a sum.
183
+ function billed_model(usage: ModelUsage): string {
184
+ const long_id = `${known_id(usage.model) ?? usage.model}${LONG_PROMPT_SUFFIX}`
185
+ const is_long = cost_usage.billed_input(usage.totals) > LONG_PROMPT_THRESHOLD
186
+
187
+ return is_long && Object.hasOwn(MODEL_PRICES, long_id) ? long_id : usage.model
188
+ }
189
+
132
190
  // Per-model, because a session routinely mixes tiers — a subagent on Haiku inside an Opus run — and
133
- // one blended rate would be wrong for both.
191
+ // one blended rate would be wrong for both. Grouped by `billed_model`, so each record is one request:
192
+ // a model priced by prompt length is split into the rows its requests were billed at.
134
193
  function cost_by_model(records: ReadonlyArray<ModelUsage>): Array<ModelCost> {
135
194
  const grouped = new Map<string, UsageTotals>()
136
195
 
137
196
  for (const record of records) {
138
- const running = grouped.get(record.model) ?? cost_usage.EMPTY_TOTALS
197
+ const model = billed_model(record)
198
+ const running = grouped.get(model) ?? cost_usage.EMPTY_TOTALS
139
199
 
140
- grouped.set(record.model, cost_usage.add_totals(running, record.totals))
200
+ grouped.set(model, cost_usage.add_totals(running, record.totals))
141
201
  }
142
202
 
143
203
  return [...grouped].map(([model, totals]) => to_model_cost(model, totals))
@@ -162,7 +222,8 @@ function cost_of(records: ReadonlyArray<ModelUsage>): number {
162
222
  const cost_pricing = {
163
223
  CACHE_WRITE_5M_MULTIPLIER,
164
224
  CACHE_WRITE_1H_MULTIPLIER,
165
- CACHE_READ_MULTIPLIER,
225
+ DEFAULT_CACHE_READ_MULTIPLIER,
226
+ LONG_PROMPT_THRESHOLD,
166
227
  resolve_price,
167
228
  estimate_cost,
168
229
  estimate_composition,
@@ -76,7 +76,7 @@ const DOCUMENT_BYTE_BUDGET: ReadonlyArray<DocumentBudget> = [
76
76
  { path: 'docs/josh-commands-automation.md', bytes: 53_248 },
77
77
  { path: 'docs/josh-commands-run.md', bytes: 65_536 },
78
78
  { path: 'docs/josh-commands-backlog.md', bytes: 57_344 },
79
- { path: 'docs/josh-commands.md', bytes: 45_056 },
79
+ { path: 'docs/josh-commands.md', bytes: 49_152 },
80
80
  { path: 'docs/labels-and-run-states.md', bytes: 8192 },
81
81
  { path: 'prompts/coding-standards.md', bytes: 8192 },
82
82
  { path: 'prompts/collaboration-workflow.md', bytes: 8192 },
@@ -9,11 +9,12 @@
9
9
  // was worth moving off the entry read.
10
10
  //
11
11
  // **The rate is not defined here.** The per-million-token input rate and the cache-read multiplier
12
- // come from `cost-pricing.ts` — the same constants `josh cost` and `josh time` price a run with — so
12
+ // come from `cost-pricing.ts` — the same price table `josh cost` and `josh time` price a run with — so
13
13
  // there is no second price list to drift. This file is the model (a token rides every request as a
14
14
  // cache read) and the one assumption the model needs (how many requests a run makes); the arithmetic
15
15
  // is `cost_pricing.estimate_cost`'s.
16
16
 
17
+ import { agent_role_profile } from '#scripts/agent/agent-role-profile'
17
18
  import { cost_pricing } from '#scripts/cost-runtime/cost-pricing'
18
19
  import { cost_usage } from '#scripts/cost-runtime/cost-usage'
19
20
 
@@ -23,10 +24,10 @@ import { cost_usage } from '#scripts/cost-runtime/cost-usage'
23
24
  // much per entry-read token, so the figure is "per run of this size", never an absolute.
24
25
  const ASSUMED_REQUESTS = 118
25
26
 
26
- // The model these runs execute on. Its base input rate and the cache-read multiplier are read from
27
- // the price table rather than written here, so the dollar figure tracks any price change with no edit
28
- // to this file.
29
- const REFERENCE_MODEL = 'claude-opus-4-8'
27
+ // The model these runs execute on — the worker's default, read from the profile that launches a lane
28
+ // rather than written here, so a model migration moves the dollar figure with no edit to this file.
29
+ // Its base input rate and cache-read multiplier come from the price table in the same way.
30
+ const REFERENCE_MODEL = agent_role_profile.DEFAULT_PROFILES.worker.model
30
31
 
31
32
  // A token read at the entry is written to cache once and then read back on every request. Modelled as
32
33
  // a pure cache read across the run — the one-time write is under 1% of the total and folding it in
@@ -45,6 +46,7 @@ function dollars_per_run(tokens: number): number {
45
46
 
46
47
  const read_set_pricing = {
47
48
  ASSUMED_REQUESTS,
49
+ REFERENCE_MODEL,
48
50
  dollars_per_run,
49
51
  }
50
52
 
@@ -111,7 +111,7 @@ function is_still_runnable(
111
111
  // A read that failed withholds the candidate rather than confirming it. "Could not tell" is not
112
112
  // "nothing blocks it", and this is the one direction the guard may not fail in, because that answer
113
113
  // *starts* work — the same reason `epic-busy.ts` answers `wait` for a listing it could not read. The
114
- // caller then offers the next candidate or waits, and the whole-run timeout bounds the wait.
114
+ // caller then offers the next candidate or waits, and the idle wait bounds the wait.
115
115
  async function read_or_withhold(
116
116
  candidate: EpicChild,
117
117
  read_blockers: BlockersReader,
@@ -80,7 +80,7 @@ const NOT_FOUND_STATUS = 404
80
80
  //
81
81
  // **`absent` is not `unreadable`.** A repository with no
82
82
  // manifest, or one whose manifest says `private`, publishes nothing — so a closed blocker there has
83
- // no release anyone could wait for, and waiting means waiting until the run's own eight-hour timeout
83
+ // no release anyone could wait for, and waiting means waiting until the run's idle wait runs out
84
84
  // with nothing an operator can edit to clear it. A read that merely failed says none of that.
85
85
  //
86
86
  // **It is read from the repository rather than from the registry, and that is the whole design.** The
@@ -123,7 +123,7 @@ type WorkspaceCheck = 'workspace' | 'single' | 'unreadable'
123
123
  //
124
124
  // **The file's existence proves nothing.** `josh sync` distributes it to every consumer regardless of
125
125
  // layout — it carries `allowBuilds`, `overrides` and the like — so probing for its presence would read
126
- // every private first-party repository as a workspace and wait the eight-hour timeout on each. What
126
+ // every private first-party repository as a workspace and wait the whole idle wait on each. What
127
127
  // makes a repository a workspace is a non-empty `packages`
128
128
  // key, so that is what is read.
129
129
  function declares_packages(yaml: string): boolean {
@@ -271,7 +271,7 @@ function read_default_branch_version(repo: string): string | undefined {
271
271
  // would start a dependent before its blocker's release existed. What is asked is only whether the
272
272
  // repository *is* a workspace — the members are not enumerated and nothing looks for which of them
273
273
  // publishes, so a workspace whose members are all private waits when it need not. That is the safe
274
- // direction: waiting ends at the run's own timeout, resolving early does not end at all.
274
+ // direction: waiting ends when the run's idle wait runs out, resolving early does not end at all.
275
275
  function publishes_nothing(repo: string): boolean {
276
276
  const answer = read_manifest(repo)
277
277
 
@@ -285,7 +285,7 @@ function publishes_nothing(repo: string): boolean {
285
285
  //
286
286
  // **"Ships nothing" is asked first, and it is a different question.** Waiting is right only where
287
287
  // something could still arrive; a closed blocker in a repository that publishes no package has no
288
- // release for `is_published` to ever find, so it would wait until the run's own eight-hour timeout
288
+ // release for `is_published` to ever find, so it would wait until the run's idle wait runs out
289
289
  // with nothing an operator could edit to clear it. It is answered from that repository's own manifest and never from
290
290
  // the registry, for the reason `ManifestAnswer` records: a registry 404 also means "this token may
291
291
  // not see it", and resolving on one would start a consumer child before its blocker's release
@@ -194,7 +194,7 @@ function refuse(reason: string): number {
194
194
 
195
195
  // The verdict as it applies to *this* repository. `run` never reaches a caller here: it means some
196
196
  // other repository has work, which for this session is something to wait on rather than a state its
197
- // loop has a branch for. The whole-run timeout is what bounds that wait.
197
+ // loop has a branch for. The idle wait is what bounds that wait.
198
198
  function repo_verdict(verdict: LaneOffer['verdict']): LaneOffer['verdict'] {
199
199
  return verdict === 'run' ? 'wait' : verdict
200
200
  }
@@ -2,6 +2,7 @@ import { read_spawn_stderr, read_spawn_stdout } from '#scripts/lib/spawn-exit'
2
2
  import { GIT_TIMEOUT_MS } from '#scripts/lib/timeouts'
3
3
  import { execaSync } from 'execa'
4
4
  import { git_utilities } from './constants'
5
+ import { git_location_environment } from './git-location-environment'
5
6
 
6
7
  // The one place a synchronous git command is spawned from — the sync
7
8
  // counterpart of `git-spawn.ts`. Every caller here wants an answer rather than an exception, so a
@@ -51,8 +52,15 @@ function read(args: ReadonlyArray<string>, options: GitSyncOptions = {}): string
51
52
  }
52
53
 
53
54
  // The configured `origin` URL, or undefined when the repository has none.
55
+ //
56
+ // **The git location variables are cleared, so `cwd` is the repository that answers.** A hook exports
57
+ // `GIT_DIR`, which beats `cwd` outright — a caller reached from inside `pre-push` would otherwise be
58
+ // told the `origin` of the repository being pushed rather than of the directory it named.
54
59
  function origin_url(cwd: string): string | undefined {
55
- return read(['config', '--get', 'remote.origin.url'], { cwd })
60
+ return read(['config', '--get', 'remote.origin.url'], {
61
+ cwd,
62
+ env: git_location_environment.location_free_environment(),
63
+ })
56
64
  }
57
65
 
58
66
  // The absolute repository (or worktree) root, asked under the C locale so a caller can tell "not a
@@ -126,20 +126,22 @@ async function outcome_of(raw_payload: string): Promise<StopOutcome> {
126
126
 
127
127
  // A live carry record is in place — `carried` alone reads as a run. Both report checks below are about
128
128
  // a carried run: the strand judge reads this very record and returns on anything but `carried`, and a
129
- // stall needs a backlog driver, which begins one before its plan. A spent (`expired`) or `unreadable`
130
- // record a crashed run left behind is no run either, so it does not re-arm the stall's backlog probe
131
- // on every stop.
129
+ // stall needs a backlog driver, which begins one before its plan. An `unreadable`
130
+ // record a crashed run left behind is no run either.
131
+ // The answer is whether that run's owner is still live, and `undefined` where there is no run at all.
132
132
  // A failed git read — outside a repository, `git rev-parse` exits non-zero — reads as no run, since this
133
133
  // gate sits ahead of the checks' own swallowing and must not throw into the stop decision either.
134
- async function has_run_record(): Promise<boolean> {
134
+ async function carried_owner_live(): Promise<boolean | undefined> {
135
135
  try {
136
136
  const directory = await run_carry.repository_directory()
137
137
 
138
- if (directory === undefined) return false
138
+ if (directory === undefined) return undefined
139
139
 
140
- return run_carry.read_carry(run_carry.carry_path(directory)).kind === 'carried'
140
+ const read = run_carry.read_carry(run_carry.carry_path(directory))
141
+
142
+ return read.kind === 'carried' ? run_carry.is_owner_live(read.carry) : undefined
141
143
  } catch {
142
- return false
144
+ return undefined
143
145
  }
144
146
  }
145
147
 
@@ -149,10 +151,19 @@ async function has_run_record(): Promise<boolean> {
149
151
  // — the budget handed off, the owner dead, no supervisor watching. Both only report and swallow their
150
152
  // own failures, so neither can change the stop decision. An ordinary stop with no run record skips
151
153
  // both, so it pays one directory read rather than their reads and a stale stream's backlog probe.
154
+ //
155
+ // **A stall is a live driver not dispatching, so a record whose owner is gone is not asked.** A record
156
+ // stands until a person carries or discards it, so a crashed run's would otherwise re-arm the stall's
157
+ // `git worktree list` and network backlog probe on every stop in the repository, for a run nobody
158
+ // drives. A gone driver is the strand check's question — handed off or not (`run-stranded.ts` →
159
+ // `had_driver`) — and that one reads local files only.
152
160
  async function run_report_checks(): Promise<void> {
153
- if (!(await has_run_record())) return
161
+ const is_owner_live = await carried_owner_live()
162
+
163
+ if (is_owner_live === undefined) return
164
+
165
+ if (is_owner_live) await backlog_stalled_detect.run_stall_check(backlog_ready.DEFAULT_PORTS)
154
166
 
155
- await backlog_stalled_detect.run_stall_check(backlog_ready.DEFAULT_PORTS)
156
167
  await run_stranded_detect.run_stranded_check()
157
168
  }
158
169
 
@@ -16,6 +16,7 @@ import { init_logic } from './init-logic'
16
16
  import { package_path, PROJECT_ROOT } from './init-paths'
17
17
  import { init_sonar } from './init-sonar'
18
18
  import type { ProjectShape } from './project-profile'
19
+ import { security_policy } from './security-policy'
19
20
 
20
21
  const WORKSPACE_YAML = 'pnpm-workspace.yaml'
21
22
  const CLAUDE_MD_FILENAME = 'CLAUDE.md'
@@ -23,7 +24,7 @@ const BASIC_CLAUDE_IMPORT = '@node_modules/@joshuafolkken/kit/dist/CLAUDE.basic.
23
24
  const PRETTIER_IGNORE = '.prettierignore'
24
25
  const GIT_ATTRIBUTES = '.gitattributes'
25
26
  const CODE_OF_CONDUCT = 'CODE_OF_CONDUCT.md'
26
- const SECURITY_MD = 'SECURITY.md'
27
+ const { SECURITY_MD } = security_policy
27
28
  const BASIC_AI_FILES = [
28
29
  'AGENTS.md',
29
30
  'GEMINI.md',
@@ -38,6 +39,10 @@ const BASIC_SOURCES: Readonly<Record<string, string>> = {
38
39
  [PRETTIER_IGNORE]: 'templates/prettierignore.basic',
39
40
  [WORKSPACE_YAML]: 'templates/pnpm-workspace.basic.yaml',
40
41
  }
42
+ // Written from a template of the consumer's own on every profile, not from kit's same-named file.
43
+ const CONSUMER_SOURCES: Readonly<Record<string, string>> = {
44
+ [SECURITY_MD]: security_policy.SECURITY_TEMPLATE,
45
+ }
41
46
  const GITHUB_FILES = new Set([CODE_OF_CONDUCT, SECURITY_MD])
42
47
  const BASIC_POINTER_MAPPING = { src: 'templates/cursorrules.basic', dest: '.cursorrules' }
43
48
  const PULL_REQUEST_TEMPLATE = '.github/pull_request_template.md'
@@ -143,14 +148,16 @@ function did_skip_workspace_yaml_copy(source_path: string, destination_path: str
143
148
  }
144
149
 
145
150
  // The package file an AI file is copied from: a basic project gets its own template for a few of
146
- // them, every other file is copied from the path it is written to.
151
+ // them, a few are a consumer template on every profile, and every other file is copied from the
152
+ // path it is written to.
147
153
  function ai_file_source(filename: string, shape?: ProjectShape): string {
148
154
  const basic_source = shape?.profile === 'basic' ? BASIC_SOURCES[filename] : undefined
149
155
 
150
- return basic_source ?? filename
156
+ return basic_source ?? CONSUMER_SOURCES[filename] ?? filename
151
157
  }
152
158
 
153
159
  function did_skip_ai_file_copy(filename: string, shape?: ProjectShape): boolean {
160
+ if (security_policy.did_skip_unresolved(filename, PROJECT_ROOT)) return false
154
161
  const source_path = package_path(ai_file_source(filename, shape))
155
162
  const destination_path = path.join(PROJECT_ROOT, filename)
156
163
 
@@ -5,6 +5,7 @@ import { claude_kit_only_settings } from './claude-kit-only-settings'
5
5
  import { claude_plugin_config } from './claude-plugin-config'
6
6
  import { hook_command_rewrite } from './hook-command-rewrite'
7
7
  import { init_logic } from './init-logic'
8
+ import { security_policy } from './security-policy'
8
9
 
9
10
  /**
10
11
  * The single transform applied to every file kit copies verbatim into a consumer
@@ -29,6 +30,10 @@ import { init_logic } from './init-logic'
29
30
  * Keeping both on this one function is the same guarantee as the workflow passes — a copy path that
30
31
  * skipped the plugin block would ship a settings file that never enables the plugin, and one that
31
32
  * skipped the rewrite would leave every consumer hook slower than it has to be.
33
+ *
34
+ * The consumer's `SECURITY.md` gets one further pass: the repository it lands in is filled into the
35
+ * template, so its advisory and release links name that repository and not kit (see
36
+ * security-policy.ts).
32
37
  */
33
38
  function transform_copied_content(destination_path: string, content: string): string {
34
39
  const with_paths = init_logic.transform_distributed_paths(content)
@@ -47,10 +52,12 @@ function transform_copied_content(destination_path: string, content: string): st
47
52
  with_kit_only_stripped,
48
53
  )
49
54
 
50
- return hook_command_rewrite.apply_hook_command_rewrite_for_destination(
55
+ const with_hooks = hook_command_rewrite.apply_hook_command_rewrite_for_destination(
51
56
  destination_path,
52
57
  with_plugin,
53
58
  )
59
+
60
+ return security_policy.apply_repository_for_destination(destination_path, with_hooks)
54
61
  }
55
62
 
56
63
  export { transform_copied_content }