@tekyzinc/gsd-t 3.16.12 → 3.18.12
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 +67 -0
- package/README.md +13 -3
- package/bin/gsd-t-depgraph-validate.cjs +140 -0
- package/bin/gsd-t-economics.cjs +287 -0
- package/bin/gsd-t-file-disjointness.cjs +227 -0
- package/bin/gsd-t-in-session-usage.cjs +213 -0
- package/bin/gsd-t-orchestrator-config.cjs +100 -3
- package/bin/gsd-t-orchestrator.js +2 -1
- package/bin/gsd-t-parallel.cjs +382 -0
- package/bin/gsd-t-report-tokens.cjs +549 -0
- package/bin/gsd-t-task-graph.cjs +366 -0
- package/bin/gsd-t-token-capture.cjs +29 -14
- package/bin/gsd-t-token-dashboard.cjs +35 -0
- package/bin/gsd-t-tool-attribution.cjs +377 -0
- package/bin/gsd-t-tool-cost.cjs +195 -0
- package/bin/gsd-t-unattended-platform.cjs +7 -1
- package/bin/gsd-t-unattended.cjs +2 -0
- package/bin/gsd-t.js +155 -5
- package/bin/headless-auto-spawn.cjs +69 -49
- package/bin/headless-auto-spawn.js +18 -24
- package/bin/runway-estimator.cjs +212 -0
- package/bin/spawn-plan-derive.cjs +163 -0
- package/bin/spawn-plan-status-updater.cjs +292 -0
- package/bin/spawn-plan-writer.cjs +204 -0
- package/commands/gsd-t-debug.md +26 -7
- package/commands/gsd-t-execute.md +36 -28
- package/commands/gsd-t-help.md +11 -0
- package/commands/gsd-t-integrate.md +27 -7
- package/commands/gsd-t-quick.md +30 -13
- package/commands/gsd-t-scan.md +5 -5
- package/commands/gsd-t-unattended-watch.md +4 -3
- package/commands/gsd-t-unattended.md +9 -3
- package/commands/gsd-t-verify.md +5 -5
- package/commands/gsd-t-wave.md +21 -8
- package/commands/gsd.md +45 -3
- package/docs/GSD-T-README.md +43 -5
- package/docs/architecture.md +423 -3
- package/docs/requirements.md +203 -0
- package/package.json +1 -1
- package/scripts/gsd-t-calibration-hook.js +256 -0
- package/scripts/gsd-t-compact-detector.js +223 -0
- package/scripts/gsd-t-compaction-scanner.js +305 -0
- package/scripts/gsd-t-dashboard-autostart.cjs +172 -0
- package/scripts/gsd-t-dashboard-server.js +179 -0
- package/scripts/gsd-t-dashboard.html +3 -3
- package/scripts/gsd-t-heartbeat.js +50 -2
- package/scripts/gsd-t-post-commit-spawn-plan.sh +86 -0
- package/scripts/gsd-t-transcript.html +546 -43
- package/scripts/hooks/gsd-t-in-session-usage-hook.js +84 -0
- package/scripts/spawn-plan-fmt-tokens.cjs +80 -0
- package/templates/CLAUDE-global.md +8 -3
- package/templates/CLAUDE-project.md +17 -14
- package/templates/hooks/post-commit-spawn-plan.sh +85 -0
package/bin/gsd-t.js
CHANGED
|
@@ -976,7 +976,7 @@ function configureFigmaMcp() {
|
|
|
976
976
|
|
|
977
977
|
// ─── Utility Scripts ─────────────────────────────────────────────────────────
|
|
978
978
|
|
|
979
|
-
const UTILITY_SCRIPTS = ["gsd-t-tools.js", "gsd-t-statusline.js", "gsd-t-event-writer.js", "gsd-t-dashboard-server.js", "gsd-t-dashboard.html"];
|
|
979
|
+
const UTILITY_SCRIPTS = ["gsd-t-tools.js", "gsd-t-statusline.js", "gsd-t-event-writer.js", "gsd-t-dashboard-server.js", "gsd-t-dashboard.html", "gsd-t-transcript.html"];
|
|
980
980
|
|
|
981
981
|
function installUtilityScripts() {
|
|
982
982
|
ensureDir(SCRIPTS_DIR);
|
|
@@ -2854,17 +2854,86 @@ function doGraphQuery(args) {
|
|
|
2854
2854
|
}
|
|
2855
2855
|
|
|
2856
2856
|
function doGraph(args) {
|
|
2857
|
+
// M44 D1-T4: `gsd-t graph --output json|table` prints the task-graph DAG
|
|
2858
|
+
// (parsed from .gsd-t/domains/*/tasks.md) for debugging. The pre-existing
|
|
2859
|
+
// `index|status|query` subcommands are the codebase entity graph (graph-
|
|
2860
|
+
// indexer) and remain unchanged.
|
|
2861
|
+
const outIdx = args.indexOf("--output");
|
|
2862
|
+
if (outIdx !== -1) {
|
|
2863
|
+
const fmt = args[outIdx + 1] || "json";
|
|
2864
|
+
return doGraphTaskOutput(fmt);
|
|
2865
|
+
}
|
|
2857
2866
|
const sub = args[0] || "status";
|
|
2858
2867
|
switch (sub) {
|
|
2859
2868
|
case "index": doGraphIndex(); break;
|
|
2860
2869
|
case "status": doGraphStatus(); break;
|
|
2861
2870
|
case "query": doGraphQuery(args.slice(1)); break;
|
|
2871
|
+
case "tasks": doGraphTaskOutput(args[1] || "table"); break;
|
|
2862
2872
|
default:
|
|
2863
2873
|
error(`Unknown graph subcommand: ${sub}`);
|
|
2864
|
-
info("Usage: gsd-t graph [index|status|query]");
|
|
2874
|
+
info("Usage: gsd-t graph [index|status|query|tasks]");
|
|
2875
|
+
info(" gsd-t graph --output json|table (task DAG)");
|
|
2865
2876
|
}
|
|
2866
2877
|
}
|
|
2867
2878
|
|
|
2879
|
+
// M44 D1-T4: print the task DAG built by bin/gsd-t-task-graph.cjs.
|
|
2880
|
+
function doGraphTaskOutput(format) {
|
|
2881
|
+
const tg = require("./gsd-t-task-graph.cjs");
|
|
2882
|
+
let graph;
|
|
2883
|
+
try {
|
|
2884
|
+
graph = tg.buildTaskGraph({ projectDir: process.cwd() });
|
|
2885
|
+
} catch (e) {
|
|
2886
|
+
if (e && e.name === "TaskGraphCycleError") {
|
|
2887
|
+
error(`Task graph cycle: ${(e.cycle || []).join(" → ")}`);
|
|
2888
|
+
process.exit(2);
|
|
2889
|
+
}
|
|
2890
|
+
error(e && e.message ? e.message : String(e));
|
|
2891
|
+
process.exit(2);
|
|
2892
|
+
}
|
|
2893
|
+
const fmt = String(format || "table").toLowerCase();
|
|
2894
|
+
if (fmt === "json") {
|
|
2895
|
+
process.stdout.write(JSON.stringify(graph, null, 2) + "\n");
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2898
|
+
if (fmt === "table") {
|
|
2899
|
+
if (!graph.nodes.length) {
|
|
2900
|
+
info("No tasks found in .gsd-t/domains/*/tasks.md");
|
|
2901
|
+
if (graph.warnings.length) graph.warnings.forEach((w) => warn(w));
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2904
|
+
// header
|
|
2905
|
+
const rows = graph.nodes.map((n) => ({
|
|
2906
|
+
id: n.id,
|
|
2907
|
+
domain: n.domain,
|
|
2908
|
+
wave: String(n.wave),
|
|
2909
|
+
status: n.status,
|
|
2910
|
+
ready: graph.ready.indexOf(n.id) !== -1 ? "yes" : "no",
|
|
2911
|
+
deps: n.deps.join(", ") || "-",
|
|
2912
|
+
}));
|
|
2913
|
+
const cols = ["id", "domain", "wave", "status", "ready", "deps"];
|
|
2914
|
+
const widths = {};
|
|
2915
|
+
for (const c of cols) {
|
|
2916
|
+
widths[c] = c.length;
|
|
2917
|
+
for (const r of rows) widths[c] = Math.max(widths[c], String(r[c]).length);
|
|
2918
|
+
}
|
|
2919
|
+
const fmtRow = (r) =>
|
|
2920
|
+
cols.map((c) => String(r[c]).padEnd(widths[c])).join(" ");
|
|
2921
|
+
log(fmtRow(Object.fromEntries(cols.map((c) => [c, c.toUpperCase()]))));
|
|
2922
|
+
log(cols.map((c) => "-".repeat(widths[c])).join(" "));
|
|
2923
|
+
for (const r of rows) log(fmtRow(r));
|
|
2924
|
+
log("");
|
|
2925
|
+
info(`${graph.nodes.length} tasks · ${graph.edges.length} edges · ${graph.ready.length} ready`);
|
|
2926
|
+
if (graph.warnings.length) {
|
|
2927
|
+
log("");
|
|
2928
|
+
warn(`${graph.warnings.length} warning(s):`);
|
|
2929
|
+
graph.warnings.forEach((w) => log(` ${w}`));
|
|
2930
|
+
}
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2933
|
+
error(`Unknown --output format: ${format} (expected: json|table)`);
|
|
2934
|
+
process.exit(2);
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2868
2937
|
// ─── Token-Log Writer (Fix 1, v3.12.12) ─────────────────────────────────────
|
|
2869
2938
|
|
|
2870
2939
|
const _TL_HEADER =
|
|
@@ -3716,6 +3785,8 @@ function showHelp() {
|
|
|
3716
3785
|
log(` ${CYAN}benchmark-orchestrator${RESET} M40 speed gate — compares orchestrator vs in-session wall-clock`);
|
|
3717
3786
|
log(` ${CYAN}stream-feed${RESET} Localhost stream-json watcher (start|status|stop) — M40 D4`);
|
|
3718
3787
|
log(` ${CYAN}design-build${RESET} Deterministic design→code pipeline (elements → widgets → pages)`);
|
|
3788
|
+
log(` ${CYAN}tool-cost${RESET} Per-tool token/cost attribution (M43 D2) — group-by tool|command|domain`);
|
|
3789
|
+
log(` ${CYAN}report tokens${RESET} Generate token-usage optimization report (Run → Iter → CW → Turn → Tool)`);
|
|
3719
3790
|
log(` ${CYAN}help${RESET} Show this help\n`);
|
|
3720
3791
|
log(`${BOLD}Examples:${RESET}`);
|
|
3721
3792
|
log(` ${DIM}$${RESET} npx @tekyzinc/gsd-t install`);
|
|
@@ -3887,6 +3958,13 @@ if (require.main === module) {
|
|
|
3887
3958
|
});
|
|
3888
3959
|
process.exit(res.status == null ? 1 : res.status);
|
|
3889
3960
|
}
|
|
3961
|
+
case "parallel": {
|
|
3962
|
+
// M44 D2 — `gsd-t parallel` wraps M40 orchestrator with task-level
|
|
3963
|
+
// parallelism + mode-aware gating math. Extends, does not replace.
|
|
3964
|
+
const { runCli: runParallelCli } = require(path.join(__dirname, "gsd-t-parallel.cjs"));
|
|
3965
|
+
const code = runParallelCli(args.slice(1), process.env);
|
|
3966
|
+
process.exit(code);
|
|
3967
|
+
}
|
|
3890
3968
|
case "benchmark-orchestrator": {
|
|
3891
3969
|
const { spawnSync } = require("child_process");
|
|
3892
3970
|
const js = path.join(__dirname, "gsd-t-benchmark-orchestrator.js");
|
|
@@ -3967,7 +4045,7 @@ if (require.main === module) {
|
|
|
3967
4045
|
break;
|
|
3968
4046
|
}
|
|
3969
4047
|
case "tokens": {
|
|
3970
|
-
const tkOpts = { projectDir: process.cwd(), since: null, milestone: null, format: 'table', regenerateLog: false };
|
|
4048
|
+
const tkOpts = { projectDir: process.cwd(), since: null, milestone: null, format: 'table', regenerateLog: false, showToolCosts: false };
|
|
3971
4049
|
for (let i = 1; i < args.length; i++) {
|
|
3972
4050
|
const a = args[i];
|
|
3973
4051
|
if (a === '--since' && args[i+1]) { tkOpts.since = args[++i]; }
|
|
@@ -3979,8 +4057,9 @@ if (require.main === module) {
|
|
|
3979
4057
|
else if (a === '--project-dir' && args[i+1]) { tkOpts.projectDir = args[++i]; }
|
|
3980
4058
|
else if (a.startsWith('--project-dir=')) { tkOpts.projectDir = a.slice(14); }
|
|
3981
4059
|
else if (a === '--regenerate-log') { tkOpts.regenerateLog = true; }
|
|
4060
|
+
else if (a === '--show-tool-costs') { tkOpts.showToolCosts = true; }
|
|
3982
4061
|
else if (a === '--help' || a === '-h') {
|
|
3983
|
-
log('Usage: gsd-t tokens [--since YYYY-MM-DD] [--milestone Mxx] [--format table|json]');
|
|
4062
|
+
log('Usage: gsd-t tokens [--since YYYY-MM-DD] [--milestone Mxx] [--format table|json] [--show-tool-costs]');
|
|
3984
4063
|
log(' gsd-t tokens --regenerate-log (rewrite .gsd-t/token-log.md from token-usage.jsonl)');
|
|
3985
4064
|
process.exit(0);
|
|
3986
4065
|
}
|
|
@@ -4008,7 +4087,22 @@ if (require.main === module) {
|
|
|
4008
4087
|
const dashboard = require(path.join(__dirname, 'gsd-t-token-dashboard.cjs'));
|
|
4009
4088
|
dashboard.aggregate(tkOpts)
|
|
4010
4089
|
.then((agg) => {
|
|
4011
|
-
|
|
4090
|
+
const baseOut = tkOpts.format === 'json' ? dashboard.renderJson(agg) : dashboard.renderTable(agg);
|
|
4091
|
+
let out = baseOut;
|
|
4092
|
+
if (tkOpts.showToolCosts) {
|
|
4093
|
+
try {
|
|
4094
|
+
out = out + '\n' + dashboard.renderToolCostsSection({
|
|
4095
|
+
projectDir: tkOpts.projectDir,
|
|
4096
|
+
since: tkOpts.since,
|
|
4097
|
+
milestone: tkOpts.milestone,
|
|
4098
|
+
format: tkOpts.format,
|
|
4099
|
+
});
|
|
4100
|
+
} catch (e) {
|
|
4101
|
+
// Non-fatal: tool-cost section failure doesn't break primary output.
|
|
4102
|
+
out = out + '\n── Top 10 tools by cost ──\n (tool-cost section unavailable: ' + (e.message || String(e)) + ')';
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
log(out);
|
|
4012
4106
|
process.exit(0);
|
|
4013
4107
|
})
|
|
4014
4108
|
.catch((e) => { error(e.message || String(e)); process.exit(3); });
|
|
@@ -4019,6 +4113,62 @@ if (require.main === module) {
|
|
|
4019
4113
|
orchestrator.run(args.slice(1)).catch(e => { console.error(e); process.exit(1); });
|
|
4020
4114
|
break;
|
|
4021
4115
|
}
|
|
4116
|
+
case "tool-cost": {
|
|
4117
|
+
const toolCost = require(path.join(__dirname, 'gsd-t-tool-cost.cjs'));
|
|
4118
|
+
process.exit(toolCost.run(args.slice(1)));
|
|
4119
|
+
break;
|
|
4120
|
+
}
|
|
4121
|
+
case "report": {
|
|
4122
|
+
// Nested dispatch: `gsd-t report tokens [--date YYYY-MM-DD] [--out PATH]`
|
|
4123
|
+
const sub = args[1];
|
|
4124
|
+
if (!sub || sub === '--help' || sub === '-h') {
|
|
4125
|
+
log('Usage: gsd-t report tokens [--date YYYY-MM-DD] [--out PATH]');
|
|
4126
|
+
log('');
|
|
4127
|
+
log('Subcommands:');
|
|
4128
|
+
log(' tokens Generate token-usage optimization report (Run → Iter → CW → Turn → Tool).');
|
|
4129
|
+
process.exit(sub ? 0 : 2);
|
|
4130
|
+
break;
|
|
4131
|
+
}
|
|
4132
|
+
if (sub !== 'tokens') {
|
|
4133
|
+
error(`report: unknown subcommand: ${sub}`);
|
|
4134
|
+
log('Usage: gsd-t report tokens [--date YYYY-MM-DD] [--out PATH]');
|
|
4135
|
+
process.exit(2);
|
|
4136
|
+
break;
|
|
4137
|
+
}
|
|
4138
|
+
const rOpts = { projectDir: process.cwd(), date: null, outPath: null };
|
|
4139
|
+
for (let i = 2; i < args.length; i++) {
|
|
4140
|
+
const a = args[i];
|
|
4141
|
+
if (a === '--date' && args[i+1]) { rOpts.date = args[++i]; }
|
|
4142
|
+
else if (a.startsWith('--date=')) { rOpts.date = a.slice(7); }
|
|
4143
|
+
else if (a === '--out' && args[i+1]) { rOpts.outPath = args[++i]; }
|
|
4144
|
+
else if (a.startsWith('--out=')) { rOpts.outPath = a.slice(6); }
|
|
4145
|
+
else if (a === '--project-dir' && args[i+1]) { rOpts.projectDir = args[++i]; }
|
|
4146
|
+
else if (a.startsWith('--project-dir=')) { rOpts.projectDir = a.slice(14); }
|
|
4147
|
+
else if (a === '--help' || a === '-h') {
|
|
4148
|
+
log('Usage: gsd-t report tokens [--date YYYY-MM-DD] [--out PATH]');
|
|
4149
|
+
log(' --date YYYY-MM-DD Date stamp in output filename (default: today UTC)');
|
|
4150
|
+
log(' --out PATH Output path (default: .gsd-t/reports/token-usage-{DATE}.md)');
|
|
4151
|
+
process.exit(0);
|
|
4152
|
+
}
|
|
4153
|
+
else {
|
|
4154
|
+
error(`report tokens: unknown arg: ${a}`);
|
|
4155
|
+
process.exit(2);
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
try {
|
|
4159
|
+
const rep = require(path.join(__dirname, 'gsd-t-report-tokens.cjs'));
|
|
4160
|
+
const res = rep.generateReport(rOpts);
|
|
4161
|
+
log(`Wrote ${res.path}`);
|
|
4162
|
+
const s = res.summary;
|
|
4163
|
+
log(` ${s.cws} CW(s) · ${s.turns} turn rows · ${s.compactions} compactions · ${s.sessions} session(s)`);
|
|
4164
|
+
log(` ${s.compactionEndedCWs}/${s.cws} CW${s.cws === 1 ? '' : 's'} ended by compaction · top tool: ${s.topTool || '—'}`);
|
|
4165
|
+
process.exit(0);
|
|
4166
|
+
} catch (e) {
|
|
4167
|
+
error(e.message || String(e));
|
|
4168
|
+
process.exit(3);
|
|
4169
|
+
}
|
|
4170
|
+
break;
|
|
4171
|
+
}
|
|
4022
4172
|
case "scan": {
|
|
4023
4173
|
const exportFlag = args.find(a => a.startsWith('--export='));
|
|
4024
4174
|
const exportFormat = exportFlag ? exportFlag.split('=')[1] : null;
|
|
@@ -3,19 +3,26 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* GSD-T Headless Auto-Spawn — Detached headless continuation
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
6
|
+
* Every GSD-T command spawn routes through `autoSpawnHeadless()` to a
|
|
7
|
+
* detached child process running `gsd-t headless {command} --log`. The
|
|
8
|
+
* interactive session never blocks on the child (`child.unref()`), so the
|
|
9
|
+
* user retains their terminal and can work on unrelated tasks. On child
|
|
10
|
+
* completion, a macOS notification fires (T2). The interactive session
|
|
11
|
+
* surfaces the result via a read-back banner on the next `gsd-t-resume`
|
|
12
|
+
* or `gsd-t-status` call (T4).
|
|
13
13
|
*
|
|
14
14
|
* Zero external dependencies (Node.js built-ins only).
|
|
15
15
|
*
|
|
16
|
-
* Contract: .gsd-t/contracts/headless-
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Contract: .gsd-t/contracts/headless-default-contract.md v2.0.0
|
|
17
|
+
* - v2.0.0 (M43 D4): channel-separation invariant. Every command spawns.
|
|
18
|
+
* No opt-out flag, no context-meter threshold gating, no `--in-session`
|
|
19
|
+
* escape hatch. `shouldSpawnHeadless` is a constant `() => true`. The
|
|
20
|
+
* `watch` parameter is accepted for caller backward-compat for one
|
|
21
|
+
* version but ignored (deprecation warning emitted once per process).
|
|
22
|
+
* Consumers: every command file that spawns subagents (execute, wave, quick,
|
|
23
|
+
* integrate, debug, scan, verify, complete-milestone, test-sync,
|
|
24
|
+
* scan, gap-analysis, populate, feature, project, partition);
|
|
25
|
+
* the `/gsd` router (Step 2 action-turn handoff).
|
|
19
26
|
*/
|
|
20
27
|
|
|
21
28
|
const fs = require("fs");
|
|
@@ -43,8 +50,17 @@ module.exports = {
|
|
|
43
50
|
writeSessionFile,
|
|
44
51
|
writeContinueHereFile,
|
|
45
52
|
markSessionCompleted,
|
|
53
|
+
// M43 D4 — channel-separation invariant. The helper is retained for
|
|
54
|
+
// backward-compat with any caller that imported it from a v1.x consumer;
|
|
55
|
+
// it now unconditionally returns true. See headless-default-contract
|
|
56
|
+
// v2.0.0 §Invariants.
|
|
57
|
+
shouldSpawnHeadless: () => true,
|
|
46
58
|
};
|
|
47
59
|
|
|
60
|
+
// M43 D4 — one-shot deprecation banner when a caller still passes `watch`
|
|
61
|
+
// (or the never-shipped `inSession`). Module-level flag avoids log spam.
|
|
62
|
+
let _deprecatedWatchWarned = false;
|
|
63
|
+
|
|
48
64
|
// ── autoSpawnHeadless ────────────────────────────────────────────────────────
|
|
49
65
|
|
|
50
66
|
/**
|
|
@@ -67,7 +83,23 @@ function autoSpawnHeadless(opts) {
|
|
|
67
83
|
const continue_from = opts.continue_from || ".";
|
|
68
84
|
const projectDir = opts.projectDir || process.cwd();
|
|
69
85
|
const context = opts.context || opts.sessionContext || null;
|
|
70
|
-
|
|
86
|
+
// M43 D4 — `watch` is accepted for caller backward-compat but IGNORED.
|
|
87
|
+
// `inSession` was never shipped; accept+ignore for the same reason.
|
|
88
|
+
// Under headless-default-contract v2.0.0 every spawn goes headless; the
|
|
89
|
+
// only in-session surface is the `/gsd` router dialog channel, which is
|
|
90
|
+
// upstream of this function. One-shot deprecation warning on stderr.
|
|
91
|
+
const legacyWatch = opts.watch === true;
|
|
92
|
+
const legacyInSession = opts.inSession === true;
|
|
93
|
+
if ((legacyWatch || legacyInSession) && !_deprecatedWatchWarned) {
|
|
94
|
+
_deprecatedWatchWarned = true;
|
|
95
|
+
try {
|
|
96
|
+
process.stderr.write(
|
|
97
|
+
"[headless-default] `watch`/`inSession` flag is deprecated under headless-default-contract v2.0.0 — every spawn is headless; caller hint ignored.\n",
|
|
98
|
+
);
|
|
99
|
+
} catch (_) {
|
|
100
|
+
/* best-effort */
|
|
101
|
+
}
|
|
102
|
+
}
|
|
71
103
|
const spawnType = opts.spawnType || "primary";
|
|
72
104
|
|
|
73
105
|
if (!command || typeof command !== "string") {
|
|
@@ -79,44 +111,6 @@ function autoSpawnHeadless(opts) {
|
|
|
79
111
|
);
|
|
80
112
|
}
|
|
81
113
|
|
|
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
|
-
// M39 D2 — append watch-progress tree below banner (best-effort).
|
|
88
|
-
// Banner here is the in-context-fallback signal printed by the caller;
|
|
89
|
-
// we don't own it, so we only render the tree to stdout. Never throws.
|
|
90
|
-
try {
|
|
91
|
-
const wp = require("./watch-progress.js");
|
|
92
|
-
const stateDir = path.join(projectDir, ".gsd-t", ".watch-state");
|
|
93
|
-
const tree = wp.buildTree(stateDir);
|
|
94
|
-
const rendered = wp.renderTree(tree, { currentAgent: null });
|
|
95
|
-
if (rendered) {
|
|
96
|
-
// eslint-disable-next-line no-console
|
|
97
|
-
console.log(rendered);
|
|
98
|
-
}
|
|
99
|
-
} catch (_) {
|
|
100
|
-
/* watch-progress is best-effort; never crash the watch */
|
|
101
|
-
}
|
|
102
|
-
return {
|
|
103
|
-
id: null,
|
|
104
|
-
pid: null,
|
|
105
|
-
logPath: null,
|
|
106
|
-
timestamp: new Date().toISOString(),
|
|
107
|
-
mode: "in-context",
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
if (watch && spawnType === "validation") {
|
|
111
|
-
try {
|
|
112
|
-
process.stderr.write(
|
|
113
|
-
`[headless-default] --watch ignored for validation spawn type: ${spawnType}\n`,
|
|
114
|
-
);
|
|
115
|
-
} catch (_) {
|
|
116
|
-
/* best effort */
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
114
|
const timestamp = new Date().toISOString();
|
|
121
115
|
const id = makeSessionId(command, new Date());
|
|
122
116
|
const logPath = path.join(projectDir, LOG_DIR_REL, `headless-${id}.log`);
|
|
@@ -124,6 +118,32 @@ function autoSpawnHeadless(opts) {
|
|
|
124
118
|
ensureDir(path.join(projectDir, LOG_DIR_REL));
|
|
125
119
|
ensureDir(path.join(projectDir, SESSIONS_DIR_REL));
|
|
126
120
|
|
|
121
|
+
// M43 D6-T4 — Ensure dashboard is running (idempotent; no-op if already up).
|
|
122
|
+
// Must happen BEFORE the URL banner print (D6-T3) so the link is live.
|
|
123
|
+
// Never throws — autostart is best-effort.
|
|
124
|
+
let autostartInfo = null;
|
|
125
|
+
try {
|
|
126
|
+
const { ensureDashboardRunning } = require("../scripts/gsd-t-dashboard-autostart.cjs");
|
|
127
|
+
autostartInfo = ensureDashboardRunning({ projectDir });
|
|
128
|
+
} catch (_) {
|
|
129
|
+
/* best-effort; fall through without banner port info */
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// M43 D6-T3 — Live transcript URL banner. Printed for every spawn so the
|
|
133
|
+
// viewer at :PORT is "the" primary watching surface. Never throws.
|
|
134
|
+
// Text is coordinated with D4 — exact line shape is part of
|
|
135
|
+
// dashboard-server-contract.md §Banner Format.
|
|
136
|
+
try {
|
|
137
|
+
let port = autostartInfo && autostartInfo.port;
|
|
138
|
+
if (!port) {
|
|
139
|
+
const { projectScopedDefaultPort } = require("../scripts/gsd-t-dashboard-server.js");
|
|
140
|
+
port = projectScopedDefaultPort(projectDir);
|
|
141
|
+
}
|
|
142
|
+
process.stdout.write(`▶ Live transcript: http://127.0.0.1:${port}/transcript/${id}\n`);
|
|
143
|
+
} catch (_) {
|
|
144
|
+
/* best-effort — never crash the spawn on banner failure */
|
|
145
|
+
}
|
|
146
|
+
|
|
127
147
|
// Handoff-lock gate (m36 gap-fix T2). Only engaged when the caller
|
|
128
148
|
// supplies a `sessionId` — existing callers that do not pass one keep
|
|
129
149
|
// the pre-m36 behavior unchanged. When engaged, the lock is held
|
|
@@ -43,8 +43,13 @@ module.exports = {
|
|
|
43
43
|
writeSessionFile,
|
|
44
44
|
writeContinueHereFile,
|
|
45
45
|
markSessionCompleted,
|
|
46
|
+
// M43 D4 — channel-separation invariant; see .cjs twin.
|
|
47
|
+
shouldSpawnHeadless: () => true,
|
|
46
48
|
};
|
|
47
49
|
|
|
50
|
+
// M43 D4 — one-shot deprecation banner (legacy `.js` copy mirrors `.cjs`).
|
|
51
|
+
let _deprecatedWatchWarned = false;
|
|
52
|
+
|
|
48
53
|
// ── autoSpawnHeadless ────────────────────────────────────────────────────────
|
|
49
54
|
|
|
50
55
|
/**
|
|
@@ -67,7 +72,19 @@ function autoSpawnHeadless(opts) {
|
|
|
67
72
|
const continue_from = opts.continue_from || ".";
|
|
68
73
|
const projectDir = opts.projectDir || process.cwd();
|
|
69
74
|
const context = opts.context || opts.sessionContext || null;
|
|
70
|
-
|
|
75
|
+
// M43 D4 — `watch`/`inSession` ignored under v2.0.0 channel-separation.
|
|
76
|
+
const legacyWatch = opts.watch === true;
|
|
77
|
+
const legacyInSession = opts.inSession === true;
|
|
78
|
+
if ((legacyWatch || legacyInSession) && !_deprecatedWatchWarned) {
|
|
79
|
+
_deprecatedWatchWarned = true;
|
|
80
|
+
try {
|
|
81
|
+
process.stderr.write(
|
|
82
|
+
"[headless-default] `watch`/`inSession` flag is deprecated under headless-default-contract v2.0.0 — every spawn is headless; caller hint ignored.\n",
|
|
83
|
+
);
|
|
84
|
+
} catch (_) {
|
|
85
|
+
/* best-effort */
|
|
86
|
+
}
|
|
87
|
+
}
|
|
71
88
|
const spawnType = opts.spawnType || "primary";
|
|
72
89
|
|
|
73
90
|
if (!command || typeof command !== "string") {
|
|
@@ -79,29 +96,6 @@ function autoSpawnHeadless(opts) {
|
|
|
79
96
|
);
|
|
80
97
|
}
|
|
81
98
|
|
|
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
|
-
}
|
|
104
|
-
|
|
105
99
|
const timestamp = new Date().toISOString();
|
|
106
100
|
const id = makeSessionId(command, new Date());
|
|
107
101
|
const logPath = path.join(projectDir, LOG_DIR_REL, `headless-${id}.log`);
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* GSD-T Runway Estimator (M43 D5 — dialog-channel growth meter)
|
|
4
|
+
*
|
|
5
|
+
* Under M43's channel-separation model, the only thing that runs in the
|
|
6
|
+
* in-session channel is the `/gsd` router dialog. Everything else spawns.
|
|
7
|
+
* This module reads the per-turn usage rows the M43 D1 capture writes to
|
|
8
|
+
* `.gsd-t/metrics/token-usage.jsonl` (schema v2) and surfaces a one-line
|
|
9
|
+
* "~N turns to `/compact`" warning to the router.
|
|
10
|
+
*
|
|
11
|
+
* Scope (revised 2026-04-21 per M43 partition.md §D5):
|
|
12
|
+
* - Read-only. Never refuses, never reroutes. Under always-headless there
|
|
13
|
+
* is nothing to reroute *to*.
|
|
14
|
+
* - Median-of-deltas growth slope (outlier-resistant; a single spike turn
|
|
15
|
+
* does not flip `shouldWarn`).
|
|
16
|
+
* - Zero external deps. `.cjs` so it loads in both ESM-default projects
|
|
17
|
+
* and CJS projects without transpilation.
|
|
18
|
+
*
|
|
19
|
+
* Contracts:
|
|
20
|
+
* - .gsd-t/contracts/context-meter-contract.md §Dialog Growth Meter
|
|
21
|
+
* - .gsd-t/contracts/metrics-schema-contract.md (schema v2)
|
|
22
|
+
*
|
|
23
|
+
* Consumers:
|
|
24
|
+
* - commands/gsd.md (router warning footer)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
const DEFAULT_K = 5;
|
|
31
|
+
const DEFAULT_MODEL_CONTEXT_CAP = 200000;
|
|
32
|
+
// Claude Code starts auto-compacting ~8% before the model window fills, so the
|
|
33
|
+
// effective dialog ceiling is 0.92 × modelContextCap.
|
|
34
|
+
const PRE_COMPACT_HEADROOM = 0.92;
|
|
35
|
+
const DEFAULT_WARN_THRESHOLD_TURNS = 5;
|
|
36
|
+
const MIN_HISTORY = 3;
|
|
37
|
+
|
|
38
|
+
// ── Row loading ──────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function _safeParse(line) {
|
|
41
|
+
const s = String(line || '').trim();
|
|
42
|
+
if (!s || s[0] !== '{') return null;
|
|
43
|
+
try { return JSON.parse(s); } catch (_) { return null; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Load in-session rows for the given session from the canonical sink.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} projectDir
|
|
50
|
+
* @param {string} sessionId
|
|
51
|
+
* @returns {object[]} schema-v2 rows, unsorted
|
|
52
|
+
*/
|
|
53
|
+
function _loadInSessionRows(projectDir, sessionId) {
|
|
54
|
+
const p = path.join(projectDir, '.gsd-t', 'metrics', 'token-usage.jsonl');
|
|
55
|
+
if (!fs.existsSync(p)) return [];
|
|
56
|
+
let text;
|
|
57
|
+
try { text = fs.readFileSync(p, 'utf8'); } catch (_) { return []; }
|
|
58
|
+
const rows = [];
|
|
59
|
+
for (const line of text.split(/\r?\n/)) {
|
|
60
|
+
const j = _safeParse(line);
|
|
61
|
+
if (!j) continue;
|
|
62
|
+
if (j.sessionType !== 'in-session') continue;
|
|
63
|
+
if (sessionId != null && j.session_id !== sessionId) continue;
|
|
64
|
+
rows.push(j);
|
|
65
|
+
}
|
|
66
|
+
return rows;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Math helpers ─────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
function _median(arr) {
|
|
72
|
+
if (!arr.length) return 0;
|
|
73
|
+
const sorted = arr.slice().sort((a, b) => a - b);
|
|
74
|
+
const mid = Math.floor(sorted.length / 2);
|
|
75
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Deterministic ordering for per-turn rows. Prefers timestamp (`ts`) when
|
|
80
|
+
* present (monotonic per session), falls back to `turn_id` string compare.
|
|
81
|
+
*/
|
|
82
|
+
function _sortTurns(rows) {
|
|
83
|
+
return rows.slice().sort((a, b) => {
|
|
84
|
+
const ta = a.ts || '';
|
|
85
|
+
const tb = b.ts || '';
|
|
86
|
+
if (ta !== tb) return ta < tb ? -1 : 1;
|
|
87
|
+
const ia = String(a.turn_id || '');
|
|
88
|
+
const ib = String(b.turn_id || '');
|
|
89
|
+
if (ia !== ib) return ia < ib ? -1 : 1;
|
|
90
|
+
return 0;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Compute the dialog-growth signal for one session.
|
|
96
|
+
*
|
|
97
|
+
* Reads the last K in-session turns for `sessionId` from
|
|
98
|
+
* `.gsd-t/metrics/token-usage.jsonl`, computes the median of turn-over-turn
|
|
99
|
+
* `input_tokens` deltas (robust to single-turn spikes), then predicts how
|
|
100
|
+
* many turns remain before dialog input crosses the pre-auto-compact ceiling
|
|
101
|
+
* (`modelContextCap × 0.92`).
|
|
102
|
+
*
|
|
103
|
+
* Returns `{ shouldWarn: false, reason: 'insufficient_history' }` when fewer
|
|
104
|
+
* than `MIN_HISTORY` in-session turns exist for the session.
|
|
105
|
+
*
|
|
106
|
+
* @param {object} opts
|
|
107
|
+
* @param {string} opts.projectDir
|
|
108
|
+
* @param {string} opts.sessionId required
|
|
109
|
+
* @param {number} [opts.k] default 5 (last K turns)
|
|
110
|
+
* @param {number} [opts.modelContextCap] default 200000
|
|
111
|
+
* @param {number} [opts.warnThresholdTurns] default 5
|
|
112
|
+
* @returns {{
|
|
113
|
+
* shouldWarn: boolean,
|
|
114
|
+
* slope: number,
|
|
115
|
+
* median_delta: number,
|
|
116
|
+
* latest_input_tokens: number,
|
|
117
|
+
* predicted_turns_to_compact: number,
|
|
118
|
+
* k: number,
|
|
119
|
+
* history_len: number,
|
|
120
|
+
* reason?: string
|
|
121
|
+
* }}
|
|
122
|
+
*/
|
|
123
|
+
function estimateDialogGrowth(opts) {
|
|
124
|
+
const projectDir = (opts && opts.projectDir) || '.';
|
|
125
|
+
const sessionId = opts && opts.sessionId;
|
|
126
|
+
const k = (opts && Number.isFinite(opts.k) && opts.k > 0) ? Math.floor(opts.k) : DEFAULT_K;
|
|
127
|
+
const cap = (opts && Number.isFinite(opts.modelContextCap) && opts.modelContextCap > 0)
|
|
128
|
+
? opts.modelContextCap
|
|
129
|
+
: DEFAULT_MODEL_CONTEXT_CAP;
|
|
130
|
+
const warnThreshold = (opts && Number.isFinite(opts.warnThresholdTurns) && opts.warnThresholdTurns > 0)
|
|
131
|
+
? opts.warnThresholdTurns
|
|
132
|
+
: DEFAULT_WARN_THRESHOLD_TURNS;
|
|
133
|
+
|
|
134
|
+
const empty = {
|
|
135
|
+
shouldWarn: false,
|
|
136
|
+
slope: 0,
|
|
137
|
+
median_delta: 0,
|
|
138
|
+
latest_input_tokens: 0,
|
|
139
|
+
predicted_turns_to_compact: Infinity,
|
|
140
|
+
k,
|
|
141
|
+
history_len: 0,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
if (!sessionId) {
|
|
145
|
+
return { ...empty, reason: 'missing_session_id' };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const allRows = _loadInSessionRows(projectDir, sessionId);
|
|
149
|
+
if (allRows.length === 0) {
|
|
150
|
+
return { ...empty, reason: 'no_rows' };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const sorted = _sortTurns(allRows);
|
|
154
|
+
const window = sorted.slice(-k);
|
|
155
|
+
|
|
156
|
+
if (window.length < MIN_HISTORY) {
|
|
157
|
+
return { ...empty, history_len: window.length, reason: 'insufficient_history' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Per-turn input token footprint. Schema v2 writes `inputTokens` (camel)
|
|
161
|
+
// via the token-capture wrapper; older rows may carry the raw envelope.
|
|
162
|
+
const inputs = window.map(r => {
|
|
163
|
+
if (Number.isFinite(r.inputTokens)) return r.inputTokens;
|
|
164
|
+
if (r.usage && Number.isFinite(r.usage.input_tokens)) return r.usage.input_tokens;
|
|
165
|
+
return 0;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const deltas = [];
|
|
169
|
+
for (let i = 1; i < inputs.length; i++) {
|
|
170
|
+
deltas.push(inputs[i] - inputs[i - 1]);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const median_delta = _median(deltas);
|
|
174
|
+
const slope = median_delta;
|
|
175
|
+
const latest_input_tokens = inputs[inputs.length - 1];
|
|
176
|
+
|
|
177
|
+
const ceiling = cap * PRE_COMPACT_HEADROOM;
|
|
178
|
+
let predicted_turns_to_compact;
|
|
179
|
+
if (slope > 0) {
|
|
180
|
+
const headroom = ceiling - latest_input_tokens;
|
|
181
|
+
predicted_turns_to_compact = headroom <= 0 ? 0 : Math.ceil(headroom / slope);
|
|
182
|
+
} else {
|
|
183
|
+
predicted_turns_to_compact = Infinity;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const shouldWarn = Number.isFinite(predicted_turns_to_compact) && predicted_turns_to_compact <= warnThreshold;
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
shouldWarn,
|
|
190
|
+
slope,
|
|
191
|
+
median_delta,
|
|
192
|
+
latest_input_tokens,
|
|
193
|
+
predicted_turns_to_compact,
|
|
194
|
+
k,
|
|
195
|
+
history_len: window.length,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = {
|
|
200
|
+
estimateDialogGrowth,
|
|
201
|
+
// Exposed for unit tests; not part of the stable contract.
|
|
202
|
+
_internal: {
|
|
203
|
+
_median,
|
|
204
|
+
_sortTurns,
|
|
205
|
+
_loadInSessionRows,
|
|
206
|
+
DEFAULT_K,
|
|
207
|
+
DEFAULT_MODEL_CONTEXT_CAP,
|
|
208
|
+
PRE_COMPACT_HEADROOM,
|
|
209
|
+
DEFAULT_WARN_THRESHOLD_TURNS,
|
|
210
|
+
MIN_HISTORY,
|
|
211
|
+
},
|
|
212
|
+
};
|