@tekyzinc/gsd-t 3.29.11 → 4.0.14

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