@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,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
- };