@hegemonart/get-design-done 1.25.0 → 1.26.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 (37) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +46 -0
  4. package/README.md +10 -6
  5. package/agents/README.md +60 -0
  6. package/agents/design-reflector.md +43 -0
  7. package/agents/gdd-intel-updater.md +34 -1
  8. package/hooks/budget-enforcer.ts +143 -4
  9. package/package.json +1 -1
  10. package/reference/model-prices.md +40 -19
  11. package/reference/prices/antigravity.md +21 -0
  12. package/reference/prices/augment.md +21 -0
  13. package/reference/prices/claude.md +42 -0
  14. package/reference/prices/cline.md +23 -0
  15. package/reference/prices/codebuddy.md +21 -0
  16. package/reference/prices/codex.md +25 -0
  17. package/reference/prices/copilot.md +21 -0
  18. package/reference/prices/cursor.md +21 -0
  19. package/reference/prices/gemini.md +25 -0
  20. package/reference/prices/kilo.md +21 -0
  21. package/reference/prices/opencode.md +23 -0
  22. package/reference/prices/qwen.md +25 -0
  23. package/reference/prices/trae.md +23 -0
  24. package/reference/prices/windsurf.md +21 -0
  25. package/reference/registry.json +107 -1
  26. package/reference/runtime-models.md +446 -0
  27. package/reference/schemas/runtime-models.schema.json +123 -0
  28. package/scripts/install.cjs +8 -0
  29. package/scripts/lib/budget-enforcer.cjs +446 -0
  30. package/scripts/lib/cost-arbitrage.cjs +294 -0
  31. package/scripts/lib/install/installer.cjs +188 -11
  32. package/scripts/lib/install/parse-runtime-models.cjs +267 -0
  33. package/scripts/lib/install/runtimes.cjs +43 -0
  34. package/scripts/lib/runtime-detect.cjs +96 -0
  35. package/scripts/lib/tier-resolver.cjs +311 -0
  36. package/scripts/validate-frontmatter.ts +138 -1
  37. package/skills/router/SKILL.md +51 -2
