@tekyzinc/gsd-t 2.39.13 → 2.45.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +17 -9
  2. package/bin/desktop.ini +2 -0
  3. package/bin/global-sync-manager.js +350 -0
  4. package/bin/gsd-t.js +592 -2
  5. package/bin/metrics-collector.js +167 -0
  6. package/bin/metrics-rollup.js +200 -0
  7. package/bin/patch-lifecycle.js +195 -0
  8. package/bin/rule-engine.js +160 -0
  9. package/commands/desktop.ini +2 -0
  10. package/commands/gsd-t-complete-milestone.md +192 -5
  11. package/commands/gsd-t-debug.md +16 -2
  12. package/commands/gsd-t-execute.md +257 -52
  13. package/commands/gsd-t-help.md +25 -10
  14. package/commands/gsd-t-integrate.md +35 -7
  15. package/commands/gsd-t-metrics.md +143 -0
  16. package/commands/gsd-t-plan.md +49 -2
  17. package/commands/gsd-t-quick.md +15 -3
  18. package/commands/gsd-t-status.md +78 -0
  19. package/commands/gsd-t-test-sync.md +2 -2
  20. package/commands/gsd-t-verify.md +140 -9
  21. package/commands/gsd-t-visualize.md +11 -1
  22. package/commands/gsd-t-wave.md +34 -19
  23. package/docs/GSD-T-README.md +9 -6
  24. package/docs/architecture.md +84 -2
  25. package/docs/ci-examples/desktop.ini +2 -0
  26. package/docs/ci-examples/github-actions.yml +104 -0
  27. package/docs/ci-examples/gitlab-ci.yml +116 -0
  28. package/docs/desktop.ini +2 -0
  29. package/docs/infrastructure.md +87 -1
  30. package/docs/prd-graph-engine.md +2 -2
  31. package/docs/prd-gsd2-hybrid.md +258 -135
  32. package/docs/requirements.md +63 -2
  33. package/examples/.gsd-t/contracts/desktop.ini +2 -0
  34. package/examples/.gsd-t/desktop.ini +2 -0
  35. package/examples/.gsd-t/domains/desktop.ini +2 -0
  36. package/examples/.gsd-t/domains/example-domain/desktop.ini +2 -0
  37. package/examples/desktop.ini +2 -0
  38. package/examples/rules/.gitkeep +0 -0
  39. package/package.json +40 -40
  40. package/scripts/desktop.ini +2 -0
  41. package/scripts/gsd-t-dashboard-server.js +19 -2
  42. package/scripts/gsd-t-dashboard.html +63 -0
  43. package/scripts/gsd-t-event-writer.js +1 -0
  44. package/templates/CLAUDE-global.md +30 -9
  45. package/templates/desktop.ini +2 -0
package/bin/gsd-t.js CHANGED
@@ -949,6 +949,56 @@ function initGsdtDir(projectDir, projectName, today) {
949
949
  writeTemplateFile("progress.md", path.join(gsdtDir, "progress.md"), ".gsd-t/progress.md", projectName, today);
950
950
  writeTemplateFile("backlog.md", path.join(gsdtDir, "backlog.md"), ".gsd-t/backlog.md", projectName, today);
951
951
  writeTemplateFile("backlog-settings.md", path.join(gsdtDir, "backlog-settings.md"), ".gsd-t/backlog-settings.md", projectName, today);
952
+
953
+ // Seed universal rules from npm package (if shipped)
954
+ seedUniversalRules(projectDir);
955
+ }
956
+
957
+ function seedUniversalRules(projectDir) {
958
+ try {
959
+ const shippedRules = path.join(PKG_ROOT, "examples", "rules", "universal-rules.jsonl");
960
+ if (!fs.existsSync(shippedRules)) return;
961
+ const content = fs.readFileSync(shippedRules, "utf8").trim();
962
+ if (!content) return;
963
+ const rules = content.split("\n").map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
964
+ if (rules.length === 0) return;
965
+ const localRulesFile = path.join(projectDir, ".gsd-t", "metrics", "rules.jsonl");
966
+ const localDir = path.dirname(localRulesFile);
967
+ if (!fs.existsSync(localDir)) fs.mkdirSync(localDir, { recursive: true });
968
+ // Read existing local rules to avoid duplicates
969
+ let existingTriggers = new Set();
970
+ if (fs.existsSync(localRulesFile)) {
971
+ const existing = fs.readFileSync(localRulesFile, "utf8").trim();
972
+ if (existing) {
973
+ existing.split("\n").forEach((l) => {
974
+ try { const r = JSON.parse(l); existingTriggers.add(JSON.stringify(r.trigger || {})); } catch {}
975
+ });
976
+ }
977
+ }
978
+ let seeded = 0;
979
+ for (const rule of rules) {
980
+ const trigger = (rule.original_rule && rule.original_rule.trigger) || {};
981
+ const fp = JSON.stringify(trigger);
982
+ if (existingTriggers.has(fp)) continue;
983
+ const candidate = {
984
+ id: `universal-${rule.global_id || "unknown"}`,
985
+ created_at: new Date().toISOString(),
986
+ name: (rule.original_rule && rule.original_rule.name) || rule.global_id || "universal",
987
+ description: (rule.original_rule && rule.original_rule.description) || "Shipped as universal rule",
988
+ trigger,
989
+ severity: (rule.original_rule && rule.original_rule.severity) || "MEDIUM",
990
+ action: (rule.original_rule && rule.original_rule.action) || "warn",
991
+ patch_template_id: null,
992
+ activation_count: 0, last_activated: null,
993
+ milestone_created: "universal", status: "active",
994
+ source_global_id: rule.global_id || null,
995
+ };
996
+ fs.appendFileSync(localRulesFile, JSON.stringify(candidate) + "\n");
997
+ existingTriggers.add(fp);
998
+ seeded++;
999
+ }
1000
+ if (seeded > 0) success(`Seeded ${seeded} universal rules from npm package`);
1001
+ } catch { /* silently skip if anything fails */ }
952
1002
  }
