@tekyzinc/gsd-t 3.11.10 → 3.12.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 +74 -0
- package/README.md +4 -10
- package/bin/event-stream.cjs +205 -0
- package/bin/gsd-t-unattended.cjs +87 -1
- package/bin/gsd-t-unattended.js +87 -1
- package/bin/gsd-t.js +41 -172
- package/bin/headless-auto-spawn.cjs +44 -4
- package/bin/headless-auto-spawn.js +44 -4
- package/bin/scan-data-collector.js +39 -11
- package/bin/token-budget.cjs +43 -126
- package/bin/unattended-watch-format.cjs +154 -0
- package/commands/gsd-t-backlog-list.md +0 -38
- package/commands/gsd-t-complete-milestone.md +2 -30
- package/commands/gsd-t-debug.md +18 -122
- package/commands/gsd-t-doc-ripple.md +0 -21
- package/commands/gsd-t-execute.md +58 -117
- package/commands/gsd-t-help.md +3 -59
- package/commands/gsd-t-integrate.md +18 -78
- package/commands/gsd-t-quick.md +22 -80
- package/commands/gsd-t-resume.md +2 -2
- package/commands/gsd-t-scan.md +15 -1
- package/commands/gsd-t-status.md +2 -32
- package/commands/gsd-t-unattended-watch.md +37 -1
- package/commands/gsd-t-verify.md +14 -2
- package/commands/gsd-t-wave.md +22 -91
- package/commands/gsd.md +43 -4
- package/docs/GSD-T-README.md +2 -6
- package/docs/architecture.md +10 -8
- package/docs/infrastructure.md +8 -14
- package/docs/methodology.md +10 -4
- package/docs/prd-harness-evolution.md +1 -1
- package/docs/requirements.md +28 -12
- package/package.json +2 -2
- package/scripts/context-meter/estimate-tokens.js +96 -0
- package/scripts/context-meter/estimate-tokens.test.js +158 -0
- package/scripts/context-meter/threshold.js +25 -46
- package/scripts/context-meter/threshold.test.js +52 -80
- package/scripts/gsd-t-agent-dashboard-server.js +4 -4
- package/scripts/gsd-t-agent-dashboard.html +699 -380
- package/scripts/gsd-t-context-meter.e2e.test.js +39 -131
- package/scripts/gsd-t-context-meter.js +13 -36
- package/scripts/gsd-t-context-meter.test.js +59 -90
- package/templates/CLAUDE-global.md +7 -25
- package/templates/CLAUDE-project.md +22 -23
- package/bin/qa-calibrator.js +0 -194
- package/bin/runway-estimator.cjs +0 -242
- package/bin/runway-estimator.js +0 -242
- package/bin/token-optimizer.cjs +0 -471
- package/bin/token-optimizer.js +0 -471
- package/bin/token-telemetry.cjs +0 -246
- package/bin/token-telemetry.js +0 -246
- package/commands/gsd-t-audit.md +0 -196
- package/commands/gsd-t-brainstorm.md +0 -201
- package/commands/gsd-t-discuss.md +0 -178
- package/commands/gsd-t-optimization-apply.md +0 -91
- package/commands/gsd-t-optimization-reject.md +0 -94
- package/commands/gsd-t-prompt.md +0 -137
- package/commands/gsd-t-reflect.md +0 -130
- package/scripts/context-meter/count-tokens-client.js +0 -221
- package/scripts/context-meter/count-tokens-client.test.js +0 -308
- package/scripts/context-meter/test-injector.js +0 -55
package/bin/gsd-t.js
CHANGED
|
@@ -433,8 +433,8 @@ function ensureGitignoreEntries(projectDir, entries) {
|
|
|
433
433
|
|
|
434
434
|
// Install the Context Meter into a project directory.
|
|
435
435
|
// Copies scripts/gsd-t-context-meter.js, scripts/context-meter/*.js (runtime
|
|
436
|
-
// only — skips .test.js
|
|
437
|
-
//
|
|
436
|
+
// only — skips .test.js), and the config template (if missing). Also appends
|
|
437
|
+
// entries to .gitignore.
|
|
438
438
|
function installContextMeter(projectDir) {
|
|
439
439
|
try {
|
|
440
440
|
// 1. Copy gsd-t-context-meter.js → {projectDir}/scripts/
|
|
@@ -486,9 +486,7 @@ function installContextMeter(projectDir) {
|
|
|
486
486
|
return false;
|
|
487
487
|
}
|
|
488
488
|
for (const fname of depFiles) {
|
|
489
|
-
// Skip test files and test-only infrastructure
|
|
490
489
|
if (fname.includes(".test.")) continue;
|
|
491
|
-
if (fname === "test-injector.js") continue;
|
|
492
490
|
const fsrc = path.join(depsSrcDir, fname);
|
|
493
491
|
const fdest = path.join(depsDestDir, fname);
|
|
494
492
|
try {
|
|
@@ -1232,9 +1230,6 @@ async function doInstall(opts = {}) {
|
|
|
1232
1230
|
saveInstalledVersion();
|
|
1233
1231
|
|
|
1234
1232
|
showInstallSummary(gsdtCommands.length, utilityCommands.length);
|
|
1235
|
-
|
|
1236
|
-
// Interactive prompt (skipped silently in non-TTY shells)
|
|
1237
|
-
await promptForApiKeyIfMissing(resolveApiKeyEnvVar(process.cwd()));
|
|
1238
1233
|
}
|
|
1239
1234
|
|
|
1240
1235
|
function showInstallSummary(gsdtCount, utilCount) {
|
|
@@ -1444,9 +1439,6 @@ async function doInit(projectName) {
|
|
|
1444
1439
|
if (registerProject(projectDir)) success("Registered in ~/.claude/.gsd-t-projects");
|
|
1445
1440
|
|
|
1446
1441
|
showInitTree(projectDir);
|
|
1447
|
-
|
|
1448
|
-
// Interactive prompt (skipped silently in non-TTY shells)
|
|
1449
|
-
await promptForApiKeyIfMissing(resolveApiKeyEnvVar(projectDir));
|
|
1450
1442
|
}
|
|
1451
1443
|
|
|
1452
1444
|
function showInitTree(projectDir) {
|
|
@@ -1523,12 +1515,7 @@ function showStatusContextMeter() {
|
|
|
1523
1515
|
const rel = state.timestamp ? formatRelativeTime(state.timestamp) : "never measured";
|
|
1524
1516
|
log(` ${RED}${BOLD}✗ CONTEXT METER DEAD${RESET} ${RED}— error: ${code}, last check: ${rel}${RESET}`);
|
|
1525
1517
|
log(` ${RED}The context-window guardrail is NOT working. Long sessions will hit /compact.${RESET}`);
|
|
1526
|
-
|
|
1527
|
-
log(` ${YELLOW}Fix: export ANTHROPIC_API_KEY in your shell profile${RESET}`);
|
|
1528
|
-
log(` ${YELLOW} (measurement only — inference stays on Claude Code subscription)${RESET}`);
|
|
1529
|
-
} else {
|
|
1530
|
-
log(` ${YELLOW}Fix: run 'gsd-t doctor' for diagnostics${RESET}`);
|
|
1531
|
-
}
|
|
1518
|
+
log(` ${YELLOW}Fix: run 'gsd-t doctor' for diagnostics${RESET}`);
|
|
1532
1519
|
return;
|
|
1533
1520
|
}
|
|
1534
1521
|
|
|
@@ -1981,8 +1968,7 @@ const PROJECT_BIN_TOOLS = [
|
|
|
1981
1968
|
"archive-progress.cjs", "log-tail.cjs", "context-budget-audit.cjs",
|
|
1982
1969
|
"context-meter-config.cjs", "token-budget.cjs",
|
|
1983
1970
|
"gsd-t-unattended.cjs", "gsd-t-unattended-platform.cjs", "gsd-t-unattended-safety.cjs",
|
|
1984
|
-
"handoff-lock.cjs", "headless-auto-spawn.cjs",
|
|
1985
|
-
"token-telemetry.cjs", "token-optimizer.cjs",
|
|
1971
|
+
"handoff-lock.cjs", "headless-auto-spawn.cjs",
|
|
1986
1972
|
];
|
|
1987
1973
|
|
|
1988
1974
|
function copyBinToolsToProject(projectDir, projectName) {
|
|
@@ -2308,8 +2294,8 @@ function checkDoctorCgc() {
|
|
|
2308
2294
|
return issues;
|
|
2309
2295
|
}
|
|
2310
2296
|
|
|
2311
|
-
// Verify context meter wiring:
|
|
2312
|
-
//
|
|
2297
|
+
// Verify context meter wiring: hook registration, hook script presence,
|
|
2298
|
+
// config validity, and a local estimation dry-run.
|
|
2313
2299
|
// Returns number of issues (RED results). Mirrors checkDoctorCgc shape.
|
|
2314
2300
|
async function checkDoctorContextMeter(projectDir) {
|
|
2315
2301
|
let issues = 0;
|
|
@@ -2317,8 +2303,8 @@ async function checkDoctorContextMeter(projectDir) {
|
|
|
2317
2303
|
|
|
2318
2304
|
const cwd = projectDir || process.cwd();
|
|
2319
2305
|
|
|
2320
|
-
// Load config (used by checks
|
|
2321
|
-
// JSON or schema-mismatch → throws (handled in Check
|
|
2306
|
+
// Load config (used by checks 3 and 4). Missing file → defaults; invalid
|
|
2307
|
+
// JSON or schema-mismatch → throws (handled in Check 3).
|
|
2322
2308
|
let cfg = null;
|
|
2323
2309
|
let cfgLoadErr = null;
|
|
2324
2310
|
try {
|
|
@@ -2327,19 +2313,8 @@ async function checkDoctorContextMeter(projectDir) {
|
|
|
2327
2313
|
} catch (e) {
|
|
2328
2314
|
cfgLoadErr = e;
|
|
2329
2315
|
}
|
|
2330
|
-
const apiKeyEnvVar = (cfg && cfg.apiKeyEnvVar) || "ANTHROPIC_API_KEY";
|
|
2331
|
-
|
|
2332
|
-
// Check 1: API key env var present
|
|
2333
|
-
const apiKeyValue = process.env[apiKeyEnvVar];
|
|
2334
|
-
const apiKeyPresent = typeof apiKeyValue === "string" && apiKeyValue.length > 0;
|
|
2335
|
-
if (apiKeyPresent) {
|
|
2336
|
-
success(`API key present ($${apiKeyEnvVar})`);
|
|
2337
|
-
} else {
|
|
2338
|
-
error(`Missing API key: set $${apiKeyEnvVar} — https://console.anthropic.com/settings/keys`);
|
|
2339
|
-
issues++;
|
|
2340
|
-
}
|
|
2341
2316
|
|
|
2342
|
-
// Check
|
|
2317
|
+
// Check 1: Hook registered in ~/.claude/settings.json
|
|
2343
2318
|
let hookRegistered = false;
|
|
2344
2319
|
try {
|
|
2345
2320
|
if (fs.existsSync(SETTINGS_JSON)) {
|
|
@@ -2367,7 +2342,7 @@ async function checkDoctorContextMeter(projectDir) {
|
|
|
2367
2342
|
issues++;
|
|
2368
2343
|
}
|
|
2369
2344
|
|
|
2370
|
-
// Check
|
|
2345
|
+
// Check 2: Hook script file exists in project
|
|
2371
2346
|
const scriptPath = path.join(cwd, "scripts", CONTEXT_METER_SCRIPT);
|
|
2372
2347
|
if (fs.existsSync(scriptPath)) {
|
|
2373
2348
|
success("Hook script present");
|
|
@@ -2376,7 +2351,7 @@ async function checkDoctorContextMeter(projectDir) {
|
|
|
2376
2351
|
issues++;
|
|
2377
2352
|
}
|
|
2378
2353
|
|
|
2379
|
-
// Check
|
|
2354
|
+
// Check 3: Config file parses via loader
|
|
2380
2355
|
const configPath = path.join(cwd, CONTEXT_METER_CONFIG_DEST);
|
|
2381
2356
|
if (cfgLoadErr) {
|
|
2382
2357
|
error(`Config file invalid: ${cfgLoadErr.message} — fix ${CONTEXT_METER_CONFIG_DEST}`);
|
|
@@ -2387,34 +2362,27 @@ async function checkDoctorContextMeter(projectDir) {
|
|
|
2387
2362
|
warn("Using default config — run gsd-t install to copy template");
|
|
2388
2363
|
}
|
|
2389
2364
|
|
|
2390
|
-
// Check
|
|
2391
|
-
|
|
2392
|
-
|
|
2365
|
+
// Check 4: Dry-run local token estimation
|
|
2366
|
+
const estimatorPath = path.join(cwd, "scripts", "context-meter", "estimate-tokens.js");
|
|
2367
|
+
if (!fs.existsSync(estimatorPath)) {
|
|
2368
|
+
error("Token estimator missing at scripts/context-meter/estimate-tokens.js — run gsd-t update");
|
|
2369
|
+
issues++;
|
|
2393
2370
|
} else {
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
system: "",
|
|
2405
|
-
messages: [{ role: "user", content: [{ type: "text", text: "ping" }] }],
|
|
2406
|
-
timeoutMs: 5000,
|
|
2407
|
-
});
|
|
2408
|
-
if (result && typeof result.inputTokens === "number") {
|
|
2409
|
-
success(`count_tokens dry-run OK (${result.inputTokens} tokens)`);
|
|
2410
|
-
} else {
|
|
2411
|
-
error("count_tokens API call failed — check API key and network");
|
|
2412
|
-
issues++;
|
|
2413
|
-
}
|
|
2414
|
-
} catch (e) {
|
|
2415
|
-
error(`count_tokens dry-run threw: ${e.message}`);
|
|
2371
|
+
try {
|
|
2372
|
+
const { estimateTokens } = require(estimatorPath);
|
|
2373
|
+
const result = estimateTokens({
|
|
2374
|
+
system: "",
|
|
2375
|
+
messages: [{ role: "user", content: [{ type: "text", text: "ping" }] }],
|
|
2376
|
+
});
|
|
2377
|
+
if (result && typeof result.inputTokens === "number") {
|
|
2378
|
+
success(`Token estimator dry-run OK (${result.inputTokens} tokens)`);
|
|
2379
|
+
} else {
|
|
2380
|
+
error("Token estimator returned null");
|
|
2416
2381
|
issues++;
|
|
2417
2382
|
}
|
|
2383
|
+
} catch (e) {
|
|
2384
|
+
error(`Token estimator dry-run threw: ${e.message}`);
|
|
2385
|
+
issues++;
|
|
2418
2386
|
}
|
|
2419
2387
|
}
|
|
2420
2388
|
|
|
@@ -2750,7 +2718,7 @@ function doHeadlessExec(command, cmdArgs, flags) {
|
|
|
2750
2718
|
let processExitCode = 0;
|
|
2751
2719
|
|
|
2752
2720
|
try {
|
|
2753
|
-
const result = execFileSync("claude", ["-p", prompt], {
|
|
2721
|
+
const result = execFileSync("claude", ["-p", "--dangerously-skip-permissions", prompt], {
|
|
2754
2722
|
encoding: "utf8",
|
|
2755
2723
|
timeout: timeoutMs,
|
|
2756
2724
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -3274,117 +3242,10 @@ function showHeadlessHelp() {
|
|
|
3274
3242
|
log(` ${DIM}$${RESET} gsd-t headless query domains\n`);
|
|
3275
3243
|
}
|
|
3276
3244
|
|
|
3277
|
-
// ─── Metrics (
|
|
3278
|
-
|
|
3279
|
-
function parseMetricsByFlag(args) {
|
|
3280
|
-
const byArg = args.find(a => a.startsWith("--by="));
|
|
3281
|
-
if (!byArg) return [];
|
|
3282
|
-
const raw = byArg.slice(5).trim();
|
|
3283
|
-
if (!raw) return [];
|
|
3284
|
-
return raw.split(",").map(s => s.trim()).filter(Boolean);
|
|
3285
|
-
}
|
|
3245
|
+
// ─── Metrics (removed in v3.12 — M38 meter reduction) ─────────────────────────
|
|
3286
3246
|
|
|
3287
|
-
function
|
|
3288
|
-
|
|
3289
|
-
return `${Number(v).toFixed(1)}%`;
|
|
3290
|
-
}
|
|
3291
|
-
|
|
3292
|
-
function formatInt(v) {
|
|
3293
|
-
if (v === null || v === undefined || Number.isNaN(v)) return "—";
|
|
3294
|
-
return String(Math.round(Number(v)));
|
|
3295
|
-
}
|
|
3296
|
-
|
|
3297
|
-
function doMetrics(args) {
|
|
3298
|
-
const projectDir = process.cwd();
|
|
3299
|
-
let tt;
|
|
3300
|
-
try {
|
|
3301
|
-
tt = require(path.join(projectDir, "bin", "token-telemetry.js"));
|
|
3302
|
-
} catch (e) {
|
|
3303
|
-
error(`bin/token-telemetry.js not found in ${projectDir} — run gsd-t install first.`);
|
|
3304
|
-
process.exit(1);
|
|
3305
|
-
}
|
|
3306
|
-
|
|
3307
|
-
const records = tt.readAll(projectDir);
|
|
3308
|
-
if (records.length === 0) {
|
|
3309
|
-
log(`${DIM}No telemetry records yet — .gsd-t/token-metrics.jsonl is empty or missing.${RESET}`);
|
|
3310
|
-
return;
|
|
3311
|
-
}
|
|
3312
|
-
|
|
3313
|
-
const isTokens = args.includes("--tokens");
|
|
3314
|
-
const isHalts = args.includes("--halts");
|
|
3315
|
-
const isContextWindow = args.includes("--context-window");
|
|
3316
|
-
|
|
3317
|
-
if (!isTokens && !isHalts && !isContextWindow) {
|
|
3318
|
-
log(`${YELLOW}Specify at least one of: --tokens, --halts, --context-window${RESET}`);
|
|
3319
|
-
return;
|
|
3320
|
-
}
|
|
3321
|
-
|
|
3322
|
-
if (isHalts) {
|
|
3323
|
-
const halts = records.filter(r => r.halt_type);
|
|
3324
|
-
log(`\n${BOLD}Halts — ${halts.length} record(s)${RESET}`);
|
|
3325
|
-
if (halts.length === 0) {
|
|
3326
|
-
log(`${DIM} (no halts recorded — all spawns completed normally)${RESET}\n`);
|
|
3327
|
-
} else {
|
|
3328
|
-
const byType = {};
|
|
3329
|
-
for (const r of halts) {
|
|
3330
|
-
const key = String(r.halt_type);
|
|
3331
|
-
byType[key] = (byType[key] || 0) + 1;
|
|
3332
|
-
}
|
|
3333
|
-
for (const [type, count] of Object.entries(byType).sort()) {
|
|
3334
|
-
log(` ${CYAN}${type.padEnd(24)}${RESET} ${count}`);
|
|
3335
|
-
}
|
|
3336
|
-
log("");
|
|
3337
|
-
}
|
|
3338
|
-
}
|
|
3339
|
-
|
|
3340
|
-
if (isTokens) {
|
|
3341
|
-
const by = parseMetricsByFlag(args);
|
|
3342
|
-
if (by.length === 0) {
|
|
3343
|
-
const totalConsumed = records.reduce((a, r) => a + (Number(r.tokens_consumed) || 0), 0);
|
|
3344
|
-
const totalDuration = records.reduce((a, r) => a + (Number(r.duration_s) || 0), 0);
|
|
3345
|
-
log(`\n${BOLD}Tokens — ${records.length} spawn(s)${RESET}`);
|
|
3346
|
-
log(` total tokens consumed: ${formatInt(totalConsumed)}`);
|
|
3347
|
-
log(` total duration (s): ${formatInt(totalDuration)}`);
|
|
3348
|
-
if (records.length > 0) {
|
|
3349
|
-
log(` mean tokens/spawn: ${formatInt(totalConsumed / records.length)}`);
|
|
3350
|
-
}
|
|
3351
|
-
log("");
|
|
3352
|
-
} else {
|
|
3353
|
-
const groups = tt.aggregate(records, { by });
|
|
3354
|
-
log(`\n${BOLD}Tokens by ${by.join(",")} — ${groups.length} group(s)${RESET}`);
|
|
3355
|
-
const keyHeader = by.join("/");
|
|
3356
|
-
log(` ${keyHeader.padEnd(36)} ${"count".padStart(7)} ${"total".padStart(12)} ${"mean".padStart(10)} ${"median".padStart(10)} ${"p95".padStart(10)}`);
|
|
3357
|
-
log(` ${"-".repeat(36)} ${"-".repeat(7)} ${"-".repeat(12)} ${"-".repeat(10)} ${"-".repeat(10)} ${"-".repeat(10)}`);
|
|
3358
|
-
for (const g of groups) {
|
|
3359
|
-
const label = by.map(k => g.key[k] == null ? "—" : String(g.key[k])).join("/");
|
|
3360
|
-
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)}`);
|
|
3361
|
-
}
|
|
3362
|
-
log("");
|
|
3363
|
-
}
|
|
3364
|
-
}
|
|
3365
|
-
|
|
3366
|
-
if (isContextWindow) {
|
|
3367
|
-
log(`\n${BOLD}Context window trend — ${records.length} record(s)${RESET}`);
|
|
3368
|
-
const withPct = records.filter(r => r.context_window_pct_after != null && !Number.isNaN(Number(r.context_window_pct_after)));
|
|
3369
|
-
if (withPct.length === 0) {
|
|
3370
|
-
log(`${DIM} (no context-window measurements in records)${RESET}\n`);
|
|
3371
|
-
} else {
|
|
3372
|
-
const sorted = [...withPct].map(r => Number(r.context_window_pct_after)).sort((a, b) => a - b);
|
|
3373
|
-
const min = sorted[0];
|
|
3374
|
-
const max = sorted[sorted.length - 1];
|
|
3375
|
-
const mean = sorted.reduce((a, b) => a + b, 0) / sorted.length;
|
|
3376
|
-
const p95 = sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))];
|
|
3377
|
-
log(` min: ${formatPct(min)}`);
|
|
3378
|
-
log(` mean: ${formatPct(mean)}`);
|
|
3379
|
-
log(` p95: ${formatPct(p95)}`);
|
|
3380
|
-
log(` max: ${formatPct(max)}`);
|
|
3381
|
-
const atWarn = withPct.filter(r => Number(r.context_window_pct_after) >= 70).length;
|
|
3382
|
-
const atStop = withPct.filter(r => Number(r.context_window_pct_after) >= 85).length;
|
|
3383
|
-
log(` spawns at warn band (≥70%): ${atWarn}`);
|
|
3384
|
-
log(` spawns at stop band (≥85%): ${atStop}`);
|
|
3385
|
-
log("");
|
|
3386
|
-
}
|
|
3387
|
-
}
|
|
3247
|
+
function doMetrics(_args) {
|
|
3248
|
+
log(`${DIM}metrics removed in v3.12 — context meter is no longer telemetry-instrumented${RESET}`);
|
|
3388
3249
|
}
|
|
3389
3250
|
|
|
3390
3251
|
function showHelp() {
|
|
@@ -3544,6 +3405,14 @@ if (require.main === module) {
|
|
|
3544
3405
|
case "headless":
|
|
3545
3406
|
doHeadless(args.slice(1));
|
|
3546
3407
|
break;
|
|
3408
|
+
case "unattended": {
|
|
3409
|
+
const { spawnSync } = require("child_process");
|
|
3410
|
+
const cjs = path.join(__dirname, "gsd-t-unattended.cjs");
|
|
3411
|
+
const res = spawnSync(process.execPath, [cjs, ...args.slice(1)], {
|
|
3412
|
+
stdio: "inherit",
|
|
3413
|
+
});
|
|
3414
|
+
process.exit(res.status == null ? 1 : res.status);
|
|
3415
|
+
}
|
|
3547
3416
|
case "metrics":
|
|
3548
3417
|
doMetrics(args.slice(1));
|
|
3549
3418
|
break;
|
|
@@ -53,20 +53,54 @@ module.exports = {
|
|
|
53
53
|
* args?: string[],
|
|
54
54
|
* continue_from?: string,
|
|
55
55
|
* projectDir?: string,
|
|
56
|
-
* context?: object
|
|
56
|
+
* context?: object,
|
|
57
|
+
* sessionContext?: object,
|
|
58
|
+
* sessionId?: string,
|
|
59
|
+
* watch?: boolean,
|
|
60
|
+
* spawnType?: 'primary' | 'validation'
|
|
57
61
|
* }} opts
|
|
58
|
-
* @returns {{ id: string, pid: number, logPath: string, timestamp: string }}
|
|
62
|
+
* @returns {{ id: string | null, pid: number | null, logPath: string | null, timestamp: string, mode: 'headless' | 'in-context' }}
|
|
59
63
|
*/
|
|
60
64
|
function autoSpawnHeadless(opts) {
|
|
61
65
|
const command = opts.command;
|
|
62
66
|
const args = opts.args || [];
|
|
63
67
|
const continue_from = opts.continue_from || ".";
|
|
64
68
|
const projectDir = opts.projectDir || process.cwd();
|
|
65
|
-
const context = opts.context || null;
|
|
69
|
+
const context = opts.context || opts.sessionContext || null;
|
|
70
|
+
const watch = opts.watch === true;
|
|
71
|
+
const spawnType = opts.spawnType || "primary";
|
|
66
72
|
|
|
67
73
|
if (!command || typeof command !== "string") {
|
|
68
74
|
throw new Error("autoSpawnHeadless: `command` is required");
|
|
69
75
|
}
|
|
76
|
+
if (spawnType !== "primary" && spawnType !== "validation") {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`autoSpawnHeadless: \`spawnType\` must be 'primary' or 'validation' (got ${JSON.stringify(spawnType)})`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Propagation rules (headless-default-contract §2):
|
|
83
|
+
// watch=true + primary → signal in-context fallback (caller uses Task)
|
|
84
|
+
// watch=true + validation → warn on stderr; proceed headless
|
|
85
|
+
// watch=false → headless (default behavior)
|
|
86
|
+
if (watch && spawnType === "primary") {
|
|
87
|
+
return {
|
|
88
|
+
id: null,
|
|
89
|
+
pid: null,
|
|
90
|
+
logPath: null,
|
|
91
|
+
timestamp: new Date().toISOString(),
|
|
92
|
+
mode: "in-context",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (watch && spawnType === "validation") {
|
|
96
|
+
try {
|
|
97
|
+
process.stderr.write(
|
|
98
|
+
`[headless-default] --watch ignored for validation spawn type: ${spawnType}\n`,
|
|
99
|
+
);
|
|
100
|
+
} catch (_) {
|
|
101
|
+
/* best effort */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
70
104
|
|
|
71
105
|
const timestamp = new Date().toISOString();
|
|
72
106
|
const id = makeSessionId(command, new Date());
|
|
@@ -145,7 +179,13 @@ function autoSpawnHeadless(opts) {
|
|
|
145
179
|
// a detached approach that survives even after the parent's `unref()`.
|
|
146
180
|
installCompletionWatcher({ projectDir, id, logPath, pid, startTimestamp: timestamp });
|
|
147
181
|
|
|
148
|
-
return {
|
|
182
|
+
return {
|
|
183
|
+
id,
|
|
184
|
+
pid,
|
|
185
|
+
logPath: path.relative(projectDir, logPath),
|
|
186
|
+
timestamp,
|
|
187
|
+
mode: "headless",
|
|
188
|
+
};
|
|
149
189
|
}
|
|
150
190
|
|
|
151
191
|
// ── makeSessionId ────────────────────────────────────────────────────────────
|
|
@@ -53,20 +53,54 @@ module.exports = {
|
|
|
53
53
|
* args?: string[],
|
|
54
54
|
* continue_from?: string,
|
|
55
55
|
* projectDir?: string,
|
|
56
|
-
* context?: object
|
|
56
|
+
* context?: object,
|
|
57
|
+
* sessionContext?: object,
|
|
58
|
+
* sessionId?: string,
|
|
59
|
+
* watch?: boolean,
|
|
60
|
+
* spawnType?: 'primary' | 'validation'
|
|
57
61
|
* }} opts
|
|
58
|
-
* @returns {{ id: string, pid: number, logPath: string, timestamp: string }}
|
|
62
|
+
* @returns {{ id: string | null, pid: number | null, logPath: string | null, timestamp: string, mode: 'headless' | 'in-context' }}
|
|
59
63
|
*/
|
|
60
64
|
function autoSpawnHeadless(opts) {
|
|
61
65
|
const command = opts.command;
|
|
62
66
|
const args = opts.args || [];
|
|
63
67
|
const continue_from = opts.continue_from || ".";
|
|
64
68
|
const projectDir = opts.projectDir || process.cwd();
|
|
65
|
-
const context = opts.context || null;
|
|
69
|
+
const context = opts.context || opts.sessionContext || null;
|
|
70
|
+
const watch = opts.watch === true;
|
|
71
|
+
const spawnType = opts.spawnType || "primary";
|
|
66
72
|
|
|
67
73
|
if (!command || typeof command !== "string") {
|
|
68
74
|
throw new Error("autoSpawnHeadless: `command` is required");
|
|
69
75
|
}
|
|
76
|
+
if (spawnType !== "primary" && spawnType !== "validation") {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`autoSpawnHeadless: \`spawnType\` must be 'primary' or 'validation' (got ${JSON.stringify(spawnType)})`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Propagation rules (headless-default-contract §2):
|
|
83
|
+
// watch=true + primary → signal in-context fallback (caller uses Task)
|
|
84
|
+
// watch=true + validation → warn on stderr; proceed headless
|
|
85
|
+
// watch=false → headless (default behavior)
|
|
86
|
+
if (watch && spawnType === "primary") {
|
|
87
|
+
return {
|
|
88
|
+
id: null,
|
|
89
|
+
pid: null,
|
|
90
|
+
logPath: null,
|
|
91
|
+
timestamp: new Date().toISOString(),
|
|
92
|
+
mode: "in-context",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (watch && spawnType === "validation") {
|
|
96
|
+
try {
|
|
97
|
+
process.stderr.write(
|
|
98
|
+
`[headless-default] --watch ignored for validation spawn type: ${spawnType}\n`,
|
|
99
|
+
);
|
|
100
|
+
} catch (_) {
|
|
101
|
+
/* best effort */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
70
104
|
|
|
71
105
|
const timestamp = new Date().toISOString();
|
|
72
106
|
const id = makeSessionId(command, new Date());
|
|
@@ -145,7 +179,13 @@ function autoSpawnHeadless(opts) {
|
|
|
145
179
|
// a detached approach that survives even after the parent's `unref()`.
|
|
146
180
|
installCompletionWatcher({ projectDir, id, logPath, pid, startTimestamp: timestamp });
|
|
147
181
|
|
|
148
|
-
return {
|
|
182
|
+
return {
|
|
183
|
+
id,
|
|
184
|
+
pid,
|
|
185
|
+
logPath: path.relative(projectDir, logPath),
|
|
186
|
+
timestamp,
|
|
187
|
+
mode: "headless",
|
|
188
|
+
};
|
|
149
189
|
}
|
|
150
190
|
|
|
151
191
|
// ── makeSessionId ────────────────────────────────────────────────────────────
|
|
@@ -25,22 +25,50 @@ function parseTestCoverage(text) {
|
|
|
25
25
|
function parseFilesAndLoc(text) {
|
|
26
26
|
const m = text.match(/\|\s*\*?\*?(?:Grand\s+)?Total[^|]*\*?\*?\s*\|\s*\*?\*?(\d+)\s+files?\*?\*?\s*\|\s*\*?\*?([\d,]+)[^|]*\*?\*?\s*\|/i);
|
|
27
27
|
if (m) return { filesScanned: parseInt(m[1], 10), totalLoc: parseInt(m[2].replace(/,/g, ''), 10) };
|
|
28
|
+
let files = 0;
|
|
29
|
+
let loc = 0;
|
|
30
|
+
const lineRe = /(\d+)\s+files?\s*\(\s*~?\s*([\d,]+)\s+LOC\s*\)/gi;
|
|
31
|
+
let match;
|
|
32
|
+
while ((match = lineRe.exec(text)) !== null) {
|
|
33
|
+
files += parseInt(match[1], 10);
|
|
34
|
+
loc += parseInt(match[2].replace(/,/g, ''), 10);
|
|
35
|
+
}
|
|
36
|
+
if (files > 0) return { filesScanned: files, totalLoc: loc };
|
|
28
37
|
return { filesScanned: 0, totalLoc: 0 };
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
function parseComponents(text) {
|
|
32
41
|
const sec = text.match(/## Component Inventory([\s\S]*?)(?=\n## |\n---|\n#[^#]|$)/);
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
if (sec) {
|
|
43
|
+
const tableRows = sec[1].split('\n')
|
|
44
|
+
.filter(l => /^\|/.test(l) && !/---/.test(l) && !/Component.*File/i.test(l))
|
|
45
|
+
.map(row => {
|
|
46
|
+
const cols = row.split('|').map(c => c.trim().replace(/\*\*/g, '').replace(/`/g, '')).filter(Boolean);
|
|
47
|
+
if (cols.length < 3) return null;
|
|
48
|
+
const name = cols[0];
|
|
49
|
+
if (!name || /^total/i.test(name)) return null;
|
|
50
|
+
return { name, filePath: cols[1] || '', size: cols[2] || '', purpose: cols[3] || '', files: 1, healthScore: 80 };
|
|
51
|
+
})
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
if (tableRows.length > 0) return tableRows;
|
|
54
|
+
}
|
|
55
|
+
const structSec = text.match(/## Structure([\s\S]*?)(?=\n## |\n---|\n#[^#]|$)/);
|
|
56
|
+
if (!structSec) return [];
|
|
57
|
+
const entryRe = /^([a-zA-Z0-9_.\-]+\/)\s+(?:~?\s*)?(?:(\d+)\s+files?\s*)?\(?\s*~?\s*([\d,]+)\s+LOC\s*\)?\s*(.*)$/gm;
|
|
58
|
+
const out = [];
|
|
59
|
+
let m;
|
|
60
|
+
while ((m = entryRe.exec(structSec[1])) !== null) {
|
|
61
|
+
const name = m[1].replace(/\/$/, '');
|
|
62
|
+
out.push({
|
|
63
|
+
name,
|
|
64
|
+
filePath: m[1],
|
|
65
|
+
size: (m[2] ? m[2] + ' files, ' : '') + m[3] + ' LOC',
|
|
66
|
+
purpose: (m[4] || '').trim(),
|
|
67
|
+
files: m[2] ? parseInt(m[2], 10) : 1,
|
|
68
|
+
healthScore: 80,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
44
72
|
}
|
|
45
73
|
|
|
46
74
|
function parseSeverityMap(text) {
|