@tekyzinc/gsd-t 3.29.11 → 4.0.12

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 (108) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/bin/gsd-t-parallel.cjs +64 -13
  3. package/bin/gsd-t.js +48 -376
  4. package/bin/journey-coverage.cjs +6 -3
  5. package/bin/parallel-cli.cjs +13 -1
  6. package/commands/gsd-t-debug.md +47 -533
  7. package/commands/gsd-t-design-decompose.md +24 -497
  8. package/commands/gsd-t-doc-ripple.md +23 -139
  9. package/commands/gsd-t-execute.md +41 -958
  10. package/commands/gsd-t-help.md +0 -2
  11. package/commands/gsd-t-impact.md +26 -295
  12. package/commands/gsd-t-integrate.md +44 -369
  13. package/commands/gsd-t-milestone.md +25 -124
  14. package/commands/gsd-t-partition.md +28 -539
  15. package/commands/gsd-t-plan.md +26 -472
  16. package/commands/gsd-t-prd.md +25 -311
  17. package/commands/gsd-t-quick.md +1 -1
  18. package/commands/gsd-t-resume.md +0 -33
  19. package/commands/gsd-t-verify.md +52 -569
  20. package/commands/gsd-t-wave.md +41 -430
  21. package/package.json +1 -1
  22. package/scripts/gsd-t-calibration-hook.js +3 -1
  23. package/scripts/hooks/pre-commit-journey-coverage +0 -2
  24. package/scripts/hooks/pre-commit-playwright-gate +0 -2
  25. package/templates/CLAUDE-global.md +43 -173
  26. package/templates/CLAUDE-project.md +118 -104
  27. package/templates/prompts/design-verify-subagent.md +3 -0
  28. package/templates/prompts/qa-subagent.md +3 -0
  29. package/templates/prompts/red-team-subagent.md +5 -2
  30. package/templates/workflows/_lib.js +176 -0
  31. package/templates/workflows/gsd-t-debug.workflow.js +86 -0
  32. package/templates/workflows/gsd-t-execute.workflow.js +206 -0
  33. package/templates/workflows/gsd-t-integrate.workflow.js +71 -0
  34. package/templates/workflows/gsd-t-phase.workflow.js +94 -0
  35. package/templates/workflows/gsd-t-quick.workflow.js +72 -0
  36. package/templates/workflows/gsd-t-verify.workflow.js +348 -0
  37. package/templates/workflows/gsd-t-wave.workflow.js +43 -0
  38. package/bin/check-headless-sessions.js +0 -140
  39. package/bin/context-budget-audit.cjs +0 -447
  40. package/bin/context-meter-config.cjs +0 -101
  41. package/bin/context-meter-config.test.cjs +0 -101
  42. package/bin/event-stream.cjs +0 -205
  43. package/bin/gsd-t-benchmark-orchestrator.js +0 -437
  44. package/bin/gsd-t-capture-lint.cjs +0 -440
  45. package/bin/gsd-t-economics.cjs +0 -315
  46. package/bin/gsd-t-in-session-usage.cjs +0 -213
  47. package/bin/gsd-t-orchestrator-config.cjs +0 -161
  48. package/bin/gsd-t-orchestrator-queue.cjs +0 -180
  49. package/bin/gsd-t-orchestrator-recover.cjs +0 -231
  50. package/bin/gsd-t-orchestrator-worker.cjs +0 -219
  51. package/bin/gsd-t-orchestrator.js +0 -535
  52. package/bin/gsd-t-parallel-probe.cjs +0 -132
  53. package/bin/gsd-t-ratelimit-probe-worker.cjs +0 -237
  54. package/bin/gsd-t-ratelimit-probe.cjs +0 -648
  55. package/bin/gsd-t-report-tokens.cjs +0 -549
  56. package/bin/gsd-t-stream-feed-client.cjs +0 -151
  57. package/bin/gsd-t-token-backfill.cjs +0 -366
  58. package/bin/gsd-t-token-capture.cjs +0 -321
  59. package/bin/gsd-t-token-dashboard.cjs +0 -353
  60. package/bin/gsd-t-token-regenerate-log.cjs +0 -129
  61. package/bin/gsd-t-tool-attribution.cjs +0 -377
  62. package/bin/gsd-t-tool-cost.cjs +0 -195
  63. package/bin/gsd-t-transcript-tee.cjs +0 -246
  64. package/bin/gsd-t-unattended-heartbeat.cjs +0 -188
  65. package/bin/gsd-t-unattended-platform.cjs +0 -551
  66. package/bin/gsd-t-unattended-platform.js +0 -381
  67. package/bin/gsd-t-unattended-safety.cjs +0 -773
  68. package/bin/gsd-t-unattended-safety.js +0 -788
  69. package/bin/gsd-t-unattended.cjs +0 -2109
  70. package/bin/gsd-t-unattended.js +0 -1367
  71. package/bin/gsd-t-worker-dispatch.cjs +0 -211
  72. package/bin/handoff-lock.cjs +0 -249
  73. package/bin/handoff-lock.js +0 -249
  74. package/bin/headless-auto-spawn.cjs +0 -711
  75. package/bin/headless-auto-spawn.js +0 -373
  76. package/bin/headless-exit-codes.cjs +0 -67
  77. package/bin/live-activity-report.cjs +0 -615
  78. package/bin/log-tail.cjs +0 -81
  79. package/bin/m44-proof-measure.cjs +0 -285
  80. package/bin/m46-iter-proof.cjs +0 -149
  81. package/bin/m46-worker-proof.cjs +0 -201
  82. package/bin/m55-substrate-proof.cjs +0 -134
  83. package/bin/metrics-rollup.js +0 -200
  84. package/bin/model-windows.cjs +0 -99
  85. package/bin/model-windows.test.cjs +0 -75
  86. package/bin/parallelism-report.cjs +0 -535
  87. package/bin/runway-estimator.cjs +0 -242
  88. package/bin/spawn-plan-derive.cjs +0 -163
  89. package/bin/spawn-plan-status-updater.cjs +0 -292
  90. package/bin/spawn-plan-writer.cjs +0 -204
  91. package/bin/supervisor-pid-fingerprint.cjs +0 -126
  92. package/bin/token-budget.cjs +0 -265
  93. package/bin/unattended-watch-format.cjs +0 -178
  94. package/bin/watch-progress.js +0 -155
  95. package/commands/gsd-t-unattended-stop.md +0 -85
  96. package/commands/gsd-t-unattended-watch.md +0 -465
  97. package/commands/gsd-t-unattended.md +0 -476
  98. package/commands/gsd-t-visualize.md +0 -116
  99. package/scripts/gsd-t-context-meter.e2e.test.js +0 -364
  100. package/scripts/gsd-t-context-meter.js +0 -341
  101. package/scripts/gsd-t-context-meter.test.js +0 -471
  102. package/scripts/gsd-t-dashboard-server.js +0 -1203
  103. package/scripts/gsd-t-post-commit-spawn-plan.sh +0 -86
  104. package/scripts/gsd-t-transcript.html +0 -1821
  105. package/scripts/hooks/gsd-t-conversation-capture.js +0 -439
  106. package/scripts/hooks/gsd-t-in-session-usage-hook.js +0 -84
  107. package/scripts/spawn-plan-fmt-tokens.cjs +0 -80
  108. package/templates/hooks/post-commit-spawn-plan.sh +0 -85
