@tekyzinc/gsd-t 3.16.12 → 3.18.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/CHANGELOG.md +61 -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-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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/**
|
|
4
|
+
* GSD-T In-Session Usage Hook (M43 D1-T2)
|
|
5
|
+
*
|
|
6
|
+
* Installed into `~/.claude/settings.json` (Stop + SessionEnd) to capture
|
|
7
|
+
* per-turn token usage for the dialog channel. Reads the hook payload from
|
|
8
|
+
* stdin, resolves the project directory from `payload.cwd`, then delegates to
|
|
9
|
+
* `bin/gsd-t-in-session-usage.cjs::processHookPayload` which reads the
|
|
10
|
+
* transcript at `payload.transcript_path` and appends schema-v2 rows to
|
|
11
|
+
* `.gsd-t/metrics/token-usage.jsonl`.
|
|
12
|
+
*
|
|
13
|
+
* Silent on success (hooks run async in Claude Code — stdout is ignored).
|
|
14
|
+
* Swallows every error: a flaky capture must never break the user's session.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const DEFAULT_SCRIPT_GUARD_MS = 5000;
|
|
21
|
+
const started = Date.now();
|
|
22
|
+
|
|
23
|
+
function _readStdin() {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
let buf = '';
|
|
26
|
+
process.stdin.setEncoding('utf8');
|
|
27
|
+
process.stdin.on('data', chunk => { buf += chunk; });
|
|
28
|
+
process.stdin.on('end', () => resolve(buf));
|
|
29
|
+
process.stdin.on('error', () => resolve(buf));
|
|
30
|
+
setTimeout(() => resolve(buf), DEFAULT_SCRIPT_GUARD_MS).unref();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function _parsePayload(raw) {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(raw || '{}');
|
|
37
|
+
} catch (_) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function _resolveProjectDir(payload) {
|
|
43
|
+
if (payload && payload.cwd && fs.existsSync(payload.cwd)) return payload.cwd;
|
|
44
|
+
if (process.cwd) return process.cwd();
|
|
45
|
+
return '.';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function _resolveInSessionModule(projectDir) {
|
|
49
|
+
const candidates = [
|
|
50
|
+
path.join(projectDir, 'bin', 'gsd-t-in-session-usage.cjs'),
|
|
51
|
+
path.join(process.env.HOME || '', '.claude', 'gsd-t', 'bin', 'gsd-t-in-session-usage.cjs'),
|
|
52
|
+
];
|
|
53
|
+
for (const p of candidates) {
|
|
54
|
+
if (fs.existsSync(p)) return p;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function main() {
|
|
60
|
+
try {
|
|
61
|
+
const raw = await _readStdin();
|
|
62
|
+
const payload = _parsePayload(raw);
|
|
63
|
+
if (!payload) return;
|
|
64
|
+
if (payload.hook_event_name !== 'Stop' && payload.hook_event_name !== 'SessionEnd') return;
|
|
65
|
+
|
|
66
|
+
const projectDir = _resolveProjectDir(payload);
|
|
67
|
+
const modulePath = _resolveInSessionModule(projectDir);
|
|
68
|
+
if (!modulePath) return;
|
|
69
|
+
|
|
70
|
+
const mod = require(modulePath);
|
|
71
|
+
mod.processHookPayload({ projectDir, payload });
|
|
72
|
+
} catch (_) {
|
|
73
|
+
// Intentionally swallow — hook must never interrupt the user session.
|
|
74
|
+
} finally {
|
|
75
|
+
const elapsed = Date.now() - started;
|
|
76
|
+
if (elapsed > DEFAULT_SCRIPT_GUARD_MS) process.exitCode = 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (require.main === module) {
|
|
81
|
+
main();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { _internal: { _parsePayload, _resolveProjectDir } };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GSD-T Spawn Plan — token formatting + cumulative totals (M44 D8 T7)
|
|
5
|
+
*
|
|
6
|
+
* Pure helper module used by the transcript right-side spawn-plan panel.
|
|
7
|
+
* Extracted from the inline script in `scripts/gsd-t-transcript.html` so the
|
|
8
|
+
* same functions can be unit-tested under Node without loading a DOM.
|
|
9
|
+
*
|
|
10
|
+
* Contract: .gsd-t/contracts/spawn-plan-contract.md v1.0.0
|
|
11
|
+
* - Token cell format: `in=12.5k out=1.7k $0.42` (k-suffix above 1000,
|
|
12
|
+
* 2-decimal USD). Returns `—` when tokens are null/missing.
|
|
13
|
+
* - Cumulative totals: sum `{in,out,cr,cc,cost_usd}` across done tasks.
|
|
14
|
+
* Returns `null` when no done-task has tokens (panel renders `—`).
|
|
15
|
+
*
|
|
16
|
+
* Zero external deps. `.cjs` so it loads in both ESM-default and CJS projects.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* k-suffix number formatter. `1500` → `1.5k`, `999` → `999`, `null` → `0`.
|
|
21
|
+
* Used by `fmtTokens` to compact large counts.
|
|
22
|
+
*
|
|
23
|
+
* @param {number|string|null|undefined} n
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
function fmtK(n) {
|
|
27
|
+
if (n == null || Number.isNaN(Number(n))) return '0';
|
|
28
|
+
const num = Number(n);
|
|
29
|
+
if (Math.abs(num) >= 1000) return (num / 1000).toFixed(1) + 'k';
|
|
30
|
+
return String(num);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Render a token-cell string from a `{in, out, cr, cc, cost_usd}` object.
|
|
35
|
+
* When `tokens` is null/undefined/non-object, returns the em-dash marker
|
|
36
|
+
* per the "zero is a measurement, dash is acknowledged gap" rule.
|
|
37
|
+
*
|
|
38
|
+
* @param {object|null|undefined} tokens
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
function fmtTokens(tokens) {
|
|
42
|
+
if (!tokens || typeof tokens !== 'object') return '—';
|
|
43
|
+
const ins = fmtK(tokens.in);
|
|
44
|
+
const out = fmtK(tokens.out);
|
|
45
|
+
const cost = (typeof tokens.cost_usd === 'number')
|
|
46
|
+
? '$' + tokens.cost_usd.toFixed(2)
|
|
47
|
+
: '';
|
|
48
|
+
return ['in=' + ins, 'out=' + out, cost].filter(Boolean).join(' ');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Sum the `tokens` field across every task whose `status === 'done'` and
|
|
53
|
+
* whose `tokens` is a populated object. Returns null when no done-task has
|
|
54
|
+
* a tokens object attached — the panel renders that state as `—`.
|
|
55
|
+
*
|
|
56
|
+
* @param {Array<{status?:string, tokens?:object}>|null|undefined} tasks
|
|
57
|
+
* @returns {{in:number,out:number,cr:number,cc:number,cost_usd:number}|null}
|
|
58
|
+
*/
|
|
59
|
+
function sumTokens(tasks) {
|
|
60
|
+
const acc = { in: 0, out: 0, cr: 0, cc: 0, cost_usd: 0 };
|
|
61
|
+
let hit = false;
|
|
62
|
+
for (const t of (tasks || [])) {
|
|
63
|
+
if (!t || t.status !== 'done' || !t.tokens) continue;
|
|
64
|
+
acc.in += Number(t.tokens.in || 0);
|
|
65
|
+
acc.out += Number(t.tokens.out || 0);
|
|
66
|
+
acc.cr += Number(t.tokens.cr || 0);
|
|
67
|
+
acc.cc += Number(t.tokens.cc || 0);
|
|
68
|
+
acc.cost_usd += Number(t.tokens.cost_usd || 0);
|
|
69
|
+
hit = true;
|
|
70
|
+
}
|
|
71
|
+
if (!hit) return null;
|
|
72
|
+
acc.cost_usd = Math.round(acc.cost_usd * 100) / 100;
|
|
73
|
+
return acc;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
fmtK,
|
|
78
|
+
fmtTokens,
|
|
79
|
+
sumTokens,
|
|
80
|
+
};
|
|
@@ -322,10 +322,15 @@ No command file ships a bare `Task(...)` or `claude -p` line outside of a wrappe
|
|
|
322
322
|
|
|
323
323
|
Rationale: the pre-M41 convention silently wrote `N/A` tokens because no caller parsed the `usage` envelope. The wrapper is the single place that parses it. Bypassing the wrapper re-introduces blind spots.
|
|
324
324
|
|
|
325
|
-
## Headless
|
|
325
|
+
## Always-Headless Spawn (M43 D4, v3.16.x+) — Channel Separation
|
|
326
326
|
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
Every GSD-T command spawns detached, unconditionally. There is no `--watch`, no `--in-session`, no `--headless` opt-in, no context-meter threshold that reroutes, no low-water-mark bypass. The dialog channel is reserved for human↔Claude conversation; everything else is a detached headless child. Interactive session shows a launch banner + live-transcript URL + event-stream path, then exits. Results surface via the read-back banner on the user's next message.
|
|
328
|
+
|
|
329
|
+
The only in-session surface is the `/gsd` router (`commands/gsd.md`), and only for dialog-only exploratory turns. The moment Step 2.5 classifies a turn as `workflow`, the router hands off to a detached spawn.
|
|
330
|
+
|
|
331
|
+
Legacy `watch` / `inSession` params are accepted-and-ignored with a one-shot stderr deprecation warning (scheduled removal in v3.0.0 of the contract). `shouldSpawnHeadless` is a constant `() => true`.
|
|
332
|
+
|
|
333
|
+
Contract: `.gsd-t/contracts/headless-default-contract.md` v2.0.0 (see also `unattended-event-stream-contract.md`, `unattended-supervisor-contract.md`).
|
|
329
334
|
|
|
330
335
|
## API Documentation Guard (Swagger/OpenAPI)
|
|
331
336
|
|
|
@@ -26,24 +26,27 @@
|
|
|
26
26
|
<!-- Claude will verify the branch before every commit. -->
|
|
27
27
|
**Expected branch**: {main | master | feature-branch-name}
|
|
28
28
|
|
|
29
|
-
## Context Meter (M34/M38, v3.
|
|
29
|
+
## Context Meter (M34/M38/M43 D4, v3.16.x+) — Observational Only
|
|
30
30
|
<!-- The Context Meter is a PostToolUse hook that uses local token estimation -->
|
|
31
31
|
<!-- and writes the current context % to .gsd-t/.context-meter-state.json. -->
|
|
32
|
-
<!--
|
|
33
|
-
<!--
|
|
34
|
-
<!--
|
|
35
|
-
<!--
|
|
36
|
-
<!--
|
|
37
|
-
<!-- Config: .gsd-t/context-meter-config.json — modelWindowSize, thresholdPct, checkFrequency. -->
|
|
32
|
+
<!-- Under M43 D4 (channel-separation inversion) the meter is OBSERVATIONAL ONLY: -->
|
|
33
|
+
<!-- the pct is recorded into the token-log Ctx% column on the next spawn, -->
|
|
34
|
+
<!-- but no threshold gates any routing decision. Every command spawns detached -->
|
|
35
|
+
<!-- unconditionally (see Always-Headless section below). -->
|
|
36
|
+
<!-- Config: .gsd-t/context-meter-config.json — modelWindowSize, checkFrequency. -->
|
|
38
37
|
<!-- Verify: `npx @tekyzinc/gsd-t doctor`. -->
|
|
39
38
|
|
|
40
|
-
## Headless
|
|
41
|
-
<!--
|
|
42
|
-
<!--
|
|
43
|
-
<!--
|
|
44
|
-
<!--
|
|
45
|
-
<!--
|
|
46
|
-
<!--
|
|
39
|
+
## Always-Headless Spawn (M43 D4, v3.16.x+) — Channel Separation
|
|
40
|
+
<!-- Every GSD-T command spawns detached, unconditionally. No --watch flag. -->
|
|
41
|
+
<!-- No --in-session flag. No --headless opt-in. No context-meter threshold -->
|
|
42
|
+
<!-- that reroutes. The dialog channel is reserved for human↔Claude conversation; -->
|
|
43
|
+
<!-- every workflow turn is a detached headless child. Interactive session shows -->
|
|
44
|
+
<!-- a launch banner + live-transcript URL + event-stream path, then exits. -->
|
|
45
|
+
<!-- Results surface via the read-back banner on the user's next message. -->
|
|
46
|
+
<!-- The only in-session surface is the /gsd router, and only for dialog-only -->
|
|
47
|
+
<!-- exploratory turns (Step 2.5 classifier → `conversational`). -->
|
|
48
|
+
<!-- Legacy watch/inSession params on autoSpawnHeadless() are accepted-and-ignored. -->
|
|
49
|
+
<!-- Contracts: headless-default-contract.md v2.0.0, unattended-event-stream-contract.md v1.0.0, -->
|
|
47
50
|
<!-- unattended-supervisor-contract.md v1.1.0. -->
|
|
48
51
|
|
|
49
52
|
## Model Selection
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# GSD-T Post-Commit Spawn-Plan Hook — installed by `gsd-t init`
|
|
3
|
+
#
|
|
4
|
+
# This is a thin shim that delegates to the canonical hook script
|
|
5
|
+
# shipped with GSD-T. If your project doesn't yet have the canonical
|
|
6
|
+
# script, this template copy handles commits by itself; once the
|
|
7
|
+
# framework script is in place, the framework wins.
|
|
8
|
+
#
|
|
9
|
+
# Contract: .gsd-t/contracts/spawn-plan-contract.md v1.0.0
|
|
10
|
+
|
|
11
|
+
set +e # silent-fail — never break the commit
|
|
12
|
+
|
|
13
|
+
PROJECT_DIR="$(git rev-parse --show-toplevel 2>/dev/null)"
|
|
14
|
+
if [ -z "$PROJECT_DIR" ] || [ ! -d "$PROJECT_DIR/.gsd-t/spawns" ]; then
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
CANONICAL="$PROJECT_DIR/scripts/gsd-t-post-commit-spawn-plan.sh"
|
|
19
|
+
if [ -x "$CANONICAL" ]; then
|
|
20
|
+
# Delegate to the framework-shipped hook.
|
|
21
|
+
"$CANONICAL"
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
UPDATER="$PROJECT_DIR/bin/spawn-plan-status-updater.cjs"
|
|
26
|
+
if [ ! -f "$UPDATER" ]; then
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
31
|
+
echo "[spawn-plan-hook] node not found — skipping" 1>&2
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
COMMIT_SHA="$(git rev-parse --short HEAD 2>/dev/null)"
|
|
36
|
+
COMMIT_MSG="$(git log -1 --format=%B 2>/dev/null)"
|
|
37
|
+
if [ -z "$COMMIT_SHA" ] || [ -z "$COMMIT_MSG" ]; then
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
TASK_IDS="$(printf '%s' "$COMMIT_MSG" | grep -oE '\[M[0-9]+-D[0-9]+-T[0-9]+\]' | sed 's/[][]//g' | awk '!seen[$0]++')"
|
|
42
|
+
if [ -z "$TASK_IDS" ]; then
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
printf '%s\n' "$TASK_IDS" | node -e '
|
|
47
|
+
"use strict";
|
|
48
|
+
try {
|
|
49
|
+
const path = require("path");
|
|
50
|
+
const fs = require("fs");
|
|
51
|
+
const projectDir = process.argv[1];
|
|
52
|
+
const commit = process.argv[2];
|
|
53
|
+
let raw = "";
|
|
54
|
+
process.stdin.setEncoding("utf8");
|
|
55
|
+
process.stdin.on("data", (c) => { raw += c; });
|
|
56
|
+
process.stdin.on("end", () => {
|
|
57
|
+
try {
|
|
58
|
+
const taskIds = raw.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
59
|
+
if (!taskIds.length) process.exit(0);
|
|
60
|
+
const updater = require(path.join(projectDir, "bin", "spawn-plan-status-updater.cjs"));
|
|
61
|
+
const activePaths = updater.listActivePlans(projectDir);
|
|
62
|
+
if (!activePaths.length) process.exit(0);
|
|
63
|
+
for (const fp of activePaths) {
|
|
64
|
+
let plan;
|
|
65
|
+
try { plan = JSON.parse(fs.readFileSync(fp, "utf8")); } catch (_) { continue; }
|
|
66
|
+
const spawnId = plan && plan.spawnId;
|
|
67
|
+
const spawnStartedAt = plan && plan.startedAt;
|
|
68
|
+
if (!spawnId) continue;
|
|
69
|
+
const planTaskIds = new Set((plan.tasks || []).map((t) => t && t.id).filter(Boolean));
|
|
70
|
+
for (const taskId of taskIds) {
|
|
71
|
+
if (!planTaskIds.has(taskId)) continue;
|
|
72
|
+
const tokens = updater.sumTokensForTask({ projectDir, taskId, spawnStartedAt });
|
|
73
|
+
updater.markTaskDone({ spawnId, taskId, commit, tokens, projectDir });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
try { process.stderr.write("[spawn-plan-hook] " + String(err && err.message || err) + "\n"); } catch (_) { /* silent */ }
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
} catch (err) {
|
|
81
|
+
try { process.stderr.write("[spawn-plan-hook] " + String(err && err.message || err) + "\n"); } catch (_) { /* silent */ }
|
|
82
|
+
}
|
|
83
|
+
' "$PROJECT_DIR" "$COMMIT_SHA" 2>/dev/null
|
|
84
|
+
|
|
85
|
+
exit 0
|