@joshuafolkken/kit 1.1373.0 → 1.1379.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/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/{chunk-BZMPVFDY.js → chunk-4RWRP3FF.js} +5 -5
- package/dist/hooks/{chunk-V4TKZHER.js → chunk-546X4PX3.js} +3 -3
- package/dist/hooks/{chunk-JGW24KKM.js → chunk-7ZM464OW.js} +3 -3
- package/dist/hooks/{chunk-GG72ANKI.js → chunk-DHSKBAQ2.js} +1 -1
- package/dist/hooks/{chunk-7WKYHEGU.js → chunk-E7LZL5AN.js} +3 -3
- package/dist/hooks/{chunk-AKGCP4LX.js → chunk-HGA4EG5S.js} +11 -11
- package/dist/hooks/{chunk-Z5TMJIPW.js → chunk-NBEAGPS4.js} +1 -1
- package/dist/hooks/{chunk-7UBFU3CL.js → chunk-PV3FWDSQ.js} +2 -2
- package/dist/hooks/{chunk-JCRDBQXB.js → chunk-QARPP36N.js} +3 -3
- package/dist/hooks/{chunk-NZN2UFI6.js → chunk-RW4XMF47.js} +37 -1
- package/dist/hooks/{chunk-YLBBSSTF.js → chunk-TG2AB3W6.js} +50 -16
- package/dist/hooks/{chunk-SLSFIWTX.js → chunk-TORRSNDJ.js} +7 -4
- package/dist/hooks/{chunk-UTHMB4O2.js → chunk-YSDAH73S.js} +4 -33
- 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 +209 -209
- 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 +6 -6
- package/dist/josh.js +39 -4
- package/package.json +4 -4
- package/scripts/adopt/adopt-logic.ts +4 -1
- package/scripts/adopt/adopt.ts +7 -4
- 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/git/git-spawn-sync.ts +9 -1
- 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-maintenance.ts +3 -0
- package/scripts/josh/josh-commands-project.ts +1 -1
- package/scripts/josh/josh-commands-versioning.ts +2 -1
- package/scripts/propagate/propagate-run.ts +17 -1
- package/scripts/propagate/propagate-steps.ts +9 -1
- 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/dist/josh.js
CHANGED
|
@@ -46,6 +46,35 @@ var git_utilities = {
|
|
|
46
46
|
get_git_command_for_spawn
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
// scripts/git/git-location-environment.ts
|
|
50
|
+
var GIT_LOCATION_VARIABLES = [
|
|
51
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
52
|
+
"GIT_COMMON_DIR",
|
|
53
|
+
"GIT_DIR",
|
|
54
|
+
"GIT_INDEX_FILE",
|
|
55
|
+
"GIT_NAMESPACE",
|
|
56
|
+
"GIT_OBJECT_DIRECTORY",
|
|
57
|
+
"GIT_WORK_TREE"
|
|
58
|
+
];
|
|
59
|
+
function location_free_environment() {
|
|
60
|
+
return Object.fromEntries(GIT_LOCATION_VARIABLES.map((name) => [name, void 0]));
|
|
61
|
+
}
|
|
62
|
+
function forget(name) {
|
|
63
|
+
Reflect.deleteProperty(process.env, name);
|
|
64
|
+
}
|
|
65
|
+
function restore_one(name, value) {
|
|
66
|
+
if (value === void 0) forget(name);
|
|
67
|
+
else process.env[name] = value;
|
|
68
|
+
}
|
|
69
|
+
function clear_git_location_variables() {
|
|
70
|
+
const previous = GIT_LOCATION_VARIABLES.map((name) => [name, process.env[name]]);
|
|
71
|
+
for (const [name] of previous) forget(name);
|
|
72
|
+
return () => {
|
|
73
|
+
for (const [name, value] of previous) restore_one(name, value);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
var git_location_environment = { clear_git_location_variables, location_free_environment };
|
|
77
|
+
|
|
49
78
|
// scripts/git/git-spawn-sync.ts
|
|
50
79
|
var SUCCESS_EXIT_CODE = 0;
|
|
51
80
|
var C_LOCALE = { LC_ALL: "C", LANGUAGE: "C" };
|
|
@@ -67,7 +96,10 @@ function read(args, options = {}) {
|
|
|
67
96
|
return result.exit_code === SUCCESS_EXIT_CODE ? result.stdout.trim() : void 0;
|
|
68
97
|
}
|
|
69
98
|
function origin_url(cwd) {
|
|
70
|
-
return read(["config", "--get", "remote.origin.url"], {
|
|
99
|
+
return read(["config", "--get", "remote.origin.url"], {
|
|
100
|
+
cwd,
|
|
101
|
+
env: git_location_environment.location_free_environment()
|
|
102
|
+
});
|
|
71
103
|
}
|
|
72
104
|
function toplevel(timeout = GIT_TIMEOUT_MS) {
|
|
73
105
|
return run(["rev-parse", "--show-toplevel"], { env: C_LOCALE, timeout });
|
|
@@ -2448,7 +2480,10 @@ var MAINTENANCE_COMMANDS = {
|
|
|
2448
2480
|
script: "scripts/version/latest-update.ts",
|
|
2449
2481
|
description: "Update all dependencies to latest",
|
|
2450
2482
|
category: "Maintenance",
|
|
2451
|
-
reference: ["", "developer", ["files", "network"]]
|
|
2483
|
+
reference: ["", "developer", ["files", "network"]],
|
|
2484
|
+
// `JOSH_PLAYWRIGHT_BROWSERS` widens the browsers a Playwright bump installs, and a project
|
|
2485
|
+
// keeps that setting in its `.env`.
|
|
2486
|
+
tsx_arguments: OPTIONAL_ENV_FILE_FLAGS
|
|
2452
2487
|
}
|
|
2453
2488
|
};
|
|
2454
2489
|
|
|
@@ -2536,7 +2571,7 @@ var PROJECT_COMMANDS = {
|
|
|
2536
2571
|
},
|
|
2537
2572
|
adopt: {
|
|
2538
2573
|
script: "scripts/adopt/adopt.ts",
|
|
2539
|
-
description: "Upgrade
|
|
2574
|
+
description: "Upgrade the @joshuafolkken toolkits and other dependencies here, open the PR",
|
|
2540
2575
|
category: "Project",
|
|
2541
2576
|
reference: ["[--dry-run]", "developer", ["files", "git", "network", "processes"]]
|
|
2542
2577
|
}
|
|
@@ -2562,7 +2597,7 @@ var VERSIONING_COMMANDS = {
|
|
|
2562
2597
|
// before chaining here, exactly as it already does for `latest:update`.
|
|
2563
2598
|
ranges: {
|
|
2564
2599
|
script: "scripts/version/publishable-range-check.ts",
|
|
2565
|
-
description: "Check that every published dependency range still resolves for a consumer",
|
|
2600
|
+
description: "Check that every published dependency range still resolves for a consumer and no peer range falls below an installed toolkit",
|
|
2566
2601
|
category: "Versioning",
|
|
2567
2602
|
reference: ["", "developer", ["network"]]
|
|
2568
2603
|
},
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
2
|
+
"version": "1.1379.0",
|
|
3
3
|
"name": "@joshuafolkken/kit",
|
|
4
4
|
"description": "Development toolkit for AI-assisted projects: formatting, AI assistant rules and a Git/GitHub Issue workflow for any repository, plus lint, type check, tests and hooks for Node projects",
|
|
5
5
|
"keywords": [
|
|
@@ -264,10 +264,10 @@
|
|
|
264
264
|
"eslint_d": "^15.0.3",
|
|
265
265
|
"globals": "^17.13.0",
|
|
266
266
|
"lefthook": "^2.2.1",
|
|
267
|
-
"prettier": "^3.9.
|
|
267
|
+
"prettier": "^3.9.10",
|
|
268
268
|
"prettier-plugin-svelte": "^4.1.1",
|
|
269
269
|
"prettier-plugin-tailwindcss": "^0.8.1",
|
|
270
|
-
"rollup": "^4.64.
|
|
270
|
+
"rollup": "^4.64.5",
|
|
271
271
|
"rollup-plugin-dts": "^6.5.1",
|
|
272
272
|
"secretlint": "^13.0.7",
|
|
273
273
|
"typescript": "^6.0.3",
|
|
@@ -289,7 +289,7 @@
|
|
|
289
289
|
"devEngines": {
|
|
290
290
|
"packageManager": {
|
|
291
291
|
"name": "pnpm",
|
|
292
|
-
"version": "12.
|
|
292
|
+
"version": "12.12.0+sha512.2ee06ac86db554cf384421461220f0e336a487bfd41b2acd04e17a86b1487832fe45e8d1743f7fc52acc77de12d2356b536fedf8229fc483239e40b13d474449",
|
|
293
293
|
"onFail": "download"
|
|
294
294
|
}
|
|
295
295
|
},
|
|
@@ -8,7 +8,10 @@ import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
|
|
|
8
8
|
// The decisions `josh adopt` makes before any step runs: whether this repository may adopt at all,
|
|
9
9
|
// which repository the issue is opened in, and what the run is carrying.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// It names `josh latest` because the issue title names only the toolkits: without this sentence the
|
|
12
|
+
// other dependency bumps in the pull request have nothing in the issue that accounts for them.
|
|
13
|
+
const ADOPT_ORIGIN =
|
|
14
|
+
'Opened by `josh adopt` in this repository, which also ran `josh latest` — the other dependencies and the pnpm pin are updated alongside.'
|
|
12
15
|
const INCOMPLETE_HEADLINE =
|
|
13
16
|
'Refusing to adopt: these declared toolkits would fall out of the plan, and the run would sync the rest without them.'
|
|
14
17
|
const INCOMPLETE_EFFECT =
|
package/scripts/adopt/adopt.ts
CHANGED
|
@@ -13,9 +13,11 @@ import { adopt_toolkits } from './adopt-toolkits'
|
|
|
13
13
|
//
|
|
14
14
|
// It is `josh propagate` seen from the other end: propagation stands in the supplier and pushes one
|
|
15
15
|
// released version out to every consumer, adoption stands in the consumer and pulls whatever is
|
|
16
|
-
// newest in. The step sequence is
|
|
17
|
-
//
|
|
18
|
-
// default branch cannot drift between the
|
|
16
|
+
// newest in. The step sequence is the same one with `josh latest` added — `propagate_run
|
|
17
|
+
// .ADOPT_STEP_ORDER`, derived from `STEP_ORDER` and run through `propagate_run.run_target` — so the
|
|
18
|
+
// working-tree pre-check, the ordering and the return to the default branch cannot drift between the
|
|
19
|
+
// two. `josh latest` is adoption's alone: catching a consumer up means its other dependencies and its
|
|
20
|
+
// pnpm pin too, in the same pull request, where propagation carries one exact release and nothing else.
|
|
19
21
|
//
|
|
20
22
|
// It stops at the open pull request. Merging is what `pnpm josh followup` does, under an
|
|
21
23
|
// authorization a CLI does not have.
|
|
@@ -62,7 +64,7 @@ function run_sequence(target: PropagateTarget, plan: ReleasePlan | undefined): n
|
|
|
62
64
|
plan === undefined ? propagate_steps.describe_step : propagate_steps.create_step_runner(plan)
|
|
63
65
|
const reason = plan === undefined ? DRY_RUN_REASON : propagate_run.PROPAGATED_REASON
|
|
64
66
|
|
|
65
|
-
return report(propagate_run.run_target(target, step, reason))
|
|
67
|
+
return report(propagate_run.run_target(target, step, reason, propagate_run.ADOPT_STEP_ORDER))
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
// A declared toolkit that cannot be carried stops the run, whichever of the two ways it fell out —
|
|
@@ -158,6 +160,7 @@ const adopt = {
|
|
|
158
160
|
KNOWN_FLAGS,
|
|
159
161
|
NOTHING_TO_ADOPT,
|
|
160
162
|
parse_options,
|
|
163
|
+
run_sequence,
|
|
161
164
|
run_here,
|
|
162
165
|
run,
|
|
163
166
|
}
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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 }
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { repo_origin } from '#scripts/discovery/repo-origin'
|
|
3
|
+
import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
|
|
4
|
+
import { file_reader } from '#scripts/lib/read-file'
|
|
5
|
+
|
|
6
|
+
// A consumer's `SECURITY.md` is its own policy, not kit's: the advisory and release links have to
|
|
7
|
+
// name the repository the file lands in, and kit's account of its own published package is not a
|
|
8
|
+
// fact about that repository. So the consumer copy is written from a template of its own, with the
|
|
9
|
+
// repository filled in, and kit's root `SECURITY.md` stays kit's.
|
|
10
|
+
const SECURITY_MD = 'SECURITY.md'
|
|
11
|
+
const SECURITY_TEMPLATE = 'templates/SECURITY.md'
|
|
12
|
+
const REPOSITORY_PLACEHOLDER = '{{REPOSITORY}}'
|
|
13
|
+
const KIT_ADVISORY_URL = 'https://github.com/joshuafolkken/kit/security/advisories/new'
|
|
14
|
+
const UNRESOLVED_REASON = '`origin` is not a GitHub repository `owner/repo` can be read from'
|
|
15
|
+
const LINK_DELIMITER = /[\s()<>[\]]+/u
|
|
16
|
+
|
|
17
|
+
// Read from the local `origin` rather than asked of GitHub: the basic profile decides whether to
|
|
18
|
+
// write the file from the same remote, and a policy file must not depend on `gh` being signed in.
|
|
19
|
+
function repository_at(root: string): string | undefined {
|
|
20
|
+
const url = git_spawn_sync.origin_url(root)
|
|
21
|
+
const identity = url === undefined ? undefined : repo_origin.parse_origin_url(url)
|
|
22
|
+
|
|
23
|
+
return identity === undefined ? undefined : `${identity.owner}/${identity.repo}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function apply_repository(content: string, repository: string): string {
|
|
27
|
+
return content.replaceAll(REPOSITORY_PLACEHOLDER, () => repository)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Throws rather than writing the placeholder through: both copy paths ask `did_skip_unresolved`
|
|
31
|
+
// first, so reaching here without a repository is a caller that skipped the question.
|
|
32
|
+
function apply_repository_for_destination(destination_path: string, content: string): string {
|
|
33
|
+
if (path.basename(destination_path) !== SECURITY_MD) return content
|
|
34
|
+
const repository = repository_at(path.dirname(destination_path))
|
|
35
|
+
|
|
36
|
+
if (repository === undefined) {
|
|
37
|
+
throw new Error(`${SECURITY_MD} needs a GitHub origin to name the repository it reports to`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return apply_repository(content, repository)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A full project synced before the template existed holds kit's own policy under this name, and the
|
|
44
|
+
// skip leaves that file on disk — still sending its readers to kit's advisory form. The form is
|
|
45
|
+
// matched as a whole link target, not as a substring: a longer address that merely starts with it
|
|
46
|
+
// is another address.
|
|
47
|
+
function does_report_to_kit(root: string): boolean {
|
|
48
|
+
const existing = file_reader.read_if_readable(path.join(root, SECURITY_MD)) ?? ''
|
|
49
|
+
|
|
50
|
+
return new Set(existing.split(LINK_DELIMITER)).has(KIT_ADVISORY_URL)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function unresolved_warning(root: string): string {
|
|
54
|
+
if (!does_report_to_kit(root)) return ` ⚠ skipped ${SECURITY_MD} (${UNRESOLVED_REASON})`
|
|
55
|
+
|
|
56
|
+
return ` ⚠ stale ${SECURITY_MD} still reports vulnerabilities to joshuafolkken/kit — replace it with your own policy or delete it (${UNRESOLVED_REASON})`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Without a GitHub `origin` there is no repository for the advisory link to name, so the file is
|
|
60
|
+
// left unwritten — the next `josh sync` after the remote is added writes it.
|
|
61
|
+
function did_skip_unresolved(filename: string, root: string): boolean {
|
|
62
|
+
if (filename !== SECURITY_MD || repository_at(root) !== undefined) return false
|
|
63
|
+
|
|
64
|
+
console.warn(unresolved_warning(root))
|
|
65
|
+
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const security_policy = {
|
|
70
|
+
SECURITY_MD,
|
|
71
|
+
SECURITY_TEMPLATE,
|
|
72
|
+
REPOSITORY_PLACEHOLDER,
|
|
73
|
+
KIT_ADVISORY_URL,
|
|
74
|
+
repository_at,
|
|
75
|
+
apply_repository_for_destination,
|
|
76
|
+
did_skip_unresolved,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { security_policy }
|
|
@@ -80,6 +80,9 @@ const MAINTENANCE_COMMANDS: Record<string, CommandEntry> = {
|
|
|
80
80
|
description: 'Update all dependencies to latest',
|
|
81
81
|
category: 'Maintenance',
|
|
82
82
|
reference: ['', 'developer', ['files', 'network']],
|
|
83
|
+
// `JOSH_PLAYWRIGHT_BROWSERS` widens the browsers a Playwright bump installs, and a project
|
|
84
|
+
// keeps that setting in its `.env`.
|
|
85
|
+
tsx_arguments: OPTIONAL_ENV_FILE_FLAGS,
|
|
83
86
|
},
|
|
84
87
|
}
|
|
85
88
|
/* eslint-enable @typescript-eslint/naming-convention */
|
|
@@ -88,7 +88,7 @@ const PROJECT_COMMANDS: Record<string, CommandEntry> = {
|
|
|
88
88
|
},
|
|
89
89
|
adopt: {
|
|
90
90
|
script: 'scripts/adopt/adopt.ts',
|
|
91
|
-
description: 'Upgrade
|
|
91
|
+
description: 'Upgrade the @joshuafolkken toolkits and other dependencies here, open the PR',
|
|
92
92
|
category: 'Project',
|
|
93
93
|
reference: ['[--dry-run]', 'developer', ['files', 'git', 'network', 'processes']],
|
|
94
94
|
},
|