@@ -1,134 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * M55-D2 Parallel-CLI Substrate Proof Harness (T5/T6)
6
- *
7
- * Charter SC2: ≥3× wall-clock speedup vs. serial baseline.
8
- *
9
- * Method:
10
- * 1. Construct N synthetic CLI workers (default N=6), each `node -e
11
- * 'setTimeout(_ => process.exit(0), 250)'` — deterministic 250 ms work
12
- * with no real I/O. Mirrors `bin/m46-iter-proof.cjs` shape.
13
- * 2. Sequential baseline: spawn each worker via `runParallel({maxConcurrency:1})`
14
- * so the captureSpawn / tee path runs identically — only concurrency
15
- * differs from the parallel run.
16
- * 3. Parallel run: `runParallel({maxConcurrency:N})`. Same N workers.
17
- * 4. Compute speedup = T_serial / T_par. Pass iff speedup ≥ 3.0.
18
- *
19
- * Output: stdout summary + appends to .gsd-t/metrics/m55-substrate-proof.txt.
20
- * Exit: 0 on pass, 1 on fail.
21
- */
22
-
23
- const fs = require('fs');
24
- const path = require('path');
25
-
26
- const { runParallel } = require(path.join(__dirname, 'parallel-cli.cjs'));
27
-
28
- const N_WORKERS = parseInt(process.env.M55_PROOF_N || '6', 10);
29
- const SLEEP_MS = parseInt(process.env.M55_PROOF_SLEEP_MS || '250', 10);
30
- const THRESHOLD_SPEEDUP = parseFloat(process.env.M55_PROOF_THRESHOLD || '3.0');
31
-
32
- function makeWorkers(n, sleepMs) {
33
- const workers = [];
34
- for (let i = 0; i < n; i++) {
35
- workers.push({
36
- id: 'w' + String(i).padStart(2, '0'),
37
- cmd: process.execPath,
38
- args: ['-e', `setTimeout(() => process.exit(0), ${sleepMs})`],
39
- });
40
- }
41
- return workers;
42
- }
43
-
44
- async function runSerial(n, sleepMs) {
45
- const t0 = process.hrtime.bigint();
46
- const r = await runParallel({
47
- workers: makeWorkers(n, sleepMs),
48
- maxConcurrency: 1,
49
- command: 'm55-substrate-proof',
50
- step: 'serial',
51
- });
52
- const t1 = process.hrtime.bigint();
53
- return { wallClockMs: Number(t1 - t0) / 1e6, envelope: r };
54
- }
55
-
56
- async function runParallelN(n, sleepMs) {
57
- const t0 = process.hrtime.bigint();
58
- const r = await runParallel({
59
- workers: makeWorkers(n, sleepMs),
60
- maxConcurrency: n,
61
- command: 'm55-substrate-proof',
62
- step: 'parallel',
63
- });
64
- const t1 = process.hrtime.bigint();
65
- return { wallClockMs: Number(t1 - t0) / 1e6, envelope: r };
66
- }
67
-
68
- async function main() {
69
- process.stdout.write(
70
- `M55-D2 Parallel-CLI Substrate Proof (N=${N_WORKERS} workers, sleep=${SLEEP_MS}ms each)\n`,
71
- );
72
- process.stdout.write('─'.repeat(60) + '\n');
73
-
74
- process.stdout.write('Sequential baseline (maxConcurrency=1)…\n');
75
- const serial = await runSerial(N_WORKERS, SLEEP_MS);
76
- const serialOk = serial.envelope.results.filter((r) => r.ok).length;
77
- process.stdout.write(` T_serial = ${serial.wallClockMs.toFixed(1)} ms (${serialOk}/${N_WORKERS} ok)\n\n`);
78
-
79
- process.stdout.write(`Parallel (maxConcurrency=${N_WORKERS})…\n`);
80
- const parallel = await runParallelN(N_WORKERS, SLEEP_MS);
81
- const parallelOk = parallel.envelope.results.filter((r) => r.ok).length;
82
- process.stdout.write(` T_par = ${parallel.wallClockMs.toFixed(1)} ms (${parallelOk}/${N_WORKERS} ok)\n\n`);
83
-
84
- const speedup = serial.wallClockMs / parallel.wallClockMs;
85
- const ratio = parallel.wallClockMs / serial.wallClockMs;
86
- const parallelismFactor = (N_WORKERS * SLEEP_MS) / parallel.wallClockMs;
87
- const passed = speedup >= THRESHOLD_SPEEDUP;
88
-
89
- process.stdout.write('─'.repeat(60) + '\n');
90
- process.stdout.write('Result\n');
91
- process.stdout.write(` speedup = ${speedup.toFixed(2)}× (threshold ≥ ${THRESHOLD_SPEEDUP})\n`);
92
- process.stdout.write(` T_par / T_serial = ${ratio.toFixed(3)}\n`);
93
- process.stdout.write(` parallelism_factor = ${parallelismFactor.toFixed(2)} (ideal ≈ ${N_WORKERS})\n`);
94
- process.stdout.write(` verdict = ${passed ? 'PASS ✓' : 'FAIL ✗'}\n`);
95
-
96
- // Append result line to metrics file (charter SC2).
97
- const metricsDir = path.join(process.cwd(), '.gsd-t', 'metrics');
98
- fs.mkdirSync(metricsDir, { recursive: true });
99
- const metricsPath = path.join(metricsDir, 'm55-substrate-proof.txt');
100
- const summaryLine = [
101
- new Date().toISOString(),
102
- `N=${N_WORKERS}`,
103
- `sleepMs=${SLEEP_MS}`,
104
- `T_serial=${serial.wallClockMs.toFixed(1)}ms`,
105
- `T_par=${parallel.wallClockMs.toFixed(1)}ms`,
106
- `speedup=${speedup.toFixed(2)}x`,
107
- `verdict=${passed ? 'PASS' : 'FAIL'}`,
108
- ].join(' | ') + '\n';
109
- fs.appendFileSync(metricsPath, summaryLine);
110
- process.stdout.write(`\nAppended: ${metricsPath}\n`);
111
-
112
- // Also dump a structured JSON sibling for downstream consumers.
113
- const reportPath = path.join(metricsDir, 'm55-substrate-proof.json');
114
- const report = {
115
- timestamp: new Date().toISOString(),
116
- n_workers: N_WORKERS,
117
- sleep_ms: SLEEP_MS,
118
- T_serial_ms: serial.wallClockMs,
119
- T_par_ms: parallel.wallClockMs,
120
- speedup,
121
- parallelism_factor: parallelismFactor,
122
- threshold_speedup: THRESHOLD_SPEEDUP,
123
- passed,
124
- };
125
- fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
126
- process.stdout.write(`Wrote: ${reportPath}\n`);
127
-
128
- process.exit(passed ? 0 : 1);
129
- }
130
-
131
- main().catch((e) => {
132
- process.stderr.write(`ERROR: ${(e && e.stack) || e}\n`);
133
- process.exit(2);
134
- });
@@ -1,200 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * GSD-T Metrics Rollup — Milestone-level aggregation, ELO, heuristics
5
- *
6
- * Reads task-metrics.jsonl, computes rollup stats and process ELO,
7
- * runs 4 detection heuristics, writes to rollup.jsonl.
8
- *
9
- * Zero external dependencies (Node.js built-ins only).
10
- */
11
-
12
- const fs = require("fs");
13
- const path = require("path");
14
-
15
- // ── Constants ────────────────────────────────────────────────────────────────
16
-
17
- const ELO_START = 1000;
18
- const ELO_K = 32;
19
-
20
- // ── Exports ──────────────────────────────────────────────────────────────────
21
-
22
- module.exports = { generateRollup, computeELO, runHeuristics, readRollups };
23
-
24
- // ── generateRollup ───────────────────────────────────────────────────────────
25
-
26
- function generateRollup(milestone, version, projectDir) {
27
- const dir = projectDir || process.env.GSD_T_PROJECT_DIR || process.cwd();
28
- const tasks = readTaskMetrics(dir, milestone);
29
- if (tasks.length === 0) throw new Error(`No task-metrics found for ${milestone}`);
30
- const prev = getPreviousRollup(dir);
31
- const eloBefore = prev ? prev.elo_after : ELO_START;
32
- const eloAfter = computeELO(eloBefore, tasks);
33
- const rollup = buildRollup(milestone, version, tasks, eloBefore, eloAfter, prev);
34
- const filePath = resolveRollupFile(dir);
35
- ensureDir(path.dirname(filePath));
36
- fs.appendFileSync(filePath, JSON.stringify(rollup) + "\n");
37
- return rollup;
38
- }
39
-
40
- // ── computeELO ───────────────────────────────────────────────────────────────
41
-
42
- function computeELO(eloBefore, tasks) {
43
- const total = tasks.length;
44
- if (total === 0) return eloBefore;
45
- const sumWeights = tasks.reduce((s, t) => s + (t.signal_weight || 0), 0);
46
- const actual = (sumWeights + total) / (2 * total);
47
- const expected = 1 / (1 + Math.pow(10, (ELO_START - eloBefore) / 400));
48
- return Math.round((eloBefore + ELO_K * (actual - expected)) * 100) / 100;
49
- }
50
-
51
- // ── runHeuristics ────────────────────────────────────────────────────────────
52
-
53
- function runHeuristics(current, previous, rawTasks) {
54
- const flags = [];
55
- if (previous) {
56
- checkFirstPassSpike(current, previous, flags);
57
- checkReworkAnomaly(current, previous, flags);
58
- checkDurationRegression(current, previous, flags);
59
- }
60
- checkContextOverflow(rawTasks || [], flags);
61
- return flags;
62
- }
63
-
64
- // ── readRollups ──────────────────────────────────────────────────────────────
65
-
66
- function readRollups(filters, projectDir) {
67
- const dir = projectDir || process.env.GSD_T_PROJECT_DIR || process.cwd();
68
- const filePath = resolveRollupFile(dir);
69
- if (!fs.existsSync(filePath)) return [];
70
- return fs.readFileSync(filePath, "utf8").trim().split("\n")
71
- .map(safeParse).filter(Boolean)
72
- .filter((r) => matchFilters(r, filters || {}));
73
- }
74
-
75
- // ── Internal: buildRollup ────────────────────────────────────────────────────
76
-
77
- function buildRollup(milestone, version, tasks, eloBefore, eloAfter, prev) {
78
- const total = tasks.length;
79
- const passCount = tasks.filter((t) => t.fix_cycles === 0 && t.pass).length;
80
- const firstPassRate = Math.round((passCount / total) * 1000) / 1000;
81
- const avgDur = Math.round(tasks.reduce((s, t) => s + t.duration_s, 0) / total * 100) / 100;
82
- const avgCtx = Math.round(tasks.reduce((s, t) => s + t.context_pct, 0) / total * 100) / 100;
83
- const totalFix = tasks.reduce((s, t) => s + t.fix_cycles, 0);
84
- const totalTokens = tasks.reduce((s, t) => s + t.tokens_used, 0);
85
- const sigDist = buildSignalDist(tasks);
86
- const domBreak = buildDomainBreakdown(tasks);
87
- const trend = prev ? buildTrendDelta(firstPassRate, avgDur, eloAfter - eloBefore, prev) : null;
88
- const rollup = {
89
- ts: new Date().toISOString(), milestone, version, total_tasks: total,
90
- first_pass_rate: firstPassRate, avg_duration_s: avgDur, avg_context_pct: avgCtx,
91
- total_fix_cycles: totalFix, total_tokens: totalTokens,
92
- elo_before: eloBefore, elo_after: eloAfter, elo_delta: Math.round((eloAfter - eloBefore) * 100) / 100,
93
- signal_distribution: sigDist, domain_breakdown: domBreak, trend_delta: trend,
94
- heuristic_flags: [],
95
- };
96
- rollup.heuristic_flags = runHeuristics(rollup, prev, tasks);
97
- return rollup;
98
- }
99
-
100
- // ── Internal: signal distribution ────────────────────────────────────────────
101
-
102
- function buildSignalDist(tasks) {
103
- const dist = {};
104
- tasks.forEach((t) => { dist[t.signal_type] = (dist[t.signal_type] || 0) + 1; });
105
- return dist;
106
- }
107
-
108
- // ── Internal: domain breakdown ───────────────────────────────────────────────
109
-
110
- function buildDomainBreakdown(tasks) {
111
- const groups = {};
112
- tasks.forEach((t) => { (groups[t.domain] = groups[t.domain] || []).push(t); });
113
- return Object.entries(groups).map(([domain, items]) => ({
114
- domain, tasks: items.length,
115
- first_pass_rate: Math.round(items.filter((t) => t.fix_cycles === 0 && t.pass).length / items.length * 1000) / 1000,
116
- avg_duration_s: Math.round(items.reduce((s, t) => s + t.duration_s, 0) / items.length * 100) / 100,
117
- }));
118
- }
119
-
120
- // ── Internal: trend delta ────────────────────────────────────────────────────
121
-
122
- function buildTrendDelta(fpr, avgDur, eloDelta, prev) {
123
- return {
124
- first_pass_rate_delta: Math.round((fpr - prev.first_pass_rate) * 1000) / 1000,
125
- avg_duration_delta: Math.round((avgDur - prev.avg_duration_s) * 100) / 100,
126
- elo_delta: Math.round(eloDelta * 100) / 100,
127
- };
128
- }
129
-
130
- // ── Internal: heuristics ─────────────────────────────────────────────────────
131
-
132
- function checkFirstPassSpike(cur, prev, flags) {
133
- if (prev.first_pass_rate - cur.first_pass_rate > 0.15) {
134
- flags.push({ heuristic: "first-pass-failure-spike", severity: "HIGH",
135
- description: `First-pass rate dropped from ${(prev.first_pass_rate * 100).toFixed(0)}% to ${(cur.first_pass_rate * 100).toFixed(0)}%` });
136
- }
137
- }
138
-
139
- function checkReworkAnomaly(cur, prev, flags) {
140
- const prevAvg = prev.total_tasks > 0 ? prev.total_fix_cycles / prev.total_tasks : 0;
141
- const curAvg = cur.total_tasks > 0 ? cur.total_fix_cycles / cur.total_tasks : 0;
142
- if (prevAvg > 0 && curAvg > 2 * prevAvg) {
143
- flags.push({ heuristic: "rework-rate-anomaly", severity: "MEDIUM",
144
- description: `Fix cycle avg ${curAvg.toFixed(1)} is >2x previous ${prevAvg.toFixed(1)}` });
145
- }
146
- }
147
-
148
- function checkContextOverflow(tasks, flags) {
149
- if (tasks.length === 0) return;
150
- const failed = tasks.filter((t) => !t.pass);
151
- if (failed.length === 0) return;
152
- const highCtx = failed.filter((t) => t.context_pct > 80);
153
- if (highCtx.length / failed.length > 0.3) {
154
- flags.push({ heuristic: "context-overflow-correlation", severity: "MEDIUM",
155
- description: `${highCtx.length}/${failed.length} failed tasks had context >80%` });
156
- }
157
- }
158
-
159
- function checkDurationRegression(cur, prev, flags) {
160
- if (prev.avg_duration_s > 0 && cur.avg_duration_s > 2 * prev.avg_duration_s) {
161
- flags.push({ heuristic: "duration-regression", severity: "LOW",
162
- description: `Avg duration ${cur.avg_duration_s.toFixed(0)}s is >2x previous ${prev.avg_duration_s.toFixed(0)}s` });
163
- }
164
- }
165
-
166
- // ── Internal: file helpers ───────────────────────────────────────────────────
167
-
168
- function readTaskMetrics(dir, milestone) {
169
- const fp = path.join(dir, ".gsd-t", "metrics", "task-metrics.jsonl");
170
- if (!fs.existsSync(fp)) return [];
171
- return fs.readFileSync(fp, "utf8").trim().split("\n")
172
- .map(safeParse).filter(Boolean)
173
- .filter((r) => r.milestone === milestone);
174
- }
175
-
176
- function getPreviousRollup(dir) {
177
- const all = readRollups({}, dir);
178
- return all.length > 0 ? all[all.length - 1] : null;
179
- }
180
-
181
- function resolveRollupFile(dir) {
182
- return path.join(dir, ".gsd-t", "metrics", "rollup.jsonl");
183
- }
184
-
185
- function ensureDir(d) { if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true }); }
186
- function safeParse(l) { try { return JSON.parse(l); } catch { return null; } }
187
- function matchFilters(r, f) { return Object.entries(f).every(([k, v]) => r[k] === v); }
188
-
189
- // ── CLI Entry ────────────────────────────────────────────────────────────────
190
-
191
- if (require.main === module) {
192
- const args = process.argv.slice(2);
193
- const milestone = args[0];
194
- const version = args[1] || "0.0.0";
195
- if (!milestone) { process.stderr.write("Usage: metrics-rollup.js <milestone> [version]\n"); process.exit(1); }
196
- try {
197
- const rollup = generateRollup(milestone, version);
198
- process.stdout.write(JSON.stringify(rollup, null, 2) + "\n");
199
- } catch (err) { process.stderr.write(err.message + "\n"); process.exit(1); }
200
- }
@@ -1,99 +0,0 @@
1
- /**
2
- * bin/model-windows.cjs
3
- *
4
- * Single source of truth for Claude model → context-window size (in input
5
- * tokens). The context meter and every downstream budget module must size the
6
- * window from the MODEL ACTUALLY RUNNING, not a hardcoded constant.
7
- *
8
- * Why this exists
9
- * ---------------
10
- * Prior to this module every budget site hardcoded `200000` with a comment
11
- * "claude-opus-4-6 default". Opus 4.6 and 4.7 (and Sonnet 4.x) ship a
12
- * 1,000,000-token context window. Hardcoding 200k made the context meter
13
- * overcount usage 5× and fire the headless handoff at ~64% of context
14
- * REMAINING. This map fixes that at the source.
15
- *
16
- * Resolution strategy
17
- * -------------------
18
- * GSD-T jumps between models per-subagent, so a static config value is wrong.
19
- * The orchestrator session whose transcript the meter reads, however, runs a
20
- * single model for its lifetime, and every assistant message in the transcript
21
- * records its `model` id. `windowForModel(modelId)` maps that id to a window.
22
- *
23
- * Matching is by longest-prefix so versioned ids resolve even if a future
24
- * dated suffix appears (e.g. "claude-opus-4-7-20260115" → opus 4.x entry).
25
- * Unknown / missing model → SAFE_DEFAULT_WINDOW (the large 1M window: a guard
26
- * that triggers late is worse than one that never undercounts a real 1M
27
- * session — but see note below; we deliberately pick the large default so the
28
- * meter does NOT regress to premature handoffs on an unrecognized new model).
29
- *
30
- * Zero dependencies. CommonJS. Pure functions.
31
- */
32
-
33
- "use strict";
34
-
35
- // The conservative fallback when a model can't be resolved. We choose the
36
- // LARGE window (1M) on purpose: the bug we are fixing is premature handoff
37
- // from a too-SMALL assumed window. An unknown future model is far more likely
38
- // to have a >=1M window than a 200k one, and an over-large window degrades
39
- // gracefully (handoff a little late) whereas an under-small one breaks the
40
- // workflow (handoff way too early, the reported symptom).
41
- const SAFE_DEFAULT_WINDOW = 1_000_000;
42
-
43
- // The legacy small window, kept as a named export for the few call sites that
44
- // must preserve old behavior explicitly (e.g. fixtures, back-compat configs).
45
- const LEGACY_SMALL_WINDOW = 200_000;
46
-
47
- // Longest-prefix map: key is a model-id prefix, value is the input-token
48
- // context window for that model family. Order does not matter — resolution
49
- // picks the LONGEST matching prefix.
50
- const MODEL_WINDOWS = Object.freeze({
51
- // Opus 4.6 / 4.7 — 1M context window.
52
- "claude-opus-4-6": 1_000_000,
53
- "claude-opus-4-7": 1_000_000,
54
- // Generic opus-4 fallback (covers any 4.x point release not listed above).
55
- "claude-opus-4": 1_000_000,
56
-
57
- // Sonnet 4.x — 1M context window.
58
- "claude-sonnet-4": 1_000_000,
59
-
60
- // Haiku 4.x — 200k context window.
61
- "claude-haiku-4": 200_000,
62
-
63
- // Pre-4 families (defensive — older long sessions / replayed transcripts).
64
- "claude-3-7-sonnet": 200_000,
65
- "claude-3-5-sonnet": 200_000,
66
- "claude-3-5-haiku": 200_000,
67
- "claude-3-opus": 200_000,
68
- });
69
-
70
- /**
71
- * Resolve a context-window size (input tokens) for a Claude model id.
72
- *
73
- * @param {string|null|undefined} modelId e.g. "claude-opus-4-7" or
74
- * "claude-opus-4-7-20260115". Non-string / empty → SAFE_DEFAULT_WINDOW.
75
- * @returns {number} positive integer window size
76
- */
77
- function windowForModel(modelId) {
78
- if (typeof modelId !== "string" || modelId.length === 0) {
79
- return SAFE_DEFAULT_WINDOW;
80
- }
81
- const id = modelId.trim().toLowerCase();
82
-
83
- let best = null;
84
- let bestLen = -1;
85
- for (const prefix of Object.keys(MODEL_WINDOWS)) {
86
- if (id.startsWith(prefix) && prefix.length > bestLen) {
87
- best = MODEL_WINDOWS[prefix];
88
- bestLen = prefix.length;
89
- }
90
- }
91
- return best != null ? best : SAFE_DEFAULT_WINDOW;
92
- }
93
-
94
- module.exports = {
95
- windowForModel,
96
- MODEL_WINDOWS,
97
- SAFE_DEFAULT_WINDOW,
98
- LEGACY_SMALL_WINDOW,
99
- };
@@ -1,75 +0,0 @@
1
- /**
2
- * Tests for bin/model-windows.cjs — model → context-window resolution.
3
- *
4
- * The bug this fixes: the context meter hardcoded a 200k window so an Opus 4.7
5
- * session (1M window) read as 5× over budget, firing the headless handoff at
6
- * ~64% of context REMAINING. These tests pin the corrected windows.
7
- */
8
-
9
- "use strict";
10
-
11
- const test = require("node:test");
12
- const assert = require("node:assert/strict");
13
-
14
- const {
15
- windowForModel,
16
- MODEL_WINDOWS,
17
- SAFE_DEFAULT_WINDOW,
18
- LEGACY_SMALL_WINDOW,
19
- } = require("./model-windows.cjs");
20
-
21
- test("Opus 4.7 resolves to a 1M window (the reported regression)", () => {
22
- assert.equal(windowForModel("claude-opus-4-7"), 1_000_000);
23
- });
24
-
25
- test("Opus 4.6 resolves to a 1M window", () => {
26
- assert.equal(windowForModel("claude-opus-4-6"), 1_000_000);
27
- });
28
-
29
- test("dated/versioned suffix still resolves via longest-prefix", () => {
30
- assert.equal(windowForModel("claude-opus-4-7-20260115"), 1_000_000);
31
- assert.equal(windowForModel("claude-sonnet-4-6-20251201"), 1_000_000);
32
- });
33
-
34
- test("Sonnet 4.x resolves to a 1M window", () => {
35
- assert.equal(windowForModel("claude-sonnet-4-6"), 1_000_000);
36
- assert.equal(windowForModel("claude-sonnet-4"), 1_000_000);
37
- });
38
-
39
- test("Haiku 4.x resolves to the 200k window", () => {
40
- assert.equal(windowForModel("claude-haiku-4-5-20251001"), 200_000);
41
- assert.equal(windowForModel("claude-haiku-4"), 200_000);
42
- });
43
-
44
- test("longest-prefix wins over a shorter generic prefix", () => {
45
- // "claude-opus-4-7" (15) must beat "claude-opus-4" (13). Both map to 1M
46
- // here, so assert the resolution mechanism via a value-independent check:
47
- // a hypothetical future divergence would surface if this regressed.
48
- assert.equal(windowForModel("claude-opus-4-7"), MODEL_WINDOWS["claude-opus-4-7"]);
49
- });
50
-
51
- test("case-insensitive and whitespace-tolerant", () => {
52
- assert.equal(windowForModel(" CLAUDE-OPUS-4-7 "), 1_000_000);
53
- });
54
-
55
- test("unknown / missing model falls back to the SAFE large default", () => {
56
- assert.equal(windowForModel("claude-future-99"), SAFE_DEFAULT_WINDOW);
57
- assert.equal(windowForModel(""), SAFE_DEFAULT_WINDOW);
58
- assert.equal(windowForModel(null), SAFE_DEFAULT_WINDOW);
59
- assert.equal(windowForModel(undefined), SAFE_DEFAULT_WINDOW);
60
- assert.equal(windowForModel(42), SAFE_DEFAULT_WINDOW);
61
- });
62
-
63
- test("SAFE_DEFAULT_WINDOW is the large (1M) window, not the legacy 200k", () => {
64
- // Core anti-regression assertion: the fallback must NOT reintroduce the
65
- // premature-handoff bug for an unrecognized model.
66
- assert.equal(SAFE_DEFAULT_WINDOW, 1_000_000);
67
- assert.equal(LEGACY_SMALL_WINDOW, 200_000);
68
- assert.notEqual(SAFE_DEFAULT_WINDOW, LEGACY_SMALL_WINDOW);
69
- });
70
-
71
- test("every mapped window is a positive integer", () => {
72
- for (const [k, v] of Object.entries(MODEL_WINDOWS)) {
73
- assert.ok(Number.isInteger(v) && v > 0, `${k} → ${v} must be a positive int`);
74
- }
75
- });