@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.
- package/CHANGELOG.md +44 -0
- package/README.md +1 -0
- package/bin/gsd-t-benchmark-orchestrator.js +437 -0
- package/bin/gsd-t-capture-lint.cjs +276 -0
- package/bin/gsd-t-completion-check.cjs +106 -0
- package/bin/gsd-t-orchestrator-config.cjs +64 -0
- package/bin/gsd-t-orchestrator-queue.cjs +180 -0
- package/bin/gsd-t-orchestrator-recover.cjs +231 -0
- package/bin/gsd-t-orchestrator-worker.cjs +219 -0
- package/bin/gsd-t-orchestrator.js +534 -0
- package/bin/gsd-t-stream-feed-client.cjs +151 -0
- package/bin/gsd-t-task-brief-compactor.cjs +89 -0
- package/bin/gsd-t-task-brief-template.cjs +96 -0
- package/bin/gsd-t-task-brief.js +249 -0
- package/bin/gsd-t-token-backfill.cjs +366 -0
- package/bin/gsd-t-token-capture.cjs +306 -0
- package/bin/gsd-t-token-dashboard.cjs +318 -0
- package/bin/gsd-t-token-regenerate-log.cjs +129 -0
- package/bin/gsd-t-transcript-tee.cjs +246 -0
- package/bin/gsd-t-unattended-heartbeat.cjs +188 -0
- package/bin/gsd-t-unattended-platform.cjs +191 -27
- package/bin/gsd-t-unattended-safety.cjs +8 -1
- package/bin/gsd-t-unattended.cjs +192 -31
- package/bin/gsd-t.js +329 -2
- package/bin/supervisor-pid-fingerprint.cjs +126 -0
- package/commands/gsd-t-debug.md +63 -51
- package/commands/gsd-t-design-decompose.md +2 -7
- package/commands/gsd-t-doc-ripple.md +20 -11
- package/commands/gsd-t-execute.md +82 -50
- package/commands/gsd-t-integrate.md +43 -16
- package/commands/gsd-t-plan.md +20 -7
- package/commands/gsd-t-prd.md +19 -12
- package/commands/gsd-t-quick.md +64 -29
- package/commands/gsd-t-resume.md +51 -4
- package/commands/gsd-t-unattended.md +19 -20
- package/commands/gsd-t-verify.md +48 -32
- package/commands/gsd-t-visualize.md +19 -17
- package/commands/gsd-t-wave.md +29 -27
- package/docs/architecture.md +16 -0
- package/docs/m40-benchmark-report.md +35 -0
- package/docs/requirements.md +20 -0
- package/package.json +1 -1
- package/scripts/gsd-t-dashboard-server.js +291 -4
- package/scripts/gsd-t-dashboard.html +31 -1
- package/scripts/gsd-t-design-review-server.js +3 -1
- package/scripts/gsd-t-stream-feed-server.js +428 -0
- package/scripts/gsd-t-stream-feed.html +1168 -0
- package/scripts/gsd-t-token-aggregator.js +373 -0
- package/scripts/gsd-t-transcript.html +422 -0
- package/scripts/hooks/gsd-t-in-session-probe.js +62 -0
- package/scripts/hooks/pre-commit-capture-lint +26 -0
- package/templates/CLAUDE-global.md +69 -0
- package/scripts/gsd-t-agent-dashboard-server.js +0 -424
- 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
|
-
|
|
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
|
+
};
|
package/commands/gsd-t-debug.md
CHANGED
|
@@ -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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
- Teammate
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
459
|
-
|
|
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
|
-
|
|
462
|
-
|
|
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.
|