@@ -0,0 +1,267 @@
1
+ 'use strict';
2
+ /**
3
+ * parse-runtime-models.cjs — pure parser for reference/runtime-models.md.
4
+ *
5
+ * Reads the per-runtime tier→model adapter source-of-truth (Phase 26 D-01..D-03)
6
+ * and returns a structured object with one entry per runtime. Used by:
7
+ *
8
+ * - scripts/lib/install/installer.cjs (26-03) at install time to emit
9
+ * `models.json` per runtime config-dir.
10
+ * - scripts/lib/tier-resolver.cjs (26-02) at runtime to resolve
11
+ * (runtime, tier) → model.
12
+ *
13
+ * Pure-JS validation against the strict schema at
14
+ * `reference/schemas/runtime-models.schema.json`. No optional dependencies.
15
+ *
16
+ * Public API:
17
+ * parseRuntimeModels({ cwd? }) → {
18
+ * runtimes: [{ id, tier_to_model, reasoning_class_to_model, provenance, single_tier? }, ...],
19
+ * schema_version: 1
20
+ * }
21
+ * parseRuntimeModelsFromString(markdown) → same shape
22
+ *
23
+ * Throws an Error with a specific message on the first validation failure
24
+ * (fail-fast: install-time validation catches typos before runtime).
25
+ */
26
+
27
+ const fs = require('node:fs');
28
+ const path = require('node:path');
29
+
30
+ const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
31
+ const DEFAULT_PATH = path.join(REPO_ROOT, 'reference', 'runtime-models.md');
32
+
33
+ // Mirrors scripts/lib/install/runtimes.cjs RUNTIMES list (Phase 24 D-02 lock).
34
+ // Re-declared rather than imported to keep this parser dependency-free for
35
+ // downstream consumers that may want to call it from outside the install/ tree.
36
+ // Round-trip tested by tests/parse-runtime-models.test.cjs against the canonical
37
+ // runtimes.cjs list.
38
+ const KNOWN_RUNTIME_IDS = Object.freeze([
39
+ 'claude',
40
+ 'codex',
41
+ 'gemini',
42
+ 'qwen',
43
+ 'kilo',
44
+ 'copilot',
45
+ 'cursor',
46
+ 'windsurf',
47
+ 'antigravity',
48
+ 'augment',
49
+ 'trae',
50
+ 'codebuddy',
51
+ 'cline',
52
+ 'opencode',
53
+ ]);
54
+
55
+ const TIER_KEYS = Object.freeze(['opus', 'sonnet', 'haiku']);
56
+ const REASONING_CLASS_KEYS = Object.freeze(['high', 'medium', 'low']);
57
+
58
+ /**
59
+ * Extract every ```json ... ``` fenced block from a markdown string.
60
+ * Returns an array of { raw, lineNumber } objects.
61
+ */
62
+ function extractJsonBlocks(markdown) {
63
+ const blocks = [];
64
+ const re = /```json\s*\n([\s\S]*?)\n```/g;
65
+ let m;
66
+ while ((m = re.exec(markdown)) !== null) {
67
+ // Compute 1-indexed line number of the fence opening for error messages.
68
+ const lineNumber = markdown.slice(0, m.index).split('\n').length;
69
+ blocks.push({ raw: m[1], lineNumber });
70
+ }
71
+ return blocks;
72
+ }
73
+
74
+ function validateModelRow(row, where) {
75
+ if (!row || typeof row !== 'object' || Array.isArray(row)) {
76
+ throw new Error(`${where}: expected object, got ${typeof row}`);
77
+ }
78
+ if (typeof row.model !== 'string' || row.model.length === 0) {
79
+ throw new Error(`${where}: 'model' must be a non-empty string`);
80
+ }
81
+ const allowedKeys = new Set(['model', 'provider_model_id']);
82
+ for (const k of Object.keys(row)) {
83
+ if (!allowedKeys.has(k)) {
84
+ throw new Error(`${where}: unknown key '${k}' (allowed: ${[...allowedKeys].join(', ')})`);
85
+ }
86
+ }
87
+ if (row.provider_model_id !== undefined) {
88
+ if (typeof row.provider_model_id !== 'string' || row.provider_model_id.length === 0) {
89
+ throw new Error(`${where}: 'provider_model_id' must be a non-empty string when present`);
90
+ }
91
+ }
92
+ }
93
+
94
+ function validateProvenance(arr, where) {
95
+ if (!Array.isArray(arr) || arr.length < 1) {
96
+ throw new Error(`${where}: 'provenance' must be a non-empty array`);
97
+ }
98
+ arr.forEach((row, i) => {
99
+ const w = `${where}[${i}]`;
100
+ if (!row || typeof row !== 'object' || Array.isArray(row)) {
101
+ throw new Error(`${w}: expected object`);
102
+ }
103
+ for (const required of ['source_url', 'retrieved_at', 'last_validated_cycle']) {
104
+ if (typeof row[required] !== 'string' || row[required].length === 0) {
105
+ throw new Error(`${w}: '${required}' must be a non-empty string`);
106
+ }
107
+ }
108
+ // ISO-8601 sanity check (Date.parse accepts the canonical form we emit).
109
+ if (Number.isNaN(Date.parse(row.retrieved_at))) {
110
+ throw new Error(`${w}: 'retrieved_at' must be a valid ISO 8601 timestamp (got ${JSON.stringify(row.retrieved_at)})`);
111
+ }
112
+ const allowedKeys = new Set(['source_url', 'retrieved_at', 'last_validated_cycle', 'note']);
113
+ for (const k of Object.keys(row)) {
114
+ if (!allowedKeys.has(k)) {
115
+ throw new Error(`${w}: unknown key '${k}' (allowed: ${[...allowedKeys].join(', ')})`);
116
+ }
117
+ }
118
+ if (row.note !== undefined && typeof row.note !== 'string') {
119
+ throw new Error(`${w}: 'note' must be a string when present`);
120
+ }
121
+ });
122
+ }
123
+
124
+ function validateRuntimeEntry(entry, where) {
125
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
126
+ throw new Error(`${where}: expected object`);
127
+ }
128
+ // Required keys
129
+ for (const required of ['id', 'tier_to_model', 'reasoning_class_to_model', 'provenance']) {
130
+ if (!(required in entry)) {
131
+ throw new Error(`${where}: missing required key '${required}'`);
132
+ }
133
+ }
134
+ // id enum check
135
+ if (typeof entry.id !== 'string' || !KNOWN_RUNTIME_IDS.includes(entry.id)) {
136
+ throw new Error(
137
+ `${where}: 'id' must be one of ${KNOWN_RUNTIME_IDS.join('|')} (got ${JSON.stringify(entry.id)})`,
138
+ );
139
+ }
140
+ // No unknown top-level keys
141
+ const allowedKeys = new Set(['id', 'single_tier', 'tier_to_model', 'reasoning_class_to_model', 'provenance']);
142
+ for (const k of Object.keys(entry)) {
143
+ if (!allowedKeys.has(k)) {
144
+ throw new Error(`${where}: unknown key '${k}' (allowed: ${[...allowedKeys].join(', ')})`);
145
+ }
146
+ }
147
+ if (entry.single_tier !== undefined && typeof entry.single_tier !== 'boolean') {
148
+ throw new Error(`${where}: 'single_tier' must be a boolean when present`);
149
+ }
150
+
151
+ // tier_to_model: requires opus/sonnet/haiku
152
+ if (!entry.tier_to_model || typeof entry.tier_to_model !== 'object' || Array.isArray(entry.tier_to_model)) {
153
+ throw new Error(`${where}.tier_to_model: expected object`);
154
+ }
155
+ for (const tier of TIER_KEYS) {
156
+ if (!(tier in entry.tier_to_model)) {
157
+ throw new Error(`${where}.tier_to_model: missing required tier '${tier}'`);
158
+ }
159
+ }
160
+ for (const k of Object.keys(entry.tier_to_model)) {
161
+ if (!TIER_KEYS.includes(k)) {
162
+ throw new Error(`${where}.tier_to_model: unknown tier '${k}' (allowed: ${TIER_KEYS.join('|')})`);
163
+ }
164
+ validateModelRow(entry.tier_to_model[k], `${where}.tier_to_model.${k}`);
165
+ }
166
+
167
+ // reasoning_class_to_model: requires high/medium/low
168
+ if (!entry.reasoning_class_to_model || typeof entry.reasoning_class_to_model !== 'object' || Array.isArray(entry.reasoning_class_to_model)) {
169
+ throw new Error(`${where}.reasoning_class_to_model: expected object`);
170
+ }
171
+ for (const klass of REASONING_CLASS_KEYS) {
172
+ if (!(klass in entry.reasoning_class_to_model)) {
173
+ throw new Error(`${where}.reasoning_class_to_model: missing required class '${klass}'`);
174
+ }
175
+ }
176
+ for (const k of Object.keys(entry.reasoning_class_to_model)) {
177
+ if (!REASONING_CLASS_KEYS.includes(k)) {
178
+ throw new Error(`${where}.reasoning_class_to_model: unknown class '${k}' (allowed: ${REASONING_CLASS_KEYS.join('|')})`);
179
+ }
180
+ validateModelRow(entry.reasoning_class_to_model[k], `${where}.reasoning_class_to_model.${k}`);
181
+ }
182
+
183
+ validateProvenance(entry.provenance, `${where}.provenance`);
184
+ }
185
+
186
+ function parseRuntimeModelsFromString(markdown, sourceLabel) {
187
+ const label = sourceLabel || '<runtime-models markdown>';
188
+ if (typeof markdown !== 'string' || markdown.length === 0) {
189
+ throw new Error(`${label}: empty or non-string input`);
190
+ }
191
+
192
+ const blocks = extractJsonBlocks(markdown);
193
+ if (blocks.length < 2) {
194
+ // We expect at least the schema-version block + 1 runtime block.
195
+ throw new Error(`${label}: expected at least one schema-version block and one runtime block, found ${blocks.length} fenced json blocks`);
196
+ }
197
+
198
+ // First block MUST be the { "$schema_version": <int> } header.
199
+ let schemaVersion = null;
200
+ let firstParsed;
201
+ try {
202
+ firstParsed = JSON.parse(blocks[0].raw);
203
+ } catch (err) {
204
+ throw new Error(`${label}: first json block (line ${blocks[0].lineNumber}) failed to parse: ${err.message}`);
205
+ }
206
+ if (
207
+ !firstParsed ||
208
+ typeof firstParsed !== 'object' ||
209
+ Array.isArray(firstParsed) ||
210
+ !('$schema_version' in firstParsed) ||
211
+ Object.keys(firstParsed).length !== 1
212
+ ) {
213
+ throw new Error(`${label}: first json block (line ${blocks[0].lineNumber}) must be { "$schema_version": <int> } and nothing else`);
214
+ }
215
+ if (firstParsed.$schema_version !== 1) {
216
+ throw new Error(
217
+ `${label}: $schema_version must be 1 (got ${JSON.stringify(firstParsed.$schema_version)}). Bump the parser when the schema breaks forward.`,
218
+ );
219
+ }
220
+ schemaVersion = firstParsed.$schema_version;
221
+
222
+ // Remaining blocks are runtime entries.
223
+ const runtimes = [];
224
+ const seenIds = new Set();
225
+ for (let i = 1; i < blocks.length; i++) {
226
+ const block = blocks[i];
227
+ let parsed;
228
+ try {
229
+ parsed = JSON.parse(block.raw);
230
+ } catch (err) {
231
+ throw new Error(`${label}: json block at line ${block.lineNumber} failed to parse: ${err.message}`);
232
+ }
233
+ const where = `${label}#runtimes[${i - 1}] (line ${block.lineNumber})`;
234
+ validateRuntimeEntry(parsed, where);
235
+ if (seenIds.has(parsed.id)) {
236
+ throw new Error(`${where}: duplicate runtime id '${parsed.id}' (already seen earlier in the file)`);
237
+ }
238
+ seenIds.add(parsed.id);
239
+ runtimes.push(parsed);
240
+ }
241
+
242
+ return { schema_version: schemaVersion, runtimes };
243
+ }
244
+
245
+ function parseRuntimeModels({ cwd } = {}) {
246
+ const filePath = cwd ? path.join(cwd, 'reference', 'runtime-models.md') : DEFAULT_PATH;
247
+ let markdown;
248
+ try {
249
+ markdown = fs.readFileSync(filePath, 'utf8');
250
+ } catch (err) {
251
+ const wrapped = new Error(
252
+ `parse-runtime-models: cannot read ${filePath}\n ${err.message}`,
253
+ );
254
+ wrapped.code = 'EPARSE_RUNTIME_MODELS_READ';
255
+ wrapped.path = filePath;
256
+ throw wrapped;
257
+ }
258
+ return parseRuntimeModelsFromString(markdown, filePath);
259
+ }
260
+
261
+ module.exports = {
262
+ parseRuntimeModels,
263
+ parseRuntimeModelsFromString,
264
+ KNOWN_RUNTIME_IDS,
265
+ TIER_KEYS,
266
+ REASONING_CLASS_KEYS,
267
+ };
@@ -161,6 +161,47 @@ function listRuntimeIds() {
161
161
  return RUNTIMES.map((r) => r.id);
162
162
  }
