@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.
- package/README.md +17 -9
- package/bin/desktop.ini +2 -0
- package/bin/global-sync-manager.js +350 -0
- package/bin/gsd-t.js +592 -2
- package/bin/metrics-collector.js +167 -0
- package/bin/metrics-rollup.js +200 -0
- package/bin/patch-lifecycle.js +195 -0
- package/bin/rule-engine.js +160 -0
- package/commands/desktop.ini +2 -0
- package/commands/gsd-t-complete-milestone.md +192 -5
- package/commands/gsd-t-debug.md +16 -2
- package/commands/gsd-t-execute.md +257 -52
- package/commands/gsd-t-help.md +25 -10
- package/commands/gsd-t-integrate.md +35 -7
- package/commands/gsd-t-metrics.md +143 -0
- package/commands/gsd-t-plan.md +49 -2
- package/commands/gsd-t-quick.md +15 -3
- package/commands/gsd-t-status.md +78 -0
- package/commands/gsd-t-test-sync.md +2 -2
- package/commands/gsd-t-verify.md +140 -9
- package/commands/gsd-t-visualize.md +11 -1
- package/commands/gsd-t-wave.md +34 -19
- package/docs/GSD-T-README.md +9 -6
- package/docs/architecture.md +84 -2
- package/docs/ci-examples/desktop.ini +2 -0
- package/docs/ci-examples/github-actions.yml +104 -0
- package/docs/ci-examples/gitlab-ci.yml +116 -0
- package/docs/desktop.ini +2 -0
- package/docs/infrastructure.md +87 -1
- package/docs/prd-graph-engine.md +2 -2
- package/docs/prd-gsd2-hybrid.md +258 -135
- package/docs/requirements.md +63 -2
- package/examples/.gsd-t/contracts/desktop.ini +2 -0
- package/examples/.gsd-t/desktop.ini +2 -0
- package/examples/.gsd-t/domains/desktop.ini +2 -0
- package/examples/.gsd-t/domains/example-domain/desktop.ini +2 -0
- package/examples/desktop.ini +2 -0
- package/examples/rules/.gitkeep +0 -0
- package/package.json +40 -40
- package/scripts/desktop.ini +2 -0
- package/scripts/gsd-t-dashboard-server.js +19 -2
- package/scripts/gsd-t-dashboard.html +63 -0
- package/scripts/gsd-t-event-writer.js +1 -0
- package/templates/CLAUDE-global.md +30 -9
- 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;
|