@tekyzinc/gsd-t 3.29.10 → 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 (112) hide show
  1. package/CHANGELOG.md +176 -0
  2. package/bin/gsd-t-parallel.cjs +64 -13
  3. package/bin/gsd-t-test-data-adapters/file-json-array.cjs +27 -3
  4. package/bin/gsd-t-test-data-adapters/localstorage-key-prefix.cjs +6 -1
  5. package/bin/gsd-t-test-data-adapters/sqlite-table-where.cjs +16 -1
  6. package/bin/gsd-t-test-data-ledger.cjs +1 -0
  7. package/bin/gsd-t.js +48 -376
  8. package/bin/journey-coverage.cjs +6 -3
  9. package/bin/parallel-cli.cjs +13 -1
  10. package/commands/gsd-t-debug.md +47 -533
  11. package/commands/gsd-t-design-decompose.md +24 -497
  12. package/commands/gsd-t-doc-ripple.md +23 -139
  13. package/commands/gsd-t-execute.md +41 -958
  14. package/commands/gsd-t-help.md +0 -2
  15. package/commands/gsd-t-impact.md +26 -295
  16. package/commands/gsd-t-integrate.md +44 -369
  17. package/commands/gsd-t-milestone.md +25 -124
  18. package/commands/gsd-t-partition.md +28 -539
  19. package/commands/gsd-t-plan.md +26 -472
  20. package/commands/gsd-t-prd.md +25 -311
  21. package/commands/gsd-t-quick.md +1 -1
  22. package/commands/gsd-t-resume.md +0 -33
  23. package/commands/gsd-t-verify.md +52 -569
  24. package/commands/gsd-t-wave.md +41 -430
  25. package/package.json +1 -1
  26. package/scripts/gsd-t-calibration-hook.js +3 -1
  27. package/scripts/hooks/pre-commit-journey-coverage +0 -2
  28. package/scripts/hooks/pre-commit-playwright-gate +0 -2
  29. package/templates/CLAUDE-global.md +43 -173
  30. package/templates/CLAUDE-project.md +118 -104
  31. package/templates/prompts/design-verify-subagent.md +3 -0
  32. package/templates/prompts/qa-subagent.md +3 -0
  33. package/templates/prompts/red-team-subagent.md +5 -2
  34. package/templates/workflows/_lib.js +176 -0
  35. package/templates/workflows/gsd-t-debug.workflow.js +86 -0
  36. package/templates/workflows/gsd-t-execute.workflow.js +206 -0
  37. package/templates/workflows/gsd-t-integrate.workflow.js +71 -0
  38. package/templates/workflows/gsd-t-phase.workflow.js +94 -0
  39. package/templates/workflows/gsd-t-quick.workflow.js +72 -0
  40. package/templates/workflows/gsd-t-verify.workflow.js +348 -0
  41. package/templates/workflows/gsd-t-wave.workflow.js +43 -0
  42. package/bin/check-headless-sessions.js +0 -140
  43. package/bin/context-budget-audit.cjs +0 -447
  44. package/bin/context-meter-config.cjs +0 -101
  45. package/bin/context-meter-config.test.cjs +0 -101
  46. package/bin/event-stream.cjs +0 -205
  47. package/bin/gsd-t-benchmark-orchestrator.js +0 -437
  48. package/bin/gsd-t-capture-lint.cjs +0 -440
  49. package/bin/gsd-t-economics.cjs +0 -315
  50. package/bin/gsd-t-in-session-usage.cjs +0 -213
  51. package/bin/gsd-t-orchestrator-config.cjs +0 -161
  52. package/bin/gsd-t-orchestrator-queue.cjs +0 -180
  53. package/bin/gsd-t-orchestrator-recover.cjs +0 -231
  54. package/bin/gsd-t-orchestrator-worker.cjs +0 -219
  55. package/bin/gsd-t-orchestrator.js +0 -535
  56. package/bin/gsd-t-parallel-probe.cjs +0 -132
  57. package/bin/gsd-t-ratelimit-probe-worker.cjs +0 -237
  58. package/bin/gsd-t-ratelimit-probe.cjs +0 -648
  59. package/bin/gsd-t-report-tokens.cjs +0 -549
  60. package/bin/gsd-t-stream-feed-client.cjs +0 -151
  61. package/bin/gsd-t-token-backfill.cjs +0 -366
  62. package/bin/gsd-t-token-capture.cjs +0 -321
  63. package/bin/gsd-t-token-dashboard.cjs +0 -353
  64. package/bin/gsd-t-token-regenerate-log.cjs +0 -129
  65. package/bin/gsd-t-tool-attribution.cjs +0 -377
  66. package/bin/gsd-t-tool-cost.cjs +0 -195
  67. package/bin/gsd-t-transcript-tee.cjs +0 -246
  68. package/bin/gsd-t-unattended-heartbeat.cjs +0 -188
  69. package/bin/gsd-t-unattended-platform.cjs +0 -551
  70. package/bin/gsd-t-unattended-platform.js +0 -381
  71. package/bin/gsd-t-unattended-safety.cjs +0 -773
  72. package/bin/gsd-t-unattended-safety.js +0 -788
  73. package/bin/gsd-t-unattended.cjs +0 -2109
  74. package/bin/gsd-t-unattended.js +0 -1367
  75. package/bin/gsd-t-worker-dispatch.cjs +0 -211
  76. package/bin/handoff-lock.cjs +0 -249
  77. package/bin/handoff-lock.js +0 -249
  78. package/bin/headless-auto-spawn.cjs +0 -711
  79. package/bin/headless-auto-spawn.js +0 -373
  80. package/bin/headless-exit-codes.cjs +0 -67
  81. package/bin/live-activity-report.cjs +0 -615
  82. package/bin/log-tail.cjs +0 -81
  83. package/bin/m44-proof-measure.cjs +0 -285
  84. package/bin/m46-iter-proof.cjs +0 -149
  85. package/bin/m46-worker-proof.cjs +0 -201
  86. package/bin/m55-substrate-proof.cjs +0 -134
  87. package/bin/metrics-rollup.js +0 -200
  88. package/bin/model-windows.cjs +0 -99
  89. package/bin/model-windows.test.cjs +0 -75
  90. package/bin/parallelism-report.cjs +0 -535
  91. package/bin/runway-estimator.cjs +0 -242
  92. package/bin/spawn-plan-derive.cjs +0 -163
  93. package/bin/spawn-plan-status-updater.cjs +0 -292
  94. package/bin/spawn-plan-writer.cjs +0 -204
  95. package/bin/supervisor-pid-fingerprint.cjs +0 -126
  96. package/bin/token-budget.cjs +0 -265
  97. package/bin/unattended-watch-format.cjs +0 -178
  98. package/bin/watch-progress.js +0 -155
  99. package/commands/gsd-t-unattended-stop.md +0 -85
  100. package/commands/gsd-t-unattended-watch.md +0 -465
  101. package/commands/gsd-t-unattended.md +0 -476
  102. package/commands/gsd-t-visualize.md +0 -116
  103. package/scripts/gsd-t-context-meter.e2e.test.js +0 -364
  104. package/scripts/gsd-t-context-meter.js +0 -341
  105. package/scripts/gsd-t-context-meter.test.js +0 -471
  106. package/scripts/gsd-t-dashboard-server.js +0 -1203
  107. package/scripts/gsd-t-post-commit-spawn-plan.sh +0 -86
  108. package/scripts/gsd-t-transcript.html +0 -1821
  109. package/scripts/hooks/gsd-t-conversation-capture.js +0 -439
  110. package/scripts/hooks/gsd-t-in-session-usage-hook.js +0 -84
  111. package/scripts/spawn-plan-fmt-tokens.cjs +0 -80
  112. 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
- });