@tekyzinc/gsd-t 3.13.16 → 3.16.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 (54) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +1 -0
  3. package/bin/gsd-t-benchmark-orchestrator.js +437 -0
  4. package/bin/gsd-t-capture-lint.cjs +276 -0
  5. package/bin/gsd-t-completion-check.cjs +106 -0
  6. package/bin/gsd-t-orchestrator-config.cjs +64 -0
  7. package/bin/gsd-t-orchestrator-queue.cjs +180 -0
  8. package/bin/gsd-t-orchestrator-recover.cjs +231 -0
  9. package/bin/gsd-t-orchestrator-worker.cjs +219 -0
  10. package/bin/gsd-t-orchestrator.js +534 -0
  11. package/bin/gsd-t-stream-feed-client.cjs +151 -0
  12. package/bin/gsd-t-task-brief-compactor.cjs +89 -0
  13. package/bin/gsd-t-task-brief-template.cjs +96 -0
  14. package/bin/gsd-t-task-brief.js +249 -0
  15. package/bin/gsd-t-token-backfill.cjs +366 -0
  16. package/bin/gsd-t-token-capture.cjs +306 -0
  17. package/bin/gsd-t-token-dashboard.cjs +318 -0
  18. package/bin/gsd-t-token-regenerate-log.cjs +129 -0
  19. package/bin/gsd-t-transcript-tee.cjs +246 -0
  20. package/bin/gsd-t-unattended-heartbeat.cjs +188 -0
  21. package/bin/gsd-t-unattended-platform.cjs +191 -27
  22. package/bin/gsd-t-unattended-safety.cjs +8 -1
  23. package/bin/gsd-t-unattended.cjs +192 -31
  24. package/bin/gsd-t.js +329 -2
  25. package/bin/supervisor-pid-fingerprint.cjs +126 -0
  26. package/commands/gsd-t-debug.md +63 -51
  27. package/commands/gsd-t-design-decompose.md +2 -7
  28. package/commands/gsd-t-doc-ripple.md +20 -11
  29. package/commands/gsd-t-execute.md +82 -50
  30. package/commands/gsd-t-integrate.md +43 -16
  31. package/commands/gsd-t-plan.md +20 -7
  32. package/commands/gsd-t-prd.md +19 -12
  33. package/commands/gsd-t-quick.md +64 -29
  34. package/commands/gsd-t-resume.md +51 -4
  35. package/commands/gsd-t-unattended.md +19 -20
  36. package/commands/gsd-t-verify.md +48 -32
  37. package/commands/gsd-t-visualize.md +19 -17
  38. package/commands/gsd-t-wave.md +29 -27
  39. package/docs/architecture.md +16 -0
  40. package/docs/m40-benchmark-report.md +35 -0
  41. package/docs/requirements.md +20 -0
  42. package/package.json +1 -1
  43. package/scripts/gsd-t-dashboard-server.js +291 -4
  44. package/scripts/gsd-t-dashboard.html +31 -1
  45. package/scripts/gsd-t-design-review-server.js +3 -1
  46. package/scripts/gsd-t-stream-feed-server.js +428 -0
  47. package/scripts/gsd-t-stream-feed.html +1168 -0
  48. package/scripts/gsd-t-token-aggregator.js +373 -0
  49. package/scripts/gsd-t-transcript.html +422 -0
  50. package/scripts/hooks/gsd-t-in-session-probe.js +62 -0
  51. package/scripts/hooks/pre-commit-capture-lint +26 -0
  52. package/templates/CLAUDE-global.md +69 -0
  53. package/scripts/gsd-t-agent-dashboard-server.js +0 -424
  54. package/scripts/gsd-t-agent-dashboard.html +0 -1043
package/bin/gsd-t.js CHANGED
@@ -567,6 +567,50 @@ function installContextMeter(projectDir) {
567
567
  }
568
568
 
569
569
  // Register the Context Meter PostToolUse hook in ~/.claude/settings.json.
