@tekyzinc/gsd-t 2.74.12 → 2.76.10
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 +130 -0
- package/README.md +71 -1
- package/bin/advisor-integration.js +93 -0
- package/bin/check-headless-sessions.js +140 -0
- package/bin/context-meter-config.cjs +101 -0
- package/bin/context-meter-config.test.cjs +101 -0
- package/bin/gsd-t.js +710 -16
- package/bin/headless-auto-spawn.js +290 -0
- package/bin/model-selector.js +224 -0
- package/bin/runway-estimator.js +242 -0
- package/bin/token-budget.js +96 -89
- package/bin/token-optimizer.js +471 -0
- package/bin/token-telemetry.js +246 -0
- package/commands/gsd-t-audit.md +3 -3
- package/commands/gsd-t-backlog-list.md +38 -0
- package/commands/gsd-t-brainstorm.md +3 -3
- package/commands/gsd-t-complete-milestone.md +24 -0
- package/commands/gsd-t-debug.md +124 -7
- package/commands/gsd-t-discuss.md +10 -3
- package/commands/gsd-t-doc-ripple.md +32 -4
- package/commands/gsd-t-execute.md +107 -52
- package/commands/gsd-t-help.md +19 -0
- package/commands/gsd-t-integrate.md +67 -4
- package/commands/gsd-t-optimization-apply.md +91 -0
- package/commands/gsd-t-optimization-reject.md +94 -0
- package/commands/gsd-t-partition.md +7 -0
- package/commands/gsd-t-pause.md +3 -0
- package/commands/gsd-t-plan.md +10 -3
- package/commands/gsd-t-prd.md +3 -3
- package/commands/gsd-t-quick.md +71 -9
- package/commands/gsd-t-reflect.md +3 -7
- package/commands/gsd-t-resume.md +36 -0
- package/commands/gsd-t-status.md +31 -0
- package/commands/gsd-t-test-sync.md +7 -0
- package/commands/gsd-t-verify.md +12 -5
- package/commands/gsd-t-visualize.md +3 -7
- package/commands/gsd-t-wave.md +82 -18
- package/docs/GSD-T-README.md +52 -0
- package/docs/architecture.md +95 -0
- package/docs/infrastructure.md +117 -0
- package/docs/methodology.md +36 -0
- package/docs/prd-harness-evolution.md +51 -37
- package/docs/requirements.md +66 -0
- package/package.json +1 -1
- package/scripts/context-meter/count-tokens-client.js +221 -0
- package/scripts/context-meter/count-tokens-client.test.js +308 -0
- package/scripts/context-meter/test-injector.js +55 -0
- package/scripts/context-meter/threshold.js +88 -0
- package/scripts/context-meter/threshold.test.js +255 -0
- package/scripts/context-meter/transcript-parser.js +252 -0
- package/scripts/context-meter/transcript-parser.test.js +320 -0
- package/scripts/gsd-t-context-meter.e2e.test.js +415 -0
- package/scripts/gsd-t-context-meter.js +350 -0
- package/scripts/gsd-t-context-meter.test.js +417 -0
- package/scripts/gsd-t-heartbeat.js +2 -2
- package/scripts/gsd-t-statusline.js +23 -8
- package/templates/CLAUDE-global.md +5 -1
- package/templates/CLAUDE-project.md +26 -6
- package/templates/context-meter-config.json +10 -0
- package/templates/prompts/README.md +1 -1
- package/bin/task-counter.cjs +0 -161
package/bin/gsd-t.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
const fs = require("fs");
|
|
19
19
|
const path = require("path");
|
|
20
20
|
const os = require("os");
|
|
21
|
+
const readline = require("readline");
|
|
21
22
|
const { execFileSync, spawn: cpSpawn } = require("child_process");
|
|
22
23
|
const debugLedger = require(path.join(__dirname, "debug-ledger.js"));
|
|
23
24
|
|
|
@@ -376,6 +377,311 @@ function addHeartbeatHook(hooks, event, cmd) {
|
|
|
376
377
|
return true;
|
|
377
378
|
}
|
|
378
379
|
|
|
380
|
+
// ─── Context Meter ──────────────────────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
const CONTEXT_METER_SCRIPT = "gsd-t-context-meter.js";
|
|
383
|
+
const CONTEXT_METER_DEPS_DIR = "context-meter";
|
|
384
|
+
const CONTEXT_METER_CONFIG_TEMPLATE = "context-meter-config.json";
|
|
385
|
+
const CONTEXT_METER_CONFIG_DEST = path.join(".gsd-t", "context-meter-config.json");
|
|
386
|
+
const CONTEXT_METER_GITIGNORE_ENTRIES = [
|
|
387
|
+
".gsd-t/.context-meter-state.json",
|
|
388
|
+
".gsd-t/context-meter.log",
|
|
389
|
+
];
|
|
390
|
+
const CONTEXT_METER_HOOK_MARKER = "gsd-t-context-meter";
|
|
391
|
+
const CONTEXT_METER_HOOK_COMMAND =
|
|
392
|
+
'node "$CLAUDE_PROJECT_DIR/scripts/gsd-t-context-meter.js"';
|
|
393
|
+
|
|
394
|
+
// Append entries to {projectDir}/.gitignore. Each entry added only if absent.
|
|
395
|
+
// Idempotent. Returns true if any entries were added, false otherwise.
|
|
396
|
+
function ensureGitignoreEntries(projectDir, entries) {
|
|
397
|
+
const gitignorePath = path.join(projectDir, ".gitignore");
|
|
398
|
+
if (isSymlink(gitignorePath)) {
|
|
399
|
+
warn("Skipping .gitignore — target is a symlink");
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
let content = "";
|
|
403
|
+
try {
|
|
404
|
+
if (fs.existsSync(gitignorePath)) {
|
|
405
|
+
content = fs.readFileSync(gitignorePath, "utf8");
|
|
406
|
+
}
|
|
407
|
+
} catch (e) {
|
|
408
|
+
warn(`Failed to read .gitignore: ${e.message}`);
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
const existingLines = new Set(
|
|
412
|
+
content.split(/\r?\n/).map((l) => l.trim()).filter(Boolean)
|
|
413
|
+
);
|
|
414
|
+
const toAdd = entries.filter((e) => !existingLines.has(e));
|
|
415
|
+
if (toAdd.length === 0) return false;
|
|
416
|
+
try {
|
|
417
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
418
|
+
fs.writeFileSync(gitignorePath, toAdd.join("\n") + "\n");
|
|
419
|
+
} else {
|
|
420
|
+
const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
421
|
+
const block =
|
|
422
|
+
"\n# GSD-T context meter (session state — do not commit)\n" +
|
|
423
|
+
toAdd.join("\n") +
|
|
424
|
+
"\n";
|
|
425
|
+
fs.appendFileSync(gitignorePath, prefix + block);
|
|
426
|
+
}
|
|
427
|
+
return true;
|
|
428
|
+
} catch (e) {
|
|
429
|
+
warn(`Failed to append to .gitignore: ${e.message}`);
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Install the Context Meter into a project directory.
|
|
435
|
+
// Copies scripts/gsd-t-context-meter.js, scripts/context-meter/*.js (runtime
|
|
436
|
+
// only — skips .test.js and test-injector.js), and the config template (if
|
|
437
|
+
// missing). Also appends entries to .gitignore.
|
|
438
|
+
function installContextMeter(projectDir) {
|
|
439
|
+
try {
|
|
440
|
+
// 1. Copy gsd-t-context-meter.js → {projectDir}/scripts/
|
|
441
|
+
const projectScriptsDir = path.join(projectDir, "scripts");
|
|
442
|
+
if (!fs.existsSync(projectScriptsDir)) {
|
|
443
|
+
try {
|
|
444
|
+
fs.mkdirSync(projectScriptsDir, { recursive: true });
|
|
445
|
+
} catch (e) {
|
|
446
|
+
warn(`Failed to create scripts/: ${e.message}`);
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const scriptSrc = path.join(PKG_SCRIPTS, CONTEXT_METER_SCRIPT);
|
|
451
|
+
const scriptDest = path.join(projectScriptsDir, CONTEXT_METER_SCRIPT);
|
|
452
|
+
if (!fs.existsSync(scriptSrc)) {
|
|
453
|
+
warn(`${CONTEXT_METER_SCRIPT} not found in package — skipping context meter`);
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
if (!isSymlink(scriptDest)) {
|
|
457
|
+
const srcContent = fs.readFileSync(scriptSrc, "utf8");
|
|
458
|
+
const destContent = fs.existsSync(scriptDest)
|
|
459
|
+
? fs.readFileSync(scriptDest, "utf8")
|
|
460
|
+
: "";
|
|
461
|
+
if (normalizeEol(srcContent) !== normalizeEol(destContent)) {
|
|
462
|
+
fs.copyFileSync(scriptSrc, scriptDest);
|
|
463
|
+
try {
|
|
464
|
+
fs.chmodSync(scriptDest, 0o755);
|
|
465
|
+
} catch {}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// 2. Copy scripts/context-meter/*.js (runtime files only)
|
|
470
|
+
const depsSrcDir = path.join(PKG_SCRIPTS, CONTEXT_METER_DEPS_DIR);
|
|
471
|
+
const depsDestDir = path.join(projectScriptsDir, CONTEXT_METER_DEPS_DIR);
|
|
472
|
+
if (fs.existsSync(depsSrcDir)) {
|
|
473
|
+
if (!fs.existsSync(depsDestDir)) {
|
|
474
|
+
try {
|
|
475
|
+
fs.mkdirSync(depsDestDir, { recursive: true });
|
|
476
|
+
} catch (e) {
|
|
477
|
+
warn(`Failed to create scripts/${CONTEXT_METER_DEPS_DIR}/: ${e.message}`);
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
let depFiles = [];
|
|
482
|
+
try {
|
|
483
|
+
depFiles = fs.readdirSync(depsSrcDir);
|
|
484
|
+
} catch (e) {
|
|
485
|
+
warn(`Failed to read ${CONTEXT_METER_DEPS_DIR}/: ${e.message}`);
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
for (const fname of depFiles) {
|
|
489
|
+
// Skip test files and test-only infrastructure
|
|
490
|
+
if (fname.includes(".test.")) continue;
|
|
491
|
+
if (fname === "test-injector.js") continue;
|
|
492
|
+
const fsrc = path.join(depsSrcDir, fname);
|
|
493
|
+
const fdest = path.join(depsDestDir, fname);
|
|
494
|
+
try {
|
|
495
|
+
const stat = fs.statSync(fsrc);
|
|
496
|
+
if (!stat.isFile()) continue;
|
|
497
|
+
} catch {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
if (isSymlink(fdest)) continue;
|
|
501
|
+
try {
|
|
502
|
+
const srcContent = fs.readFileSync(fsrc, "utf8");
|
|
503
|
+
const destContent = fs.existsSync(fdest)
|
|
504
|
+
? fs.readFileSync(fdest, "utf8")
|
|
505
|
+
: "";
|
|
506
|
+
if (normalizeEol(srcContent) !== normalizeEol(destContent)) {
|
|
507
|
+
fs.copyFileSync(fsrc, fdest);
|
|
508
|
+
}
|
|
509
|
+
} catch (e) {
|
|
510
|
+
warn(`Failed to copy ${CONTEXT_METER_DEPS_DIR}/${fname}: ${e.message}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// 3. Copy config template → {projectDir}/.gsd-t/context-meter-config.json
|
|
516
|
+
// ONLY if the destination does not already exist (never overwrite user config).
|
|
517
|
+
const configSrc = path.join(PKG_TEMPLATES, CONTEXT_METER_CONFIG_TEMPLATE);
|
|
518
|
+
const configDest = path.join(projectDir, CONTEXT_METER_CONFIG_DEST);
|
|
519
|
+
const gsdtDir = path.dirname(configDest);
|
|
520
|
+
if (!fs.existsSync(gsdtDir)) {
|
|
521
|
+
try {
|
|
522
|
+
fs.mkdirSync(gsdtDir, { recursive: true });
|
|
523
|
+
} catch (e) {
|
|
524
|
+
warn(`Failed to create .gsd-t/: ${e.message}`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (fs.existsSync(configSrc) && !fs.existsSync(configDest)) {
|
|
528
|
+
if (!isSymlink(configDest)) {
|
|
529
|
+
try {
|
|
530
|
+
fs.copyFileSync(configSrc, configDest);
|
|
531
|
+
} catch (e) {
|
|
532
|
+
warn(`Failed to copy context-meter-config.json: ${e.message}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// 4. Append .gitignore entries
|
|
538
|
+
ensureGitignoreEntries(projectDir, CONTEXT_METER_GITIGNORE_ENTRIES);
|
|
539
|
+
|
|
540
|
+
return true;
|
|
541
|
+
} catch (e) {
|
|
542
|
+
warn(`installContextMeter failed: ${e.message}`);
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Register the Context Meter PostToolUse hook in ~/.claude/settings.json.
|
|
548
|
+
// Idempotent — if an existing hook references CONTEXT_METER_HOOK_MARKER the
|
|
549
|
+
// command string is refreshed in-place. All other settings/hooks preserved.
|
|
550
|
+
// Returns { installed: bool, action: "added"|"updated"|"noop" }.
|
|
551
|
+
function configureContextMeterHooks(settingsPath) {
|
|
552
|
+
const targetPath = settingsPath || SETTINGS_JSON;
|
|
553
|
+
let settings = {};
|
|
554
|
+
const fileExists = fs.existsSync(targetPath);
|
|
555
|
+
if (fileExists) {
|
|
556
|
+
try {
|
|
557
|
+
settings = JSON.parse(fs.readFileSync(targetPath, "utf8"));
|
|
558
|
+
if (!settings || typeof settings !== "object") settings = {};
|
|
559
|
+
} catch {
|
|
560
|
+
warn("settings.json has invalid JSON — cannot configure context meter hook");
|
|
561
|
+
return { installed: false, action: "noop" };
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (!settings.hooks) settings.hooks = {};
|
|
566
|
+
if (!Array.isArray(settings.hooks.PostToolUse)) settings.hooks.PostToolUse = [];
|
|
567
|
+
|
|
568
|
+
const cmd = CONTEXT_METER_HOOK_COMMAND;
|
|
569
|
+
let action = "noop";
|
|
570
|
+
let found = false;
|
|
571
|
+
|
|
572
|
+
for (const entry of settings.hooks.PostToolUse) {
|
|
573
|
+
if (!entry || !Array.isArray(entry.hooks)) continue;
|
|
574
|
+
for (const h of entry.hooks) {
|
|
575
|
+
if (h && typeof h.command === "string" && h.command.includes(CONTEXT_METER_HOOK_MARKER)) {
|
|
576
|
+
found = true;
|
|
577
|
+
if (h.command !== cmd) {
|
|
578
|
+
h.command = cmd;
|
|
579
|
+
action = "updated";
|
|
580
|
+
} else if (action === "noop") {
|
|
581
|
+
action = "noop";
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (!found) {
|
|
588
|
+
settings.hooks.PostToolUse.push({
|
|
589
|
+
matcher: "*",
|
|
590
|
+
hooks: [{ type: "command", command: cmd }],
|
|
591
|
+
});
|
|
592
|
+
action = "added";
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (action === "noop") {
|
|
596
|
+
return { installed: true, action: "noop" };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (isSymlink(targetPath)) {
|
|
600
|
+
warn("Skipping settings.json write — target is a symlink");
|
|
601
|
+
return { installed: false, action: "noop" };
|
|
602
|
+
}
|
|
603
|
+
try {
|
|
604
|
+
fs.writeFileSync(targetPath, JSON.stringify(settings, null, 2));
|
|
605
|
+
} catch (e) {
|
|
606
|
+
warn(`Failed to write settings.json: ${e.message}`);
|
|
607
|
+
return { installed: false, action: "noop" };
|
|
608
|
+
}
|
|
609
|
+
return { installed: true, action };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Interactive prompt for the Anthropic API key env var.
|
|
613
|
+
// Skips if not a TTY or if the env var is already set.
|
|
614
|
+
// Never writes the key anywhere — just prints the export command for the user
|
|
615
|
+
// to paste into their shell profile themselves. Always non-blocking.
|
|
616
|
+
async function promptForApiKeyIfMissing(envVarName) {
|
|
617
|
+
const varName = envVarName || "ANTHROPIC_API_KEY";
|
|
618
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) return "";
|
|
619
|
+
if (process.env[varName]) return process.env[varName];
|
|
620
|
+
|
|
621
|
+
heading("Context Meter — Anthropic API Key");
|
|
622
|
+
log("");
|
|
623
|
+
log(` ${YELLOW}⚠${RESET} Context Meter: ${varName} is not set.`);
|
|
624
|
+
log(` The hook uses Anthropic's count_tokens API (free, not billed) to measure`);
|
|
625
|
+
log(` real context window usage. Without it, the meter falls back to heuristics.`);
|
|
626
|
+
log(` Get a key: https://console.anthropic.com/settings/keys`);
|
|
627
|
+
log("");
|
|
628
|
+
|
|
629
|
+
let rl;
|
|
630
|
+
try {
|
|
631
|
+
rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
632
|
+
} catch (e) {
|
|
633
|
+
warn(`Could not open API key prompt: ${e.message}`);
|
|
634
|
+
return "";
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return new Promise((resolve) => {
|
|
638
|
+
try {
|
|
639
|
+
rl.question(" Paste key now, or press Enter to skip: ", (answer) => {
|
|
640
|
+
try {
|
|
641
|
+
const key = (answer || "").trim();
|
|
642
|
+
if (key.length === 0) {
|
|
643
|
+
log("");
|
|
644
|
+
info(`Skipped. Set ${varName} later or run 'gsd-t doctor' to re-check.`);
|
|
645
|
+
log("");
|
|
646
|
+
} else {
|
|
647
|
+
log("");
|
|
648
|
+
success("Got it. Add this line to your shell profile (~/.zshrc or ~/.bashrc):");
|
|
649
|
+
log("");
|
|
650
|
+
log(` ${DIM}export ${varName}="${key}"${RESET}`);
|
|
651
|
+
log("");
|
|
652
|
+
log(" Re-open your shell or run the export command to activate.");
|
|
653
|
+
log("");
|
|
654
|
+
}
|
|
655
|
+
resolve(key);
|
|
656
|
+
} finally {
|
|
657
|
+
try { rl.close(); } catch {}
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
} catch (e) {
|
|
661
|
+
warn(`API key prompt failed: ${e.message}`);
|
|
662
|
+
try { rl.close(); } catch {}
|
|
663
|
+
resolve("");
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Resolve the apiKeyEnvVar from the project's context-meter-config.json
|
|
669
|
+
// Falls back to "ANTHROPIC_API_KEY" if loader unavailable or config absent.
|
|
670
|
+
function resolveApiKeyEnvVar(projectDir) {
|
|
671
|
+
try {
|
|
672
|
+
const loader = require(path.join(PKG_ROOT, "bin", "context-meter-config.cjs"));
|
|
673
|
+
if (loader && typeof loader.loadConfig === "function") {
|
|
674
|
+
const cfg = loader.loadConfig(projectDir || process.cwd());
|
|
675
|
+
if (cfg && typeof cfg.apiKeyEnvVar === "string" && cfg.apiKeyEnvVar.length > 0) {
|
|
676
|
+
return cfg.apiKeyEnvVar;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
} catch {
|
|
680
|
+
// Fall through to default
|
|
681
|
+
}
|
|
682
|
+
return "ANTHROPIC_API_KEY";
|
|
683
|
+
}
|
|
684
|
+
|
|
379
685
|
// ─── Update Check Hook ──────────────────────────────────────────────────────
|
|
380
686
|
|
|
381
687
|
const UPDATE_CHECK_SCRIPT = "gsd-t-update-check.js";
|
|
@@ -884,7 +1190,7 @@ function appendGsdtToClaudeMd(template) {
|
|
|
884
1190
|
info("Your existing content was preserved.");
|
|
885
1191
|
}
|
|
886
1192
|
|
|
887
|
-
function doInstall(opts = {}) {
|
|
1193
|
+
async function doInstall(opts = {}) {
|
|
888
1194
|
const isUpdate = opts.update || false;
|
|
889
1195
|
heading(`${isUpdate ? "Updating" : "Installing"} GSD-T ${versionLink()}`);
|
|
890
1196
|
log("");
|
|
@@ -912,12 +1218,23 @@ function doInstall(opts = {}) {
|
|
|
912
1218
|
heading("Utility Scripts");
|
|
913
1219
|
installUtilityScripts();
|
|
914
1220
|
|
|
1221
|
+
heading("Context Meter (PostToolUse)");
|
|
1222
|
+
const cmHook = configureContextMeterHooks(SETTINGS_JSON);
|
|
1223
|
+
if (cmHook.installed) {
|
|
1224
|
+
if (cmHook.action === "added") success("Context meter PostToolUse hook added");
|
|
1225
|
+
else if (cmHook.action === "updated") success("Context meter hook command refreshed");
|
|
1226
|
+
else info("Context meter hook already configured");
|
|
1227
|
+
}
|
|
1228
|
+
|
|
915
1229
|
heading("Graph Engine (CGC)");
|
|
916
1230
|
installCgc();
|
|
917
1231
|
|
|
918
1232
|
saveInstalledVersion();
|
|
919
1233
|
|
|
920
1234
|
showInstallSummary(gsdtCommands.length, utilityCommands.length);
|
|
1235
|
+
|
|
1236
|
+
// Interactive prompt (skipped silently in non-TTY shells)
|
|
1237
|
+
await promptForApiKeyIfMissing(resolveApiKeyEnvVar(process.cwd()));
|
|
921
1238
|
}
|
|
922
1239
|
|
|
923
1240
|
function showInstallSummary(gsdtCount, utilCount) {
|
|
@@ -942,7 +1259,7 @@ function showInstallSummary(gsdtCount, utilCount) {
|
|
|
942
1259
|
log("");
|
|
943
1260
|
}
|
|
944
1261
|
|
|
945
|
-
function doUpdate() {
|
|
1262
|
+
async function doUpdate() {
|
|
946
1263
|
const installedVersion = getInstalledVersion();
|
|
947
1264
|
|
|
948
1265
|
if (installedVersion === PKG_VERSION) {
|
|
@@ -959,7 +1276,7 @@ function doUpdate() {
|
|
|
959
1276
|
heading(`Updating GSD-T: ${versionLink(installedVersion)} → ${versionLink()}`);
|
|
960
1277
|
}
|
|
961
1278
|
|
|
962
|
-
doInstall({ update: true });
|
|
1279
|
+
await doInstall({ update: true });
|
|
963
1280
|
}
|
|
964
1281
|
|
|
965
1282
|
function initClaudeMd(projectDir, projectName, today) {
|
|
@@ -1092,7 +1409,7 @@ function writeTemplateFile(templateName, destPath, label, projectName, today) {
|
|
|
1092
1409
|
}
|
|
1093
1410
|
}
|
|
1094
1411
|
|
|
1095
|
-
function doInit(projectName) {
|
|
1412
|
+
async function doInit(projectName) {
|
|
1096
1413
|
if (!projectName) projectName = path.basename(process.cwd());
|
|
1097
1414
|
|
|
1098
1415
|
if (!validateProjectName(projectName)) {
|
|
@@ -1110,10 +1427,26 @@ function doInit(projectName) {
|
|
|
1110
1427
|
initClaudeMd(projectDir, projectName, today);
|
|
1111
1428
|
initDocs(projectDir, projectName, today);
|
|
1112
1429
|
initGsdtDir(projectDir, projectName, today);
|
|
1430
|
+
copyBinToolsToProject(projectDir, projectName);
|
|
1431
|
+
|
|
1432
|
+
// Context Meter: copy script + deps + config template, append .gitignore,
|
|
1433
|
+
// register the global PostToolUse hook.
|
|
1434
|
+
if (installContextMeter(projectDir)) {
|
|
1435
|
+
success("Context meter installed (scripts/, config template, .gitignore)");
|
|
1436
|
+
}
|
|
1437
|
+
const cmHook = configureContextMeterHooks(SETTINGS_JSON);
|
|
1438
|
+
if (cmHook.installed) {
|
|
1439
|
+
if (cmHook.action === "added") success("Context meter PostToolUse hook added");
|
|
1440
|
+
else if (cmHook.action === "updated") success("Context meter hook command refreshed");
|
|
1441
|
+
else info("Context meter hook already configured");
|
|
1442
|
+
}
|
|
1113
1443
|
|
|
1114
1444
|
if (registerProject(projectDir)) success("Registered in ~/.claude/.gsd-t-projects");
|
|
1115
1445
|
|
|
1116
1446
|
showInitTree(projectDir);
|
|
1447
|
+
|
|
1448
|
+
// Interactive prompt (skipped silently in non-TTY shells)
|
|
1449
|
+
await promptForApiKeyIfMissing(resolveApiKeyEnvVar(projectDir));
|
|
1117
1450
|
}
|
|
1118
1451
|
|
|
1119
1452
|
function showInitTree(projectDir) {
|
|
@@ -1148,10 +1481,83 @@ function doStatus() {
|
|
|
1148
1481
|
showStatusCommands();
|
|
1149
1482
|
showStatusConfig();
|
|
1150
1483
|
showStatusTeams();
|
|
1484
|
+
showStatusContextMeter();
|
|
1151
1485
|
showStatusProject();
|
|
1152
1486
|
log("");
|
|
1153
1487
|
}
|
|
1154
1488
|
|
|
1489
|
+
function formatRelativeTime(timestampIso) {
|
|
1490
|
+
const then = Date.parse(timestampIso);
|
|
1491
|
+
if (!Number.isFinite(then)) return "unknown";
|
|
1492
|
+
const deltaSec = Math.max(0, Math.floor((Date.now() - then) / 1000));
|
|
1493
|
+
if (deltaSec < 60) return `${deltaSec}s ago`;
|
|
1494
|
+
if (deltaSec < 3600) return `${Math.floor(deltaSec / 60)}m ago`;
|
|
1495
|
+
if (deltaSec < 86400) return `${Math.floor(deltaSec / 3600)}h ago`;
|
|
1496
|
+
return `${Math.floor(deltaSec / 86400)} days ago`;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function showStatusContextMeter() {
|
|
1500
|
+
heading("Context Meter");
|
|
1501
|
+
const cwd = process.cwd();
|
|
1502
|
+
const statePath = path.join(cwd, ".gsd-t", ".context-meter-state.json");
|
|
1503
|
+
|
|
1504
|
+
let state = null;
|
|
1505
|
+
try {
|
|
1506
|
+
if (fs.existsSync(statePath)) {
|
|
1507
|
+
state = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
1508
|
+
}
|
|
1509
|
+
} catch {
|
|
1510
|
+
state = null;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
if (!state || typeof state !== "object") {
|
|
1514
|
+
log(` ${DIM}Context: N/A (meter hook not run this session)${RESET}`);
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// Error case: inputTokens === 0 AND lastError set
|
|
1519
|
+
if (state.lastError && state.inputTokens === 0) {
|
|
1520
|
+
const code = (state.lastError && state.lastError.code) || "unknown";
|
|
1521
|
+
const rel = state.timestamp ? formatRelativeTime(state.timestamp) : "unknown";
|
|
1522
|
+
log(` ${DIM}Context: N/A (meter error: ${code}) — last check ${rel}${RESET}`);
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// Require minimum viable fields for a fresh reading
|
|
1527
|
+
if (
|
|
1528
|
+
typeof state.pct !== "number" ||
|
|
1529
|
+
typeof state.modelWindowSize !== "number" ||
|
|
1530
|
+
typeof state.threshold !== "string" ||
|
|
1531
|
+
typeof state.timestamp !== "string"
|
|
1532
|
+
) {
|
|
1533
|
+
log(` ${DIM}Context: N/A (meter hook not run this session)${RESET}`);
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
const pctStr = state.pct.toFixed(1);
|
|
1538
|
+
const rel = formatRelativeTime(state.timestamp);
|
|
1539
|
+
const ageMs = Date.now() - Date.parse(state.timestamp);
|
|
1540
|
+
const stale = !Number.isFinite(ageMs) || ageMs > 5 * 60 * 1000;
|
|
1541
|
+
const staleSuffix = stale ? " (stale)" : "";
|
|
1542
|
+
|
|
1543
|
+
// v3.0.0 three-band: normal (green) / warn (yellow) / stop (bold red).
|
|
1544
|
+
let color = DIM;
|
|
1545
|
+
switch (state.threshold) {
|
|
1546
|
+
case "normal":
|
|
1547
|
+
color = GREEN;
|
|
1548
|
+
break;
|
|
1549
|
+
case "warn":
|
|
1550
|
+
color = YELLOW;
|
|
1551
|
+
break;
|
|
1552
|
+
case "stop":
|
|
1553
|
+
color = BOLD + RED;
|
|
1554
|
+
break;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
const line = `Context: ${pctStr}% of ${state.modelWindowSize} tokens (${state.threshold} band) — last check ${rel}${staleSuffix}`;
|
|
1558
|
+
log(` ${color}${line}${RESET}`);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1155
1561
|
function showStatusVersion() {
|
|
1156
1562
|
const installedVersion = getInstalledVersion();
|
|
1157
1563
|
if (installedVersion) {
|
|
@@ -1485,8 +1891,8 @@ function exportUniversalRulesForNpm() {
|
|
|
1485
1891
|
}
|
|
1486
1892
|
}
|
|
1487
1893
|
|
|
1488
|
-
function doUpdateAll() {
|
|
1489
|
-
updateGlobalCommands();
|
|
1894
|
+
async function doUpdateAll() {
|
|
1895
|
+
await updateGlobalCommands();
|
|
1490
1896
|
heading("Updating registered projects...");
|
|
1491
1897
|
log("");
|
|
1492
1898
|
|
|
@@ -1510,9 +1916,9 @@ function doUpdateAll() {
|
|
|
1510
1916
|
showUpdateAllSummary(projects.length, counts, playwrightMissing, swaggerMissing, syncCount);
|
|
1511
1917
|
}
|
|
1512
1918
|
|
|
1513
|
-
function updateGlobalCommands() {
|
|
1919
|
+
async function updateGlobalCommands() {
|
|
1514
1920
|
if (getInstalledVersion() !== PKG_VERSION) {
|
|
1515
|
-
doInstall({ update: true });
|
|
1921
|
+
await doInstall({ update: true });
|
|
1516
1922
|
} else {
|
|
1517
1923
|
heading(`GSD-T ${versionLink()}`);
|
|
1518
1924
|
success("Global commands already up to date");
|
|
@@ -1548,7 +1954,8 @@ function updateSingleProject(projectDir, counts) {
|
|
|
1548
1954
|
const changelogCreated = createProjectChangelog(projectDir, projectName);
|
|
1549
1955
|
const binToolsCopied = copyBinToolsToProject(projectDir, projectName);
|
|
1550
1956
|
const archiveRan = runProgressArchiveMigration(projectDir, projectName);
|
|
1551
|
-
|
|
1957
|
+
const taskCounterRetired = runTaskCounterRetirementMigration(projectDir, projectName);
|
|
1958
|
+
if (guardAdded || changelogCreated || binToolsCopied || archiveRan || taskCounterRetired) {
|
|
1552
1959
|
counts.updated++;
|
|
1553
1960
|
} else {
|
|
1554
1961
|
info(`${projectName} — already up to date`);
|
|
@@ -1559,7 +1966,7 @@ function updateSingleProject(projectDir, counts) {
|
|
|
1559
1966
|
// Bin tools that should ship with every registered project. Listed here so adding
|
|
1560
1967
|
// a new tool only requires appending to this array. Use .cjs extension so they
|
|
1561
1968
|
// always run as CommonJS regardless of the project's package.json "type" field.
|
|
1562
|
-
const PROJECT_BIN_TOOLS = ["archive-progress.cjs", "log-tail.cjs", "context-budget-audit.cjs"];
|
|
1969
|
+
const PROJECT_BIN_TOOLS = ["archive-progress.cjs", "log-tail.cjs", "context-budget-audit.cjs", "context-meter-config.cjs"];
|
|
1563
1970
|
|
|
1564
1971
|
function copyBinToolsToProject(projectDir, projectName) {
|
|
1565
1972
|
const projectBinDir = path.join(projectDir, "bin");
|
|
@@ -1633,6 +2040,55 @@ function runProgressArchiveMigration(projectDir, projectName) {
|
|
|
1633
2040
|
}
|
|
1634
2041
|
}
|
|
1635
2042
|
|
|
2043
|
+
// One-shot migration: retire the v2.74.12 task-counter proxy. Removes the stale
|
|
2044
|
+
// bin/task-counter.cjs and any leftover .task-counter* state/config files from
|
|
2045
|
+
// downstream projects, then writes a marker so subsequent runs are no-ops. The
|
|
2046
|
+
// context meter hook (M34) is the authoritative context-burn signal now.
|
|
2047
|
+
function runTaskCounterRetirementMigration(projectDir, projectName) {
|
|
2048
|
+
const gsdtDir = path.join(projectDir, ".gsd-t");
|
|
2049
|
+
if (!fs.existsSync(gsdtDir)) return false;
|
|
2050
|
+
|
|
2051
|
+
const markerPath = path.join(gsdtDir, ".task-counter-retired-v1");
|
|
2052
|
+
if (fs.existsSync(markerPath)) return false;
|
|
2053
|
+
|
|
2054
|
+
const targets = [
|
|
2055
|
+
path.join(projectDir, "bin", "task-counter.cjs"),
|
|
2056
|
+
path.join(gsdtDir, "task-counter-config.json"),
|
|
2057
|
+
path.join(gsdtDir, ".task-counter-state.json"),
|
|
2058
|
+
path.join(gsdtDir, ".task-counter"),
|
|
2059
|
+
];
|
|
2060
|
+
|
|
2061
|
+
let removed = 0;
|
|
2062
|
+
for (const target of targets) {
|
|
2063
|
+
try {
|
|
2064
|
+
if (fs.existsSync(target)) {
|
|
2065
|
+
try { if (fs.lstatSync(target).isSymbolicLink()) continue; } catch {}
|
|
2066
|
+
fs.unlinkSync(target);
|
|
2067
|
+
removed++;
|
|
2068
|
+
}
|
|
2069
|
+
} catch (e) {
|
|
2070
|
+
warn(`${projectName} — failed to remove ${path.basename(target)}: ${e.message}`);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
try {
|
|
2075
|
+
fs.writeFileSync(
|
|
2076
|
+
markerPath,
|
|
2077
|
+
`# task-counter-retired-v1\nApplied: ${new Date().toISOString()}\nReplaced-by: scripts/gsd-t-context-meter.js (M34)\n`
|
|
2078
|
+
);
|
|
2079
|
+
} catch (e) {
|
|
2080
|
+
warn(`${projectName} — failed to write retirement marker: ${e.message}`);
|
|
2081
|
+
return false;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
if (removed > 0) {
|
|
2085
|
+
info(`${projectName} — retired task-counter (removed ${removed} file(s))`);
|
|
2086
|
+
} else {
|
|
2087
|
+
info(`${projectName} — retired task-counter (no legacy files found)`);
|
|
2088
|
+
}
|
|
2089
|
+
return true;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
1636
2092
|
function showUpdateAllSummary(total, counts, playwrightMissing, swaggerMissing, syncCount) {
|
|
1637
2093
|
log("");
|
|
1638
2094
|
heading("Update All Complete");
|
|
@@ -1786,7 +2242,120 @@ function checkDoctorCgc() {
|
|
|
1786
2242
|
return issues;
|
|
1787
2243
|
}
|
|
1788
2244
|
|
|
1789
|
-
|
|
2245
|
+
// Verify context meter wiring: API key env var, hook registration,
|
|
2246
|
+
// hook script presence, config validity, and a live count_tokens dry-run.
|
|
2247
|
+
// Returns number of issues (RED results). Mirrors checkDoctorCgc shape.
|
|
2248
|
+
async function checkDoctorContextMeter(projectDir) {
|
|
2249
|
+
let issues = 0;
|
|
2250
|
+
heading("Context Meter");
|
|
2251
|
+
|
|
2252
|
+
const cwd = projectDir || process.cwd();
|
|
2253
|
+
|
|
2254
|
+
// Load config (used by checks 1, 4, and 5). Missing file → defaults; invalid
|
|
2255
|
+
// JSON or schema-mismatch → throws (handled in Check 4).
|
|
2256
|
+
let cfg = null;
|
|
2257
|
+
let cfgLoadErr = null;
|
|
2258
|
+
try {
|
|
2259
|
+
const { loadConfig } = require("./context-meter-config.cjs");
|
|
2260
|
+
cfg = loadConfig(cwd);
|
|
2261
|
+
} catch (e) {
|
|
2262
|
+
cfgLoadErr = e;
|
|
2263
|
+
}
|
|
2264
|
+
const apiKeyEnvVar = (cfg && cfg.apiKeyEnvVar) || "ANTHROPIC_API_KEY";
|
|
2265
|
+
|
|
2266
|
+
// Check 1: API key env var present
|
|
2267
|
+
const apiKeyValue = process.env[apiKeyEnvVar];
|
|
2268
|
+
const apiKeyPresent = typeof apiKeyValue === "string" && apiKeyValue.length > 0;
|
|
2269
|
+
if (apiKeyPresent) {
|
|
2270
|
+
success(`API key present ($${apiKeyEnvVar})`);
|
|
2271
|
+
} else {
|
|
2272
|
+
error(`Missing API key: set $${apiKeyEnvVar} — https://console.anthropic.com/settings/keys`);
|
|
2273
|
+
issues++;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
// Check 2: Hook registered in ~/.claude/settings.json
|
|
2277
|
+
let hookRegistered = false;
|
|
2278
|
+
try {
|
|
2279
|
+
if (fs.existsSync(SETTINGS_JSON)) {
|
|
2280
|
+
const raw = fs.readFileSync(SETTINGS_JSON, "utf8");
|
|
2281
|
+
const settings = JSON.parse(raw);
|
|
2282
|
+
const postHooks = (settings && settings.hooks && settings.hooks.PostToolUse) || [];
|
|
2283
|
+
for (const entry of postHooks) {
|
|
2284
|
+
if (!entry || !Array.isArray(entry.hooks)) continue;
|
|
2285
|
+
for (const h of entry.hooks) {
|
|
2286
|
+
if (h && typeof h.command === "string" && h.command.includes(CONTEXT_METER_HOOK_MARKER)) {
|
|
2287
|
+
hookRegistered = true;
|
|
2288
|
+
break;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
if (hookRegistered) break;
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
} catch {
|
|
2295
|
+
// Fall through — treat as not registered
|
|
2296
|
+
}
|
|
2297
|
+
if (hookRegistered) {
|
|
2298
|
+
success("Context meter hook registered in settings.json");
|
|
2299
|
+
} else {
|
|
2300
|
+
error("Context meter hook not installed — run gsd-t install");
|
|
2301
|
+
issues++;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
// Check 3: Hook script file exists in project
|
|
2305
|
+
const scriptPath = path.join(cwd, "scripts", CONTEXT_METER_SCRIPT);
|
|
2306
|
+
if (fs.existsSync(scriptPath)) {
|
|
2307
|
+
success("Hook script present");
|
|
2308
|
+
} else {
|
|
2309
|
+
error(`Hook script missing at scripts/${CONTEXT_METER_SCRIPT} — run gsd-t update`);
|
|
2310
|
+
issues++;
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
// Check 4: Config file parses via loader
|
|
2314
|
+
const configPath = path.join(cwd, CONTEXT_METER_CONFIG_DEST);
|
|
2315
|
+
if (cfgLoadErr) {
|
|
2316
|
+
error(`Config file invalid: ${cfgLoadErr.message} — fix ${CONTEXT_METER_CONFIG_DEST}`);
|
|
2317
|
+
issues++;
|
|
2318
|
+
} else if (fs.existsSync(configPath)) {
|
|
2319
|
+
success(`Config valid (threshold ${cfg.thresholdPct}%, check every ${cfg.checkFrequency} calls)`);
|
|
2320
|
+
} else {
|
|
2321
|
+
warn("Using default config — run gsd-t install to copy template");
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
// Check 5: Dry-run count_tokens API call (skip if no API key)
|
|
2325
|
+
if (!apiKeyPresent) {
|
|
2326
|
+
log(` ${DIM}Skipped count_tokens dry-run (no API key)${RESET}`);
|
|
2327
|
+
} else {
|
|
2328
|
+
const clientPath = path.join(cwd, "scripts", "context-meter", "count-tokens-client.js");
|
|
2329
|
+
if (!fs.existsSync(clientPath)) {
|
|
2330
|
+
error("count_tokens client missing at scripts/context-meter/count-tokens-client.js — run gsd-t update");
|
|
2331
|
+
issues++;
|
|
2332
|
+
} else {
|
|
2333
|
+
try {
|
|
2334
|
+
const { countTokens } = require(clientPath);
|
|
2335
|
+
const result = await countTokens({
|
|
2336
|
+
apiKey: apiKeyValue,
|
|
2337
|
+
model: "claude-opus-4-6",
|
|
2338
|
+
system: "",
|
|
2339
|
+
messages: [{ role: "user", content: [{ type: "text", text: "ping" }] }],
|
|
2340
|
+
timeoutMs: 5000,
|
|
2341
|
+
});
|
|
2342
|
+
if (result && typeof result.inputTokens === "number") {
|
|
2343
|
+
success(`count_tokens dry-run OK (${result.inputTokens} tokens)`);
|
|
2344
|
+
} else {
|
|
2345
|
+
error("count_tokens API call failed — check API key and network");
|
|
2346
|
+
issues++;
|
|
2347
|
+
}
|
|
2348
|
+
} catch (e) {
|
|
2349
|
+
error(`count_tokens dry-run threw: ${e.message}`);
|
|
2350
|
+
issues++;
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
return issues;
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
async function doDoctor() {
|
|
1790
2359
|
heading("GSD-T Doctor");
|
|
1791
2360
|
log("");
|
|
1792
2361
|
let issues = 0;
|
|
@@ -1794,6 +2363,7 @@ function doDoctor() {
|
|
|
1794
2363
|
issues += checkDoctorInstallation();
|
|
1795
2364
|
issues += checkDoctorProject();
|
|
1796
2365
|
issues += checkDoctorCgc();
|
|
2366
|
+
issues += await checkDoctorContextMeter(process.cwd());
|
|
1797
2367
|
log("");
|
|
1798
2368
|
if (issues === 0) {
|
|
1799
2369
|
log(`${GREEN}${BOLD} All checks passed!${RESET}`);
|
|
@@ -1801,6 +2371,7 @@ function doDoctor() {
|
|
|
1801
2371
|
log(`${YELLOW}${BOLD} ${issues} issue${issues > 1 ? "s" : ""} found${RESET}`);
|
|
1802
2372
|
}
|
|
1803
2373
|
log("");
|
|
2374
|
+
if (issues > 0) process.exit(1);
|
|
1804
2375
|
}
|
|
1805
2376
|
|
|
1806
2377
|
function doRegister() {
|
|
@@ -2626,6 +3197,119 @@ function showHeadlessHelp() {
|
|
|
2626
3197
|
log(` ${DIM}$${RESET} gsd-t headless query domains\n`);
|
|
2627
3198
|
}
|
|
2628
3199
|
|
|
3200
|
+
// ─── Metrics (M35 token telemetry CLI) ────────────────────────────────────────
|
|
3201
|
+
|
|
3202
|
+
function parseMetricsByFlag(args) {
|
|
3203
|
+
const byArg = args.find(a => a.startsWith("--by="));
|
|
3204
|
+
if (!byArg) return [];
|
|
3205
|
+
const raw = byArg.slice(5).trim();
|
|
3206
|
+
if (!raw) return [];
|
|
3207
|
+
return raw.split(",").map(s => s.trim()).filter(Boolean);
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
function formatPct(v) {
|
|
3211
|
+
if (v === null || v === undefined || Number.isNaN(v)) return "—";
|
|
3212
|
+
return `${Number(v).toFixed(1)}%`;
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
function formatInt(v) {
|
|
3216
|
+
if (v === null || v === undefined || Number.isNaN(v)) return "—";
|
|
3217
|
+
return String(Math.round(Number(v)));
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
function doMetrics(args) {
|
|
3221
|
+
const projectDir = process.cwd();
|
|
3222
|
+
let tt;
|
|
3223
|
+
try {
|
|
3224
|
+
tt = require(path.join(projectDir, "bin", "token-telemetry.js"));
|
|
3225
|
+
} catch (e) {
|
|
3226
|
+
error(`bin/token-telemetry.js not found in ${projectDir} — run gsd-t install first.`);
|
|
3227
|
+
process.exit(1);
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
const records = tt.readAll(projectDir);
|
|
3231
|
+
if (records.length === 0) {
|
|
3232
|
+
log(`${DIM}No telemetry records yet — .gsd-t/token-metrics.jsonl is empty or missing.${RESET}`);
|
|
3233
|
+
return;
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
const isTokens = args.includes("--tokens");
|
|
3237
|
+
const isHalts = args.includes("--halts");
|
|
3238
|
+
const isContextWindow = args.includes("--context-window");
|
|
3239
|
+
|
|
3240
|
+
if (!isTokens && !isHalts && !isContextWindow) {
|
|
3241
|
+
log(`${YELLOW}Specify at least one of: --tokens, --halts, --context-window${RESET}`);
|
|
3242
|
+
return;
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
if (isHalts) {
|
|
3246
|
+
const halts = records.filter(r => r.halt_type);
|
|
3247
|
+
log(`\n${BOLD}Halts — ${halts.length} record(s)${RESET}`);
|
|
3248
|
+
if (halts.length === 0) {
|
|
3249
|
+
log(`${DIM} (no halts recorded — all spawns completed normally)${RESET}\n`);
|
|
3250
|
+
} else {
|
|
3251
|
+
const byType = {};
|
|
3252
|
+
for (const r of halts) {
|
|
3253
|
+
const key = String(r.halt_type);
|
|
3254
|
+
byType[key] = (byType[key] || 0) + 1;
|
|
3255
|
+
}
|
|
3256
|
+
for (const [type, count] of Object.entries(byType).sort()) {
|
|
3257
|
+
log(` ${CYAN}${type.padEnd(24)}${RESET} ${count}`);
|
|
3258
|
+
}
|
|
3259
|
+
log("");
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
if (isTokens) {
|
|
3264
|
+
const by = parseMetricsByFlag(args);
|
|
3265
|
+
if (by.length === 0) {
|
|
3266
|
+
const totalConsumed = records.reduce((a, r) => a + (Number(r.tokens_consumed) || 0), 0);
|
|
3267
|
+
const totalDuration = records.reduce((a, r) => a + (Number(r.duration_s) || 0), 0);
|
|
3268
|
+
log(`\n${BOLD}Tokens — ${records.length} spawn(s)${RESET}`);
|
|
3269
|
+
log(` total tokens consumed: ${formatInt(totalConsumed)}`);
|
|
3270
|
+
log(` total duration (s): ${formatInt(totalDuration)}`);
|
|
3271
|
+
if (records.length > 0) {
|
|
3272
|
+
log(` mean tokens/spawn: ${formatInt(totalConsumed / records.length)}`);
|
|
3273
|
+
}
|
|
3274
|
+
log("");
|
|
3275
|
+
} else {
|
|
3276
|
+
const groups = tt.aggregate(records, { by });
|
|
3277
|
+
log(`\n${BOLD}Tokens by ${by.join(",")} — ${groups.length} group(s)${RESET}`);
|
|
3278
|
+
const keyHeader = by.join("/");
|
|
3279
|
+
log(` ${keyHeader.padEnd(36)} ${"count".padStart(7)} ${"total".padStart(12)} ${"mean".padStart(10)} ${"median".padStart(10)} ${"p95".padStart(10)}`);
|
|
3280
|
+
log(` ${"-".repeat(36)} ${"-".repeat(7)} ${"-".repeat(12)} ${"-".repeat(10)} ${"-".repeat(10)} ${"-".repeat(10)}`);
|
|
3281
|
+
for (const g of groups) {
|
|
3282
|
+
const label = by.map(k => g.key[k] == null ? "—" : String(g.key[k])).join("/");
|
|
3283
|
+
log(` ${label.padEnd(36)} ${formatInt(g.count).padStart(7)} ${formatInt(g.total_tokens).padStart(12)} ${formatInt(g.mean).padStart(10)} ${formatInt(g.median).padStart(10)} ${formatInt(g.p95).padStart(10)}`);
|
|
3284
|
+
}
|
|
3285
|
+
log("");
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
if (isContextWindow) {
|
|
3290
|
+
log(`\n${BOLD}Context window trend — ${records.length} record(s)${RESET}`);
|
|
3291
|
+
const withPct = records.filter(r => r.context_window_pct_after != null && !Number.isNaN(Number(r.context_window_pct_after)));
|
|
3292
|
+
if (withPct.length === 0) {
|
|
3293
|
+
log(`${DIM} (no context-window measurements in records)${RESET}\n`);
|
|
3294
|
+
} else {
|
|
3295
|
+
const sorted = [...withPct].map(r => Number(r.context_window_pct_after)).sort((a, b) => a - b);
|
|
3296
|
+
const min = sorted[0];
|
|
3297
|
+
const max = sorted[sorted.length - 1];
|
|
3298
|
+
const mean = sorted.reduce((a, b) => a + b, 0) / sorted.length;
|
|
3299
|
+
const p95 = sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))];
|
|
3300
|
+
log(` min: ${formatPct(min)}`);
|
|
3301
|
+
log(` mean: ${formatPct(mean)}`);
|
|
3302
|
+
log(` p95: ${formatPct(p95)}`);
|
|
3303
|
+
log(` max: ${formatPct(max)}`);
|
|
3304
|
+
const atWarn = withPct.filter(r => Number(r.context_window_pct_after) >= 70).length;
|
|
3305
|
+
const atStop = withPct.filter(r => Number(r.context_window_pct_after) >= 85).length;
|
|
3306
|
+
log(` spawns at warn band (≥70%): ${atWarn}`);
|
|
3307
|
+
log(` spawns at stop band (≥85%): ${atStop}`);
|
|
3308
|
+
log("");
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
|
|
2629
3313
|
function showHelp() {
|
|
2630
3314
|
log(`\n${BOLD}GSD-T${RESET} — Contract-Driven Development for Claude Code\n`);
|
|
2631
3315
|
log(`${BOLD}Usage:${RESET} npx @tekyzinc/gsd-t ${CYAN}<command>${RESET} [options]\n`);
|
|
@@ -2734,6 +3418,13 @@ module.exports = {
|
|
|
2734
3418
|
syncGlobalRulesToProject,
|
|
2735
3419
|
syncGlobalRules,
|
|
2736
3420
|
exportUniversalRulesForNpm,
|
|
3421
|
+
// M34: Context Meter installer integration
|
|
3422
|
+
ensureGitignoreEntries,
|
|
3423
|
+
installContextMeter,
|
|
3424
|
+
configureContextMeterHooks,
|
|
3425
|
+
promptForApiKeyIfMissing,
|
|
3426
|
+
resolveApiKeyEnvVar,
|
|
3427
|
+
runTaskCounterRetirementMigration,
|
|
2737
3428
|
};
|
|
2738
3429
|
|
|
2739
3430
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
@@ -2744,16 +3435,16 @@ if (require.main === module) {
|
|
|
2744
3435
|
|
|
2745
3436
|
switch (command) {
|
|
2746
3437
|
case "install":
|
|
2747
|
-
doInstall();
|
|
3438
|
+
doInstall().catch((e) => { error(e.message || String(e)); process.exit(1); });
|
|
2748
3439
|
break;
|
|
2749
3440
|
case "update":
|
|
2750
|
-
doUpdate();
|
|
3441
|
+
doUpdate().catch((e) => { error(e.message || String(e)); process.exit(1); });
|
|
2751
3442
|
break;
|
|
2752
3443
|
case "update-all":
|
|
2753
|
-
doUpdateAll();
|
|
3444
|
+
doUpdateAll().catch((e) => { error(e.message || String(e)); process.exit(1); });
|
|
2754
3445
|
break;
|
|
2755
3446
|
case "init":
|
|
2756
|
-
doInit(args[1]);
|
|
3447
|
+
doInit(args[1]).catch((e) => { error(e.message || String(e)); process.exit(1); });
|
|
2757
3448
|
break;
|
|
2758
3449
|
case "register":
|
|
2759
3450
|
doRegister();
|
|
@@ -2765,7 +3456,7 @@ if (require.main === module) {
|
|
|
2765
3456
|
doUninstall();
|
|
2766
3457
|
break;
|
|
2767
3458
|
case "doctor":
|
|
2768
|
-
doDoctor();
|
|
3459
|
+
doDoctor().catch((e) => { error(e.message || String(e)); process.exit(1); });
|
|
2769
3460
|
break;
|
|
2770
3461
|
case "changelog":
|
|
2771
3462
|
doChangelog();
|
|
@@ -2776,6 +3467,9 @@ if (require.main === module) {
|
|
|
2776
3467
|
case "headless":
|
|
2777
3468
|
doHeadless(args.slice(1));
|
|
2778
3469
|
break;
|
|
3470
|
+
case "metrics":
|
|
3471
|
+
doMetrics(args.slice(1));
|
|
3472
|
+
break;
|
|
2779
3473
|
case "design-build": {
|
|
2780
3474
|
const orchestrator = require("./design-orchestrator.js");
|
|
2781
3475
|
orchestrator.run(args.slice(1)).catch(e => { console.error(e); process.exit(1); });
|