@tekyzinc/gsd-t 3.16.12 → 3.18.11

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 (52) hide show
  1. package/CHANGELOG.md +61 -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-heartbeat.js +50 -2
  46. package/scripts/gsd-t-post-commit-spawn-plan.sh +86 -0
  47. package/scripts/gsd-t-transcript.html +546 -43
  48. package/scripts/hooks/gsd-t-in-session-usage-hook.js +84 -0
  49. package/scripts/spawn-plan-fmt-tokens.cjs +80 -0
  50. package/templates/CLAUDE-global.md +8 -3
  51. package/templates/CLAUDE-project.md +17 -14
  52. package/templates/hooks/post-commit-spawn-plan.sh +85 -0
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * GSD-T In-Session Usage Hook (M43 D1-T2)
5
+ *
6
+ * Installed into `~/.claude/settings.json` (Stop + SessionEnd) to capture
7
+ * per-turn token usage for the dialog channel. Reads the hook payload from
8
+ * stdin, resolves the project directory from `payload.cwd`, then delegates to
9
+ * `bin/gsd-t-in-session-usage.cjs::processHookPayload` which reads the
10
+ * transcript at `payload.transcript_path` and appends schema-v2 rows to
11
+ * `.gsd-t/metrics/token-usage.jsonl`.
12
+ *
13
+ * Silent on success (hooks run async in Claude Code — stdout is ignored).
14
+ * Swallows every error: a flaky capture must never break the user's session.
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ const DEFAULT_SCRIPT_GUARD_MS = 5000;
21
+ const started = Date.now();
22
+
23
+ function _readStdin() {
24
+ return new Promise((resolve) => {
25
+ let buf = '';
26
+ process.stdin.setEncoding('utf8');
27
+ process.stdin.on('data', chunk => { buf += chunk; });
28
+ process.stdin.on('end', () => resolve(buf));
29
+ process.stdin.on('error', () => resolve(buf));
30
+ setTimeout(() => resolve(buf), DEFAULT_SCRIPT_GUARD_MS).unref();
31
+ });
32
+ }
33
+
34
+ function _parsePayload(raw) {
35
+ try {
36
+ return JSON.parse(raw || '{}');
37
+ } catch (_) {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ function _resolveProjectDir(payload) {
43
+ if (payload && payload.cwd && fs.existsSync(payload.cwd)) return payload.cwd;
44
+ if (process.cwd) return process.cwd();
45
+ return '.';
46
+ }
47
+
48
+ function _resolveInSessionModule(projectDir) {
49
+ const candidates = [
50
+ path.join(projectDir, 'bin', 'gsd-t-in-session-usage.cjs'),
51
+ path.join(process.env.HOME || '', '.claude', 'gsd-t', 'bin', 'gsd-t-in-session-usage.cjs'),
52
+ ];
53
+ for (const p of candidates) {
54
+ if (fs.existsSync(p)) return p;
55
+ }
56
+ return null;
57
+ }
58
+
59
+ async function main() {
60
+ try {
61
+ const raw = await _readStdin();
62
+ const payload = _parsePayload(raw);
63
+ if (!payload) return;
64
+ if (payload.hook_event_name !== 'Stop' && payload.hook_event_name !== 'SessionEnd') return;
65
+
66
+ const projectDir = _resolveProjectDir(payload);
67
+ const modulePath = _resolveInSessionModule(projectDir);
68
+ if (!modulePath) return;
69
+
70
+ const mod = require(modulePath);
71
+ mod.processHookPayload({ projectDir, payload });
72
+ } catch (_) {
73
+ // Intentionally swallow — hook must never interrupt the user session.
74
+ } finally {
75
+ const elapsed = Date.now() - started;
76
+ if (elapsed > DEFAULT_SCRIPT_GUARD_MS) process.exitCode = 0;
77
+ }
78
+ }
79
+
80
+ if (require.main === module) {
81
+ main();
82
+ }
83
+
84
+ module.exports = { _internal: { _parsePayload, _resolveProjectDir } };
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * GSD-T Spawn Plan — token formatting + cumulative totals (M44 D8 T7)
5
+ *
6
+ * Pure helper module used by the transcript right-side spawn-plan panel.
7
+ * Extracted from the inline script in `scripts/gsd-t-transcript.html` so the
8
+ * same functions can be unit-tested under Node without loading a DOM.
9
+ *
10
+ * Contract: .gsd-t/contracts/spawn-plan-contract.md v1.0.0
11
+ * - Token cell format: `in=12.5k out=1.7k $0.42` (k-suffix above 1000,
12
+ * 2-decimal USD). Returns `—` when tokens are null/missing.
13
+ * - Cumulative totals: sum `{in,out,cr,cc,cost_usd}` across done tasks.
14
+ * Returns `null` when no done-task has tokens (panel renders `—`).
15
+ *
16
+ * Zero external deps. `.cjs` so it loads in both ESM-default and CJS projects.
17
+ */
18
+
19
+ /**
20
+ * k-suffix number formatter. `1500` → `1.5k`, `999` → `999`, `null` → `0`.
21
+ * Used by `fmtTokens` to compact large counts.
22
+ *
23
+ * @param {number|string|null|undefined} n
24
+ * @returns {string}
25
+ */
26
+ function fmtK(n) {
27
+ if (n == null || Number.isNaN(Number(n))) return '0';
28
+ const num = Number(n);
29
+ if (Math.abs(num) >= 1000) return (num / 1000).toFixed(1) + 'k';
30
+ return String(num);
31
+ }
32
+
33
+ /**
34
+ * Render a token-cell string from a `{in, out, cr, cc, cost_usd}` object.
35
+ * When `tokens` is null/undefined/non-object, returns the em-dash marker
36
+ * per the "zero is a measurement, dash is acknowledged gap" rule.
37
+ *
38
+ * @param {object|null|undefined} tokens
39
+ * @returns {string}
40
+ */
41
+ function fmtTokens(tokens) {
42
+ if (!tokens || typeof tokens !== 'object') return '—';
43
+ const ins = fmtK(tokens.in);
44
+ const out = fmtK(tokens.out);
45
+ const cost = (typeof tokens.cost_usd === 'number')
46
+ ? '$' + tokens.cost_usd.toFixed(2)
47
+ : '';
48
+ return ['in=' + ins, 'out=' + out, cost].filter(Boolean).join(' ');
49
+ }
50
+
51
+ /**
52
+ * Sum the `tokens` field across every task whose `status === 'done'` and
53
+ * whose `tokens` is a populated object. Returns null when no done-task has
54
+ * a tokens object attached — the panel renders that state as `—`.
55
+ *
56
+ * @param {Array<{status?:string, tokens?:object}>|null|undefined} tasks
57
+ * @returns {{in:number,out:number,cr:number,cc:number,cost_usd:number}|null}
58
+ */
59
+ function sumTokens(tasks) {
60
+ const acc = { in: 0, out: 0, cr: 0, cc: 0, cost_usd: 0 };
61
+ let hit = false;
62
+ for (const t of (tasks || [])) {
63
+ if (!t || t.status !== 'done' || !t.tokens) continue;
64
+ acc.in += Number(t.tokens.in || 0);
65
+ acc.out += Number(t.tokens.out || 0);
66
+ acc.cr += Number(t.tokens.cr || 0);
67
+ acc.cc += Number(t.tokens.cc || 0);
68
+ acc.cost_usd += Number(t.tokens.cost_usd || 0);
69
+ hit = true;
70
+ }
71
+ if (!hit) return null;
72
+ acc.cost_usd = Math.round(acc.cost_usd * 100) / 100;
73
+ return acc;
74
+ }
75
+
76
+ module.exports = {
77
+ fmtK,
78
+ fmtTokens,
79
+ sumTokens,
80
+ };
@@ -322,10 +322,15 @@ No command file ships a bare `Task(...)` or `claude -p` line outside of a wrappe
322
322
 
323
323
  Rationale: the pre-M41 convention silently wrote `N/A` tokens because no caller parsed the `usage` envelope. The wrapper is the single place that parses it. Bypassing the wrapper re-introduces blind spots.
324
324
 
325
- ## Headless-by-Default Spawn (M38, v3.12.10+)
325
+ ## Always-Headless Spawn (M43 D4, v3.16.x+) — Channel Separation
326
326
 
327
- Long-running work (execute, wave, integrate, debug repair loops) spawns detached by default. Interactive session shows a banner, event-stream path, then exits no mid-session `/compact` wall. `--watch` keeps a ScheduleWakeup-driven status block in the caller; events stream JSONL to `.gsd-t/events/YYYY-MM-DD.jsonl`. Router mode (`/gsd`) answers exploratory requests inline without a command spawn see `commands/gsd.md` Step 2.5.
328
- Contract: `.gsd-t/contracts/headless-default-contract.md` (see also `unattended-event-stream-contract.md`, `unattended-supervisor-contract.md`).
327
+ Every GSD-T command spawns detached, unconditionally. There is no `--watch`, no `--in-session`, no `--headless` opt-in, no context-meter threshold that reroutes, no low-water-mark bypass. The dialog channel is reserved for human↔Claude conversation; everything else is a detached headless child. Interactive session shows a launch banner + live-transcript URL + event-stream path, then exits. Results surface via the read-back banner on the user's next message.
328
+
329
+ The only in-session surface is the `/gsd` router (`commands/gsd.md`), and only for dialog-only exploratory turns. The moment Step 2.5 classifies a turn as `workflow`, the router hands off to a detached spawn.
330
+
331
+ Legacy `watch` / `inSession` params are accepted-and-ignored with a one-shot stderr deprecation warning (scheduled removal in v3.0.0 of the contract). `shouldSpawnHeadless` is a constant `() => true`.
332
+
333
+ Contract: `.gsd-t/contracts/headless-default-contract.md` v2.0.0 (see also `unattended-event-stream-contract.md`, `unattended-supervisor-contract.md`).
329
334
 
330
335
  ## API Documentation Guard (Swagger/OpenAPI)
331
336
 
@@ -26,24 +26,27 @@
26
26
  <!-- Claude will verify the branch before every commit. -->
27
27
  **Expected branch**: {main | master | feature-branch-name}
28
28
 
29
- ## Context Meter (M34/M38, v3.12.10+)
29
+ ## Context Meter (M34/M38/M43 D4, v3.16.x+) — Observational Only
30
30
  <!-- The Context Meter is a PostToolUse hook that uses local token estimation -->
31
31
  <!-- and writes the current context % to .gsd-t/.context-meter-state.json. -->
32
- <!-- bin/token-budget.cjs reads that file as the spawn-time routing signal. -->
33
- <!-- Single-band model (context-meter-contract v1.3.0): one threshold (default 85%), -->
34
- <!-- one action hand off to a detached headless spawn via the unattended supervisor. -->
35
- <!-- No three-band routing, no silent model downgrades. The meter informs spawn-time -->
36
- <!-- decisions, not in-flight pauses. -->
37
- <!-- Config: .gsd-t/context-meter-config.json — modelWindowSize, thresholdPct, checkFrequency. -->
32
+ <!-- Under M43 D4 (channel-separation inversion) the meter is OBSERVATIONAL ONLY: -->
33
+ <!-- the pct is recorded into the token-log Ctx% column on the next spawn, -->
34
+ <!-- but no threshold gates any routing decision. Every command spawns detached -->
35
+ <!-- unconditionally (see Always-Headless section below). -->
36
+ <!-- Config: .gsd-t/context-meter-config.json — modelWindowSize, checkFrequency. -->
38
37
  <!-- Verify: `npx @tekyzinc/gsd-t doctor`. -->
39
38
 
40
- ## Headless-by-Default Spawn (M38, v3.12.10+)
41
- <!-- Long-running workflow commands (execute, wave, integrate, debug repair loops) -->
42
- <!-- route through the unattended supervisor from the start. Interactive session -->
43
- <!-- invokes, sees a launch banner and event-stream log location, and exits. -->
44
- <!-- Pass --watch to keep a live status block in the session (270s ScheduleWakeup ticks). -->
45
- <!-- Event stream: .gsd-t/events/YYYY-MM-DD.jsonl (JSONL, phase boundaries). -->
46
- <!-- Contracts: headless-default-contract.md v1.0.0, unattended-event-stream-contract.md v1.0.0, -->
39
+ ## Always-Headless Spawn (M43 D4, v3.16.x+) — Channel Separation
40
+ <!-- Every GSD-T command spawns detached, unconditionally. No --watch flag. -->
41
+ <!-- No --in-session flag. No --headless opt-in. No context-meter threshold -->
42
+ <!-- that reroutes. The dialog channel is reserved for human↔Claude conversation; -->
43
+ <!-- every workflow turn is a detached headless child. Interactive session shows -->
44
+ <!-- a launch banner + live-transcript URL + event-stream path, then exits. -->
45
+ <!-- Results surface via the read-back banner on the user's next message. -->
46
+ <!-- The only in-session surface is the /gsd router, and only for dialog-only -->
47
+ <!-- exploratory turns (Step 2.5 classifier → `conversational`). -->
48
+ <!-- Legacy watch/inSession params on autoSpawnHeadless() are accepted-and-ignored. -->
49
+ <!-- Contracts: headless-default-contract.md v2.0.0, unattended-event-stream-contract.md v1.0.0, -->
47
50
  <!-- unattended-supervisor-contract.md v1.1.0. -->
48
51
 
49
52
  ## Model Selection
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env bash
2
+ # GSD-T Post-Commit Spawn-Plan Hook — installed by `gsd-t init`
3
+ #
4
+ # This is a thin shim that delegates to the canonical hook script
5
+ # shipped with GSD-T. If your project doesn't yet have the canonical
6
+ # script, this template copy handles commits by itself; once the
7
+ # framework script is in place, the framework wins.
8
+ #
9
+ # Contract: .gsd-t/contracts/spawn-plan-contract.md v1.0.0
10
+
11
+ set +e # silent-fail — never break the commit
12
+
13
+ PROJECT_DIR="$(git rev-parse --show-toplevel 2>/dev/null)"
14
+ if [ -z "$PROJECT_DIR" ] || [ ! -d "$PROJECT_DIR/.gsd-t/spawns" ]; then
15
+ exit 0
16
+ fi
17
+
18
+ CANONICAL="$PROJECT_DIR/scripts/gsd-t-post-commit-spawn-plan.sh"
19
+ if [ -x "$CANONICAL" ]; then
20
+ # Delegate to the framework-shipped hook.
21
+ "$CANONICAL"
22
+ exit 0
23
+ fi
24
+
25
+ UPDATER="$PROJECT_DIR/bin/spawn-plan-status-updater.cjs"
26
+ if [ ! -f "$UPDATER" ]; then
27
+ exit 0
28
+ fi
29
+
30
+ if ! command -v node >/dev/null 2>&1; then
31
+ echo "[spawn-plan-hook] node not found — skipping" 1>&2
32
+ exit 0
33
+ fi
34
+
35
+ COMMIT_SHA="$(git rev-parse --short HEAD 2>/dev/null)"
36
+ COMMIT_MSG="$(git log -1 --format=%B 2>/dev/null)"
37
+ if [ -z "$COMMIT_SHA" ] || [ -z "$COMMIT_MSG" ]; then
38
+ exit 0
39
+ fi
40
+
41
+ TASK_IDS="$(printf '%s' "$COMMIT_MSG" | grep -oE '\[M[0-9]+-D[0-9]+-T[0-9]+\]' | sed 's/[][]//g' | awk '!seen[$0]++')"
42
+ if [ -z "$TASK_IDS" ]; then
43
+ exit 0
44
+ fi
45
+
46
+ printf '%s\n' "$TASK_IDS" | node -e '
47
+ "use strict";
48
+ try {
49
+ const path = require("path");
50
+ const fs = require("fs");
51
+ const projectDir = process.argv[1];
52
+ const commit = process.argv[2];
53
+ let raw = "";
54
+ process.stdin.setEncoding("utf8");
55
+ process.stdin.on("data", (c) => { raw += c; });
56
+ process.stdin.on("end", () => {
57
+ try {
58
+ const taskIds = raw.split("\n").map((s) => s.trim()).filter(Boolean);
59
+ if (!taskIds.length) process.exit(0);
60
+ const updater = require(path.join(projectDir, "bin", "spawn-plan-status-updater.cjs"));
61
+ const activePaths = updater.listActivePlans(projectDir);
62
+ if (!activePaths.length) process.exit(0);
63
+ for (const fp of activePaths) {
64
+ let plan;
65
+ try { plan = JSON.parse(fs.readFileSync(fp, "utf8")); } catch (_) { continue; }
66
+ const spawnId = plan && plan.spawnId;
67
+ const spawnStartedAt = plan && plan.startedAt;
68
+ if (!spawnId) continue;
69
+ const planTaskIds = new Set((plan.tasks || []).map((t) => t && t.id).filter(Boolean));
70
+ for (const taskId of taskIds) {
71
+ if (!planTaskIds.has(taskId)) continue;
72
+ const tokens = updater.sumTokensForTask({ projectDir, taskId, spawnStartedAt });
73
+ updater.markTaskDone({ spawnId, taskId, commit, tokens, projectDir });
74
+ }
75
+ }
76
+ } catch (err) {
77
+ try { process.stderr.write("[spawn-plan-hook] " + String(err && err.message || err) + "\n"); } catch (_) { /* silent */ }
78
+ }
79
+ });
80
+ } catch (err) {
81
+ try { process.stderr.write("[spawn-plan-hook] " + String(err && err.message || err) + "\n"); } catch (_) { /* silent */ }
82
+ }
83
+ ' "$PROJECT_DIR" "$COMMIT_SHA" 2>/dev/null
84
+
85
+ exit 0