953
1003
 
954
1004
  function writeTemplateFile(templateName, destPath, label, projectName, today) {
@@ -1223,6 +1273,140 @@ function checkProjectHealth(projects) {
1223
1273
  return { playwrightMissing, swaggerMissing };
1224
1274
  }
1225
1275
 
1276
+ // ── Global Rule Sync (M27) ──────────────────────────────────────────────────
1277
+
1278
+ function syncGlobalRulesToProject(projectDir) {
1279
+ try {
1280
+ const gsm = require("./global-sync-manager.js");
1281
+ const globalRules = gsm.readGlobalRules();
1282
+ if (globalRules.length === 0) return 0;
1283
+
1284
+ // Filter: universal OR promotion_count >= 2
1285
+ const qualifying = globalRules.filter((r) =>
1286
+ r.is_universal === true || (r.promotion_count || 0) >= 2);
1287
+ if (qualifying.length === 0) return 0;
1288
+
1289
+ // Load local rules to check for duplicates
1290
+ let localRules = [];
1291
+ try {
1292
+ const re = require("./rule-engine.js");
1293
+ localRules = re.getActiveRules(projectDir);
1294
+ } catch { /* rule-engine may not exist in target project */ }
1295
+
1296
+ const rulesFile = path.join(projectDir, ".gsd-t", "metrics", "rules.jsonl");
1297
+ let injected = 0;
1298
+
1299
+ for (const globalRule of qualifying) {
1300
+ const triggerFp = JSON.stringify(
1301
+ globalRule.original_rule && globalRule.original_rule.trigger
1302
+ ? globalRule.original_rule.trigger : {});
1303
+ const alreadyExists = localRules.some((lr) =>
1304
+ JSON.stringify(lr.trigger || {}) === triggerFp);
1305
+ if (alreadyExists) continue;
1306
+
1307
+ // Inject as candidate rule
1308
+ const candidate = {
1309
+ id: `global-${globalRule.global_id}`,
1310
+ created_at: new Date().toISOString(),
1311
+ name: (globalRule.original_rule && globalRule.original_rule.name) || globalRule.global_id,
1312
+ description: (globalRule.original_rule && globalRule.original_rule.description) || "Synced from global rules",
1313
+ trigger: (globalRule.original_rule && globalRule.original_rule.trigger) || {},
1314
+ severity: (globalRule.original_rule && globalRule.original_rule.severity) || "MEDIUM",
1315
+ action: (globalRule.original_rule && globalRule.original_rule.action) || "warn",
1316
+ patch_template_id: null,
1317
+ activation_count: 0,
1318
+ last_activated: null,
1319
+ milestone_created: "global",
1320
+ status: "active",
1321
+ source_global_id: globalRule.global_id,
1322
+ };
1323
+
1324
+ // Append to local rules.jsonl
1325
+ const dir = path.dirname(rulesFile);
1326
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1327
+ fs.appendFileSync(rulesFile, JSON.stringify(candidate) + "\n");
1328
+ localRules.push(candidate); // track to avoid re-injecting within same sync
1329
+ injected++;
1330
+ }
1331
+ return injected;
1332
+ } catch {
1333
+ return 0;
1334
+ }
1335
+ }
1336
+
1337
+ function syncGlobalRules(projects) {
1338
+ let totalSynced = 0;
1339
+ try {
1340
+ const gsm = require("./global-sync-manager.js");
1341
+ const globalRules = gsm.readGlobalRules();
1342
+ if (globalRules.length === 0) {
1343
+ log(`${DIM} ℹ No global rules to sync${RESET}`);
1344
+ return 0;
1345
+ }
1346
+
1347
+ heading("Global Rule Sync");
1348
+ for (const projectDir of projects) {
1349
+ if (!fs.existsSync(projectDir)) continue;
1350
+ const count = syncGlobalRulesToProject(projectDir);
1351
+ if (count > 0) {
1352
+ success(`Synced ${count} global rules to ${path.basename(projectDir)}`);
1353
+ totalSynced += count;
1354
+ }
1355
+ }
1356
+ if (totalSynced === 0) {
1357
+ info("All projects already have qualifying global rules");
1358
+ }
1359
+ } catch {
1360
+ // global-sync-manager may not exist yet
1361
+ }
1362
+ return totalSynced;
1363
+ }
1364
+
1365
+ function exportUniversalRulesForNpm() {
1366
+ try {
1367
+ const gsm = require("./global-sync-manager.js");
1368
+ const globalRules = gsm.readGlobalRules();
1369
+ const npmCandidates = globalRules.filter((r) => r.is_npm_candidate === true);
1370
+ if (npmCandidates.length === 0) return 0;
1371
+
1372
+ const rulesDir = path.join(PKG_ROOT, "examples", "rules");
1373
+ if (!fs.existsSync(rulesDir)) fs.mkdirSync(rulesDir, { recursive: true });
1374
+
1375
+ const version = PKG_VERSION;
1376
+ const exported = npmCandidates.map((r) => ({
1377
+ ...r,
1378
+ shipped_in_version: r.shipped_in_version || version,
1379
+ }));
1380
+
1381
+ const filePath = path.join(rulesDir, "universal-rules.jsonl");
1382
+ fs.writeFileSync(filePath, exported.map((r) => JSON.stringify(r)).join("\n") + "\n");
1383
+
1384
+ // Update shipped_in_version on global rules
1385
+ for (const r of npmCandidates) {
1386
+ if (!r.shipped_in_version) {
1387
+ r.shipped_in_version = version;
1388
+ }
1389
+ }
1390
+ // Write updated rules back to global
1391
+ const allRules = gsm.readGlobalRules();
1392
+ for (const r of allRules) {
1393
+ const match = npmCandidates.find((c) => c.global_id === r.global_id);
1394
+ if (match) r.shipped_in_version = match.shipped_in_version;
1395
+ }
1396
+ // Re-read and write to ensure consistency
1397
+ const rulesFile = path.join(os.homedir(), ".claude", "metrics", "global-rules.jsonl");
1398
+ if (fs.existsSync(rulesFile)) {
1399
+ const tmp = rulesFile + ".tmp." + process.pid;
1400
+ fs.writeFileSync(tmp, allRules.map((r) => JSON.stringify(r)).join("\n") + "\n");
1401
+ fs.renameSync(tmp, rulesFile);
1402
+ }
1403
+
1404
+ return exported.length;
1405
+ } catch {
1406
+ return 0;
1407
+ }
1408
+ }
1409
+
1226
1410
  function doUpdateAll() {
1227
1411
  updateGlobalCommands();
1228
1412
  heading("Updating registered projects...");
@@ -1241,8 +1425,11 @@ function doUpdateAll() {
1241
1425
  }
1242
1426
  }
1243
1427
 
1428
+ // Global rule sync — propagate proven rules across projects
1429
+ const syncCount = syncGlobalRules(projects);
1430
+
1244
1431
  const { playwrightMissing, swaggerMissing } = checkProjectHealth(projects);
1245
- showUpdateAllSummary(projects.length, counts, playwrightMissing, swaggerMissing);
1432
+ showUpdateAllSummary(projects.length, counts, playwrightMissing, swaggerMissing, syncCount);
1246
1433
  }
