@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 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
@@ -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
- const workerEnv = { ...process.env, GSD_T_UNATTENDED_WORKER: "1" };
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 "$CLAUDE_PROJECT_DIR/scripts/gsd-t-context-meter.js"';
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. All other settings/hooks preserved.
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 && typeof h.command === "string" && h.command.includes(CONTEXT_METER_HOOK_MARKER)) {
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 (h.command !== cmd) {
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: process.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: new Date().toISOString(),
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.10",
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"]),