@tekyzinc/gsd-t 3.12.10 → 3.12.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 +37 -0
- package/bin/gsd-t-unattended.cjs +55 -1
- package/bin/gsd-t.js +116 -7
- package/bin/headless-auto-spawn.cjs +58 -2
- package/package.json +1 -1
- package/scripts/gsd-t-event-writer.js +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [3.12.12] - 2026-04-17
|
|
6
|
+
|
|
7
|
+
### Fixed — Token-Log Observability for Headless/Unattended Workers
|
|
8
|
+
|
|
9
|
+
**Background**: M38 headless-by-default left `.gsd-t/token-log.md` blind to all supervisor and headless-exec worker activity. Rows were only written by interactive `T_START/T_END` bash blocks in command files. All event-stream `tool_call` entries from workers had `command: null`, `phase: null`, `trace_id: null`.
|
|
10
|
+
|
|
11
|
+
#### Fix 1: headless worker spawns append to token-log.md
|
|
12
|
+
|
|
13
|
+
Three spawn paths now write rows to `{projectDir}/.gsd-t/token-log.md`:
|
|
14
|
+
|
|
15
|
+
- **`bin/headless-auto-spawn.cjs`** — `installCompletionWatcher` appends a row when the detached child exits (poll-based, graceful — never halts on write failure). Creates the file with the canonical header if it does not exist. Migrates files created before this fix (adds header if missing).
|
|
16
|
+
- **`bin/gsd-t-unattended.cjs`** — supervisor worker loop appends a row after each `_spawnWorker` call completes, recording iteration number, duration, exit code. New `_appendTokenLog` helper follows the same schema as interactive command observability blocks.
|
|
17
|
+
- **`bin/gsd-t.js` `doHeadlessExec`** — `gsd-t headless <command>` invocations append a row synchronously after the `claude -p` process exits.
|
|
18
|
+
|
|
19
|
+
Row format matches the existing token-log schema:
|
|
20
|
+
`| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Domain | Task | Ctx% |`
|
|
21
|
+
Tokens are logged as `unknown` (no API access in worker contexts); duration is wall-clock.
|
|
22
|
+
|
|
23
|
+
#### Fix 2: command/phase propagate to event-stream entries in worker contexts
|
|
24
|
+
|
|
25
|
+
Env-var approach chosen (cleaner, no call-site changes needed):
|
|
26
|
+
|
|
27
|
+
- **`scripts/gsd-t-event-writer.js` `buildEvent`** — reads `GSD_T_COMMAND` and `GSD_T_PHASE` env vars as defaults when `--command`/`--phase` flags are absent. Explicit flags always win.
|
|
28
|
+
- **`bin/headless-auto-spawn.cjs`** — sets `GSD_T_COMMAND={command}` and `GSD_T_PHASE={phase}` on every detached child's env before spawn.
|
|
29
|
+
- **`bin/gsd-t-unattended.cjs` `_spawnWorker`** — sets `GSD_T_COMMAND=gsd-t-resume` and `GSD_T_PHASE={state.phase||execute}` on each `claude -p` worker env.
|
|
30
|
+
- **`bin/gsd-t.js` `doHeadlessExec`** — sets `GSD_T_COMMAND=gsd-t-{command}` on the `execFileSync` env.
|
|
31
|
+
|
|
32
|
+
Result: all `tool_call` events in worker contexts are tagged with the originating command and phase instead of `null`.
|
|
33
|
+
|
|
34
|
+
## [3.12.11] - 2026-04-17
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- **Installer owns global PostToolUse context-meter hook** — the hook command now targets the globally-installed npm package (`$(npm root -g)/@tekyzinc/gsd-t/scripts/gsd-t-context-meter.js`) instead of `$CLAUDE_PROJECT_DIR/scripts/...`. The old path caused `Cannot find module` errors in every non-GSD-T project and when `CLAUDE_PROJECT_DIR` was unset.
|
|
38
|
+
- **Auto-migration of stale hook entries** — `install`, `update`, `update-all`, and `init` now detect and replace any PostToolUse entry whose command matches the prior `$CLAUDE_PROJECT_DIR`-based pattern, upgrading it in-place to the canonical global form.
|
|
39
|
+
- **Existence guard** — the hook command is wrapped in a `bash -c '[ -f ... ] && node ... || true'` guard so it silently exits 0 when the package is not present (non-GSD-T projects, uninstalled state).
|
|
40
|
+
- **Uninstall removes the hook** — `gsd-t uninstall` now removes any PostToolUse hook containing `gsd-t-context-meter` from `~/.claude/settings.json`, leaving all other hooks intact.
|
|
41
|
+
|
|
5
42
|
## [3.12.10] - 2026-04-17
|
|
6
43
|
|
|
7
44
|
### M38: Headless-by-Default + Meter Reduction
|
package/bin/gsd-t-unattended.cjs
CHANGED
|
@@ -939,6 +939,17 @@ function runMainLoop(state, dir, opts, deps, ctx) {
|
|
|
939
939
|
// Append the full worker output to run.log (never truncate).
|
|
940
940
|
_appendRunLog(dir, state.iter, workerEnd, exitCode, stdout, stderr);
|
|
941
941
|
|
|
942
|
+
// Append to token-log.md (Fix 1, v3.12.12) — supervisor workers write rows
|
|
943
|
+
// so the log captures headless/unattended activity, not just interactive spawns.
|
|
944
|
+
_appendTokenLog(projectDir, {
|
|
945
|
+
dtStart: workerStart.toISOString().slice(0, 16).replace("T", " "),
|
|
946
|
+
dtEnd: workerEnd.toISOString().slice(0, 16).replace("T", " "),
|
|
947
|
+
command: "gsd-t-resume",
|
|
948
|
+
durationS: Math.round(elapsedMs / 1000),
|
|
949
|
+
exitCode,
|
|
950
|
+
iter: state.iter,
|
|
951
|
+
});
|
|
952
|
+
|
|
942
953
|
// Post-spawn state update
|
|
943
954
|
state.lastExit = exitCode;
|
|
944
955
|
state.lastWorkerFinishedAt = workerEnd.toISOString();
|
|
@@ -1060,6 +1071,41 @@ function runMainLoop(state, dir, opts, deps, ctx) {
|
|
|
1060
1071
|
return state;
|
|
1061
1072
|
}
|
|
1062
1073
|
|
|
1074
|
+
// ── _appendTokenLog (Fix 1, v3.12.12) ───────────────────────────────────────
|
|
1075
|
+
|
|
1076
|
+
const _TOKEN_LOG_HEADER =
|
|
1077
|
+
"| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Domain | Task | Ctx% |\n" +
|
|
1078
|
+
"|---|---|---|---|---|---|---|---|---|---|\n";
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Append one row to {projectDir}/.gsd-t/token-log.md for a supervisor worker
|
|
1082
|
+
* iteration. Matches the schema used by interactive command-file observability.
|
|
1083
|
+
*/
|
|
1084
|
+
function _appendTokenLog(projectDir, entry) {
|
|
1085
|
+
try {
|
|
1086
|
+
const logPath = path.join(projectDir, ".gsd-t", "token-log.md");
|
|
1087
|
+
const note = entry.exitCode === 0
|
|
1088
|
+
? `supervisor iter=${entry.iter}: ok`
|
|
1089
|
+
: `supervisor iter=${entry.iter}: exit ${entry.exitCode}`;
|
|
1090
|
+
const row =
|
|
1091
|
+
`| ${entry.dtStart} | ${entry.dtEnd} | ${entry.command} | supervisor-iter-${entry.iter} | unknown | ${entry.durationS}s | ${note} | - | - | unknown |\n`;
|
|
1092
|
+
const gsdtDir = path.join(projectDir, ".gsd-t");
|
|
1093
|
+
if (!fs.existsSync(gsdtDir)) fs.mkdirSync(gsdtDir, { recursive: true });
|
|
1094
|
+
if (!fs.existsSync(logPath)) {
|
|
1095
|
+
fs.writeFileSync(logPath, `# GSD-T Token Log\n\n${_TOKEN_LOG_HEADER}${row}`);
|
|
1096
|
+
} else {
|
|
1097
|
+
const existing = fs.readFileSync(logPath, "utf8");
|
|
1098
|
+
if (!existing.includes("| Datetime-start |")) {
|
|
1099
|
+
fs.writeFileSync(logPath, `# GSD-T Token Log\n\n${_TOKEN_LOG_HEADER}${existing}${row}`);
|
|
1100
|
+
} else {
|
|
1101
|
+
fs.appendFileSync(logPath, row);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
} catch (_) {
|
|
1105
|
+
/* best-effort — never halt the supervisor loop */
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1063
1109
|
// ── _spawnWorker ────────────────────────────────────────────────────────────
|
|
1064
1110
|
|
|
1065
1111
|
/**
|
|
@@ -1073,7 +1119,15 @@ function runMainLoop(state, dir, opts, deps, ctx) {
|
|
|
1073
1119
|
*/
|
|
1074
1120
|
function _spawnWorker(state, opts) {
|
|
1075
1121
|
const bin = (state && state.claudeBin) || resolveClaudePath();
|
|
1076
|
-
|
|
1122
|
+
// Inject command/phase so event-stream tool_call entries are tagged in worker
|
|
1123
|
+
// contexts (Fix 2, v3.12.12). Supervisor always runs gsd-t-resume workers;
|
|
1124
|
+
// phase is inferred from state when available.
|
|
1125
|
+
const workerEnv = {
|
|
1126
|
+
...process.env,
|
|
1127
|
+
GSD_T_UNATTENDED_WORKER: "1",
|
|
1128
|
+
GSD_T_COMMAND: "gsd-t-resume",
|
|
1129
|
+
GSD_T_PHASE: (state && state.phase) || "execute",
|
|
1130
|
+
};
|
|
1077
1131
|
const res = platformSpawnWorker(opts.cwd, opts.timeout, {
|
|
1078
1132
|
bin,
|
|
1079
1133
|
args: [
|
package/bin/gsd-t.js
CHANGED
|
@@ -388,8 +388,14 @@ const CONTEXT_METER_GITIGNORE_ENTRIES = [
|
|
|
388
388
|
".gsd-t/context-meter.log",
|
|
389
389
|
];
|
|
390
390
|
const CONTEXT_METER_HOOK_MARKER = "gsd-t-context-meter";
|
|
391
|
+
// Canonical global hook — runs the script from the globally-installed npm package.
|
|
392
|
+
// Guarded so it silently exits 0 if the package is not present (non-GSD-T projects).
|
|
391
393
|
const CONTEXT_METER_HOOK_COMMAND =
|
|
392
|
-
'node "$
|
|
394
|
+
'bash -c \'[ -f "$(npm root -g)/@tekyzinc/gsd-t/scripts/gsd-t-context-meter.js" ] && node "$(npm root -g)/@tekyzinc/gsd-t/scripts/gsd-t-context-meter.js" || true\'';
|
|
395
|
+
// Legacy command patterns that must be migrated on install/update/init.
|
|
396
|
+
const CONTEXT_METER_STALE_PATTERNS = [
|
|
397
|
+
/node\s+"?\$CLAUDE_PROJECT_DIR\/scripts\/gsd-t-context-meter\.js"?/,
|
|
398
|
+
];
|
|
393
399
|
|
|
394
400
|
// Append entries to {projectDir}/.gitignore. Each entry added only if absent.
|
|
395
401
|
// Idempotent. Returns true if any entries were added, false otherwise.
|
|
@@ -544,7 +550,9 @@ function installContextMeter(projectDir) {
|
|
|
544
550
|
|
|
545
551
|
// Register the Context Meter PostToolUse hook in ~/.claude/settings.json.
|
|
546
552
|
// Idempotent — if an existing hook references CONTEXT_METER_HOOK_MARKER the
|
|
547
|
-
// command string is refreshed in-place
|
|
553
|
+
// command string is refreshed/migrated in-place to the canonical form.
|
|
554
|
+
// Stale entries matching CONTEXT_METER_STALE_PATTERNS are migrated on the spot.
|
|
555
|
+
// All other settings/hooks are preserved.
|
|
548
556
|
// Returns { installed: bool, action: "added"|"updated"|"noop" }.
|
|
549
557
|
function configureContextMeterHooks(settingsPath) {
|
|
550
558
|
const targetPath = settingsPath || SETTINGS_JSON;
|
|
@@ -570,13 +578,17 @@ function configureContextMeterHooks(settingsPath) {
|
|
|
570
578
|
for (const entry of settings.hooks.PostToolUse) {
|
|
571
579
|
if (!entry || !Array.isArray(entry.hooks)) continue;
|
|
572
580
|
for (const h of entry.hooks) {
|
|
573
|
-
if (h
|
|
581
|
+
if (!h || typeof h.command !== "string") continue;
|
|
582
|
+
const isCurrentCanonical = h.command === cmd;
|
|
583
|
+
const isMarkerMatch = h.command.includes(CONTEXT_METER_HOOK_MARKER);
|
|
584
|
+
const isStaleMatch = !isCurrentCanonical &&
|
|
585
|
+
CONTEXT_METER_STALE_PATTERNS.some((re) => re.test(h.command));
|
|
586
|
+
|
|
587
|
+
if (isCurrentCanonical || isMarkerMatch || isStaleMatch) {
|
|
574
588
|
found = true;
|
|
575
|
-
if (
|
|
589
|
+
if (!isCurrentCanonical) {
|
|
576
590
|
h.command = cmd;
|
|
577
591
|
action = "updated";
|
|
578
|
-
} else if (action === "noop") {
|
|
579
|
-
action = "noop";
|
|
580
592
|
}
|
|
581
593
|
}
|
|
582
594
|
}
|
|
@@ -607,6 +619,46 @@ function configureContextMeterHooks(settingsPath) {
|
|
|
607
619
|
return { installed: true, action };
|
|
608
620
|
}
|
|
609
621
|
|
|
622
|
+
// Remove any context meter PostToolUse hooks from settings.json.
|
|
623
|
+
// Used during uninstall. Leaves all other hooks intact.
|
|
624
|
+
function removeContextMeterHook(settingsPath) {
|
|
625
|
+
const targetPath = settingsPath || SETTINGS_JSON;
|
|
626
|
+
if (!fs.existsSync(targetPath)) return false;
|
|
627
|
+
let settings;
|
|
628
|
+
try {
|
|
629
|
+
settings = JSON.parse(fs.readFileSync(targetPath, "utf8"));
|
|
630
|
+
if (!settings || typeof settings !== "object") return false;
|
|
631
|
+
} catch {
|
|
632
|
+
warn("settings.json has invalid JSON — cannot remove context meter hook");
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (!settings.hooks || !Array.isArray(settings.hooks.PostToolUse)) return false;
|
|
637
|
+
|
|
638
|
+
const before = settings.hooks.PostToolUse.length;
|
|
639
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter((entry) => {
|
|
640
|
+
if (!entry || !Array.isArray(entry.hooks)) return true;
|
|
641
|
+
// Keep the entry only if NONE of its hooks reference the context meter
|
|
642
|
+
return !entry.hooks.some(
|
|
643
|
+
(h) => h && typeof h.command === "string" && h.command.includes(CONTEXT_METER_HOOK_MARKER)
|
|
644
|
+
);
|
|
645
|
+
});
|
|
646
|
+
const removed = before - settings.hooks.PostToolUse.length;
|
|
647
|
+
if (removed === 0) return false;
|
|
648
|
+
|
|
649
|
+
if (isSymlink(targetPath)) {
|
|
650
|
+
warn("Skipping settings.json write — target is a symlink");
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
fs.writeFileSync(targetPath, JSON.stringify(settings, null, 2));
|
|
655
|
+
return true;
|
|
656
|
+
} catch (e) {
|
|
657
|
+
warn(`Failed to write settings.json: ${e.message}`);
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
610
662
|
// Interactive prompt for the Anthropic API key env var.
|
|
611
663
|
// Skips if not a TTY or if the env var is already set.
|
|
612
664
|
// Never writes the key anywhere — just prints the export command for the user
|
|
@@ -1649,6 +1701,11 @@ function doUninstall() {
|
|
|
1649
1701
|
removeInstalledCommands();
|
|
1650
1702
|
removeVersionFile();
|
|
1651
1703
|
|
|
1704
|
+
// Remove context meter PostToolUse hook from settings.json
|
|
1705
|
+
if (removeContextMeterHook(SETTINGS_JSON)) {
|
|
1706
|
+
success("Context meter PostToolUse hook removed from settings.json");
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1652
1709
|
warn("~/.claude/CLAUDE.md was NOT removed (may contain your customizations)");
|
|
1653
1710
|
info("Remove manually if desired: delete the GSD-T section from ~/.claude/CLAUDE.md");
|
|
1654
1711
|
info("Project files (.gsd-t/, docs/, CLAUDE.md) were NOT removed");
|
|
@@ -2600,6 +2657,39 @@ function doGraph(args) {
|
|
|
2600
2657
|
}
|
|
2601
2658
|
}
|
|
2602
2659
|
|
|
2660
|
+
// ─── Token-Log Writer (Fix 1, v3.12.12) ─────────────────────────────────────
|
|
2661
|
+
|
|
2662
|
+
const _TL_HEADER =
|
|
2663
|
+
"| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Domain | Task | Ctx% |\n" +
|
|
2664
|
+
"|---|---|---|---|---|---|---|---|---|---|\n";
|
|
2665
|
+
|
|
2666
|
+
/**
|
|
2667
|
+
* Append one row to {projectDir}/.gsd-t/token-log.md for a headless exec
|
|
2668
|
+
* invocation. Best-effort — never throws.
|
|
2669
|
+
*/
|
|
2670
|
+
function appendHeadlessTokenLog(projectDir, entry) {
|
|
2671
|
+
try {
|
|
2672
|
+
const logPath = path.join(projectDir, ".gsd-t", "token-log.md");
|
|
2673
|
+
const note = entry.exitCode === 0 ? "headless exec: ok" : `headless exec: exit ${entry.exitCode}`;
|
|
2674
|
+
const row =
|
|
2675
|
+
`| ${entry.dtStart} | ${entry.dtEnd} | ${entry.command} | headless | unknown | ${entry.durationS}s | ${note} | - | - | unknown |\n`;
|
|
2676
|
+
const gsdtDir = path.join(projectDir, ".gsd-t");
|
|
2677
|
+
if (!fs.existsSync(gsdtDir)) fs.mkdirSync(gsdtDir, { recursive: true });
|
|
2678
|
+
if (!fs.existsSync(logPath)) {
|
|
2679
|
+
fs.writeFileSync(logPath, `# GSD-T Token Log\n\n${_TL_HEADER}${row}`);
|
|
2680
|
+
} else {
|
|
2681
|
+
const existing = fs.readFileSync(logPath, "utf8");
|
|
2682
|
+
if (!existing.includes("| Datetime-start |")) {
|
|
2683
|
+
fs.writeFileSync(logPath, `# GSD-T Token Log\n\n${_TL_HEADER}${existing}${row}`);
|
|
2684
|
+
} else {
|
|
2685
|
+
fs.appendFileSync(logPath, row);
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
} catch (_) {
|
|
2689
|
+
/* best-effort */
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2603
2693
|
// ─── Headless Mode ────────────────────────────────────────────────────────────
|
|
2604
2694
|
|
|
2605
2695
|
/**
|
|
@@ -2717,12 +2807,20 @@ function doHeadlessExec(command, cmdArgs, flags) {
|
|
|
2717
2807
|
let output = "";
|
|
2718
2808
|
let processExitCode = 0;
|
|
2719
2809
|
|
|
2810
|
+
// Inject command/phase env vars so worker event-stream entries are tagged
|
|
2811
|
+
// (Fix 2, v3.12.12).
|
|
2812
|
+
const workerEnv = Object.assign({}, process.env, {
|
|
2813
|
+
GSD_T_COMMAND: `gsd-t-${command}`,
|
|
2814
|
+
GSD_T_PHASE: process.env.GSD_T_PHASE || "execute",
|
|
2815
|
+
});
|
|
2816
|
+
|
|
2720
2817
|
try {
|
|
2721
2818
|
const result = execFileSync("claude", ["-p", "--dangerously-skip-permissions", prompt], {
|
|
2722
2819
|
encoding: "utf8",
|
|
2723
2820
|
timeout: timeoutMs,
|
|
2724
2821
|
stdio: ["pipe", "pipe", "pipe"],
|
|
2725
|
-
cwd: process.cwd()
|
|
2822
|
+
cwd: process.cwd(),
|
|
2823
|
+
env: workerEnv,
|
|
2726
2824
|
});
|
|
2727
2825
|
output = result;
|
|
2728
2826
|
} catch (e) {
|
|
@@ -2738,6 +2836,16 @@ function doHeadlessExec(command, cmdArgs, flags) {
|
|
|
2738
2836
|
const gsdtExitCode = mapHeadlessExitCode(processExitCode, output);
|
|
2739
2837
|
const duration = Date.now() - startTime;
|
|
2740
2838
|
|
|
2839
|
+
// Append to token-log.md (Fix 1, v3.12.12) — headless exec writes a row so
|
|
2840
|
+
// `gsd-t headless <command>` spawns are visible in the log.
|
|
2841
|
+
appendHeadlessTokenLog(process.cwd(), {
|
|
2842
|
+
dtStart: new Date(startTime).toISOString().slice(0, 16).replace("T", " "),
|
|
2843
|
+
dtEnd: new Date(startTime + duration).toISOString().slice(0, 16).replace("T", " "),
|
|
2844
|
+
command: `gsd-t-${command}`,
|
|
2845
|
+
durationS: Math.round(duration / 1000),
|
|
2846
|
+
exitCode: gsdtExitCode,
|
|
2847
|
+
});
|
|
2848
|
+
|
|
2741
2849
|
// Write log file if requested
|
|
2742
2850
|
if (logMode && logFile) {
|
|
2743
2851
|
try {
|
|
@@ -3360,6 +3468,7 @@ module.exports = {
|
|
|
3360
3468
|
ensureGitignoreEntries,
|
|
3361
3469
|
installContextMeter,
|
|
3362
3470
|
configureContextMeterHooks,
|
|
3471
|
+
removeContextMeterHook,
|
|
3363
3472
|
promptForApiKeyIfMissing,
|
|
3364
3473
|
resolveApiKeyEnvVar,
|
|
3365
3474
|
runTaskCounterRetirementMigration,
|
|
@@ -135,11 +135,18 @@ function autoSpawnHeadless(opts) {
|
|
|
135
135
|
const gsdtCli = path.join(projectDir, "bin", "gsd-t.js");
|
|
136
136
|
const childArgs = [gsdtCli, "headless", stripGsdtPrefix(command), ...args, "--log"];
|
|
137
137
|
|
|
138
|
+
// Inject command/phase into worker env so event-stream entries are tagged
|
|
139
|
+
// (Fix 2, v3.12.12). GSD_T_PHASE defaults to "execute" for primary spawns.
|
|
140
|
+
const workerEnv = Object.assign({}, process.env, {
|
|
141
|
+
GSD_T_COMMAND: command,
|
|
142
|
+
GSD_T_PHASE: process.env.GSD_T_PHASE || "execute",
|
|
143
|
+
});
|
|
144
|
+
|
|
138
145
|
const child = spawn("node", childArgs, {
|
|
139
146
|
cwd: projectDir,
|
|
140
147
|
detached: true,
|
|
141
148
|
stdio: ["ignore", logFd, logFd],
|
|
142
|
-
env:
|
|
149
|
+
env: workerEnv,
|
|
143
150
|
});
|
|
144
151
|
|
|
145
152
|
child.unref();
|
|
@@ -291,6 +298,7 @@ function installCompletionWatcher(opts) {
|
|
|
291
298
|
const POLL_MS = 2000;
|
|
292
299
|
const MAX_WAIT_MS = 60 * 60 * 1000; // 1 hour safety cap
|
|
293
300
|
const startMs = Date.now();
|
|
301
|
+
const dtStart = new Date(startTimestamp).toLocaleString("sv-SE", { hour12: false }).slice(0, 16);
|
|
294
302
|
|
|
295
303
|
const timer = setInterval(() => {
|
|
296
304
|
let alive = false;
|
|
@@ -305,9 +313,20 @@ function installCompletionWatcher(opts) {
|
|
|
305
313
|
// Exit code is unknown from a signal-based probe. Best-effort: read
|
|
306
314
|
// the log's last lines to guess, otherwise default to 0.
|
|
307
315
|
const exitCode = guessExitCodeFromLog(projectDir, id);
|
|
316
|
+
const endTimestamp = new Date().toISOString();
|
|
308
317
|
markSessionCompleted(projectDir, id, {
|
|
309
318
|
exitCode,
|
|
310
|
-
endTimestamp
|
|
319
|
+
endTimestamp,
|
|
320
|
+
});
|
|
321
|
+
// Append token-log row (Fix 1, v3.12.12)
|
|
322
|
+
const dtEnd = new Date(endTimestamp).toLocaleString("sv-SE", { hour12: false }).slice(0, 16);
|
|
323
|
+
const durationS = Math.round((Date.now() - startMs) / 1000);
|
|
324
|
+
appendTokenLog(projectDir, {
|
|
325
|
+
dtStart,
|
|
326
|
+
dtEnd,
|
|
327
|
+
command: extractCommand(id),
|
|
328
|
+
durationS,
|
|
329
|
+
exitCode,
|
|
311
330
|
});
|
|
312
331
|
fireMacNotification({ id, command: extractCommand(id), startTimestamp });
|
|
313
332
|
} else if (Date.now() - startMs > MAX_WAIT_MS) {
|
|
@@ -356,6 +375,43 @@ function fireMacNotification({ id, command }) {
|
|
|
356
375
|
}
|
|
357
376
|
}
|
|
358
377
|
|
|
378
|
+
// ── Token-Log Writer (Fix 1, v3.12.12) ───────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
const TOKEN_LOG_HEADER =
|
|
381
|
+
"| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Domain | Task | Ctx% |\n" +
|
|
382
|
+
"|---|---|---|---|---|---|---|---|---|---|\n";
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Append one row to {projectDir}/.gsd-t/token-log.md matching the schema used
|
|
386
|
+
* by interactive command-file observability blocks.
|
|
387
|
+
*
|
|
388
|
+
* @param {string} projectDir
|
|
389
|
+
* @param {{ dtStart: string, dtEnd: string, command: string, durationS: number, exitCode: number }} entry
|
|
390
|
+
*/
|
|
391
|
+
function appendTokenLog(projectDir, entry) {
|
|
392
|
+
try {
|
|
393
|
+
const logPath = path.join(projectDir, ".gsd-t", "token-log.md");
|
|
394
|
+
const note = entry.exitCode === 0 ? "headless spawn: ok" : `headless spawn: exit ${entry.exitCode}`;
|
|
395
|
+
const row =
|
|
396
|
+
`| ${entry.dtStart} | ${entry.dtEnd} | ${entry.command} | headless | unknown | ${entry.durationS}s | ${note} | - | - | unknown |\n`;
|
|
397
|
+
if (!fs.existsSync(logPath)) {
|
|
398
|
+
// Create with header
|
|
399
|
+
ensureDir(path.dirname(logPath));
|
|
400
|
+
fs.writeFileSync(logPath, `# GSD-T Token Log\n\n${TOKEN_LOG_HEADER}${row}`);
|
|
401
|
+
} else {
|
|
402
|
+
// Check if header row exists; if not prepend it (migration for files created before this fix)
|
|
403
|
+
const existing = fs.readFileSync(logPath, "utf8");
|
|
404
|
+
if (!existing.includes("| Datetime-start |")) {
|
|
405
|
+
fs.writeFileSync(logPath, `# GSD-T Token Log\n\n${TOKEN_LOG_HEADER}${existing}${row}`);
|
|
406
|
+
} else {
|
|
407
|
+
fs.appendFileSync(logPath, row);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
} catch (_) {
|
|
411
|
+
/* best-effort — never halt the completion watcher */
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
359
415
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
360
416
|
|
|
361
417
|
function ensureDir(d) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "3.12.
|
|
3
|
+
"version": "3.12.12",
|
|
4
4
|
"description": "GSD-T: Contract-Driven Development for Claude Code — 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
|
|
5
5
|
"author": "Tekyz, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -71,11 +71,17 @@ function nullify(val) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
function buildEvent(args) {
|
|
74
|
+
// Env-var fallbacks: workers spawned by supervisor/headless-auto-spawn inherit
|
|
75
|
+
// GSD_T_COMMAND and GSD_T_PHASE so tool_call events are tagged even when the
|
|
76
|
+
// worker doesn't pass --command/--phase explicitly (Fix 2, v3.12.12).
|
|
77
|
+
const envCommand = process.env.GSD_T_COMMAND || null;
|
|
78
|
+
const envPhase = process.env.GSD_T_PHASE || null;
|
|
79
|
+
|
|
74
80
|
return {
|
|
75
81
|
ts: new Date().toISOString(),
|
|
76
82
|
event_type: nullify(args["type"]),
|
|
77
|
-
command: nullify(args["command"]),
|
|
78
|
-
phase: nullify(args["phase"]),
|
|
83
|
+
command: nullify(args["command"]) || envCommand,
|
|
84
|
+
phase: nullify(args["phase"]) || envPhase,
|
|
79
85
|
agent_id: nullify(args["agent-id"]),
|
|
80
86
|
parent_agent_id: nullify(args["parent-id"]),
|
|
81
87
|
trace_id: nullify(args["trace-id"]),
|