1247
1434
 
1248
1435
  function updateGlobalCommands() {
@@ -1289,7 +1476,7 @@ function updateSingleProject(projectDir, counts) {
1289
1476
  }
1290
1477
  }
1291
1478
 
1292
- function showUpdateAllSummary(total, counts, playwrightMissing, swaggerMissing) {
1479
+ function showUpdateAllSummary(total, counts, playwrightMissing, swaggerMissing, syncCount) {
1293
1480
  log("");
1294
1481
  heading("Update All Complete");
1295
1482
  log(` Projects registered: ${total}`);
@@ -1299,6 +1486,7 @@ function showUpdateAllSummary(total, counts, playwrightMissing, swaggerMissing)
1299
1486
  if (counts.errors > 0) log(` Errors: ${counts.errors}`);
1300
1487
  if (playwrightMissing.length > 0) log(` Missing Playwright: ${playwrightMissing.length}`);
1301
1488
  if (swaggerMissing.length > 0) log(` Missing Swagger: ${swaggerMissing.length}`);
1489
+ if (syncCount > 0) log(` Global rules synced: ${syncCount}`);
1302
1490
  log("");
1303
1491
  }
1304
1492
 
@@ -1650,6 +1838,384 @@ function doGraph(args) {
1650
1838
  }
1651
1839
  }