570
+ // Opt-in pre-commit hook installer (M41 D5). Appends the capture-lint block
571
+ // to .git/hooks/pre-commit; if the file doesn't exist, copies our stock
572
+ // script. Never overwrites an existing hook.
573
+ const CAPTURE_LINT_HOOK_MARKER = "# GSD-T capture lint";
574
+ function installCaptureLintHook(projectDir) {
575
+ const gitDir = path.join(projectDir, ".git");
576
+ if (!fs.existsSync(gitDir)) {
577
+ warn("No .git directory — not a git repo; skipping hook install");
578
+ return false;
579
+ }
580
+ const hooksDir = path.join(gitDir, "hooks");
581
+ try { fs.mkdirSync(hooksDir, { recursive: true }); } catch (_) {}
582
+ const hookPath = path.join(hooksDir, "pre-commit");
583
+ const stockSrc = path.join(PKG_ROOT, "scripts", "hooks", "pre-commit-capture-lint");
584
+ let stock = "";
585
+ try { stock = fs.readFileSync(stockSrc, "utf8"); } catch (_) {
586
+ warn("Could not read pre-commit-capture-lint script from package");
587
+ return false;
588
+ }
589
+
590
+ if (!fs.existsSync(hookPath)) {
591
+ fs.writeFileSync(hookPath, stock);
592
+ try { fs.chmodSync(hookPath, 0o755); } catch (_) {}
593
+ success(`Hook installed at ${path.relative(projectDir, hookPath)}`);
594
+ info("Test with: gsd-t capture-lint --staged");
595
+ return true;
596
+ }
597
+
598
+ const existing = fs.readFileSync(hookPath, "utf8");
599
+ if (existing.includes(CAPTURE_LINT_HOOK_MARKER)) {
600
+ info("Capture-lint block already present in pre-commit hook — no change");
601
+ return true;
602
+ }
603
+
604
+ const appended = existing.trimEnd() +
605
+ "\n\n" + CAPTURE_LINT_HOOK_MARKER + "\n" +
606
+ stock.replace(/^#!.*\n/, "") + "\n";
607
+ fs.writeFileSync(hookPath, appended);
608
+ try { fs.chmodSync(hookPath, 0o755); } catch (_) {}
609
+ success(`Capture-lint block appended to ${path.relative(projectDir, hookPath)}`);
610
+ info("Test with: gsd-t capture-lint --staged");
611
+ return true;
612
+ }
613
+
570
614
  // Idempotent — if an existing hook references CONTEXT_METER_HOOK_MARKER the
571
615
  // command string is refreshed/migrated in-place to the canonical form.
572
616
  // Stale entries matching CONTEXT_METER_STALE_PATTERNS are migrated on the spot.
@@ -1545,9 +1589,29 @@ function doStatus() {
1545
1589
  showStatusTeams();
1546
1590
  showStatusContextMeter();
1547
1591
  showStatusProject();
1592
+ showStatusTokenBlock();
1548
1593
  log("");
1549
1594
  }
1550
1595
 
1596
+ function showStatusTokenBlock() {
1597
+ const cwd = process.cwd();
1598
+ if (!fs.existsSync(path.join(cwd, ".gsd-t"))) return;
1599
+ let milestone = null;
1600
+ try {
1601
+ const progressPath = path.join(cwd, ".gsd-t", "progress.md");
1602
+ if (fs.existsSync(progressPath)) {
1603
+ const src = fs.readFileSync(progressPath, "utf8");
1604
+ const m = src.match(/## Current Milestone:\s*(\S+)/) || src.match(/Milestone:\s*(M\d+)/);
1605
+ if (m) milestone = m[1];
1606
+ }
1607
+ } catch (_) {}
1608
+ try {
1609
+ const dashboard = require(path.join(__dirname, "gsd-t-token-dashboard.cjs"));
1610
+ const agg = dashboard.aggregateSync({ projectDir: cwd, milestone });
1611
+ log(dashboard.renderStatusBlock(agg));
1612
+ } catch (_) {}
1613
+ }
1614
+
1551
1615
  function formatRelativeTime(timestampIso) {
1552
1616
  const then = Date.parse(timestampIso);
1553
1617
  if (!Number.isFinite(then)) return "unknown";
@@ -3517,6 +3581,122 @@ function doMetrics(_args) {
3517
3581
  log(`${DIM}metrics removed in v3.12 — context meter is no longer telemetry-instrumented${RESET}`);
3518
3582
  }
3519
3583
 
3584
+ function doStreamFeed(args) {
3585
+ const sub = args[0];
3586
+ const projectDir = process.cwd();
3587
+ const pidFile = path.join(projectDir, ".gsd-t", "stream-feed", ".server.pid");
3588
+ const portFile = path.join(projectDir, ".gsd-t", "stream-feed", ".server.port");
3589
+
3590
+ function readPid() {
3591
+ try { return parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10); }
3592
+ catch { return null; }
3593
+ }
3594
+ function readPort() {
3595
+ try { return parseInt(fs.readFileSync(portFile, "utf8").trim(), 10); }
3596
+ catch { return null; }
3597
+ }
3598
+ function isAlive(pid) {
3599
+ if (!pid) return false;
3600
+ try { process.kill(pid, 0); return true; } catch { return false; }
3601
+ }
3602
+
3603
+ if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
3604
+ log(`\n${BOLD}gsd-t stream-feed${RESET} — Localhost stream-json watcher (M40 D4)\n`);
3605
+ log(`${BOLD}Usage:${RESET}`);
3606
+ log(` gsd-t stream-feed ${CYAN}start${RESET} [--port N]`);
3607
+ log(` gsd-t stream-feed ${CYAN}status${RESET}`);
3608
+ log(` gsd-t stream-feed ${CYAN}stop${RESET}`);
3609
+ log(`\nDefault port: 7842. Env override: GSD_T_STREAM_FEED_PORT.`);
3610
+ return;
3611
+ }
3612
+
3613
+ if (sub === "start") {
3614
+ const existing = readPid();
3615
+ if (isAlive(existing)) {
3616
+ log(`${YELLOW}stream-feed-server already running${RESET} (pid ${existing}, port ${readPort() || "?"})`);
3617
+ return;
3618
+ }
3619
+ let port = null;
3620
+ for (let i = 1; i < args.length; i++) {
3621
+ if (args[i] === "--port") port = Number(args[++i]);
3622
+ }
3623
+ const { spawn } = require("child_process");
3624
+ const serverScript = path.join(__dirname, "..", "scripts", "gsd-t-stream-feed-server.js");
3625
+ const forward = ["--project-dir", projectDir];
3626
+ if (port) forward.push("--port", String(port));
3627
+ const feedDir = path.join(projectDir, ".gsd-t", "stream-feed");
3628
+ try { fs.mkdirSync(feedDir, { recursive: true }); } catch { /* exists */ }
3629
+ const out = fs.openSync(path.join(feedDir, "server.log"), "a");
3630
+ const err = fs.openSync(path.join(feedDir, "server.log"), "a");
3631
+ const child = spawn(process.execPath, [serverScript, ...forward], {
3632
+ detached: true, stdio: ["ignore", out, err], cwd: projectDir,
3633
+ });
3634
+ child.unref();
3635
+ try { fs.writeFileSync(pidFile, String(child.pid)); } catch { /* noop */ }
3636
+ try { fs.writeFileSync(portFile, String(port || process.env.GSD_T_STREAM_FEED_PORT || 7842)); } catch { /* noop */ }
3637
+ log(`${GREEN}✓${RESET} stream-feed-server started (pid ${child.pid}, port ${port || 7842})`);
3638
+ log(` log: .gsd-t/stream-feed/server.log`);
3639
+ return;
3640
+ }
3641
+
3642
+ if (sub === "status") {
3643
+ const pid = readPid();
3644
+ const port = readPort();
3645
+ if (!isAlive(pid)) {
3646
+ log(`${DIM}stream-feed-server not running${RESET}`);
3647
+ return;
3648
+ }
3649
+ log(`${GREEN}●${RESET} stream-feed-server running`);
3650
+ log(` pid: ${pid}`);
3651
+ log(` port: ${port || 7842}`);
3652
+ // Try to fetch live stats
3653
+ try {
3654
+ const http = require("http");
3655
+ const done = { v: false };
3656
+ const req = http.get({ host: "127.0.0.1", port: port || 7842, path: "/status", timeout: 1000 }, (res) => {
3657
+ let body = "";
3658
+ res.on("data", (c) => body += c);
3659
+ res.on("end", () => {
3660
+ try {
3661
+ const j = JSON.parse(body);
3662
+ log(` frames today: ${j.framesToday}`);
3663
+ log(` clients: ${j.clients}`);
3664
+ log(` stats: ingested=${j.stats.framesIngested} broadcast=${j.stats.framesBroadcast} kicked=${j.stats.kicked}`);
3665
+ } catch { /* noop */ }
3666
+ done.v = true;
3667
+ });
3668
+ });
3669
+ req.on("error", () => { done.v = true; });
3670
+ req.on("timeout", () => { req.destroy(); done.v = true; });
3671
+ } catch { /* noop */ }
3672
+ return;
3673
+ }
3674
+
3675
+ if (sub === "stop") {
3676
+ const pid = readPid();
3677
+ if (!isAlive(pid)) {
3678
+ log(`${DIM}stream-feed-server not running${RESET}`);
3679
+ try { fs.unlinkSync(pidFile); } catch { /* noop */ }
3680
+ try { fs.unlinkSync(portFile); } catch { /* noop */ }
3681
+ return;
3682
+ }
3683
+ try { process.kill(pid, "SIGTERM"); } catch { /* noop */ }
3684
+ log(`${GREEN}✓${RESET} stream-feed-server stopped (pid ${pid})`);
3685
+ // Clean up stale PID files after a short wait
3686
+ setTimeout(() => {
3687
+ if (!isAlive(pid)) {
3688
+ try { fs.unlinkSync(pidFile); } catch { /* noop */ }
3689
+ try { fs.unlinkSync(portFile); } catch { /* noop */ }
3690
+ }
3691
+ }, 500);
3692
+ return;
3693
+ }
3694
+
3695
+ error(`Unknown stream-feed subcommand: ${sub}`);
3696
+ log(`Try: gsd-t stream-feed --help`);
3697
+ process.exit(1);
3698
+ }
3699
+
3520
3700
  function showHelp() {
3521
3701
  log(`\n${BOLD}GSD-T${RESET} — Contract-Driven Development for Claude Code\n`);
3522
3702
  log(`${BOLD}Usage:${RESET} npx @tekyzinc/gsd-t ${CYAN}<command>${RESET} [options]\n`);
@@ -3532,6 +3712,9 @@ function showHelp() {
3532
3712
  log(` ${CYAN}changelog${RESET} Open changelog in the browser`);
3533
3713
  log(` ${CYAN}graph${RESET} Code graph operations (index, status, query)`);
3534
3714
  log(` ${CYAN}headless${RESET} Non-interactive execution via claude -p + fast state queries`);
3715
+ log(` ${CYAN}orchestrate${RESET} External task orchestrator — one claude -p spawn per task (M40)`);
3716
+ log(` ${CYAN}benchmark-orchestrator${RESET} M40 speed gate — compares orchestrator vs in-session wall-clock`);
3717
+ log(` ${CYAN}stream-feed${RESET} Localhost stream-json watcher (start|status|stop) — M40 D4`);
3535
3718
  log(` ${CYAN}design-build${RESET} Deterministic design→code pipeline (elements → widgets → pages)`);
3536
3719
  log(` ${CYAN}help${RESET} Show this help\n`);
3537
3720
  log(`${BOLD}Examples:${RESET}`);
@@ -3652,9 +3835,21 @@ if (require.main === module) {
3652
3835
  case "update-all":
3653
3836
  doUpdateAll().catch((e) => { error(e.message || String(e)); process.exit(1); });
3654
3837
  break;
3655
- case "init":
3656
- doInit(args[1]).catch((e) => { error(e.message || String(e)); process.exit(1); });
3838
+ case "init": {
3839
+ let initProject = null;
3840
+ let installHooks = false;
3841
+ for (let i = 1; i < args.length; i++) {
3842
+ const a = args[i];
3843
+ if (a === '--install-hooks') installHooks = true;
3844
+ else if (!a.startsWith('-')) initProject = a;
3845
+ }
3846
+ doInit(initProject)
3847
+ .then(() => {
3848
+ if (installHooks) installCaptureLintHook(process.cwd());
3849
+ })
3850
+ .catch((e) => { error(e.message || String(e)); process.exit(1); });
3657
3851
  break;
3852
+ }
3658
3853
  case "register":
3659
3854
  doRegister();
3660
3855
  break;
@@ -3684,9 +3879,141 @@ if (require.main === module) {
3684
3879
  });
3685
3880
  process.exit(res.status == null ? 1 : res.status);
3686
3881
  }
3882
+ case "orchestrate": {
3883
+ const { spawnSync } = require("child_process");
3884
+ const js = path.join(__dirname, "gsd-t-orchestrator.js");
3885
+ const res = spawnSync(process.execPath, [js, ...args.slice(1)], {
3886
+ stdio: "inherit",
3887
+ });
3888
+ process.exit(res.status == null ? 1 : res.status);
3889
+ }
3890
+ case "benchmark-orchestrator": {
3891
+ const { spawnSync } = require("child_process");
3892
+ const js = path.join(__dirname, "gsd-t-benchmark-orchestrator.js");
3893
+ const res = spawnSync(process.execPath, [js, ...args.slice(1)], {
3894
+ stdio: "inherit",
3895
+ });
3896
+ process.exit(res.status == null ? 1 : res.status);
3897
+ }
3898
+ case "stream-feed": {
3899
+ doStreamFeed(args.slice(1));
3900
+ break;
3901
+ }
3687
3902
  case "metrics":
3688
3903
  doMetrics(args.slice(1));
3689
3904
  break;
3905
+ case "backfill-tokens": {
3906
+ const bfOpts = { projectDir: process.cwd(), since: null, patchLog: false, dryRun: false };
3907
+ for (let i = 1; i < args.length; i++) {
3908
+ const a = args[i];
3909
+ if (a === '--since' && args[i+1]) { bfOpts.since = args[++i]; }
3910
+ else if (a.startsWith('--since=')) { bfOpts.since = a.slice(8); }
3911
+ else if (a === '--patch-log') { bfOpts.patchLog = true; }
3912
+ else if (a === '--dry-run') { bfOpts.dryRun = true; }
3913
+ else if (a === '--project-dir' && args[i+1]) { bfOpts.projectDir = args[++i]; }
3914
+ else if (a.startsWith('--project-dir=')) { bfOpts.projectDir = a.slice(14); }
3915
+ else if (a === '--help' || a === '-h') {
3916
+ log('Usage: gsd-t backfill-tokens [--since YYYY-MM-DD] [--patch-log] [--dry-run] [--project-dir PATH]');
3917
+ process.exit(0);
3918
+ }
3919
+ else {
3920
+ error(`backfill-tokens: unknown arg: ${a}`);
3921
+ process.exit(2);
3922
+ }
3923
+ }
3924
+ const backfill = require(path.join(__dirname, 'gsd-t-token-backfill.cjs'));
3925
+ backfill.main(bfOpts)
3926
+ .then(({ exitCode }) => process.exit(exitCode || 0))
3927
+ .catch((e) => { error(e.message || String(e)); process.exit(3); });
3928
+ break;
3929
+ }
3930
+ case "capture-lint": {
3931
+ const clOpts = { projectDir: process.cwd(), mode: 'staged' };
3932
+ for (let i = 1; i < args.length; i++) {
3933
+ const a = args[i];
3934
+ if (a === '--staged') { clOpts.mode = 'staged'; }
3935
+ else if (a === '--all') { clOpts.mode = 'all'; }
3936
+ else if (a === '--project-dir' && args[i+1]) { clOpts.projectDir = args[++i]; }
3937
+ else if (a.startsWith('--project-dir=')) { clOpts.projectDir = a.slice(14); }
3938
+ else if (a === '--help' || a === '-h') {
3939
+ log('Usage: gsd-t capture-lint [--staged] [--all] [--project-dir PATH]');
3940
+ process.exit(0);
3941
+ }
3942
+ else {
3943
+ error(`capture-lint: unknown arg: ${a}`);
3944
+ process.exit(2);
3945
+ }
3946
+ }
3947
+ try {
3948
+ const linter = require(path.join(__dirname, 'gsd-t-capture-lint.cjs'));
3949
+ const res = linter.main(clOpts);
3950
+ if (res.error) {
3951
+ error(`capture-lint: ${res.error}`);
3952
+ process.exit(2);
3953
+ }
3954
+ for (const v of res.violations) {
3955
+ log(`${v.file}:${v.line}: ${v.message}`);
3956
+ }
3957
+ if (res.violations.length === 0) {
3958
+ log(`capture-lint: ${res.files.length} file(s) checked — clean`);
3959
+ } else {
3960
+ log(`capture-lint: ${res.violations.length} violation(s) across ${res.files.length} file(s)`);
3961
+ }
3962
+ process.exit(res.exitCode);
3963
+ } catch (e) {
3964
+ error(e.message || String(e));
3965
+ process.exit(2);
3966
+ }
3967
+ break;
3968
+ }
3969
+ case "tokens": {
3970
+ const tkOpts = { projectDir: process.cwd(), since: null, milestone: null, format: 'table', regenerateLog: false };
3971
+ for (let i = 1; i < args.length; i++) {
3972
+ const a = args[i];
3973
+ if (a === '--since' && args[i+1]) { tkOpts.since = args[++i]; }
3974
+ else if (a.startsWith('--since=')) { tkOpts.since = a.slice(8); }
3975
+ else if (a === '--milestone' && args[i+1]) { tkOpts.milestone = args[++i]; }
3976
+ else if (a.startsWith('--milestone=')) { tkOpts.milestone = a.slice(12); }
3977
+ else if (a === '--format' && args[i+1]) { tkOpts.format = args[++i]; }
3978
+ else if (a.startsWith('--format=')) { tkOpts.format = a.slice(9); }
3979
+ else if (a === '--project-dir' && args[i+1]) { tkOpts.projectDir = args[++i]; }
3980
+ else if (a.startsWith('--project-dir=')) { tkOpts.projectDir = a.slice(14); }
3981
+ else if (a === '--regenerate-log') { tkOpts.regenerateLog = true; }
3982
+ else if (a === '--help' || a === '-h') {
3983
+ log('Usage: gsd-t tokens [--since YYYY-MM-DD] [--milestone Mxx] [--format table|json]');
3984
+ log(' gsd-t tokens --regenerate-log (rewrite .gsd-t/token-log.md from token-usage.jsonl)');
3985
+ process.exit(0);
3986
+ }
3987
+ else {
3988
+ error(`tokens: unknown arg: ${a}`);
3989
+ process.exit(2);
3990
+ }
3991
+ }
3992
+ if (tkOpts.regenerateLog) {
3993
+ try {
3994
+ const regen = require(path.join(__dirname, 'gsd-t-token-regenerate-log.cjs'));
3995
+ const res = regen.regenerateLog({ projectDir: tkOpts.projectDir });
3996
+ log(`Regenerated ${res.wrote} (${res.rowCount} row${res.rowCount === 1 ? '' : 's'})`);
3997
+ process.exit(0);
3998
+ } catch (e) {
3999
+ error(e.message || String(e));
4000
+ process.exit(3);
4001
+ }
4002
+ break;
4003
+ }
4004
+ if (tkOpts.format !== 'table' && tkOpts.format !== 'json') {
4005
+ error(`tokens: --format must be 'table' or 'json' (got: ${tkOpts.format})`);
4006
+ process.exit(2);
4007
+ }
4008
+ const dashboard = require(path.join(__dirname, 'gsd-t-token-dashboard.cjs'));
4009
+ dashboard.aggregate(tkOpts)
4010
+ .then((agg) => {
4011
+ log(tkOpts.format === 'json' ? dashboard.renderJson(agg) : dashboard.renderTable(agg));
4012
+ process.exit(0);
4013
+ })
4014
+ .catch((e) => { error(e.message || String(e)); process.exit(3); });
4015
+ break;
4016
+ }
3690
4017
  case "design-build": {
3691
4018
  const orchestrator = require("./design-orchestrator.js");
3692
4019
  orchestrator.run(args.slice(1)).catch(e => { console.error(e); process.exit(1); });
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Supervisor PID fingerprint.
3
+ *
4
+ * The supervisor.pid file is written as JSON `{pid, projectDir, startedAt}`
5
+ * so that resume-time liveness checks can distinguish "our supervisor is
6
+ * running" from "some other process recycled this PID."
7
+ *
8
+ * Backward-compat: if the file parses as a bare integer (legacy format),
9
+ * treat it as `{pid, projectDir: null, startedAt: null, form: "legacy"}`
10
+ * and let callers decide whether to trust it.
11
+ */
12
+ "use strict";
13
+
14
+ const fs = require("node:fs");
15
+ const path = require("node:path");
16
+ const { execSync } = require("node:child_process");
17
+
18
+ const PID_REL = path.join(".gsd-t", ".unattended", "supervisor.pid");
19
+
20
+ function pidPathFor(projectDir) {
21
+ return path.join(path.resolve(projectDir), PID_REL);
22
+ }
23
+
24
+ function writePidFile(projectDir, pid) {
25
+ if (!projectDir || typeof projectDir !== "string") {
26
+ throw new Error("writePidFile: projectDir required");
27
+ }
28
+ if (!Number.isInteger(pid) || pid <= 0) {
29
+ throw new Error(`writePidFile: invalid pid ${pid}`);
30
+ }
31
+ const entry = {
32
+ pid,
33
+ projectDir: path.resolve(projectDir),
34
+ startedAt: new Date().toISOString(),
35
+ };
36
+ const p = pidPathFor(projectDir);
37
+ fs.mkdirSync(path.dirname(p), { recursive: true });
38
+ fs.writeFileSync(p, JSON.stringify(entry) + "\n", "utf8");
39
+ return entry;
40
+ }
41
+
42
+ function readPidFile(projectDir) {
43
+ const p = pidPathFor(projectDir);
44
+ if (!fs.existsSync(p)) return null;
45
+ const raw = fs.readFileSync(p, "utf8").trim();
46
+ if (!raw) return null;
47
+
48
+ // Try JSON form first.
49
+ if (raw.startsWith("{")) {
50
+ try {
51
+ const obj = JSON.parse(raw);
52
+ if (obj && Number.isInteger(obj.pid)) {
53
+ return {
54
+ pid: obj.pid,
55
+ projectDir: obj.projectDir || null,
56
+ startedAt: obj.startedAt || null,
57
+ form: "json",
58
+ };
59
+ }
60
+ } catch {
61
+ // fall through to legacy attempt
62
+ }
63
+ }
64
+
65
+ // Legacy bare-integer form.
66
+ const n = Number.parseInt(raw, 10);
67
+ if (Number.isInteger(n) && n > 0) {
68
+ return { pid: n, projectDir: null, startedAt: null, form: "legacy" };
69
+ }
70
+ return null;
71
+ }
72
+
73
+ /**
74
+ * verifyFingerprint(entry, projectDir, opts?)
75
+ *
76
+ * Returns { ok, reason, command? }:
77
+ * ok: true → entry matches this project AND ps confirms gsd-t command line
78
+ * ok: false → mismatch (see reason)
79
+ * ok: null → inconclusive (legacy entry, can't verify)
80
+ *
81
+ * reason values when ok=false:
82
+ * "project_mismatch" — entry.projectDir !== resolved projectDir
83
+ * "process_not_found" — ps -p returned nothing
84
+ * "command_not_gsd_t" — ps succeeded but command line doesn't match /gsd-t|unattended/i
85
+ * "ps_failed" — ps threw (couldn't introspect)
86
+ *
87
+ * opts._execSync — injection point for tests
88
+ */
89
+ function verifyFingerprint(entry, projectDir, opts = {}) {
90
+ if (!entry || typeof entry !== "object") {
91
+ return { ok: false, reason: "no_entry" };
92
+ }
93
+ if (entry.form === "legacy" || !entry.projectDir) {
94
+ return { ok: null, reason: "legacy_fingerprint" };
95
+ }
96
+ const resolved = path.resolve(projectDir);
97
+ if (entry.projectDir !== resolved) {
98
+ return { ok: false, reason: "project_mismatch" };
99
+ }
100
+
101
+ const exec = opts._execSync || execSync;
102
+ let cmd;
103
+ try {
104
+ const out = exec(`ps -p ${entry.pid} -o command=`, {
105
+ encoding: "utf8",
106
+ stdio: ["ignore", "pipe", "ignore"],
107
+ });
108
+ cmd = (out || "").trim();
109
+ } catch {
110
+ return { ok: false, reason: "ps_failed" };
111
+ }
112
+ if (!cmd) {
113
+ return { ok: false, reason: "process_not_found" };
114
+ }
115
+ if (!/gsd-t|unattended/i.test(cmd)) {
116
+ return { ok: false, reason: "command_not_gsd_t", command: cmd };
117
+ }
118
+ return { ok: true, reason: "verified", command: cmd };
119
+ }
120
+
121
+ module.exports = {
122
+ pidPathFor,
123
+ writePidFile,
124
+ readPidFile,
125
+ verifyFingerprint,
126
+ };
@@ -96,23 +96,31 @@ Violations are task failures, not warnings.
96
96
 
97
97
  If STACK_RULES is empty (no templates/stacks/ dir or no matches), skip silently.
98
98
 
99
- **OBSERVABILITY LOGGING (MANDATORY):**
100
- Before spawning — run via Bash:
101
- `T_START=$(date +%s) && DT_START=$(date +"%Y-%m-%d %H:%M")`
99
+ Spawn a fresh subagent via `captureSpawn` — `spawnType: 'primary'` (respects `--watch`: headless by default, in-context when `WATCH_FLAG=true`):
100
+
101
+ **OBSERVABILITY LOGGING (MANDATORY) wrap the primary subagent spawn with `captureSpawn`:**
102
102
 
103
- Spawn a fresh subagent using the Task tool — `spawnType: 'primary'` (respects `--watch`: headless by default, in-context when `WATCH_FLAG=true`):
104
103
  ```
105
- subagent_type: general-purpose
106
- spawnType: primary
107
- prompt: "You are running gsd-t-debug for this issue: {$ARGUMENTS}
108
- Working directory: {current project root}
109
- Read CLAUDE.md and .gsd-t/progress.md for project context, then execute gsd-t-debug starting at Step 1."
104
+ node -e "
105
+ const { captureSpawn } = require('./bin/gsd-t-token-capture.cjs');
106
+ (async () => {
107
+ await captureSpawn({
108
+ command: 'gsd-t-debug',
109
+ step: 'Step 0',
110
+ model: 'sonnet',
111
+ description: 'debug: {issue summary}',
112
+ projectDir: '.',
113
+ notes: 'debug: {issue summary}',
114
+ spawnFn: async () => { /* Task subagent (general-purpose, spawnType: primary, model: sonnet):
115
+ 'You are running gsd-t-debug for this issue: {\$ARGUMENTS}
116
+ Working directory: {current project root}
117
+ Read CLAUDE.md and .gsd-t/progress.md for project context, then execute gsd-t-debug starting at Step 1.' */ },
118
+ });
119
+ })();
120
+ "
110
121
  ```
111
122
 
112
- After subagent returns run via Bash:
113
- `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START))`
114
- Append to `.gsd-t/token-log.md` (create with header `| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Ctx% |` if missing):
115
- `| {DT_START} | {DT_END} | gsd-t-debug | Step 0 | sonnet | {DURATION}s | debug: {issue summary} | {CTX_PCT} |`
123
+ `captureSpawn` parses `result.usage` and writes the row to `.gsd-t/token-log.md` under the canonical header. Tokens column renders as `in=N out=N cr=N cc=N $X.XX` or `—`, never `N/A`.
116
124
 
117
125
  Relay the subagent's summary to the user. **Do not execute Steps 1–5 yourself.**
118
126
 
@@ -154,40 +162,30 @@ Before attempting any fix, check whether this issue has been through multiple fa
154
162
 
155
163
  The current approach has failed 3+ times. This means the root cause is not yet understood. A different strategy — possibly a fundamentally different technical approach — is required.
156
164
 
157
- **OBSERVABILITY LOGGING (MANDATORY):**
158
- Before spawning — run via Bash:
159
- `T_START=$(date +%s) && DT_START=$(date +"%Y-%m-%d %H:%M")`
165
+ **OBSERVABILITY LOGGING (MANDATORY) — wrap the Deep Research team spawn with `captureSpawn`:**
160
166
 
161
167
  ```
162
- Spawn a deep research team (run all three in parallel):
163
-
164
- - Teammate "researcher-root-cause": Take the broadest possible look at
165
- the problem. Ignore prior fix attempts. Read the full component,
166
- its dependencies, contracts, and all error traces from scratch.
167
- What is the actual root cause — not the symptom? Consider that the
168
- real issue may be architectural, not in the code being patched.
169
-
170
- - Teammate "researcher-alternatives": Enumerate 3–5 fundamentally
171
- different ways to solve this problem. Include approaches that would
172
- require refactoring or changing the technical direction entirely.
173
- For each: what are the trade-offs, effort, and risk?
174
-
175
- - Teammate "researcher-prior-art": Search external sources, docs,
176
- GitHub issues, and known patterns for this class of bug. Has this
177
- problem been documented elsewhere? What did others find? Are there
178
- framework-specific pitfalls or known workarounds?
179
-
180
- Lead: Wait for all three researchers to complete. Then synthesize:
181
- 1. What is the true root cause based on full investigation?
182
- 2. What are the viable solution paths (ranked by confidence)?
183
- 3. Does any path require a different technical approach than what has been tried?
184
- 4. What is the recommended path and why?
168
+ node -e "
169
+ const { captureSpawn } = require('./bin/gsd-t-token-capture.cjs');
170
+ (async () => {
171
+ await captureSpawn({
172
+ command: 'gsd-t-debug',
173
+ step: 'Step 1.5',
174
+ model: 'sonnet',
175
+ description: 'deep research loop break: {issue summary}',
176
+ projectDir: '.',
177
+ notes: 'deep research loop break: {issue summary}',
178
+ spawnFn: async () => { /* Deep research team (three teammates in parallel):
179
+ - Teammate 'researcher-root-cause': broadest look at the problem, ignore prior fix attempts, identify true root cause (possibly architectural, not in code being patched).
180
+ - Teammate 'researcher-alternatives': enumerate 3–5 fundamentally different solutions with trade-offs, effort, and risk.
181
+ - Teammate 'researcher-prior-art': search external sources, docs, GitHub issues for this class of bug and known workarounds.
182
+ Lead synthesizes after all three complete: true root cause, ranked solution paths, whether a different technical approach is required, and the recommended path. */ },
183
+ });
184
+ })();
185
+ "
185
186
  ```
186
187
 
187
- After team completes run via Bash:
188
- `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.cjs'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
189
- Append to `.gsd-t/token-log.md`:
190
- `| {DT_START} | {DT_END} | gsd-t-debug | Step 1.5 | sonnet | {DURATION}s | deep research loop break: {issue summary} | {CTX_PCT} |`
188
+ `captureSpawn` parses `result.usage` and writes the row to `.gsd-t/token-log.md` under the canonical header. Tokens column renders as `in=N out=N cr=N cc=N $X.XX` or `—`, never `N/A`.
191
189
 
192
190
  **STOP. Present findings to the user before making any changes:**
193
191
 
@@ -447,19 +445,33 @@ Resolve the templated prompt path via Bash:
447
445
  ```
448
446
  RT_PROMPT="$(npm root -g 2>/dev/null)/@tekyzinc/gsd-t/templates/prompts/red-team-subagent.md"
449
447
  [ -f "$RT_PROMPT" ] || RT_PROMPT="templates/prompts/red-team-subagent.md"
450
- T_START=$(date +%s) && DT_START=$(date +"%Y-%m-%d %H:%M")
451
448
  ```
452
449
 
453
- Spawn Task subagent (spawnType: validation, general-purpose, model: opus) always headless, `--watch` ignored:
454
- > "Read `$RT_PROMPT` and follow it. Context: post-fix validation for a debug session. **Additional categories for this run:** (a) **Regression Around the Fix** — test every code path adjacent to the changed lines; fixes frequently break neighboring functionality. (b) **Original Bug Variants** — the original bug was {one-line description}; search for SIMILAR bugs in related code (same pattern, different location). Write findings to `.gsd-t/red-team-report.md`."
450
+ **OBSERVABILITY LOGGING (MANDATORY) wrap the Red Team subagent spawn with `captureSpawn`:**
455
451
 
456
- After subagent returns — run via Bash:
457
452
  ```
458
- T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START))
459
- CTX_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs'); process.stdout.write(String(tb.getSessionStatus('.').pct))}catch(_){process.stdout.write('N/A')}")
453
+ node -e "
454
+ const { captureSpawn } = require('./bin/gsd-t-token-capture.cjs');
455
+ (async () => {
456
+ await captureSpawn({
457
+ command: 'gsd-t-debug',
458
+ step: 'Red Team',
459
+ model: 'opus',
460
+ description: 'adversarial validation of debug fix',
461
+ projectDir: '.',
462
+ notes: '{VERDICT} — {N} bugs found',
463
+ spawnFn: async () => { /* Task subagent (spawnType: validation, general-purpose, model: opus) — always headless, --watch ignored:
464
+ 'Read \$RT_PROMPT and follow it. Context: post-fix validation for a debug session.
465
+ Additional categories for this run:
466
+ (a) Regression Around the Fix — test every code path adjacent to the changed lines; fixes frequently break neighboring functionality.
467
+ (b) Original Bug Variants — the original bug was {one-line description}; search for SIMILAR bugs in related code (same pattern, different location).
468
+ Write findings to .gsd-t/red-team-report.md.' */ },
469
+ });
470
+ })();
471
+ "
460
472
  ```
461
- Append to `.gsd-t/token-log.md`:
462
- `| {DT_START} | {DT_END} | gsd-t-debug | Red Team | opus | {DURATION}s | {VERDICT} {N} bugs found | | | {CTX_PCT} |`
473
+
474
+ `captureSpawn` parses `result.usage` and writes the row to `.gsd-t/token-log.md` under the canonical header. Tokens column renders as `in=N out=N cr=N cc=N $X.XX` or `—`, never `N/A`.
463
475
 
464
476
  **If FAIL:** fix CRITICAL/HIGH bugs (≤2 cycles) → re-run. Persistent bugs → `.gsd-t/deferred-items.md`.
465
477
  **If GRUDGING PASS:** proceed to metrics and doc-ripple.