@tekyzinc/gsd-t 3.12.13 → 3.12.15

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,54 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [3.12.15] - 2026-04-17
6
+
7
+ ### Fixed — Decision Log Trim — stop live progress.md bloat
8
+
9
+ `commands/gsd-t-complete-milestone.md` Step 7 previously instructed "Keep all prior decision log entries — they are valuable context". Because every prior milestone's full log is already frozen into its archive snapshot by Step 4, carrying the same entries forward on the live `.gsd-t/progress.md` produced unbounded file growth (GSD-T's live file had reached 168,921 bytes / 658 lines — 10× the size of a healthy project).
10
+
11
+ **Fix**: Step 7 now explicitly trims the live Decision Log to the just-completed milestone's entries after the archive snapshot is written. The instruction reads:
12
+
13
+ > Delete all decision-log entries older than the just-completed milestone's start date. Those entries are preserved in the milestone archive created in Step 4. Keep only the completion entry plus any entries logged on or after the cutoff — typically the live log is near-empty when the next milestone begins.
14
+
15
+ The archive at `.gsd-t/milestones/{name}-{date}/progress.md` remains the source-of-truth for the full history. A pointer line (`> Prior decision log entries preserved in .gsd-t/milestones/*/progress.md`) is added to the live file so future readers know where to look.
16
+
17
+ **One-time cleanup on the GSD-T repo**: live `.gsd-t/progress.md` trimmed from 168,921 B / 658 lines to 11,733 B / 67 lines (93% reduction), cut at M38 start (2026-04-16 14:25). Historical decision log preserved by copying the pre-trim file into `.gsd-t/milestones/M38-headless-by-default-2026-04-17/progress.md` (Step 4 missed that archive copy when M38 was completed earlier today; fix-forward).
18
+
19
+ **Users with bloated `.gsd-t/progress.md`** can run the same one-time cleanup manually: find the current milestone's `[milestone-defined]` start entry in the Decision Log, copy the pre-trim file to the milestone archive directory (if it's not already there), then delete all Decision Log entries older than that start date. Keep the pointer line at the top of the Decision Log section.
20
+
21
+ **Tests**: Unit 1186/1186 pass (no code paths changed — pure doc/template edits + one-time live file rewrite). E2E N/A.
22
+
23
+ ## [3.12.14] - 2026-04-17
24
+
25
+ ### Fixed — Telemetry Env-Propagation Regression (Tag All Worker Events)
26
+
27
+ v3.12.12 added `GSD_T_COMMAND`/`GSD_T_PHASE` env-var fallbacks to `scripts/gsd-t-event-writer.js` but two critical call sites were missed — producing mostly-null telemetry in production.
28
+
29
+ **Evidence from bee-poc (50 min observation)**: 908 events, only 1/908 had `command` populated; 836 `tool_call` events had command/phase/trace_id all null; only 2 `.gsd-t/token-log.md` rows (both from the outer supervisor process; 37 inner subagents wrote zero rows); supervisor row showed `model=unknown`.
30
+
31
+ **Root causes**:
32
+ 1. `scripts/gsd-t-heartbeat.js::buildEventStreamEntry` — this PostToolUse hook fires on every tool call in every child process (the source of ~90% of events) and hardcoded `{command: null, phase: null, trace_id: null}` into every event it wrote.
33
+ 2. Neither the writer nor the heartbeat read `GSD_T_TRACE_ID` or `GSD_T_MODEL` from env — so even when spawners set them, they never appeared on events.
34
+ 3. Several spawn sites (orchestrator, `spawnClaudeSession`, `runLedgerCompaction`, design-review claude spawn) never set the GSD_T_* env block at all.
35
+
36
+ **Fixes**:
37
+ - `scripts/gsd-t-event-writer.js::buildEvent` now reads `GSD_T_TRACE_ID` and `GSD_T_MODEL` env fallbacks alongside command/phase.
38
+ - `scripts/gsd-t-heartbeat.js::buildEventStreamEntry` replaced hardcoded null triple with `process.env.GSD_T_COMMAND||null` / `GSD_T_PHASE||null` / `GSD_T_TRACE_ID||null`.
39
+ - `bin/headless-auto-spawn.{cjs,js}` workerEnv sets `GSD_T_COMMAND` + `GSD_T_PHASE` + `GSD_T_PROJECT_DIR`, and conditionally forwards parent `GSD_T_TRACE_ID` / `GSD_T_MODEL`. `appendTokenLog` reads `process.env.GSD_T_MODEL` instead of the `"unknown"` literal.
40
+ - `bin/gsd-t-unattended.cjs::_spawnWorker` workerEnv populates the full GSD_T_* block from `state` + env fallbacks. `_appendTokenLog` reads `process.env.GSD_T_MODEL`.
41
+ - `bin/gsd-t.js` three sites patched: `doHeadlessExec` workerEnv, `spawnClaudeSession` (fallback command=`gsd-t-debug` / phase=`debug`), `runLedgerCompaction` (fallback model=`haiku`). `appendHeadlessTokenLog` reads `process.env.GSD_T_MODEL`.
42
+ - `bin/orchestrator.js` new `_buildOrchestratorEnv(opts, projectDir)` helper threaded through `spawnClaude` (sync) and `spawnClaudeAsync`.
43
+ - `scripts/gsd-t-design-review-server.js` claude spawn now injects the GSD_T_* env block.
44
+
45
+ **Reproduction test**: NEW `test/telemetry-env-propagation.test.js` — 6 tests that exercise the REAL production spawn code paths (not hand-rolled mocks): writer + heartbeat env-fallback unit coverage, `autoSpawnHeadless` real-spawn via env-dump shim at `bin/gsd-t.js`, unattended `platform.spawnWorker` with a real env-dump script. Failed 3/6 before fix as expected; 6/6 pass after.
46
+
47
+ **Tests**: Unit 1186/1186 pass. E2E N/A (no `playwright.config.*` or `cypress.config.*`).
48
+
49
+ **Red Team** (opus, adversarial sweep categories: regression-around-fix + original-bug-variants covering context-meter hook and PostToolUse hook paths): verdict **GRUDGING PASS** — 5 additional claude-worker spawn sites found and patched in this same release; no untagged claude-worker spawn paths remain.
50
+
51
+ **Doc ripple**: `.gsd-t/contracts/event-schema-contract.md` new "Env-Var Fallbacks (v3.12.14)" section with flag/env/caller table; `.gsd-t/contracts/headless-default-contract.md` new "Worker Env Propagation (v3.12.14)" section; `.gsd-t/contracts/unattended-supervisor-contract.md` §14b v1.2.0 Worker Env Propagation + version history entry.
52
+
5
53
  ## [3.12.13] - 2026-04-17