1652
1840
 
1841
+ // ─── Headless Mode ────────────────────────────────────────────────────────────
1842
+
1843
+ /**
1844
+ * Parse headless flags from args array.
1845
+ * Extracts --json, --timeout=N, --log from args, returns remainder as positional args.
1846
+ */
1847
+ function parseHeadlessFlags(args) {
1848
+ const flags = { json: false, timeout: 300, log: false };
1849
+ const positional = [];
1850
+ for (const arg of args) {
1851
+ if (arg === "--json") {
1852
+ flags.json = true;
1853
+ } else if (arg.startsWith("--timeout=")) {
1854
+ const n = parseInt(arg.slice("--timeout=".length), 10);
1855
+ if (!isNaN(n) && n > 0) flags.timeout = n;
1856
+ } else if (arg === "--log") {
1857
+ flags.log = true;
1858
+ } else {
1859
+ positional.push(arg);
1860
+ }
1861
+ }
1862
+ return { flags, positional };
1863
+ }
1864
+
1865
+ /**
1866
+ * Build the claude -p invocation string for a GSD-T command.
1867
+ */
1868
+ function buildHeadlessCmd(command, cmdArgs) {
1869
+ const argStr = cmdArgs.length > 0 ? " " + cmdArgs.join(" ") : "";
1870
+ return `/user:gsd-t-${command}${argStr}`;
1871
+ }
1872
+
1873
+ /**
1874
+ * Map claude output + process exit code to a GSD-T headless exit code.
1875
+ * Exit codes: 0=success, 1=verify-fail, 2=context-budget-exceeded, 3=error, 4=blocked-needs-human
1876
+ */
1877
+ function mapHeadlessExitCode(processExitCode, output) {
1878
+ if (processExitCode !== 0 && processExitCode !== null) return 3;
1879
+ const lower = (output || "").toLowerCase();
1880
+ if (lower.includes("context budget exceeded") || lower.includes("context window exceeded") ||
1881
+ lower.includes("budget exceeded") || lower.includes("token limit")) return 2;
1882
+ if (lower.includes("blocked") && (lower.includes("needs human") || lower.includes("need human") ||
1883
+ lower.includes("human input") || lower.includes("human approval"))) return 4;
1884
+ if (lower.includes("verification failed") || lower.includes("verify failed") ||
1885
+ lower.includes("quality gate failed") || lower.includes("tests failed")) return 1;
1886
+ return 0;
1887
+ }
1888
+
1889
+ /**
1890
+ * Generate a headless log file path.
1891
+ */
1892
+ function headlessLogPath(projectDir, timestamp) {
1893
+ const ts = timestamp || Date.now();
1894
+ return path.join(projectDir, ".gsd-t", `headless-${ts}.log`);
1895
+ }
1896
+
1897
+ /**
1898
+ * Execute a GSD-T command via claude -p (non-interactive).
1899
+ */
1900
+ function doHeadlessExec(command, cmdArgs, flags) {
1901
+ const opts = flags || {};
1902
+ const jsonMode = opts.json || false;
1903
+ const timeoutMs = (opts.timeout || 300) * 1000;
1904
+ const logMode = opts.log || false;
1905
+ const startTime = Date.now();
1906
+ const timestamp = new Date(startTime).toISOString();
1907
+
1908
+ // Verify claude CLI is available
1909
+ try {
1910
+ execFileSync("claude", ["--version"], {
1911
+ encoding: "utf8", timeout: 5000,
1912
+ stdio: ["pipe", "pipe", "pipe"]
1913
+ });
1914
+ } catch {
1915
+ const msg = "claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code";
1916
+ if (jsonMode) {
1917
+ process.stdout.write(JSON.stringify({
1918
+ success: false, exitCode: 3, gsdtExitCode: 3,
1919
+ command, args: cmdArgs, output: msg,
1920
+ timestamp, duration: Date.now() - startTime, logFile: null
1921
+ }) + "\n");
1922
+ } else {
1923
+ error(msg);
1924
+ }
1925
+ process.exit(3);
1926
+ }
1927
+
1928
+ const prompt = buildHeadlessCmd(command, cmdArgs);
1929
+ let logFile = null;
1930
+
1931
+ if (!jsonMode) {
1932
+ heading(`GSD-T Headless — ${command}`);
1933
+ info(`Prompt: ${prompt}`);
1934
+ info(`Timeout: ${opts.timeout || 300}s`);
1935
+ if (logMode) {
1936
+ logFile = headlessLogPath(process.cwd(), startTime);
1937
+ info(`Log: ${logFile}`);
1938
+ }
1939
+ log("");
1940
+ } else if (logMode) {
1941
+ logFile = headlessLogPath(process.cwd(), startTime);
1942
+ }
1943
+
1944
+ let output = "";
1945
+ let processExitCode = 0;
1946
+
1947
+ try {
1948
+ const result = execFileSync("claude", ["-p", prompt], {
1949
+ encoding: "utf8",
1950
+ timeout: timeoutMs,
1951
+ stdio: ["pipe", "pipe", "pipe"],
1952
+ cwd: process.cwd()
1953
+ });
1954
+ output = result;
1955
+ } catch (e) {
1956
+ // execFileSync throws on non-zero exit or timeout
1957
+ output = (e.stdout || "") + (e.stderr || "");
1958
+ processExitCode = e.status || 1;
1959
+ if (e.signal === "SIGTERM" || e.code === "ETIMEDOUT") {
1960
+ processExitCode = 3;
1961
+ output += "\n[headless: process timed out]";
1962
+ }
1963
+ }
1964
+
1965
+ const gsdtExitCode = mapHeadlessExitCode(processExitCode, output);
1966
+ const duration = Date.now() - startTime;
1967
+
1968
+ // Write log file if requested
1969
+ if (logMode && logFile) {
1970
+ try {
1971
+ const gsdtDir = path.join(process.cwd(), ".gsd-t");
1972
+ ensureDir(gsdtDir);
1973
+ const logContent = [
1974
+ `GSD-T Headless Log`,
1975
+ `Command: ${command}`,
1976
+ `Args: ${cmdArgs.join(" ")}`,
1977
+ `Timestamp: ${timestamp}`,
1978
+ `Duration: ${duration}ms`,
1979
+ `Exit Code: ${gsdtExitCode}`,
1980
+ `---`,
1981
+ output
1982
+ ].join("\n");
1983
+ fs.writeFileSync(logFile, logContent);
1984
+ } catch (e) {
1985
+ if (!jsonMode) warn(`Failed to write log: ${e.message}`);
1986
+ }
1987
+ }
1988
+
1989
+ if (jsonMode) {
1990
+ process.stdout.write(JSON.stringify({
1991
+ success: gsdtExitCode === 0,
1992
+ exitCode: processExitCode,
1993
+ gsdtExitCode,
1994
+ command,
1995
+ args: cmdArgs,
1996
+ output,
1997
+ timestamp,
1998
+ duration,
1999
+ logFile
2000
+ }) + "\n");
2001
+ } else {
2002
+ process.stdout.write(output);
2003
+ if (!output.endsWith("\n")) process.stdout.write("\n");
2004
+ if (gsdtExitCode !== 0) {
2005
+ log("");
2006
+ warn(`Exit code: ${gsdtExitCode}`);
2007
+ }
2008
+ }
2009
+
2010
+ process.exit(gsdtExitCode);
2011
+ }
2012
+
2013
+ // ─── Headless Query ──────────────────────────────────────────────────────────
2014
+
2015
+ const VALID_QUERY_TYPES = ["status", "domains", "contracts", "debt", "context", "backlog", "graph"];
2016
+
2017
+ function queryResult(type, data) {
2018
+ return { type, timestamp: new Date().toISOString(), data };
2019
+ }
2020
+
2021
+ function queryStatus(projectDir) {
2022
+ const progressPath = path.join(projectDir, ".gsd-t", "progress.md");
2023
+ if (!fs.existsSync(progressPath)) {
2024
+ return queryResult("status", { error: "progress.md not found" });
2025
+ }
2026
+ const content = fs.readFileSync(progressPath, "utf8");
2027
+ const versionMatch = content.match(/##\s*Version:\s*(.+)/);
2028
+ const projectMatch = content.match(/##\s*Project:\s*(.+)/);
2029
+ const statusMatch = content.match(/##\s*Status:\s*(.+)/);
2030
+ const milestoneMatch = content.match(/##\s*Active Milestone\s*[\r\n]+\s*(.+)/);
2031
+ const phaseMatch = content.match(/Phase:\s*(\w+)/);
2032
+ return queryResult("status", {
2033
+ version: versionMatch ? versionMatch[1].trim() : null,
2034
+ project: projectMatch ? projectMatch[1].trim() : null,
2035
+ status: statusMatch ? statusMatch[1].trim() : null,
2036
+ activeMilestone: milestoneMatch ? milestoneMatch[1].trim() : null,
2037
+ phase: phaseMatch ? phaseMatch[1].trim() : null
2038
+ });
2039
+ }
2040
+
2041
+ function queryDomains(projectDir) {
2042
+ const domainsDir = path.join(projectDir, ".gsd-t", "domains");
2043
+ if (!fs.existsSync(domainsDir)) {
2044
+ return queryResult("domains", { domains: [] });
2045
+ }
2046
+ const entries = fs.readdirSync(domainsDir).filter((f) => {
2047
+ const fp = path.join(domainsDir, f);
2048
+ return fs.statSync(fp).isDirectory();
2049
+ });
2050
+ const domains = entries.map((name) => {
2051
+ const domainDir = path.join(domainsDir, name);
2052
+ return {
2053
+ name,
2054
+ hasScope: fs.existsSync(path.join(domainDir, "scope.md")),
2055
+ hasTasks: fs.existsSync(path.join(domainDir, "tasks.md")),
2056
+ hasConstraints: fs.existsSync(path.join(domainDir, "constraints.md"))
2057
+ };
2058
+ });
2059
+ return queryResult("domains", { domains });
2060
+ }
2061
+
2062
+ function queryContracts(projectDir) {
2063
+ const contractsDir = path.join(projectDir, ".gsd-t", "contracts");
2064
+ if (!fs.existsSync(contractsDir)) {
2065
+ return queryResult("contracts", { contracts: [] });
2066
+ }
2067
+ const contracts = fs.readdirSync(contractsDir)
2068
+ .filter((f) => f.endsWith(".md") && f !== ".gitkeep");
2069
+ return queryResult("contracts", { contracts });
2070
+ }
2071
+
2072
+ function queryDebt(projectDir) {
2073
+ const debtPath = path.join(projectDir, ".gsd-t", "techdebt.md");
2074
+ if (!fs.existsSync(debtPath)) {
2075
+ return queryResult("debt", { items: [], count: 0 });
2076
+ }
2077
+ const content = fs.readFileSync(debtPath, "utf8");
2078
+ // Parse table rows: | ID | Severity | Description | ...
2079
+ const rows = content.split("\n").filter((line) => {
2080
+ return line.startsWith("| ") && !line.startsWith("| ID") && !line.startsWith("| ---") && !line.startsWith("| #");
2081
+ });
2082
+ const items = rows.map((row) => {
2083
+ const cells = row.split("|").slice(1, -1).map((c) => c.trim());
2084
+ return cells.length >= 2 ? { id: cells[0], severity: cells[1], description: cells[2] || "" } : null;
2085
+ }).filter(Boolean);
2086
+ return queryResult("debt", { items, count: items.length });
2087
+ }
2088
+
2089
+ function queryContext(projectDir) {
2090
+ const tokenLogPath = path.join(projectDir, ".gsd-t", "token-log.md");
2091
+ if (!fs.existsSync(tokenLogPath)) {
2092
+ return queryResult("context", { entries: [], totalTokens: 0, entryCount: 0 });
2093
+ }
2094
+ const content = fs.readFileSync(tokenLogPath, "utf8");
2095
+ const rows = content.split("\n").filter((line) => {
2096
+ return line.startsWith("| ") && !line.startsWith("| Datetime") && !line.startsWith("| ---");
2097
+ });
2098
+ const entries = rows.map((row) => {
2099
+ const cells = row.split("|").slice(1, -1).map((c) => c.trim());
2100
+ if (cells.length < 8) return null;
2101
+ return {
2102
+ datetimeStart: cells[0],
2103
+ datetimeEnd: cells[1],
2104
+ command: cells[2],
2105
+ step: cells[3],
2106
+ model: cells[4],
2107
+ duration: cells[5],
2108
+ notes: cells[6],
2109
+ tokens: parseInt(cells[7]) || 0
2110
+ };
2111
+ }).filter(Boolean);
2112
+ const totalTokens = entries.reduce((sum, e) => sum + (e.tokens || 0), 0);
2113
+ return queryResult("context", { entries, totalTokens, entryCount: entries.length });
2114
+ }
2115
+
2116
+ function queryBacklog(projectDir) {
2117
+ const backlogPath = path.join(projectDir, ".gsd-t", "backlog.md");
2118
+ if (!fs.existsSync(backlogPath)) {
2119
+ return queryResult("backlog", { items: [], count: 0 });
2120
+ }
2121
+ const content = fs.readFileSync(backlogPath, "utf8");
2122
+ const rows = content.split("\n").filter((line) => {
2123
+ return line.startsWith("| ") && !line.startsWith("| #") && !line.startsWith("| ID") && !line.startsWith("| ---");
2124
+ });
2125
+ const items = rows.map((row) => {
2126
+ const cells = row.split("|").slice(1, -1).map((c) => c.trim());
2127
+ return cells.length >= 2 ? { id: cells[0], title: cells[1], status: cells[2] || "" } : null;
2128
+ }).filter(Boolean);
2129
+ return queryResult("backlog", { items, count: items.length });
2130
+ }
2131
+
2132
+ function queryGraph(projectDir) {
2133
+ const metaPath = path.join(projectDir, ".gsd-t", "graph-index", "meta.json");
2134
+ if (!fs.existsSync(metaPath)) {
2135
+ return queryResult("graph", { available: false });
2136
+ }
2137
+ try {
2138
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
2139
+ return queryResult("graph", {
2140
+ available: true,
2141
+ provider: meta.provider || "native",
2142
+ entityCount: meta.entityCount || 0,
2143
+ relationshipCount: meta.relationshipCount || 0,
2144
+ lastIndexed: meta.lastIndexed || null
2145
+ });
2146
+ } catch {
2147
+ return queryResult("graph", { available: false, error: "meta.json parse error" });
2148
+ }
2149
+ }
2150
+
2151
+ function doHeadlessQuery(type) {
2152
+ const projectDir = process.cwd();
2153
+
2154
+ if (!type || !VALID_QUERY_TYPES.includes(type)) {
2155
+ const result = { error: "unknown query type", validTypes: VALID_QUERY_TYPES };
2156
+ process.stdout.write(JSON.stringify(result) + "\n");
2157
+ process.exit(3);
2158
+ return;
2159
+ }
2160
+
2161
+ let result;
2162
+ switch (type) {
2163
+ case "status": result = queryStatus(projectDir); break;
2164
+ case "domains": result = queryDomains(projectDir); break;
2165
+ case "contracts": result = queryContracts(projectDir); break;
2166
+ case "debt": result = queryDebt(projectDir); break;
2167
+ case "context": result = queryContext(projectDir); break;
2168
+ case "backlog": result = queryBacklog(projectDir); break;
2169
+ case "graph": result = queryGraph(projectDir); break;
2170
+ default:
2171
+ result = { error: "unknown query type", validTypes: VALID_QUERY_TYPES };
2172
+ }
2173
+
2174
+ process.stdout.write(JSON.stringify(result) + "\n");
2175
+ }
2176
+
2177
+ function doHeadless(args) {
2178
+ const sub = args[0];
2179
+ if (!sub || sub === "--help" || sub === "-h") {
2180
+ showHeadlessHelp();
2181
+ return;
2182
+ }
2183
+
2184
+ if (sub === "query") {
2185
+ const type = args[1];
2186
+ doHeadlessQuery(type);
2187
+ return;
2188
+ }
2189
+
2190
+ // headless exec: gsd-t headless <command> [cmdArgs...] [flags]
2191
+ const { flags, positional } = parseHeadlessFlags(args.slice(1));
2192
+ doHeadlessExec(sub, positional, flags);
2193
+ }
2194
+
2195
+ function showHeadlessHelp() {
2196
+ log(`\n${BOLD}GSD-T Headless Mode${RESET}\n`);
2197
+ log(`${BOLD}Usage:${RESET}`);
2198
+ log(` ${CYAN}gsd-t headless${RESET} <command> [args] [--json] [--timeout=N] [--log]`);
2199
+ log(` ${CYAN}gsd-t headless query${RESET} <type>\n`);
2200
+ log(`${BOLD}Exec flags:${RESET}`);
2201
+ log(` ${CYAN}--json${RESET} Structured JSON output`);
2202
+ log(` ${CYAN}--timeout=N${RESET} Kill after N seconds (default: 300)`);
2203
+ log(` ${CYAN}--log${RESET} Write output to .gsd-t/headless-{timestamp}.log\n`);
2204
+ log(`${BOLD}Exit codes:${RESET}`);
2205
+ log(` 0 success`);
2206
+ log(` 1 verify-fail`);
2207
+ log(` 2 context-budget-exceeded`);
2208
+ log(` 3 error`);
2209
+ log(` 4 blocked-needs-human\n`);
2210
+ log(`${BOLD}Query types:${RESET}`);
2211
+ log(` ${VALID_QUERY_TYPES.join(", ")}\n`);
2212
+ log(`${BOLD}Examples:${RESET}`);
2213
+ log(` ${DIM}$${RESET} gsd-t headless verify --json`);
2214
+ log(` ${DIM}$${RESET} gsd-t headless execute --timeout=600 --log`);
2215
+ log(` ${DIM}$${RESET} gsd-t headless query status`);
2216
+ log(` ${DIM}$${RESET} gsd-t headless query domains\n`);
2217
+ }
2218
+
1653
2219
  function showHelp() {
1654
2220
  log(`\n${BOLD}GSD-T${RESET} — Contract-Driven Development for Claude Code\n`);
1655
2221
  log(`${BOLD}Usage:${RESET} npx @tekyzinc/gsd-t ${CYAN}<command>${RESET} [options]\n`);
@@ -1664,6 +2230,7 @@ function showHelp() {
1664
2230
  log(` ${CYAN}doctor${RESET} Diagnose common issues`);
1665
2231
  log(` ${CYAN}changelog${RESET} Open changelog in the browser`);
1666
2232
  log(` ${CYAN}graph${RESET} Code graph operations (index, status, query)`);
2233
+ log(` ${CYAN}headless${RESET} Non-interactive execution via claude -p + fast state queries`);
1667
2234
  log(` ${CYAN}help${RESET} Show this help\n`);
1668
2235
  log(`${BOLD}Examples:${RESET}`);
1669
2236
  log(` ${DIM}$${RESET} npx @tekyzinc/gsd-t install`);
@@ -1729,9 +2296,29 @@ module.exports = {
1729
2296
  doGraphIndex,
1730
2297
  doGraphStatus,
1731
2298
  doGraphQuery,
2299
+ // Headless mode
2300
+ parseHeadlessFlags,
2301
+ buildHeadlessCmd,
2302
+ mapHeadlessExitCode,
2303
+ headlessLogPath,
2304
+ doHeadlessExec,
2305
+ doHeadlessQuery,
2306
+ doHeadless,
2307
+ queryStatus,
2308
+ queryDomains,
2309
+ queryContracts,
2310
+ queryDebt,
2311
+ queryContext,
2312
+ queryBacklog,
2313
+ queryGraph,
2314
+ VALID_QUERY_TYPES,
1732
2315
  PKG_VERSION,
1733
2316
  PKG_ROOT,
1734
2317
  PKG_COMMANDS,
2318
+ // M27: Cross-project sync
2319
+ syncGlobalRulesToProject,
2320
+ syncGlobalRules,
2321
+ exportUniversalRulesForNpm,
1735
2322
  };
1736
2323
 
1737
2324
  // ─── Main ────────────────────────────────────────────────────────────────────
@@ -1771,6 +2358,9 @@ if (require.main === module) {
1771
2358
  case "graph":
1772
2359
  doGraph(args.slice(1));
1773
2360
  break;
2361
+ case "headless":
2362
+ doHeadless(args.slice(1));
2363
+ break;
1774
2364
  case "scan": {
1775
2365
  const exportFlag = args.find(a => a.startsWith('--export='));
1776
2366
  const exportFormat = exportFlag ? exportFlag.split('=')[1] : null;