@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.
- package/.claude/skills/dependency-update/SKILL.md +6 -3
- package/.claude/skills/workflow-commands/backlogrun-progress.md +2 -3
- package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -4
- package/.claude/skills/workflow-commands/retrospective.md +1 -2
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/{chunk-UTHMB4O2.js → chunk-2X2ART5G.js} +4 -33
- package/dist/hooks/{chunk-NZN2UFI6.js → chunk-4MGHBTOM.js} +162 -233
- package/dist/hooks/{chunk-YLBBSSTF.js → chunk-6AC4C2IO.js} +91 -67
- package/dist/hooks/{chunk-AKGCP4LX.js → chunk-6MAW7BCM.js} +12 -12
- package/dist/hooks/{chunk-JCRDBQXB.js → chunk-7VSZ2N4B.js} +3 -3
- package/dist/hooks/{chunk-GG72ANKI.js → chunk-KC6LUZZ2.js} +1 -1
- package/dist/hooks/{chunk-BZMPVFDY.js → chunk-KCF2YE7O.js} +5 -5
- package/dist/hooks/{chunk-7WKYHEGU.js → chunk-LC56R53A.js} +3 -3
- package/dist/hooks/{chunk-V4TKZHER.js → chunk-LOJTBU5V.js} +3 -3
- package/dist/hooks/{chunk-JGW24KKM.js → chunk-RYP3TSUJ.js} +3 -3
- package/dist/hooks/{chunk-SLSFIWTX.js → chunk-TT742SIO.js} +8 -5
- package/dist/hooks/{chunk-Z5TMJIPW.js → chunk-Y5R47QHU.js} +1 -1
- package/dist/hooks/{chunk-7UBFU3CL.js → chunk-YKUITFT7.js} +2 -2
- package/dist/hooks/codex-hook-adapter.js +13 -13
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +3 -3
- package/dist/hooks/inputs.json +190 -191
- package/dist/hooks/investigation-guard.js +7 -7
- package/dist/hooks/pretool-guard.js +12 -12
- package/dist/hooks/session-lang.js +3 -3
- package/dist/hooks/stop-guard.js +19 -15
- package/dist/josh.js +40 -5
- package/package.json +4 -4
- package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
- package/scripts/adopt/adopt-logic.ts +4 -1
- package/scripts/adopt/adopt.ts +7 -4
- package/scripts/backlog/backlog-budget-cli.ts +2 -20
- package/scripts/backlog/backlog-budget.ts +72 -109
- package/scripts/backlog/backlog-drive-cli.ts +10 -3
- package/scripts/backlog/backlog-drive.ts +5 -2
- package/scripts/backlog/backlog-offer-cli.ts +1 -15
- package/scripts/cost-runtime/context-cut-payback.ts +9 -4
- package/scripts/cost-runtime/cost-pricing.ts +94 -33
- package/scripts/document/document-byte-budget.ts +1 -1
- package/scripts/document/read-set-pricing.ts +7 -5
- package/scripts/epic/epic-candidate-confirm.ts +1 -1
- package/scripts/epic/epic-cross-repo.ts +4 -4
- package/scripts/epic/epic-next.ts +1 -1
- package/scripts/git/git-spawn-sync.ts +9 -1
- package/scripts/hooks/stop-guard.ts +20 -9
- package/scripts/init/init-ai-copy.ts +10 -3
- package/scripts/init/init-copy-content.ts +8 -1
- package/scripts/init/security-policy.ts +79 -0
- package/scripts/josh/josh-commands-ai.ts +1 -1
- package/scripts/josh/josh-commands-maintenance.ts +3 -0
- package/scripts/josh/josh-commands-project.ts +1 -1
- package/scripts/josh/josh-commands-versioning.ts +2 -1
- package/scripts/lane/lane-cli.ts +1 -3
- package/scripts/propagate/propagate-run.ts +17 -1
- package/scripts/propagate/propagate-steps.ts +9 -1
- package/scripts/run/board/run-board-header.ts +107 -84
- package/scripts/run/board/run-board-labels.ts +18 -17
- package/scripts/run/board/run-board-machine.ts +21 -8
- package/scripts/run/board/run-board-render.ts +1 -5
- package/scripts/run/board/run-board-span.ts +0 -1
- package/scripts/run/board/run-board-status.ts +0 -17
- package/scripts/run/board/run-board-tick.ts +1 -0
- package/scripts/run/carry/run-carry-cli.ts +8 -62
- package/scripts/run/carry/run-carry-ended.ts +1 -1
- package/scripts/run/carry/run-carry.ts +22 -68
- package/scripts/run/cut/run-cut.ts +4 -4
- package/scripts/run/event/run-event-render.ts +0 -1
- package/scripts/run/event/run-event-scope.ts +4 -8
- package/scripts/run/event/run-event-stream-emit.ts +0 -25
- package/scripts/run/event/run-event-stream.ts +23 -31
- package/scripts/run/merge/run-merge-collect.ts +1 -1
- package/scripts/run/merge/run-merge-steps.ts +1 -1
- package/scripts/run/progress/run-progress-read.ts +1 -1
- package/scripts/run/run-headless.ts +2 -2
- package/scripts/run/run-status-cli.ts +1 -5
- package/scripts/run/run-step.ts +2 -9
- package/scripts/run/run-stop-notify.ts +1 -1
- package/scripts/run/run-stranded-detect.ts +3 -3
- package/scripts/run/run-stranded.ts +25 -17
- package/scripts/run/wake/run-wake-cli.ts +7 -9
- package/scripts/run/wake/run-wake-describe.ts +3 -5
- package/scripts/run/wake/run-wake-driver.ts +3 -3
- package/scripts/run/wake/run-wake-loop.ts +4 -21
- package/scripts/run/wake/run-wake-restart.ts +1 -1
- package/scripts/run/wake/run-wake.ts +6 -7
- package/scripts/sync/sync-ai-files.ts +2 -0
- package/scripts/version/latest-update.ts +20 -6
- package/scripts/version/peer-floor.ts +99 -0
- package/scripts/version/playwright-browsers.ts +109 -0
- package/scripts/version/publishable-range-check.ts +41 -4
- package/templates/SECURITY.md +23 -0
- 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'
|
|
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 =
|
|
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
|
|
356
|
-
//
|
|
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
|
-
// `
|
|
30
|
-
// preamble rewrite costs this many per-request cache reads. The base input rate is common to both
|
|
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.
|
|
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
|
|
4
|
-
//
|
|
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
|
|
7
|
-
// cache write 1.25x, a 1-hour cache write 2x, and output 5x. On a measured
|
|
8
|
-
// `cache_read_input_tokens` 97,190 against `input_tokens` 2, an estimate that ignored
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// rate as a 9k-token request)".
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// concern.)
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
.toSorted((left, right) => right.length - left.length)
|
|
107
|
+
return id === undefined ? undefined : MODEL_PRICES[id]
|
|
108
|
+
}
|
|
64
109
|
|
|
65
|
-
|
|
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 *
|
|
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
|
-
//
|
|
81
|
-
// `estimate_cost(totals, price)`. It is a decomposition of the same arithmetic, not a second
|
|
82
|
-
// list, which is why the weights live
|
|
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 *
|
|
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
|
|
197
|
+
const model = billed_model(record)
|
|
198
|
+
const running = grouped.get(model) ?? cost_usage.EMPTY_TOTALS
|
|
139
199
|
|
|
140
|
-
grouped.set(
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
const REFERENCE_MODEL =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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'], {
|
|
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.
|
|
130
|
-
// record a crashed run left behind is no run either
|
|
131
|
-
//
|
|
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
|
|
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
|
|
138
|
+
if (directory === undefined) return undefined
|
|
139
139
|
|
|
140
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 }
|