6
54
 
7
55
  ### Fixed — `/` Prefix Strip Sitewide
@@ -1087,8 +1087,10 @@ function _appendTokenLog(projectDir, entry) {
1087
1087
  const note = entry.exitCode === 0
1088
1088
  ? `supervisor iter=${entry.iter}: ok`
1089
1089
  : `supervisor iter=${entry.iter}: exit ${entry.exitCode}`;
1090
+ // v3.12.14: prefer env-var model over the hardcoded "unknown" placeholder.
1091
+ const model = process.env.GSD_T_MODEL || "unknown";
1090
1092
  const row =
1091
- `| ${entry.dtStart} | ${entry.dtEnd} | ${entry.command} | supervisor-iter-${entry.iter} | unknown | ${entry.durationS}s | ${note} | - | - | unknown |\n`;
1093
+ `| ${entry.dtStart} | ${entry.dtEnd} | ${entry.command} | supervisor-iter-${entry.iter} | ${model} | ${entry.durationS}s | ${note} | - | - | unknown |\n`;
1092
1094
  const gsdtDir = path.join(projectDir, ".gsd-t");
1093
1095
  if (!fs.existsSync(gsdtDir)) fs.mkdirSync(gsdtDir, { recursive: true });
1094
1096
  if (!fs.existsSync(logPath)) {
@@ -1119,15 +1121,23 @@ function _appendTokenLog(projectDir, entry) {
1119
1121
  */
1120
1122
  function _spawnWorker(state, opts) {
1121
1123
  const bin = (state && state.claudeBin) || resolveClaudePath();
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.
1124
+ // Inject command/phase/trace/model/project-dir so event-stream tool_call
1125
+ // entries (writer CLI + heartbeat hook) are tagged in worker contexts
1126
+ // (Fix 2, v3.12.12; trace/model/project-dir added v3.12.14 for the
1127
+ // null-telemetry regression fix). Supervisor always runs gsd-t-resume
1128
+ // workers; phase is inferred from state when available. Trace/model flow
1129
+ // through from parent process.env when set.
1125
1130
  const workerEnv = {
1126
1131
  ...process.env,
1127
1132
  GSD_T_UNATTENDED_WORKER: "1",
1128
1133
  GSD_T_COMMAND: "gsd-t-resume",
1129
1134
  GSD_T_PHASE: (state && state.phase) || "execute",
1135
+ GSD_T_PROJECT_DIR: process.env.GSD_T_PROJECT_DIR || opts.cwd || state.projectDir,
1130
1136
  };
1137
+ if (state && state.traceId) workerEnv.GSD_T_TRACE_ID = state.traceId;
1138
+ else if (process.env.GSD_T_TRACE_ID) workerEnv.GSD_T_TRACE_ID = process.env.GSD_T_TRACE_ID;
1139
+ if (state && state.model) workerEnv.GSD_T_MODEL = state.model;
1140
+ else if (process.env.GSD_T_MODEL) workerEnv.GSD_T_MODEL = process.env.GSD_T_MODEL;
1131
1141
  const res = platformSpawnWorker(opts.cwd, opts.timeout, {
1132
1142
  bin,
1133
1143
  args: [
package/bin/gsd-t.js CHANGED
@@ -2743,8 +2743,10 @@ function appendHeadlessTokenLog(projectDir, entry) {
2743
2743
  try {
2744
2744
  const logPath = path.join(projectDir, ".gsd-t", "token-log.md");
2745
2745
  const note = entry.exitCode === 0 ? "headless exec: ok" : `headless exec: exit ${entry.exitCode}`;
2746
+ // v3.12.14: prefer env-var model over hardcoded "unknown".
2747
+ const model = process.env.GSD_T_MODEL || "unknown";
2746
2748
  const row =
2747
- `| ${entry.dtStart} | ${entry.dtEnd} | ${entry.command} | headless | unknown | ${entry.durationS}s | ${note} | - | - | unknown |\n`;
2749
+ `| ${entry.dtStart} | ${entry.dtEnd} | ${entry.command} | headless | ${model} | ${entry.durationS}s | ${note} | - | - | unknown |\n`;
2748
2750
  const gsdtDir = path.join(projectDir, ".gsd-t");
2749
2751
  if (!fs.existsSync(gsdtDir)) fs.mkdirSync(gsdtDir, { recursive: true });
2750
2752
  if (!fs.existsSync(logPath)) {
@@ -2879,12 +2881,17 @@ function doHeadlessExec(command, cmdArgs, flags) {
2879
2881
  let output = "";
2880
2882
  let processExitCode = 0;
2881
2883
 
2882
- // Inject command/phase env vars so worker event-stream entries are tagged
2883
- // (Fix 2, v3.12.12).
2884
+ // Inject command/phase/trace/model/project-dir env vars so worker
2885
+ // event-stream entries (writer CLI + heartbeat hook) are tagged in the
2886
+ // child's context (Fix 2, v3.12.12; trace/model/project-dir added v3.12.14
2887
+ // for the null-telemetry regression fix).
2884
2888
  const workerEnv = Object.assign({}, process.env, {
2885
2889
  GSD_T_COMMAND: `gsd-t-${command}`,
2886
2890
  GSD_T_PHASE: process.env.GSD_T_PHASE || "execute",
2891
+ GSD_T_PROJECT_DIR: process.env.GSD_T_PROJECT_DIR || process.cwd(),
2887
2892
  });
2893
+ if (process.env.GSD_T_TRACE_ID) workerEnv.GSD_T_TRACE_ID = process.env.GSD_T_TRACE_ID;
2894
+ if (process.env.GSD_T_MODEL) workerEnv.GSD_T_MODEL = process.env.GSD_T_MODEL;
2888
2895
 
2889
2896
  try {
2890
2897
  const result = execFileSync("claude", ["-p", "--dangerously-skip-permissions", prompt], {
@@ -3169,9 +3176,22 @@ function getEscalationModel(iteration) {
3169
3176
  */
3170
3177
  function spawnClaudeSession(prompt, model) {
3171
3178
  try {
3179
+ // v3.12.14: propagate GSD_T_* env vars so the worker's heartbeat hook +
3180
+ // event-writer entries are tagged with the parent command/phase/trace and
3181
+ // this session's model. Without these, tool_call events from the debug
3182
+ // worker appear as command=null/phase=null.
3183
+ const env = Object.assign({}, process.env, {
3184
+ GSD_T_COMMAND: process.env.GSD_T_COMMAND || "gsd-t-debug",
3185
+ GSD_T_PHASE: process.env.GSD_T_PHASE || "debug",
3186
+ GSD_T_MODEL: model || process.env.GSD_T_MODEL || null,
3187
+ GSD_T_PROJECT_DIR: process.env.GSD_T_PROJECT_DIR || process.cwd(),
3188
+ });
3189
+ if (env.GSD_T_MODEL === null) delete env.GSD_T_MODEL;
3190
+ if (process.env.GSD_T_TRACE_ID) env.GSD_T_TRACE_ID = process.env.GSD_T_TRACE_ID;
3172
3191
  return execFileSync("claude", ["-p", prompt, "--model", model], {
3173
3192
  encoding: "utf8", timeout: 300000,
3174
3193
  stdio: ["pipe", "pipe", "pipe"],
3194
+ env,
3175
3195
  });
3176
3196
  } catch (e) {
3177
3197
  return (e.stdout || "") + (e.stderr || "") || null;
@@ -3208,8 +3228,17 @@ function runLedgerCompaction(projectDir, jsonMode) {
3208
3228
  JSON.stringify(entries, null, 2);
3209
3229
  let summary = "Compacted — see previous entries.";
3210
3230
  try {
3231
+ // v3.12.14: propagate GSD_T_* env for telemetry tagging.
3232
+ const env = Object.assign({}, process.env, {
3233
+ GSD_T_COMMAND: process.env.GSD_T_COMMAND || "gsd-t-debug",
3234
+ GSD_T_PHASE: process.env.GSD_T_PHASE || "debug",
3235
+ GSD_T_MODEL: "haiku",
3236
+ GSD_T_PROJECT_DIR: process.env.GSD_T_PROJECT_DIR || projectDir,
3237
+ });
3238
+ if (process.env.GSD_T_TRACE_ID) env.GSD_T_TRACE_ID = process.env.GSD_T_TRACE_ID;
3211
3239
  const out = execFileSync("claude", ["-p", compactPrompt, "--model", "haiku"], {
3212
3240
  encoding: "utf8", timeout: 120000, stdio: ["pipe", "pipe", "pipe"],
3241
+ env,
3213
3242
  });
3214
3243
  summary = (out || "").trim() || summary;
3215
3244
  } catch (e) {
@@ -135,12 +135,27 @@ 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.
138
+ // Inject command/phase/trace/model into worker env so event-stream entries
139
+ // (both the writer CLI and the heartbeat PostToolUse hook) are tagged in
140
+ // the child's context (Fix 2, v3.12.12; trace/model/project-dir added
141
+ // v3.12.14 to close the null-telemetry regression).
142
+ //
143
+ // GSD_T_PHASE defaults to "execute" for primary spawns.
144
+ // GSD_T_TRACE_ID / GSD_T_MODEL are inherited from parent env if set;
145
+ // parents (orchestrator, command files) should set them before spawning.
146
+ // GSD_T_PROJECT_DIR gives the writer a stable target when the child cwd
147
+ // drifts (e.g., temp dirs in subagents).
140
148
  const workerEnv = Object.assign({}, process.env, {
141
149
  GSD_T_COMMAND: command,
142
150
  GSD_T_PHASE: process.env.GSD_T_PHASE || "execute",
151
+ GSD_T_PROJECT_DIR: process.env.GSD_T_PROJECT_DIR || projectDir,
143
152
  });
153
+ if (process.env.GSD_T_TRACE_ID) {
154
+ workerEnv.GSD_T_TRACE_ID = process.env.GSD_T_TRACE_ID;
155
+ }
156
+ if (process.env.GSD_T_MODEL) {
157
+ workerEnv.GSD_T_MODEL = process.env.GSD_T_MODEL;
158
+ }
144
159
 
145
160
  const child = spawn("node", childArgs, {
146
161
  cwd: projectDir,
@@ -392,8 +407,12 @@ function appendTokenLog(projectDir, entry) {
392
407
  try {
393
408
  const logPath = path.join(projectDir, ".gsd-t", "token-log.md");
394
409
  const note = entry.exitCode === 0 ? "headless spawn: ok" : `headless spawn: exit ${entry.exitCode}`;
410
+ // v3.12.14: prefer env-var model over the old hardcoded "unknown". The
411
+ // parent session should have set GSD_T_MODEL before invoking spawn; fall
412
+ // back to "unknown" if not (graceful).
413
+ const model = process.env.GSD_T_MODEL || "unknown";
395
414
  const row =
396
- `| ${entry.dtStart} | ${entry.dtEnd} | ${entry.command} | headless | unknown | ${entry.durationS}s | ${note} | - | - | unknown |\n`;
415
+ `| ${entry.dtStart} | ${entry.dtEnd} | ${entry.command} | headless | ${model} | ${entry.durationS}s | ${note} | - | - | unknown |\n`;
397
416
  if (!fs.existsSync(logPath)) {
398
417
  // Create with header
399
418
  ensureDir(path.dirname(logPath));
@@ -135,11 +135,22 @@ 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
+ // v3.12.14 — env-var propagation for telemetry tagging (mirror of the
139
+ // authoritative .cjs variant). PRODUCTION consumers import the .cjs; this
140
+ // .js copy is retained only to keep the legacy test green.
141
+ const workerEnv = Object.assign({}, process.env, {
142
+ GSD_T_COMMAND: command,
143
+ GSD_T_PHASE: process.env.GSD_T_PHASE || "execute",
144
+ GSD_T_PROJECT_DIR: process.env.GSD_T_PROJECT_DIR || projectDir,
145
+ });
146
+ if (process.env.GSD_T_TRACE_ID) workerEnv.GSD_T_TRACE_ID = process.env.GSD_T_TRACE_ID;
147
+ if (process.env.GSD_T_MODEL) workerEnv.GSD_T_MODEL = process.env.GSD_T_MODEL;
148
+
138
149
  const child = spawn("node", childArgs, {
139
150
  cwd: projectDir,
140
151
  detached: true,
141
152
  stdio: ["ignore", logFd, logFd],
142
- env: process.env,
153
+ env: workerEnv,
143
154
  });
144
155
 
145
156
  child.unref();
@@ -82,6 +82,25 @@ function isPortInUse(port) {
82
82
  }
83
83
  }
84
84
 
85
+ /**
86
+ * Build the env block for orchestrator-spawned `claude -p` sessions.
87
+ * v3.12.14 — propagates GSD_T_COMMAND/PHASE/TRACE_ID/MODEL/PROJECT_DIR so the
88
+ * worker's heartbeat hook and event-writer entries are tagged. Accepts the
89
+ * opts object (for `label`) that callers already pass and defaults missing
90
+ * fields from the parent process env.
91
+ */
92
+ function _buildOrchestratorEnv(opts, projectDir) {
93
+ opts = opts || {};
94
+ const env = Object.assign({}, process.env, {
95
+ GSD_T_COMMAND: process.env.GSD_T_COMMAND || "gsd-t-orchestrator",
96
+ GSD_T_PHASE: process.env.GSD_T_PHASE || opts.label || "orchestrator",
97
+ GSD_T_PROJECT_DIR: process.env.GSD_T_PROJECT_DIR || projectDir,
98
+ });
99
+ if (process.env.GSD_T_TRACE_ID) env.GSD_T_TRACE_ID = process.env.GSD_T_TRACE_ID;
100
+ if (process.env.GSD_T_MODEL) env.GSD_T_MODEL = process.env.GSD_T_MODEL;
101
+ return env;
102
+ }
103
+
85
104
  // ─── Orchestrator Class ────────────────────────────────────────────────────
86
105
 
87
106
  class Orchestrator {
@@ -212,11 +231,15 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
212
231
  const signalFile = path.join(this.getReviewDir(projectDir), `_sync-done-${Date.now()}.json`);
213
232
  let result = { output: "", exitCode: 1, duration: 0 };
214
233
 
234
+ // v3.12.14: propagate GSD_T_* env so the worker's heartbeat hook and
235
+ // event-writer entries are tagged. opts.label → phase/command hints.
236
+ const env = _buildOrchestratorEnv(opts, projectDir);
215
237
  const child = execFile("claude", args, {
216
238
  encoding: "utf8",
217
239
  timeout: effectiveTimeout,
218
240
  cwd: projectDir,
219
241
  maxBuffer: 10 * 1024 * 1024,
242
+ env,
220
243
  }, (err, stdout, stderr) => {
221
244
  this.untrackChild(child.pid);
222
245
  const raw = err ? ((err.stdout || "") + (err.stderr || "")) : (stdout || "");
@@ -264,12 +287,15 @@ ${BOLD}Phases:${RESET} ${this.wf.phases.join(" → ")}
264
287
  );
265
288
  }
266
289
 
290
+ // v3.12.14: propagate GSD_T_* env for telemetry tagging in the child.
291
+ const env = _buildOrchestratorEnv(opts, projectDir);
267
292
  return new Promise((resolve) => {
268
293
  const child = execFile("claude", args, {
269
294
  encoding: "utf8",
270
295
  timeout: timeout || 120_000,
271
296
  cwd: projectDir,
272
297
  maxBuffer: 10 * 1024 * 1024,
298
+ env,
273
299
  }, (err, stdout, stderr) => {
274
300
  this.untrackChild(child.pid);
275
301
  const raw = err ? ((err.stdout || "") + (err.stderr || "")) : (stdout || "");
@@ -351,6 +351,14 @@ Reset `.gsd-t/` for next milestone:
351
351
  4. Clear `.gsd-t/impact-report.md`, `.gsd-t/test-coverage.md`
352
352
  5. Update `.gsd-t/progress.md`:
353
353
 
354
+ **CRITICAL — Decision Log Trim**: This trim happens AFTER Step 4's archive copy of `.gsd-t/progress.md` is written. The archive snapshot at `.gsd-t/milestones/{name}-{date}/progress.md` is the source-of-truth for full history. The live Decision Log is reset to the just-completed milestone's entries only — everything dated before the milestone's start date is deleted from the live file (it remains in the archive). This prevents the live progress.md from growing without bound.
355
+
356
+ Steps to apply the trim:
357
+ - Identify the just-completed milestone's start date (typically the `[milestone-defined]` entry for this milestone)
358
+ - Delete all Decision Log entries dated BEFORE that start date from the live `.gsd-t/progress.md`
359
+ - Keep entries from the start date onward (they cover the milestone arc + the completion entry you're about to add)
360
+ - Prepend the pointer line shown below
361
+
354
362
  ```markdown
355
363
  # GSD-T Progress
356
364
 
@@ -365,8 +373,11 @@ None — ready for next milestone
365
373
  | {previous} | {version} | {date} | v{version} |
366
374
 
367
375
  ## Decision Log
376
+
377
+ > Prior decision log entries preserved in `.gsd-t/milestones/*/progress.md` — see archive snapshots for pre-{next-milestone-name} history.
378
+
368
379
  - {date}: [success] Milestone "{name}" completed — {summary of what was built}. v{version}
369
- {Keep all prior decision log entries they are valuable context}
380
+ {Trim: delete all decision-log entries older than the just-completed milestone's start date. Those entries are preserved in the milestone archive created in Step 4. Keep only the completion entry above plus any entries logged on or after the cutoff — typically the live log is near-empty when the next milestone begins.}
370
381
  ```
371
382
 
372
383
  ## Step 8: Update README.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "3.12.13",
3
+ "version": "3.12.15",
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",
@@ -685,12 +685,22 @@ const server = http.createServer((req, res) => {
685
685
  "Access-Control-Allow-Origin": "*",
686
686
  });
687
687
 
688
+ // v3.12.14: propagate GSD_T_* env for telemetry tagging in the worker.
689
+ const childEnv = {
690
+ ...process.env,
691
+ NO_COLOR: "1",
692
+ GSD_T_COMMAND: process.env.GSD_T_COMMAND || "gsd-t-design-review",
693
+ GSD_T_PHASE: process.env.GSD_T_PHASE || "design-review",
694
+ GSD_T_MODEL: model || process.env.GSD_T_MODEL || "unknown",
695
+ };
696
+ if (process.env.GSD_T_TRACE_ID) childEnv.GSD_T_TRACE_ID = process.env.GSD_T_TRACE_ID;
697
+ if (process.env.GSD_T_PROJECT_DIR) childEnv.GSD_T_PROJECT_DIR = process.env.GSD_T_PROJECT_DIR;
688
698
  const claude = spawn("claude", [
689
699
  "-p", fullPrompt,
690
700
  "--model", model,
691
701
  "--output-format", "stream-json",
692
702
  "--verbose",
693
- ], { env: { ...process.env, NO_COLOR: "1" } });
703
+ ], { env: childEnv });
694
704
 
695
705
  let buf = "";
696
706
  let textSent = 0;
@@ -72,10 +72,13 @@ function nullify(val) {
72
72
 
73
73
  function buildEvent(args) {
74
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).
75
+ // GSD_T_COMMAND, GSD_T_PHASE, GSD_T_TRACE_ID, GSD_T_MODEL so tool_call events
76
+ // are tagged even when the worker doesn't pass --command/--phase/--trace-id/
77
+ // --model explicitly (Fix 2, v3.12.12; trace/model added v3.12.14).
77
78
  const envCommand = process.env.GSD_T_COMMAND || null;
78
79
  const envPhase = process.env.GSD_T_PHASE || null;
80
+ const envTraceId = process.env.GSD_T_TRACE_ID || null;
81
+ const envModel = process.env.GSD_T_MODEL || null;
79
82
 
80
83
  return {
81
84
  ts: new Date().toISOString(),
@@ -84,10 +87,10 @@ function buildEvent(args) {
84
87
  phase: nullify(args["phase"]) || envPhase,
85
88
  agent_id: nullify(args["agent-id"]),
86
89
  parent_agent_id: nullify(args["parent-id"]),
87
- trace_id: nullify(args["trace-id"]),
90
+ trace_id: nullify(args["trace-id"]) || envTraceId,
88
91
  reasoning: nullify(args["reasoning"]),
89
92
  outcome: nullify(args["outcome"]),
90
- model: nullify(args["model"]),
93
+ model: nullify(args["model"]) || envModel,
91
94
  };
92
95
  }
93
96
 
@@ -187,7 +187,17 @@ function shortPath(p) {
187
187
 
188
188
  function buildEventStreamEntry(hook) {
189
189
  const ts = new Date().toISOString();
190
- const base = { ts, command: null, phase: null, trace_id: null };
190
+ // Env-var fallbacks: supervisor/headless spawned workers inherit these on
191
+ // their process.env so PostToolUse tool_call events are tagged instead of
192
+ // landing as null/null/null in the stream (v3.12.14 telemetry fix — the
193
+ // writer had these fallbacks since v3.12.12 but the heartbeat hook — source
194
+ // of ~90% of tool_call events — hardcoded nulls).
195
+ const base = {
196
+ ts,
197
+ command: process.env.GSD_T_COMMAND || null,
198
+ phase: process.env.GSD_T_PHASE || null,
199
+ trace_id: process.env.GSD_T_TRACE_ID || null,
200
+ };
191
201
  const n = hook.hook_event_name;
192
202
  if (n === "SessionStart") {
193
203
  return { ...base, event_type: "session_start",