@tekyzinc/gsd-t 3.16.12 → 3.18.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 (53) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +13 -3
  3. package/bin/gsd-t-depgraph-validate.cjs +140 -0
  4. package/bin/gsd-t-economics.cjs +287 -0
  5. package/bin/gsd-t-file-disjointness.cjs +227 -0
  6. package/bin/gsd-t-in-session-usage.cjs +213 -0
  7. package/bin/gsd-t-orchestrator-config.cjs +100 -3
  8. package/bin/gsd-t-orchestrator.js +2 -1
  9. package/bin/gsd-t-parallel.cjs +382 -0
  10. package/bin/gsd-t-report-tokens.cjs +549 -0
  11. package/bin/gsd-t-task-graph.cjs +366 -0
  12. package/bin/gsd-t-token-capture.cjs +29 -14
  13. package/bin/gsd-t-token-dashboard.cjs +35 -0
  14. package/bin/gsd-t-tool-attribution.cjs +377 -0
  15. package/bin/gsd-t-tool-cost.cjs +195 -0
  16. package/bin/gsd-t-unattended-platform.cjs +7 -1
  17. package/bin/gsd-t-unattended.cjs +2 -0
  18. package/bin/gsd-t.js +155 -5
  19. package/bin/headless-auto-spawn.cjs +69 -49
  20. package/bin/headless-auto-spawn.js +18 -24
  21. package/bin/runway-estimator.cjs +212 -0
  22. package/bin/spawn-plan-derive.cjs +163 -0
  23. package/bin/spawn-plan-status-updater.cjs +292 -0
  24. package/bin/spawn-plan-writer.cjs +204 -0
  25. package/commands/gsd-t-debug.md +26 -7
  26. package/commands/gsd-t-execute.md +36 -28
  27. package/commands/gsd-t-help.md +11 -0
  28. package/commands/gsd-t-integrate.md +27 -7
  29. package/commands/gsd-t-quick.md +30 -13
  30. package/commands/gsd-t-scan.md +5 -5
  31. package/commands/gsd-t-unattended-watch.md +4 -3
  32. package/commands/gsd-t-unattended.md +9 -3
  33. package/commands/gsd-t-verify.md +5 -5
  34. package/commands/gsd-t-wave.md +21 -8
  35. package/commands/gsd.md +45 -3
  36. package/docs/GSD-T-README.md +43 -5
  37. package/docs/architecture.md +423 -3
  38. package/docs/requirements.md +203 -0
  39. package/package.json +1 -1
  40. package/scripts/gsd-t-calibration-hook.js +256 -0
  41. package/scripts/gsd-t-compact-detector.js +223 -0
  42. package/scripts/gsd-t-compaction-scanner.js +305 -0
  43. package/scripts/gsd-t-dashboard-autostart.cjs +172 -0
  44. package/scripts/gsd-t-dashboard-server.js +179 -0
  45. package/scripts/gsd-t-dashboard.html +3 -3
  46. package/scripts/gsd-t-heartbeat.js +50 -2
  47. package/scripts/gsd-t-post-commit-spawn-plan.sh +86 -0
  48. package/scripts/gsd-t-transcript.html +546 -43
  49. package/scripts/hooks/gsd-t-in-session-usage-hook.js +84 -0
  50. package/scripts/spawn-plan-fmt-tokens.cjs +80 -0
  51. package/templates/CLAUDE-global.md +8 -3
  52. package/templates/CLAUDE-project.md +17 -14
  53. package/templates/hooks/post-commit-spawn-plan.sh +85 -0