163
163
 
164
+ // Phase 26 D-06 — `tier_to_model` lookup helper.
165
+ //
166
+ // `getRuntimeModels(runtimeId, { cwd? })` resolves the per-runtime tier→model
167
+ // adapter from `reference/runtime-models.md` via `parse-runtime-models.cjs`.
168
+ // Returns `null` when the runtime has no entry in runtime-models.md (i.e.,
169
+ // the data source ships rows for fewer than 14 runtimes during the rolling
170
+ // research tail described in CONTEXT D-02). Caller is responsible for
171
+ // degrading gracefully (e.g., installer skips models.json emission when null).
172
+ //
173
+ // The parsed payload is cached per `cwd` to avoid re-reading the markdown
174
+ // on each runtime in a multi-runtime install loop.
175
+
176
+ const _modelsCache = new Map();
177
+
178
+ function getParsedRuntimeModels(opts) {
179
+ const cwd = (opts && opts.cwd) || null;
180
+ const cacheKey = cwd || '<default>';
181
+ if (_modelsCache.has(cacheKey)) return _modelsCache.get(cacheKey);
182
+ // Lazy require avoids a hard dep cycle if runtimes.cjs is imported in
183
+ // contexts that don't ship the reference/ tree (theoretical — not used today).
184
+ const { parseRuntimeModels } = require('./parse-runtime-models.cjs');
185
+ const parsed = parseRuntimeModels(cwd ? { cwd } : {});
186
+ _modelsCache.set(cacheKey, parsed);
187
+ return parsed;
188
+ }
189
+
190
+ function getRuntimeModels(runtimeId, opts) {
191
+ // Validate the runtime id up-front — this catches typos in the installer
192
+ // entry rather than silently returning null for "claud" vs "claude".
193
+ getRuntime(runtimeId);
194
+ const parsed = getParsedRuntimeModels(opts);
195
+ const entry = parsed.runtimes.find((r) => r.id === runtimeId);
196
+ return entry || null;
197
+ }
198
+
199
+ // Test-only hook: drop the cached parse result. Used by tests that mutate
200
+ // the source markdown between assertions.
201
+ function _resetRuntimeModelsCache() {
202
+ _modelsCache.clear();
203
+ }
204
+
164
205
  module.exports = {
165
206
  RUNTIMES,
166
207
  REPO,
@@ -169,4 +210,6 @@ module.exports = {
169
210
  getRuntime,
170
211
  listRuntimes,
171
212
  listRuntimeIds,
213
+ getRuntimeModels,
214
+ _resetRuntimeModelsCache,
172
215
  };
@@ -0,0 +1,96 @@
1
+ // scripts/lib/runtime-detect.cjs
2
+ //
3
+ // Plan 26-02 — runtime detection from env-vars.
4
+ //
5
+ // Identifies which AI-coding-CLI host process the current Node script is
6
+ // running inside, by reading the same `*_CONFIG_DIR` / `*_HOME` env-vars
7
+ // the Phase 24 installer uses to decide where to drop runtime files.
8
+ //
9
+ // The env-var → runtime-ID mapping is owned by Phase 24's
10
+ // `scripts/lib/install/runtimes.cjs`. This module imports `RUNTIMES` from
11
+ // there and derives the lookup table — DO NOT duplicate the mapping
12
+ // (D-05). Adding a new runtime to runtimes.cjs automatically extends
13
+ // detection here.
14
+ //
15
+ // Lookup order is the runtimes.cjs declaration order. When a host
16
+ // happens to set multiple env-vars (e.g. a parent CLI spawns a child CLI
17
+ // and inherits env), the first-declared runtime wins. Phase 24's order
18
+ // puts Claude Code first, then OpenCode, Gemini, Kilo, Codex, …; that's
19
+ // also the order this module returns for ambiguous hosts.
20
+ //
21
+ // Pure module — no top-level side effects. Reads `process.env` only when
22
+ // `detect()` is called. Returns null when no recognized env-var is set
23
+ // (e.g. running tests in CI matrix, or a bare Node script invoked outside
24
+ // any of the 14 runtime hosts).
25
+ //
26
+ // `.cjs` extension matches existing Phase 22 primitives and lets both
27
+ // `.cjs` callers and `.ts` callers (under --experimental-strip-types)
28
+ // require it without ESM-interop friction.
29
+
30
+ 'use strict';
31
+
32
+ const { RUNTIMES } = require('./install/runtimes.cjs');
33
+
34
+ /**
35
+ * Build the env-var → runtime-ID lookup map at module load. Frozen so
36
+ * accidental mutation by callers can't drift the map away from the
37
+ * Phase 24 source of truth.
38
+ *
39
+ * Shape: `[{ env: 'CLAUDE_CONFIG_DIR', id: 'claude' }, …]` — array of
40
+ * pairs to preserve declaration order from RUNTIMES (Map / Object key
41
+ * order is guaranteed by spec but the array form documents intent).
42
+ */
43
+ const ENV_TO_RUNTIME = Object.freeze(
44
+ RUNTIMES.map((r) => Object.freeze({ env: r.configDirEnv, id: r.id })),
45
+ );
46
+
47
+ /**
48
+ * Detect which runtime host the current process is running inside, by
49
+ * scanning `process.env` for the runtime-ID env-vars in declaration
50
+ * order and returning the first match.
51
+ *
52
+ * The env-var must be a non-empty string to count as set — runtime
53
+ * harnesses that export an empty value (`CLAUDE_CONFIG_DIR=`) are
54
+ * treated as unset, since the empty string is not a usable config-dir
55
+ * path and likely indicates "exported but not assigned".
56
+ *
57
+ * @returns {string | null} runtime-ID (e.g. 'claude', 'codex') or null
58
+ * when no recognized env-var is set in the current environment.
59
+ *
60
+ * @example
61
+ * process.env.CLAUDE_CONFIG_DIR = '/Users/me/.claude';
62
+ * detect(); // → 'claude'
63
+ *
64
+ * @example
65
+ * // No runtime env-var set:
66
+ * detect(); // → null
67
+ */
68
+ function detect() {
69
+ const env = process.env;
70
+ for (const { env: name, id } of ENV_TO_RUNTIME) {
71
+ const v = env[name];
72
+ if (typeof v === 'string' && v.length > 0) {
73
+ return id;
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+
79
+ /**
80
+ * Return the env-var → runtime-ID map as a plain array of pairs. Useful
81
+ * for diagnostic logging and tests that want to verify the mapping
82
+ * matches Phase 24 without depending on `runtimes.cjs` internals.
83
+ *
84
+ * The returned array is a fresh copy; mutating it has no effect on
85
+ * future `detect()` calls.
86
+ *
87
+ * @returns {Array<{env: string, id: string}>}
88
+ */
89
+ function envVarMap() {
90
+ return ENV_TO_RUNTIME.map((p) => ({ env: p.env, id: p.id }));
91
+ }
92
+
93
+ module.exports = {
94
+ detect,
95
+ envVarMap,
96
+ };