@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,315 +0,0 @@
1
- "use strict";
2
-
3
- /**
4
- * gsd-t-economics — M44 D6 (pre-spawn economics estimator)
5
- *
6
- * Contract: .gsd-t/contracts/economics-estimator-contract.md
7
- *
8
- * Hard invariants:
9
- * - Zero external runtime deps (Node built-ins only).
10
- * - Corpus loaded ONCE per projectDir at module init (sync read, cached).
11
- * - NEVER returns undefined — global median fallback guarantees a number.
12
- * - D6 is a HINT only; D2 owns the final gate decision.
13
- * - Event emission is best-effort; failures never fail the estimate.
14
- */
15
-
16
- const fs = require("node:fs");
17
- const path = require("node:path");
18
-
19
- // ─── Constants ────────────────────────────────────────────────────────────
20
-
21
- const { SAFE_DEFAULT_WINDOW } = require("./model-windows.cjs");
22
-
23
- /**
24
- * Effective CW ceiling in tokens. This IS the model context window, used for
25
- * `tokens / ceiling` percentages and parallel-mode gate decisions. Resolved
26
- * model-aware (see resolveCwCeiling) — the old hardcoded 200K was correct only
27
- * for pre-4 models and overstated every CW% 5× on Opus/Sonnet (1M window),
28
- * skewing parallel-mode gates. Kept as the safe fallback when no meter state.
29
- * - bin/token-budget.cjs (FALLBACK_WINDOW = model-windows safe)
30
- * - bin/context-meter-config.cjs (modelWindowSize default 200000, now a fallback)
31
- * - bin/runway-estimator.cjs (DEFAULT_MODEL_CONTEXT_CAP = model-windows safe)
32
- */
33
- const CW_CEILING_TOKENS = SAFE_DEFAULT_WINDOW;
34
- const METER_STATE_REL = ".gsd-t/.context-meter-state.json";
35
- const METER_STATE_STALE_MS = 5 * 60 * 1000;
36
-
37
- /**
38
- * Resolve the effective CW ceiling for a project. Prefers a fresh Context
39
- * Meter reading (model-aware modelWindowSize, written by the meter hook from
40
- * the running model), else the safe large default.
41
- */
42
- function resolveCwCeiling(projectDir) {
43
- try {
44
- const fp = path.join(projectDir || ".", METER_STATE_REL);
45
- const s = JSON.parse(fs.readFileSync(fp, "utf8"));
46
- if (s && typeof s.modelWindowSize === "number" && s.modelWindowSize > 0 && s.timestamp) {
47
- const age = Date.now() - Date.parse(s.timestamp);
48
- if (!isNaN(age) && age >= 0 && age <= METER_STATE_STALE_MS) {
49
- return s.modelWindowSize;
50
- }
51
- }
52
- } catch (_) { /* fall through */ }
53
- return CW_CEILING_TOKENS;
54
- }
55
-
56
- /** Confidence tier cutoffs (exact-match row counts). */
57
- const HIGH_CONFIDENCE_MIN = 5; // ≥5 exact matches
58
- // MEDIUM: 1-4 exact matches.
59
- // LOW: fuzzy match (domain-only or command-only).
60
- // FALLBACK: global median.
61
-
62
- /** Mode-specific gate thresholds (percent of CW ceiling). */
63
- const IN_SESSION_PARALLEL_OK_PCT = 85;
64
- const UNATTENDED_PARALLEL_OK_PCT = 60;
65
- const UNATTENDED_SPLIT_PCT = 60;
66
-
67
- // ─── Corpus loading (ONCE per projectDir) ─────────────────────────────────
68
-
69
- const _corpusCache = new Map(); // projectDir → loaded corpus index
70
-
71
- /**
72
- * Synchronously load the token-usage corpus for a given projectDir.
73
- * Cached indefinitely per process per projectDir.
74
- *
75
- * @param {string} projectDir
76
- * @returns {{
77
- * rows: object[],
78
- * exact: Map<string, number[]>, // "cmd|step|dom" → [totals]
79
- * byDomain: Map<string, number[]>, // dom → [totals]
80
- * byCommand: Map<string, number[]>, // cmd → [totals]
81
- * globalMedian: number,
82
- * globalPct: number,
83
- * }}
84
- */
85
- function loadCorpus(projectDir) {
86
- if (_corpusCache.has(projectDir)) return _corpusCache.get(projectDir);
87
-
88
- const corpusPath = path.join(projectDir, ".gsd-t", "metrics", "token-usage.jsonl");
89
- let raw = "";
90
- try {
91
- raw = fs.readFileSync(corpusPath, "utf8");
92
- } catch {
93
- raw = "";
94
- }
95
-
96
- const rows = [];
97
- for (const line of raw.split(/\r?\n/)) {
98
- if (!line) continue;
99
- try {
100
- const r = JSON.parse(line);
101
- if (r && typeof r === "object") rows.push(r);
102
- } catch {
103
- /* skip malformed */
104
- }
105
- }
106
-
107
- const exact = new Map();
108
- const byDomain = new Map();
109
- const byCommand = new Map();
110
- const allTotals = [];
111
-
112
- for (const r of rows) {
113
- const total = rowTotalTokens(r);
114
- const cmd = r.command || "-";
115
- const step = r.step || "-";
116
- const dom = r.domain || "-";
117
- const key = `${cmd}|${step}|${dom}`;
118
- pushMap(exact, key, total);
119
- pushMap(byDomain, dom, total);
120
- pushMap(byCommand, cmd, total);
121
- allTotals.push(total);
122
- }
123
-
124
- const globalMedian = median(allTotals);
125
- const globalPct = tokensToCwPct(globalMedian, resolveCwCeiling(projectDir));
126
-
127
- const idx = { rows, exact, byDomain, byCommand, globalMedian, globalPct };
128
- _corpusCache.set(projectDir, idx);
129
- return idx;
130
- }
131
-
132
- /** Row-level total = input + output + cacheRead + cacheCreation. */
133
- function rowTotalTokens(r) {
134
- return (r.inputTokens || 0)
135
- + (r.outputTokens || 0)
136
- + (r.cacheReadInputTokens || 0)
137
- + (r.cacheCreationInputTokens || 0);
138
- }
139
-
140
- function pushMap(m, k, v) {
141
- if (!m.has(k)) m.set(k, []);
142
- m.get(k).push(v);
143
- }
144
-
145
- function median(arr) {
146
- if (!arr || arr.length === 0) return 0;
147
- const a = arr.slice().sort((x, y) => x - y);
148
- const mid = Math.floor(a.length / 2);
149
- return a.length % 2 === 1 ? a[mid] : (a[mid - 1] + a[mid]) / 2;
150
- }
151
-
152
- function tokensToCwPct(tokens, ceiling) {
153
- if (!Number.isFinite(tokens) || tokens <= 0) return 0;
154
- const c = Number.isFinite(ceiling) && ceiling > 0 ? ceiling : CW_CEILING_TOKENS;
155
- return (tokens / c) * 100;
156
- }
157
-
158
- // ─── Event emission (best-effort) ─────────────────────────────────────────
159
-
160
- function writeEconomicsEvent(projectDir, ev) {
161
- try {
162
- const dir = path.join(projectDir, ".gsd-t", "events");
163
- fs.mkdirSync(dir, { recursive: true });
164
- const now = new Date(ev.ts || Date.now());
165
- const y = now.getUTCFullYear();
166
- const m = String(now.getUTCMonth() + 1).padStart(2, "0");
167
- const d = String(now.getUTCDate()).padStart(2, "0");
168
- const file = path.join(dir, `${y}-${m}-${d}.jsonl`);
169
- const line = JSON.stringify(ev) + "\n";
170
- fs.appendFileSync(file, line, "utf8");
171
- return true;
172
- } catch {
173
- return false; // best-effort — never fails the estimate
174
- }
175
- }
176
-
177
- // ─── Lookup algorithm ─────────────────────────────────────────────────────
178
-
179
- /**
180
- * Three-tier lookup against the corpus:
181
- * 1. Exact `command|step|domain` match → HIGH (≥5) or MEDIUM (1–4).
182
- * 2. Fuzzy match (domain-only, then command-only) → LOW.
183
- * 3. Global median → FALLBACK.
184
- *
185
- * Returns { estimatedTokens, matchedRows, confidence }.
186
- */
187
- function lookupInCorpus(taskNode, corpus) {
188
- const cmd = (taskNode && taskNode.command) || "-";
189
- const step = (taskNode && taskNode.step) || "-";
190
- const dom = (taskNode && taskNode.domain) || "-";
191
-
192
- // Tier 1: exact triplet.
193
- const exactKey = `${cmd}|${step}|${dom}`;
194
- const exactRows = corpus.exact.get(exactKey);
195
- if (exactRows && exactRows.length > 0) {
196
- const n = exactRows.length;
197
- return {
198
- estimatedTokens: median(exactRows),
199
- matchedRows: n,
200
- confidence: n >= HIGH_CONFIDENCE_MIN ? "HIGH" : "MEDIUM",
201
- };
202
- }
203
-
204
- // Tier 2a: domain-only fuzzy match.
205
- if (dom && dom !== "-") {
206
- const domRows = corpus.byDomain.get(dom);
207
- if (domRows && domRows.length > 0) {
208
- return {
209
- estimatedTokens: median(domRows),
210
- matchedRows: domRows.length,
211
- confidence: "LOW",
212
- };
213
- }
214
- }
215
-
216
- // Tier 2b: command-only fuzzy match.
217
- if (cmd && cmd !== "-") {
218
- const cmdRows = corpus.byCommand.get(cmd);
219
- if (cmdRows && cmdRows.length > 0) {
220
- return {
221
- estimatedTokens: median(cmdRows),
222
- matchedRows: cmdRows.length,
223
- confidence: "LOW",
224
- };
225
- }
226
- }
227
-
228
- // Tier 3: global median fallback.
229
- return {
230
- estimatedTokens: corpus.globalMedian,
231
- matchedRows: 0,
232
- confidence: "FALLBACK",
233
- };
234
- }
235
-
236
- // ─── Gate arithmetic ──────────────────────────────────────────────────────
237
-
238
- function decideGates(mode, estimatedCwPct, confidence) {
239
- let parallelOk;
240
- let split;
241
-
242
- if (mode === "unattended") {
243
- parallelOk = estimatedCwPct <= UNATTENDED_PARALLEL_OK_PCT;
244
- split = estimatedCwPct > UNATTENDED_SPLIT_PCT;
245
- } else {
246
- // 'in-session' (default)
247
- parallelOk = estimatedCwPct <= IN_SESSION_PARALLEL_OK_PCT;
248
- split = false;
249
- }
250
-
251
- // Worker-count recommendation: 1 by default; halve (floor 1) for FALLBACK
252
- // confidence per §5 guidance. LOW confidence keeps 1 worker at this level —
253
- // D2 applies the "reduce by 1–2" heuristic itself (it owns pool sizing).
254
- let workerCount = 1;
255
- if (confidence === "FALLBACK") workerCount = 1; // already 1; documented for clarity
256
- return { parallelOk, split, workerCount };
257
- }
258
-
259
- // ─── Public API ───────────────────────────────────────────────────────────
260
-
261
- /**
262
- * Estimate a task's CW footprint and produce a mode-specific recommendation.
263
- *
264
- * @param {object} opts
265
- * @param {{id?:string, command?:string, step?:string, domain?:string}} opts.taskNode
266
- * @param {'in-session'|'unattended'} opts.mode
267
- * @param {string} [opts.projectDir]
268
- * @returns {{estimatedCwPct:number, parallelOk:boolean, split:boolean, workerCount:number, matchedRows:number, confidence:'HIGH'|'MEDIUM'|'LOW'|'FALLBACK'}}
269
- */
270
- function estimateTaskFootprint(opts) {
271
- const taskNode = (opts && opts.taskNode) || {};
272
- const mode = (opts && opts.mode) || "in-session";
273
- const projectDir = (opts && opts.projectDir) || process.cwd();
274
-
275
- const corpus = loadCorpus(projectDir);
276
-
277
- const { estimatedTokens, matchedRows, confidence } = lookupInCorpus(taskNode, corpus);
278
- const estimatedCwPct = tokensToCwPct(estimatedTokens, resolveCwCeiling(projectDir));
279
-
280
- const { parallelOk, split, workerCount } = decideGates(mode, estimatedCwPct, confidence);
281
-
282
- // Best-effort event emission.
283
- writeEconomicsEvent(projectDir, {
284
- type: "economics_decision",
285
- ts: new Date().toISOString(),
286
- task_id: taskNode.id || null,
287
- mode,
288
- estimatedCwPct,
289
- parallelOk,
290
- split,
291
- confidence,
292
- matchedRows,
293
- });
294
-
295
- return {
296
- estimatedCwPct,
297
- parallelOk,
298
- split,
299
- workerCount,
300
- matchedRows,
301
- confidence,
302
- };
303
- }
304
-
305
- module.exports = {
306
- estimateTaskFootprint,
307
- // Internals exposed for unit tests + calibration tooling:
308
- _CW_CEILING_TOKENS: CW_CEILING_TOKENS,
309
- _loadCorpus: loadCorpus,
310
- _lookupInCorpus: lookupInCorpus,
311
- _rowTotalTokens: rowTotalTokens,
312
- _median: median,
313
- _tokensToCwPct: tokensToCwPct,
314
- _resetCorpusCache: function resetCorpusCache() { _corpusCache.clear(); },
315
- };
@@ -1,213 +0,0 @@
1
- 'use strict';
2
- /**
3
- * GSD-T In-Session Usage Capture (M43 D1)
4
- *
5
- * Writes one schema-v2 row per **assistant turn** of an interactive Claude Code
6
- * session to `.gsd-t/metrics/token-usage.jsonl`, so the dialog channel is
7
- * observable alongside the headless spawns M41 already covers.
8
- *
9
- * Branch B (transcript-sourced). Claude Code hook payloads (Stop / SessionEnd /
10
- * PostToolUse) do not carry `usage`, but every payload contains
11
- * `transcript_path` — the on-disk jsonl Claude Code appends assistant turns to.
12
- * This module reads the transcript, extracts `message.usage` envelopes per
13
- * assistant turn (keyed by `message.id` for dedup), and hands each new envelope
14
- * to `recordSpawnRow` from `bin/gsd-t-token-capture.cjs` with
15
- * `sessionType: 'in-session'`.
16
- *
17
- * Dedup state lives per `session_id` in `.gsd-t/.in-session-cursor.json` —
18
- * tracks the last-seen `message.id` so repeated Stop fires within the same
19
- * session only append new rows.
20
- *
21
- * Zero external deps. `.cjs` for ESM/CJS compat.
22
- */
23
-
24
- const fs = require('fs');
25
- const path = require('path');
26
-
27
- const capture = require('./gsd-t-token-capture.cjs');
28
-
29
- const CURSOR_REL = path.join('.gsd-t', '.in-session-cursor.json');
30
-
31
- // ── Cursor state ─────────────────────────────────────────────────────
32
-
33
- function _loadCursor(projectDir) {
34
- const p = path.join(projectDir, CURSOR_REL);
35
- try {
36
- return JSON.parse(fs.readFileSync(p, 'utf8'));
37
- } catch (_) {
38
- return {};
39
- }
40
- }
41
-
42
- function _saveCursor(projectDir, state) {
43
- const p = path.join(projectDir, CURSOR_REL);
44
- const dir = path.dirname(p);
45
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
46
- fs.writeFileSync(p, JSON.stringify(state, null, 2));
47
- }
48
-
49
- // ── Transcript parsing ───────────────────────────────────────────────
50
-
51
- function _parseJsonLineSafe(line) {
52
- const s = String(line || '').trim();
53
- if (!s || s[0] !== '{') return null;
54
- try { return JSON.parse(s); } catch (_) { return null; }
55
- }
56
-
57
- /**
58
- * Walk a Claude Code transcript jsonl and return assistant-turn usage entries.
59
- *
60
- * Each returned entry: { messageId, model, usage, timestamp, turnIndex }
61
- * - `messageId` from `message.id` (stable per assistant turn)
62
- * - `model` from `message.model`
63
- * - `usage` from `message.usage` (the full envelope — schema lines up with
64
- * Anthropic API usage + Claude Code's additions like `cache_creation`,
65
- * `service_tier`, `iterations[]`).
66
- * - `timestamp` from the transcript line's `timestamp` field (ISO-8601)
67
- * - `turnIndex` 0-based position among usage-bearing lines in the transcript
68
- */
69
- function extractTurns(transcriptPath) {
70
- if (!transcriptPath || !fs.existsSync(transcriptPath)) return [];
71
- const text = fs.readFileSync(transcriptPath, 'utf8');
72
- const lines = text.split(/\r?\n/);
73
- const out = [];
74
- let turnIndex = 0;
75
- for (const line of lines) {
76
- const j = _parseJsonLineSafe(line);
77
- if (!j) continue;
78
- const msg = j.message;
79
- if (!msg || typeof msg !== 'object') continue;
80
- if (msg.role && msg.role !== 'assistant') continue;
81
- const usage = msg.usage;
82
- if (!usage || typeof usage !== 'object') continue;
83
- out.push({
84
- messageId: msg.id || j.uuid || null,
85
- model: msg.model || null,
86
- usage,
87
- timestamp: j.timestamp || null,
88
- turnIndex: turnIndex++,
89
- });
90
- }
91
- return out;
92
- }
93
-
94
- // ── Row emission ─────────────────────────────────────────────────────
95
-
96
- function pad2(n) { return String(n).padStart(2, '0'); }
97
- function _fmtDateTime(iso) {
98
- const d = iso ? new Date(iso) : new Date();
99
- if (isNaN(d.getTime())) return _fmtDateTime(new Date().toISOString());
100
- return `${d.getFullYear()}-${pad2(d.getMonth()+1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
101
- }
102
-
103
- /**
104
- * Capture usage for one in-session turn.
105
- *
106
- * Low-level entry point: caller supplies the usage envelope directly. Used by
107
- * unit tests and by the hook handler after extracting a turn from the
108
- * transcript.
109
- *
110
- * @param {object} opts
111
- * @param {string} opts.projectDir
112
- * @param {string} opts.sessionId
113
- * @param {string|number} opts.turnId unique-per-session identifier
114
- * @param {object|null} opts.usage Claude usage envelope (may be null)
115
- * @param {string} [opts.model]
116
- * @param {string} [opts.command] e.g. 'in-session' or 'dialog'
117
- * @param {string} [opts.ts] ISO-8601; defaults to now
118
- * @returns {{jsonlPath: string, tokenLogPath: string}}
119
- */
120
- function captureInSessionUsage(opts) {
121
- if (!opts || !opts.projectDir) throw new Error('captureInSessionUsage: projectDir required');
122
- if (opts.sessionId == null) throw new Error('captureInSessionUsage: sessionId required');
123
- if (opts.turnId == null) throw new Error('captureInSessionUsage: turnId required');
124
-
125
- const stamp = _fmtDateTime(opts.ts);
126
- return capture.recordSpawnRow({
127
- projectDir: opts.projectDir,
128
- command: opts.command || 'in-session',
129
- step: 'turn',
130
- model: opts.model || 'claude',
131
- startedAt: stamp,
132
- endedAt: stamp,
133
- usage: opts.usage || undefined,
134
- notes: 'in-session turn',
135
- sessionId: opts.sessionId,
136
- turnId: opts.turnId,
137
- sessionType: 'in-session',
138
- // Canonical sink is JSONL; token-log.md is regenerated via
139
- // `gsd-t tokens --regenerate-log` (D3). Avoid polluting the markdown
140
- // log with per-turn rows (one session can have hundreds of turns).
141
- skipMarkdownLog: true,
142
- });
143
- }
144
-
145
- /**
146
- * Process a hook payload (Stop / SessionEnd) — read the transcript it points
147
- * at, emit rows for every assistant turn we haven't seen yet.
148
- *
149
- * @param {object} opts
150
- * @param {string} opts.projectDir
151
- * @param {object} opts.payload raw hook payload as written by Claude Code
152
- * @returns {{sessionId: string|null, emitted: number, skipped: number, reason?: string}}
153
- */
154
- function processHookPayload(opts) {
155
- const projectDir = (opts && opts.projectDir) || '.';
156
- const payload = opts && opts.payload;
157
- if (!payload || typeof payload !== 'object') {
158
- return { sessionId: null, emitted: 0, skipped: 0, reason: 'no-payload' };
159
- }
160
- const sessionId = payload.session_id || payload.sessionId || null;
161
- const transcriptPath = payload.transcript_path || payload.transcriptPath || null;
162
- if (!sessionId || !transcriptPath) {
163
- return { sessionId, emitted: 0, skipped: 0, reason: 'missing-session-or-transcript' };
164
- }
165
-
166
- const turns = extractTurns(transcriptPath);
167
- if (!turns.length) {
168
- return { sessionId, emitted: 0, skipped: 0, reason: 'no-turns' };
169
- }
170
-
171
- const cursor = _loadCursor(projectDir);
172
- const lastSeen = cursor[sessionId] && cursor[sessionId].lastMessageId;
173
-
174
- let startAt = 0;
175
- if (lastSeen) {
176
- const idx = turns.findIndex(t => t.messageId === lastSeen);
177
- if (idx >= 0) startAt = idx + 1;
178
- }
179
-
180
- let emitted = 0;
181
- let skipped = startAt;
182
- for (let i = startAt; i < turns.length; i++) {
183
- const turn = turns[i];
184
- const turnId = turn.messageId || `idx-${turn.turnIndex}`;
185
- captureInSessionUsage({
186
- projectDir,
187
- sessionId,
188
- turnId,
189
- usage: turn.usage,
190
- model: turn.model,
191
- command: 'in-session',
192
- ts: turn.timestamp,
193
- });
194
- emitted++;
195
- }
196
-
197
- const newLast = turns[turns.length - 1];
198
- cursor[sessionId] = {
199
- lastMessageId: newLast ? newLast.messageId : null,
200
- lastTurnIndex: newLast ? newLast.turnIndex : -1,
201
- lastUpdatedAt: new Date().toISOString(),
202
- };
203
- _saveCursor(projectDir, cursor);
204
-
205
- return { sessionId, emitted, skipped };
206
- }
207
-
208
- module.exports = {
209
- captureInSessionUsage,
210
- processHookPayload,
211
- extractTurns,
212
- _internal: { _loadCursor, _saveCursor },
213
- };
@@ -1,161 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const os = require('os');
5
- const path = require('path');
6
-
7
- const DEFAULTS = Object.freeze({
8
- maxParallel: 3,
9
- workerTimeoutMs: 270000,
10
- retryOnFail: true,
11
- haltOnSecondFail: true
12
- });
13
-
14
- const MAX_PARALLEL_CEILING = 15;
15
- const WORKER_RAM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
16
- const ADAPTIVE_FLOOR = 3;
17
-
18
- // ─── M44 D2 — mode-aware gating math ──────────────────────────────────────
19
- // Contract: `.gsd-t/contracts/wave-join-contract.md` v1.1.0 §Mode-Aware Gating Math.
20
-
21
- const IN_SESSION_CW_CEILING_PCT = 85;
22
- const UNATTENDED_PER_WORKER_CW_PCT = 60;
23
- const DEFAULT_SUMMARY_SIZE_PCT = 4;
24
-
25
- /**
26
- * [in-session] headroom gate.
27
- *
28
- * Returns `{ok, reducedCount}`.
29
- * ok=true iff `ctxPct + workerCount * summarySize ≤ IN_SESSION_CW_CEILING_PCT`.
30
- * Otherwise reduces N repeatedly; final floor is N=1. NEVER refuses
31
- * (constraints.md: never throw a pause/resume prompt under any condition).
32
- *
33
- * - reducedCount = the largest N ≤ requested workerCount such that the
34
- * headroom inequality holds. If the inequality fails for every N ≥ 1
35
- * (e.g. ctxPct already > ceiling), returns { ok: true, reducedCount: 1 }
36
- * — sequential always remains feasible because the 4% summary is only
37
- * spent *post*-worker, and one worker is the irreducible floor.
38
- */
39
- function computeInSessionHeadroom(opts) {
40
- const o = opts || {};
41
- const ctxPct = Number.isFinite(o.ctxPct) ? o.ctxPct : 0;
42
- const requested = Number.isFinite(o.workerCount) ? Math.max(0, Math.floor(o.workerCount)) : 0;
43
- const summarySize = Number.isFinite(o.summarySize) ? o.summarySize : DEFAULT_SUMMARY_SIZE_PCT;
44
- const ceiling = IN_SESSION_CW_CEILING_PCT;
45
-
46
- // Direct fit.
47
- if (ctxPct + requested * summarySize <= ceiling) {
48
- return { ok: true, reducedCount: requested };
49
- }
50
- // Reduce N until it fits or we hit the floor.
51
- let n = requested - 1;
52
- while (n > 1) {
53
- if (ctxPct + n * summarySize <= ceiling) {
54
- return { ok: true, reducedCount: n };
55
- }
56
- n -= 1;
57
- }
58
- // Floor: 1 worker (sequential). Never refuses.
59
- return { ok: true, reducedCount: 1 };
60
- }
61
-
62
- /**
63
- * [unattended] per-worker CW gate.
64
- *
65
- * Returns `{ok, split}`.
66
- * ok=true, split=false if `estimatedCwPct ≤ threshold` (default 60).
67
- * ok=false, split=true otherwise — caller MUST slice the task into
68
- * multiple `claude -p` iters (actual splitting is scheduled by the
69
- * caller; this function only signals the split requirement).
70
- */
71
- function computeUnattendedGate(opts) {
72
- const o = opts || {};
73
- const estimatedCwPct = Number.isFinite(o.estimatedCwPct) ? o.estimatedCwPct : 0;
74
- const threshold = Number.isFinite(o.threshold) ? o.threshold : UNATTENDED_PER_WORKER_CW_PCT;
75
- if (estimatedCwPct > threshold) {
76
- return { ok: false, split: true };
77
- }
78
- return { ok: true, split: false };
79
- }
80
-
81
- function computeAdaptiveMaxParallel(freeBytes) {
82
- const free = typeof freeBytes === 'number' ? freeBytes : os.freemem();
83
- if (!Number.isFinite(free) || free <= 0) return ADAPTIVE_FLOOR;
84
- const byMemory = Math.floor(free / WORKER_RAM_BUDGET_BYTES);
85
- const clamped = Math.max(ADAPTIVE_FLOOR, Math.min(MAX_PARALLEL_CEILING, byMemory));
86
- return clamped;
87
- }
88
-
89
- function loadConfigFile(projectDir) {
90
- const p = path.join(projectDir, '.gsd-t', 'orchestrator.config.json');
91
- if (!fs.existsSync(p)) return {};
92
- try {
93
- return JSON.parse(fs.readFileSync(p, 'utf8'));
94
- } catch (err) {
95
- throw new Error(`orchestrator.config.json parse error: ${err.message}`);
96
- }
97
- }
98
-
99
- function parseIntStrict(v, name) {
100
- if (typeof v === 'number' && Number.isInteger(v)) return v;
101
- if (typeof v === 'string' && /^\d+$/.test(v)) return parseInt(v, 10);
102
- throw new Error(`${name} must be a non-negative integer, got ${JSON.stringify(v)}`);
103
- }
104
-
105
- function loadConfig(opts) {
106
- const { projectDir, cliFlags = {}, env = process.env, freeMemBytes } = opts || {};
107
- if (!projectDir) throw new Error('loadConfig requires projectDir');
108
-
109
- const fileCfg = loadConfigFile(projectDir);
110
- const fileSetMaxParallel = Object.prototype.hasOwnProperty.call(fileCfg, 'maxParallel');
111
- const merged = { ...DEFAULTS, ...fileCfg };
112
- let maxParallelSource = fileSetMaxParallel ? 'config-file' : 'adaptive';
113
- if (!fileSetMaxParallel) {
114
- merged.maxParallel = computeAdaptiveMaxParallel(freeMemBytes);
115
- }
116
-
117
- if (cliFlags.maxParallel != null) {
118
- merged.maxParallel = parseIntStrict(cliFlags.maxParallel, '--max-parallel');
119
- maxParallelSource = 'cli';
120
- }
121
- if (cliFlags.workerTimeoutMs != null) merged.workerTimeoutMs = parseIntStrict(cliFlags.workerTimeoutMs, '--worker-timeout');
122
- if (cliFlags.retryOnFail != null) merged.retryOnFail = !!cliFlags.retryOnFail;
123
- if (cliFlags.haltOnSecondFail != null) merged.haltOnSecondFail = !!cliFlags.haltOnSecondFail;
124
-
125
- if (env.GSD_T_MAX_PARALLEL != null && env.GSD_T_MAX_PARALLEL !== '') {
126
- merged.maxParallel = parseIntStrict(env.GSD_T_MAX_PARALLEL, 'GSD_T_MAX_PARALLEL');
127
- maxParallelSource = 'env';
128
- }
129
- if (env.GSD_T_WORKER_TIMEOUT_MS != null && env.GSD_T_WORKER_TIMEOUT_MS !== '') {
130
- merged.workerTimeoutMs = parseIntStrict(env.GSD_T_WORKER_TIMEOUT_MS, 'GSD_T_WORKER_TIMEOUT_MS');
131
- }
132
-
133
- if (merged.maxParallel < 1) {
134
- throw new Error(`maxParallel must be >= 1, got ${merged.maxParallel}`);
135
- }
136
- if (merged.maxParallel > MAX_PARALLEL_CEILING) {
137
- throw new Error(`maxParallel ${merged.maxParallel} exceeds Team Mode §15 ceiling (${MAX_PARALLEL_CEILING})`);
138
- }
139
- if (merged.workerTimeoutMs < 1000) {
140
- throw new Error(`workerTimeoutMs must be >= 1000, got ${merged.workerTimeoutMs}`);
141
- }
142
-
143
- merged.projectDir = projectDir;
144
- merged.maxParallelSource = maxParallelSource;
145
- return merged;
146
- }
147
-
148
- module.exports = {
149
- loadConfig,
150
- DEFAULTS,
151
- MAX_PARALLEL_CEILING,
152
- computeAdaptiveMaxParallel,
153
- WORKER_RAM_BUDGET_BYTES,
154
- ADAPTIVE_FLOOR,
155
- // M44 D2 — mode-aware gating math
156
- computeInSessionHeadroom,
157
- computeUnattendedGate,
158
- IN_SESSION_CW_CEILING_PCT,
159
- UNATTENDED_PER_WORKER_CW_PCT,
160
- DEFAULT_SUMMARY_SIZE_PCT,
161
- };