@@ -0,0 +1,382 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * gsd-t-parallel — M44 D2
5
+ *
6
+ * `gsd-t parallel` subcommand. Wraps the M40 orchestrator with task-level
7
+ * (not just domain-level) parallelism and mode-aware gating math.
8
+ *
9
+ * Consumes:
10
+ * - D1 task graph (bin/gsd-t-task-graph.cjs)
11
+ * - D4 depgraph validation (bin/gsd-t-depgraph-validate.cjs)
12
+ * - D5 file-disjointness (bin/gsd-t-file-disjointness.cjs)
13
+ * - D6 economics estimator (bin/gsd-t-economics.cjs)
14
+ * - token-budget (bin/token-budget.cjs) — in-session ctxPct
15
+ * - mode-aware gating math (bin/gsd-t-orchestrator-config.cjs)
16
+ *
17
+ * Does NOT replace `bin/gsd-t-orchestrator.js`. `runParallel` prepares the
18
+ * validated ready-task set and plan; the existing orchestrator machinery
19
+ * owns the actual worker spawn. In T2 this module emits the plan;
20
+ * the downstream orchestrator consumes it.
21
+ *
22
+ * Contract: `.gsd-t/contracts/wave-join-contract.md` v1.1.0 (§Mode-Aware Gating Math).
23
+ *
24
+ * Hard rules (from constraints.md):
25
+ * - Zero external runtime deps (Node built-ins only)
26
+ * - Never throws pause/resume prompts under any condition
27
+ * - All three invariants (disjointness, auto-merge, economics) apply to both modes
28
+ * - `--dry-run` MUST be supported; prints plan without spawning
29
+ * - `--mode` auto-detect fallback: `GSD_T_UNATTENDED=1` → `unattended`, else `in-session`
30
+ */
31
+
32
+ const fs = require("node:fs");
33
+ const path = require("node:path");
34
+
35
+ const { buildTaskGraph, getReadyTasks } = require(path.join(__dirname, "gsd-t-task-graph.cjs"));
36
+ const { validateDepGraph } = require(path.join(__dirname, "gsd-t-depgraph-validate.cjs"));
37
+ const { proveDisjointness } = require(path.join(__dirname, "gsd-t-file-disjointness.cjs"));
38
+ const { estimateTaskFootprint } = require(path.join(__dirname, "gsd-t-economics.cjs"));
39
+ const {
40
+ computeInSessionHeadroom,
41
+ computeUnattendedGate,
42
+ } = require(path.join(__dirname, "gsd-t-orchestrator-config.cjs"));
43
+
44
+ // token-budget is optional at require-time so unit tests can stub via dependency injection.
45
+ let _tokenBudget = null;
46
+ function loadTokenBudget() {
47
+ if (_tokenBudget) return _tokenBudget;
48
+ try {
49
+ _tokenBudget = require(path.join(__dirname, "token-budget.cjs"));
50
+ } catch {
51
+ _tokenBudget = { getSessionStatus: () => ({ pct: 0 }) };
52
+ }
53
+ return _tokenBudget;
54
+ }
55
+
56
+ // ─── event stream writer ──────────────────────────────────────────────────
57
+
58
+ function appendEvent(projectDir, event) {
59
+ try {
60
+ const eventsDir = path.join(projectDir, ".gsd-t", "events");
61
+ fs.mkdirSync(eventsDir, { recursive: true });
62
+ const day = (event.ts || new Date().toISOString()).slice(0, 10);
63
+ const file = path.join(eventsDir, `${day}.jsonl`);
64
+ fs.appendFileSync(file, JSON.stringify(event) + "\n");
65
+ } catch {
66
+ // Best-effort; event-log failures must not break control flow.
67
+ }
68
+ }
69
+
70
+ // ─── mode detection ───────────────────────────────────────────────────────
71
+
72
+ function detectMode(opts, env) {
73
+ if (opts && typeof opts.mode === "string" && opts.mode) return opts.mode;
74
+ const e = env || process.env;
75
+ if (e.GSD_T_UNATTENDED === "1") return "unattended";
76
+ return "in-session";
77
+ }
78
+
79
+ // ─── CLI arg parsing ──────────────────────────────────────────────────────
80
+
81
+ function parseArgv(argv) {
82
+ const out = { help: false, dryRun: false, mode: null, milestone: null, domain: null };
83
+ for (let i = 0; i < argv.length; i++) {
84
+ const a = argv[i];
85
+ if (a === "--help" || a === "-h") out.help = true;
86
+ else if (a === "--dry-run") out.dryRun = true;
87
+ else if (a === "--mode") out.mode = argv[++i] || null;
88
+ else if (a.startsWith("--mode=")) out.mode = a.slice("--mode=".length);
89
+ else if (a === "--milestone") out.milestone = argv[++i] || null;
90
+ else if (a.startsWith("--milestone=")) out.milestone = a.slice("--milestone=".length);
91
+ else if (a === "--domain") out.domain = argv[++i] || null;
92
+ else if (a.startsWith("--domain=")) out.domain = a.slice("--domain=".length);
93
+ }
94
+ return out;
95
+ }
96
+
97
+ const HELP_TEXT = `Usage: gsd-t parallel [options]
98
+
99
+ Dispatch M44 task-level parallelism through the M40 orchestrator with
100
+ mode-aware gating math. Extends — does not replace — the orchestrator.
101
+
102
+ Options:
103
+ --mode <in-session|unattended> Explicit mode. Auto-detects from
104
+ GSD_T_UNATTENDED=1 env when omitted;
105
+ defaults to in-session otherwise.
106
+ --milestone <Mxx> Limit planning to a single milestone.
107
+ --domain <name> Limit planning to a single domain.
108
+ --dry-run Print the proposed worker plan table
109
+ and exit without spawning any workers.
110
+ --help, -h Show this message and exit 0.
111
+
112
+ Gates applied before any fan-out (in order):
113
+ 1. D4 depgraph validation — any task with unmet deps is vetoed.
114
+ 2. D5 file-disjointness prover — overlap → sequential fallback.
115
+ 3. D6 economics estimator — per-task CW% footprint.
116
+
117
+ Modes:
118
+ in-session Never throws pause/resume prompts. Before fan-out,
119
+ computes ctxPct + N × summarySize ≤ 85. If not, reduces
120
+ N until it fits; final floor is N=1 (sequential).
121
+ unattended Per-worker CW headroom is the binding gate. Tasks whose
122
+ estimated CW% > 60 emit a task_split signal.
123
+
124
+ Contract: .gsd-t/contracts/wave-join-contract.md v1.1.0
125
+ `;
126
+
127
+ // ─── runParallel — the exported entrypoint ────────────────────────────────
128
+
129
+ /**
130
+ * runParallel({projectDir, mode, milestone, domain, dryRun}) → plan object
131
+ *
132
+ * Applies D4 dep-graph + D5 disjointness + D6 economics gates, then the
133
+ * mode-aware headroom/split gate, and returns the resolved worker plan.
134
+ *
135
+ * Does not spawn. The caller (M40 orchestrator) owns actual worker launch.
136
+ */
137
+ function runParallel(opts) {
138
+ const projectDir = (opts && opts.projectDir) || process.cwd();
139
+ const mode = detectMode(opts, opts && opts.env);
140
+ const milestone = (opts && opts.milestone) || null;
141
+ const domain = (opts && opts.domain) || null;
142
+ const dryRun = !!(opts && opts.dryRun);
143
+ const summarySize = Number.isFinite(opts && opts.summarySize)
144
+ ? opts.summarySize
145
+ : require(path.join(__dirname, "gsd-t-orchestrator-config.cjs")).DEFAULT_SUMMARY_SIZE_PCT;
146
+
147
+ const graph = buildTaskGraph({ projectDir });
148
+ let candidates = getReadyTasks(graph);
149
+
150
+ // Optional filtering by milestone / domain.
151
+ if (domain) candidates = candidates.filter((t) => t.domain === domain);
152
+ if (milestone) {
153
+ // Milestone id prefixes task ids in this codebase (M44-D2-T1 → M44).
154
+ const prefix = String(milestone).toUpperCase();
155
+ candidates = candidates.filter((t) => String(t.id).toUpperCase().startsWith(prefix + "-"));
156
+ }
157
+
158
+ // ── D4 depgraph gate ──
159
+ const depResult = validateDepGraph({ graph: { ...graph, ready: candidates.map((t) => t.id) }, projectDir });
160
+ const depReady = depResult.ready;
161
+ const depVetoed = depResult.vetoed || [];
162
+ for (const v of depVetoed) {
163
+ appendEvent(projectDir, {
164
+ type: "gate_veto",
165
+ task_id: v.task && v.task.id,
166
+ gate: "depgraph",
167
+ reason: `unmet_deps:${(v.unmet_deps || []).join(",")}`,
168
+ ts: new Date().toISOString(),
169
+ });
170
+ }
171
+
172
+ // ── D5 disjointness gate ──
173
+ const disj = proveDisjointness({ tasks: depReady, projectDir });
174
+ const disjointTaskIds = new Set();
175
+ for (const group of disj.parallel || []) {
176
+ for (const t of group) disjointTaskIds.add(t.id);
177
+ }
178
+ // Sequential groups + unprovable are NOT candidates for parallel fan-out
179
+ // but still allowed as single-worker (sequential) — surfaced as gate_veto
180
+ // events so "why wasn't this parallelized?" is observable.
181
+ const sequentialFallback = new Set();
182
+ for (const group of disj.sequential || []) {
183
+ for (const t of group) {
184
+ sequentialFallback.add(t.id);
185
+ appendEvent(projectDir, {
186
+ type: "gate_veto",
187
+ task_id: t.id,
188
+ gate: "disjointness",
189
+ reason: "write-target-overlap-or-unprovable",
190
+ ts: new Date().toISOString(),
191
+ });
192
+ }
193
+ }
194
+
195
+ // ── D6 economics gate (per-task estimate) ──
196
+ const perTask = new Map();
197
+ for (const t of depReady) {
198
+ let est;
199
+ try {
200
+ est = estimateTaskFootprint({ taskNode: t, mode, projectDir });
201
+ } catch {
202
+ est = { estimatedCwPct: 0, parallelOk: true, split: false, workerCount: 1, confidence: "low" };
203
+ }
204
+ perTask.set(t.id, est);
205
+ }
206
+
207
+ // ── Mode-aware gating math ──
208
+ let finalParallelTasks = [];
209
+ let reducedCount = null;
210
+ let ctxPctObserved = null;
211
+ if (mode === "unattended") {
212
+ // Each parallel-candidate task gets an unattended gate check.
213
+ for (const t of depReady) {
214
+ const est = perTask.get(t.id);
215
+ if (!disjointTaskIds.has(t.id)) continue; // already sequential
216
+ const gate = computeUnattendedGate({ estimatedCwPct: est.estimatedCwPct, threshold: 60 });
217
+ if (gate.split) {
218
+ appendEvent(projectDir, {
219
+ type: "task_split",
220
+ task_id: t.id,
221
+ estimatedCwPct: est.estimatedCwPct,
222
+ ts: new Date().toISOString(),
223
+ });
224
+ // Actual slicing is the caller's responsibility — the task stays in
225
+ // the parallel set; the orchestrator (or caller) treats it as
226
+ // "needs split". Per D2-T2 acceptance: emitting the event and
227
+ // returning the plan is sufficient.
228
+ }
229
+ finalParallelTasks.push(t);
230
+ }
231
+ } else {
232
+ // in-session path
233
+ const tb = loadTokenBudget();
234
+ let status;
235
+ try { status = tb.getSessionStatus(projectDir); } catch { status = { pct: 0 }; }
236
+ const ctxPct = Number.isFinite(status && status.pct) ? status.pct : 0;
237
+ ctxPctObserved = ctxPct;
238
+ const parallelCandidates = depReady.filter((t) => disjointTaskIds.has(t.id));
239
+ const requested = parallelCandidates.length;
240
+ const headroom = computeInSessionHeadroom({ ctxPct, workerCount: requested, summarySize });
241
+ reducedCount = headroom.reducedCount;
242
+ if (reducedCount < requested) {
243
+ appendEvent(projectDir, {
244
+ type: "parallelism_reduced",
245
+ original_count: requested,
246
+ reduced_count: reducedCount,
247
+ reason: "in_session_headroom",
248
+ ts: new Date().toISOString(),
249
+ });
250
+ }
251
+ finalParallelTasks = parallelCandidates.slice(0, reducedCount);
252
+ }
253
+
254
+ // Build the plan table rows (all ready tasks, labeled by decision).
255
+ const plan = depReady.map((t) => {
256
+ const est = perTask.get(t.id) || {};
257
+ const disjointOk = disjointTaskIds.has(t.id);
258
+ const isFinalParallel = finalParallelTasks.some((x) => x.id === t.id);
259
+ let decision;
260
+ if (isFinalParallel) {
261
+ decision = mode === "unattended" && est.split ? "parallel-split" : "parallel";
262
+ } else if (sequentialFallback.has(t.id)) {
263
+ decision = "sequential";
264
+ } else {
265
+ decision = "sequential";
266
+ }
267
+ return {
268
+ task_id: t.id,
269
+ domain: t.domain,
270
+ estimatedCwPct: Number.isFinite(est.estimatedCwPct) ? est.estimatedCwPct : null,
271
+ disjoint: disjointOk,
272
+ depsOk: true,
273
+ decision,
274
+ };
275
+ });
276
+ // Also show dep-vetoed tasks so the dry-run table is complete.
277
+ for (const v of depVetoed) {
278
+ if (!v.task) continue;
279
+ plan.push({
280
+ task_id: v.task.id,
281
+ domain: v.task.domain,
282
+ estimatedCwPct: null,
283
+ disjoint: null,
284
+ depsOk: false,
285
+ decision: "veto-deps",
286
+ });
287
+ }
288
+
289
+ return {
290
+ mode,
291
+ milestone,
292
+ domain,
293
+ dryRun,
294
+ projectDir,
295
+ plan,
296
+ workerCount: finalParallelTasks.length || (plan.length ? 1 : 0),
297
+ parallelTasks: finalParallelTasks.map((t) => t.id),
298
+ reducedCount,
299
+ ctxPct: ctxPctObserved,
300
+ warnings: graph.warnings || [],
301
+ };
302
+ }
303
+
304
+ // ─── CLI entry ────────────────────────────────────────────────────────────
305
+
306
+ // ─── dry-run table formatter ──────────────────────────────────────────────
307
+
308
+ const PLAN_HEADER = ["task_id", "domain", "estimated CW%", "disjoint?", "deps ok?", "decision"];
309
+
310
+ function formatPlanTable(plan) {
311
+ const rows = [PLAN_HEADER.slice()];
312
+ for (const r of plan) {
313
+ rows.push([
314
+ String(r.task_id),
315
+ String(r.domain || "-"),
316
+ r.estimatedCwPct == null ? "-" : String(Math.round(r.estimatedCwPct)),
317
+ r.disjoint == null ? "-" : (r.disjoint ? "yes" : "no"),
318
+ r.depsOk ? "yes" : "no",
319
+ String(r.decision),
320
+ ]);
321
+ }
322
+ const widths = PLAN_HEADER.map((_, col) =>
323
+ rows.reduce((w, row) => Math.max(w, String(row[col]).length), 0),
324
+ );
325
+ const sep = widths.map((w) => "-".repeat(w)).join(" ");
326
+ const lines = [];
327
+ for (let i = 0; i < rows.length; i++) {
328
+ const row = rows[i].map((cell, col) => String(cell).padEnd(widths[col])).join(" ");
329
+ lines.push(row);
330
+ if (i === 0) lines.push(sep);
331
+ }
332
+ return lines.join("\n") + "\n";
333
+ }
334
+
335
+ function runCli(argv, env) {
336
+ const args = parseArgv(argv || []);
337
+ if (args.help) {
338
+ process.stdout.write(HELP_TEXT);
339
+ return 0;
340
+ }
341
+ const mode = args.mode || detectMode({}, env);
342
+ const result = runParallel({
343
+ projectDir: process.cwd(),
344
+ mode,
345
+ milestone: args.milestone,
346
+ domain: args.domain,
347
+ dryRun: args.dryRun,
348
+ env,
349
+ });
350
+ if (args.dryRun) {
351
+ process.stdout.write(formatPlanTable(result.plan));
352
+ process.stdout.write(
353
+ `\nTotal workers: ${result.workerCount} Mode: ${result.mode}` +
354
+ (result.reducedCount != null && result.reducedCount !== result.parallelTasks.length + (result.parallelTasks.length === 0 ? 0 : 0)
355
+ ? ` reducedCount: ${result.reducedCount}`
356
+ : "") +
357
+ "\n",
358
+ );
359
+ return 0;
360
+ }
361
+ process.stdout.write(
362
+ `gsd-t parallel — mode=${result.mode} workers=${result.workerCount}\n`,
363
+ );
364
+ return 0;
365
+ }
366
+
367
+ module.exports = {
368
+ runParallel,
369
+ runCli,
370
+ formatPlanTable,
371
+ PLAN_HEADER,
372
+ // Exposed for tests:
373
+ _parseArgv: parseArgv,
374
+ _detectMode: detectMode,
375
+ _appendEvent: appendEvent,
376
+ _HELP_TEXT: HELP_TEXT,
377
+ };
378
+
379
+ if (require.main === module) {
380
+ const code = runCli(process.argv.slice(2), process.env);
381
+ process.exit(code);
382
+ }