@tekyzinc/gsd-t 2.74.13 → 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 +116 -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 +709 -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)) {
|
|
@@ -1112,9 +1429,24 @@ function doInit(projectName) {
|
|
|
1112
1429
|
initGsdtDir(projectDir, projectName, today);
|
|
1113
1430
|
copyBinToolsToProject(projectDir, projectName);
|
|
1114
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
|
+
}
|
|
1443
|
+
|
|
1115
1444
|
if (registerProject(projectDir)) success("Registered in ~/.claude/.gsd-t-projects");
|
|
1116
1445
|
|
|
1117
1446
|
showInitTree(projectDir);
|
|
1447
|
+
|
|
1448
|
+
// Interactive prompt (skipped silently in non-TTY shells)
|
|
1449
|
+
await promptForApiKeyIfMissing(resolveApiKeyEnvVar(projectDir));
|
|
1118
1450
|
}
|
|
1119
1451
|
|
|
1120
1452
|
function showInitTree(projectDir) {
|
|
@@ -1149,10 +1481,83 @@ function doStatus() {
|
|
|
1149
1481
|
showStatusCommands();
|
|
1150
1482
|
showStatusConfig();
|
|
1151
1483
|
showStatusTeams();
|
|
1484
|
+
showStatusContextMeter();
|
|
1152
1485
|
showStatusProject();
|
|
1153
1486
|
log("");
|
|
1154
1487
|
}
|
|
1155
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
|
+
|
|
1156
1561
|
function showStatusVersion() {
|
|
1157
1562
|
const installedVersion = getInstalledVersion();
|
|
1158
1563
|
if (installedVersion) {
|
|
@@ -1486,8 +1891,8 @@ function exportUniversalRulesForNpm() {
|
|
|
1486
1891
|
}
|
|
1487
1892
|
}
|
|
1488
1893
|
|
|
1489
|
-
function doUpdateAll() {
|
|
1490
|
-
updateGlobalCommands();
|
|
1894
|
+
async function doUpdateAll() {
|
|
1895
|
+
await updateGlobalCommands();
|
|
1491
1896
|
heading("Updating registered projects...");
|
|
1492
1897
|
log("");
|
|
1493
1898
|
|
|
@@ -1511,9 +1916,9 @@ function doUpdateAll() {
|
|
|
1511
1916
|
showUpdateAllSummary(projects.length, counts, playwrightMissing, swaggerMissing, syncCount);
|
|
1512
1917
|
}
|
|
1513
1918
|
|
|
1514
|
-
function updateGlobalCommands() {
|
|
1919
|
+
async function updateGlobalCommands() {
|
|
1515
1920
|
if (getInstalledVersion() !== PKG_VERSION) {
|
|
1516
|
-
doInstall({ update: true });
|
|
1921
|
+
await doInstall({ update: true });
|
|
1517
1922
|
} else {
|
|
1518
1923
|
heading(`GSD-T ${versionLink()}`);
|
|
1519
1924
|
success("Global commands already up to date");
|
|
@@ -1549,7 +1954,8 @@ function updateSingleProject(projectDir, counts) {
|
|
|
1549
1954
|
const changelogCreated = createProjectChangelog(projectDir, projectName);
|
|
1550
1955
|
const binToolsCopied = copyBinToolsToProject(projectDir, projectName);
|
|
1551
1956
|
const archiveRan = runProgressArchiveMigration(projectDir, projectName);
|
|
1552
|
-
|
|
1957
|
+
const taskCounterRetired = runTaskCounterRetirementMigration(projectDir, projectName);
|
|
1958
|
+
if (guardAdded || changelogCreated || binToolsCopied || archiveRan || taskCounterRetired) {
|
|
1553
1959
|
counts.updated++;
|
|
1554
1960
|
} else {
|
|
1555
1961
|
info(`${projectName} — already up to date`);
|
|
@@ -1560,7 +1966,7 @@ function updateSingleProject(projectDir, counts) {
|
|
|
1560
1966
|
// Bin tools that should ship with every registered project. Listed here so adding
|
|
1561
1967
|
// a new tool only requires appending to this array. Use .cjs extension so they
|
|
1562
1968
|
// always run as CommonJS regardless of the project's package.json "type" field.
|
|
1563
|
-
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"];
|
|
1564
1970
|
|
|
1565
1971
|
function copyBinToolsToProject(projectDir, projectName) {
|
|
1566
1972
|
const projectBinDir = path.join(projectDir, "bin");
|
|
@@ -1634,6 +2040,55 @@ function runProgressArchiveMigration(projectDir, projectName) {
|
|
|
1634
2040
|
}
|
|
1635
2041
|
}
|
|
1636
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
|
+
|
|
1637
2092
|
function showUpdateAllSummary(total, counts, playwrightMissing, swaggerMissing, syncCount) {
|
|
1638
2093
|
log("");
|
|
1639
2094
|
heading("Update All Complete");
|
|
@@ -1787,7 +2242,120 @@ function checkDoctorCgc() {
|
|
|
1787
2242
|
return issues;
|
|
1788
2243
|
}
|
|
1789
2244
|
|
|
1790
|
-
|
|
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() {
|
|
1791
2359
|
heading("GSD-T Doctor");
|
|
1792
2360
|
log("");
|
|
1793
2361
|
let issues = 0;
|
|
@@ -1795,6 +2363,7 @@ function doDoctor() {
|
|
|
1795
2363
|
issues += checkDoctorInstallation();
|
|
1796
2364
|
issues += checkDoctorProject();
|
|
1797
2365
|
issues += checkDoctorCgc();
|
|
2366
|
+
issues += await checkDoctorContextMeter(process.cwd());
|
|
1798
2367
|
log("");
|
|
1799
2368
|
if (issues === 0) {
|
|
1800
2369
|
log(`${GREEN}${BOLD} All checks passed!${RESET}`);
|
|
@@ -1802,6 +2371,7 @@ function doDoctor() {
|
|
|
1802
2371
|
log(`${YELLOW}${BOLD} ${issues} issue${issues > 1 ? "s" : ""} found${RESET}`);
|
|
1803
2372
|
}
|
|
1804
2373
|
log("");
|
|
2374
|
+
if (issues > 0) process.exit(1);
|
|
1805
2375
|
}
|
|
1806
2376
|
|
|
1807
2377
|
function doRegister() {
|
|
@@ -2627,6 +3197,119 @@ function showHeadlessHelp() {
|
|
|
2627
3197
|
log(` ${DIM}$${RESET} gsd-t headless query domains\n`);
|
|
2628
3198
|
}
|
|
2629
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
|
+
|
|
2630
3313
|
function showHelp() {
|
|
2631
3314
|
log(`\n${BOLD}GSD-T${RESET} — Contract-Driven Development for Claude Code\n`);
|
|
2632
3315
|
log(`${BOLD}Usage:${RESET} npx @tekyzinc/gsd-t ${CYAN}<command>${RESET} [options]\n`);
|
|
@@ -2735,6 +3418,13 @@ module.exports = {
|
|
|
2735
3418
|
syncGlobalRulesToProject,
|
|
2736
3419
|
syncGlobalRules,
|
|
2737
3420
|
exportUniversalRulesForNpm,
|
|
3421
|
+
// M34: Context Meter installer integration
|
|
3422
|
+
ensureGitignoreEntries,
|
|
3423
|
+
installContextMeter,
|
|
3424
|
+
configureContextMeterHooks,
|
|
3425
|
+
promptForApiKeyIfMissing,
|
|
3426
|
+
resolveApiKeyEnvVar,
|
|
3427
|
+
runTaskCounterRetirementMigration,
|
|
2738
3428
|
};
|
|
2739
3429
|
|
|
2740
3430
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
@@ -2745,16 +3435,16 @@ if (require.main === module) {
|
|
|
2745
3435
|
|
|
2746
3436
|
switch (command) {
|
|
2747
3437
|
case "install":
|
|
2748
|
-
doInstall();
|
|
3438
|
+
doInstall().catch((e) => { error(e.message || String(e)); process.exit(1); });
|
|
2749
3439
|
break;
|
|
2750
3440
|
case "update":
|
|
2751
|
-
doUpdate();
|
|
3441
|
+
doUpdate().catch((e) => { error(e.message || String(e)); process.exit(1); });
|
|
2752
3442
|
break;
|
|
2753
3443
|
case "update-all":
|
|
2754
|
-
doUpdateAll();
|
|
3444
|
+
doUpdateAll().catch((e) => { error(e.message || String(e)); process.exit(1); });
|
|
2755
3445
|
break;
|
|
2756
3446
|
case "init":
|
|
2757
|
-
doInit(args[1]);
|
|
3447
|
+
doInit(args[1]).catch((e) => { error(e.message || String(e)); process.exit(1); });
|
|
2758
3448
|
break;
|
|
2759
3449
|
case "register":
|
|
2760
3450
|
doRegister();
|
|
@@ -2766,7 +3456,7 @@ if (require.main === module) {
|
|
|
2766
3456
|
doUninstall();
|
|
2767
3457
|
break;
|
|
2768
3458
|
case "doctor":
|
|
2769
|
-
doDoctor();
|
|
3459
|
+
doDoctor().catch((e) => { error(e.message || String(e)); process.exit(1); });
|
|
2770
3460
|
break;
|
|
2771
3461
|
case "changelog":
|
|
2772
3462
|
doChangelog();
|
|
@@ -2777,6 +3467,9 @@ if (require.main === module) {
|
|
|
2777
3467
|
case "headless":
|
|
2778
3468
|
doHeadless(args.slice(1));
|
|
2779
3469
|
break;
|
|
3470
|
+
case "metrics":
|
|
3471
|
+
doMetrics(args.slice(1));
|
|
3472
|
+
break;
|
|
2780
3473
|
case "design-build": {
|
|
2781
3474
|
const orchestrator = require("./design-orchestrator.js");
|
|
2782
3475
|
orchestrator.run(args.slice(1)).catch(e => { console.error(e); process.exit(1); });
|