@tekyzinc/gsd-t 3.29.11 → 4.0.12

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