@tekyzinc/gsd-t 3.10.12 → 3.10.14

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,42 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [3.10.14] - 2026-04-15
6
+
7
+ ### Fixed — transcript parser orphaned tool_use blocks cause count_tokens 400
8
+
9
+ **Background**: With `ANTHROPIC_API_KEY` now set (v3.10.12-13 fix), the context meter hook passed the key check but the `count_tokens` API returned HTTP 400: `"tool_use ids were found without tool_result blocks immediately after"`. The transcript parser (`scripts/context-meter/transcript-parser.js`) faithfully reconstructed the JSONL transcript but didn't enforce the API's strict adjacency constraint: every assistant `tool_use` must be immediately followed by a user `tool_result` with matching ids. Mid-session compaction and summarization can orphan these blocks.
10
+
11
+ ### Changed
12
+ - **`scripts/context-meter/transcript-parser.js`** — added `sanitizeToolPairs()` post-processing pass after message reconstruction. Walks the message list enforcing adjacency: assistant `tool_use` blocks are kept only if the immediately following user message contains a `tool_result` with a matching id, and vice versa. Messages that become empty after stripping are dropped. This is a structural fix — any transcript shape (compacted, summarized, interrupted) now produces a valid `count_tokens` payload.
13
+ - **`scripts/context-meter/transcript-parser.test.js`** — updated 2 existing tests that created orphaned `tool_result` messages (now include matching `tool_use` predecessors). Added 1 new test: `orphaned tool_use without matching tool_result is stripped`. 1229/1229 tests pass.
14
+
15
+ ### Verification
16
+ - Real transcript (626→649 messages, 473KB payload) now returns HTTP 200 with `input_tokens: 153597` (was 400 before fix)
17
+ - Context meter state file flipped from `lastError: api_error` to `lastError: null`, `inputTokens: 158543`, `pct: 79.3%`, `threshold: warn`
18
+ - First successful real-time context measurement since M34 was built
19
+
20
+ ## [3.10.13] - 2026-04-15
21
+
22
+ ### Fixed — P0 v3.10.12 propagation gap (same regression, downstream projects)
23
+
24
+ **Background**: v3.10.12 shipped the `stale` band fix to `bin/token-budget.js` in the GSD-T repo, but verification revealed the fix was **never visible in any downstream project**. Every command file gate snippet is `require('./bin/token-budget.js')` resolved against the **project cwd** — and no downstream project has a local `bin/token-budget.js` file. `PROJECT_BIN_TOOLS` in `bin/gsd-t.js` (the list that `update-all` copies to each registered project) did not include `token-budget.js`, so downstream projects never received any copy. The require throws `MODULE_NOT_FOUND`, the surrounding `try{…}catch(_){process.stdout.write('0')}` swallows it, and the gate sees `pct: 0` = normal band. Identical failure mode to the original regression.
25
+
26
+ ### Changed
27
+ - **`bin/token-budget.js` → `bin/token-budget.cjs`** — renamed to `.cjs` so it runs as CommonJS regardless of downstream `package.json` `"type"` field. Some registered projects use `"type": "module"`, which would have broken `require('./bin/token-budget.js')` even if the file were propagated. The `.cjs` extension is the same convention used by all other tools in `PROJECT_BIN_TOOLS` (`archive-progress.cjs`, `log-tail.cjs`, `context-budget-audit.cjs`, `context-meter-config.cjs`).
28
+ - **`bin/gsd-t.js`** `PROJECT_BIN_TOOLS` — appended `"token-budget.cjs"`. Now `update-all` copies the file to every registered project's `bin/` on update.
29
+ - **All 17 command files** referencing `./bin/token-budget.js` — updated to `./bin/token-budget.cjs`: `gsd-t-execute`, `gsd-t-wave`, `gsd-t-quick`, `gsd-t-debug`, `gsd-t-integrate`, `gsd-t-doc-ripple`, `gsd-t-verify`, `gsd-t-plan`, `gsd-t-discuss`, `gsd-t-visualize`, `gsd-t-reflect`, `gsd-t-brainstorm`, `gsd-t-audit`, `gsd-t-prd`, `gsd-t-resume`, `gsd-t-unattended`, `gsd-t-help`.
30
+ - **`test/token-budget.test.js`** — require path updated to `../bin/token-budget.cjs`. All 1228 tests pass.
31
+
32
+ ### Why this matters
33
+ Without this patch, v3.10.12's `stale` band fix is **dead code in every downstream project**. The command files fail the require, catch silently, and the gate goes back to reporting 0% normal — exactly the invisible failure mode that caused the M36 regression in the first place. The two patches are a single logical fix; shipping v3.10.12 alone was incomplete.
34
+
35
+ ### Verification
36
+ - `npm test` → 1228/1228 pass (same baseline as v3.10.12)
37
+ - No runtime references to `token-budget.js` remain under `commands/`
38
+ - `bin/token-budget.cjs` is 13867 bytes (verbatim copy of the v3.10.12 `token-budget.js`)
39
+ - `PROJECT_BIN_TOOLS` now has 5 entries — `update-all` will copy to all 15 registered projects on next invocation
40
+
5
41
  ## [3.10.12] - 2026-04-15
6
42
 
7
43
  ### Fixed — P0 context meter regression (M36 /compact incidents)
package/bin/gsd-t.js CHANGED
@@ -1975,7 +1975,7 @@ function updateSingleProject(projectDir, counts) {
1975
1975
  // Bin tools that should ship with every registered project. Listed here so adding
1976
1976
  // a new tool only requires appending to this array. Use .cjs extension so they
1977
1977
  // always run as CommonJS regardless of the project's package.json "type" field.
1978
- const PROJECT_BIN_TOOLS = ["archive-progress.cjs", "log-tail.cjs", "context-budget-audit.cjs", "context-meter-config.cjs"];
1978
+ const PROJECT_BIN_TOOLS = ["archive-progress.cjs", "log-tail.cjs", "context-budget-audit.cjs", "context-meter-config.cjs", "token-budget.cjs"];
1979
1979
 
1980
1980
  function copyBinToolsToProject(projectDir, projectName) {
1981
1981
  const projectBinDir = path.join(projectDir, "bin");
@@ -22,7 +22,7 @@ Read CLAUDE.md and .gsd-t/progress.md for project context, then execute gsd-t-au
22
22
  ```
23
23
 
24
24
  After subagent returns — run via Bash:
25
- `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.js'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
25
+ `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.cjs'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
26
26
  Append to `.gsd-t/token-log.md` (create with header `| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Domain | Task | Ctx% |` if missing):
27
27
  `| {DT_START} | {DT_END} | gsd-t-audit | Step 0 | sonnet | {DURATION}s | audit: {args summary} | | | {CTX_PCT} |`
28
28
 
@@ -123,7 +123,7 @@ Do NOT proceed to Step 5 until this synthesis is complete.
123
123
  ```
124
124
 
125
125
  After team completes — run via Bash:
126
- `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.js'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
126
+ `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.cjs'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
127
127
  Append to `.gsd-t/token-log.md` (create with header `| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Ctx% |` if missing):
128
128
  `| {DT_START} | {DT_END} | gsd-t-brainstorm | Step 3 | sonnet | {DURATION}s | deep research: {topic summary} | {CTX_PCT} |`
129
129
 
@@ -19,14 +19,14 @@ Per `.gsd-t/contracts/token-telemetry-contract.md` v1.0.0. Every Task subagent s
19
19
 
20
20
  ```bash
21
21
  T0_TOKENS=$(node -e "try{const s=require('fs').readFileSync('.gsd-t/.context-meter-state.json','utf8');process.stdout.write(String(JSON.parse(s).inputTokens||0))}catch(_){process.stdout.write('0')}")
22
- T0_PCT=$(node -e "try{const tb=require('./bin/token-budget.js');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
22
+ T0_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
23
23
  ```
24
24
 
25
25
  **After each spawn — record the bracket:**
26
26
 
27
27
  ```bash
28
28
  T1_TOKENS=$(node -e "try{const s=require('fs').readFileSync('.gsd-t/.context-meter-state.json','utf8');process.stdout.write(String(JSON.parse(s).inputTokens||0))}catch(_){process.stdout.write('0')}")
29
- T1_PCT=$(node -e "try{const tb=require('./bin/token-budget.js');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
29
+ T1_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
30
30
  node -e "require('./bin/token-telemetry.js').recordSpawn({timestamp:new Date().toISOString(),milestone:process.env.GSD_T_MILESTONE||'',command:'gsd-t-debug',phase:'debug',step:'${STEP:-}',domain:'${DOMAIN:-}',domain_type:'${DOMAIN_TYPE:-}',task:'${TASK:-}',model:'${MODEL:-opus}',duration_s:${DURATION:-0},input_tokens_before:${T0_TOKENS},input_tokens_after:${T1_TOKENS},tokens_consumed:${T1_TOKENS}-${T0_TOKENS},context_window_pct_before:${T0_PCT},context_window_pct_after:${T1_PCT},outcome:'${OUTCOME:-success}',halt_type:${HALT_TYPE:-null},escalated_via_advisor:${ESCALATED_VIA_ADVISOR:-false}})" 2>/dev/null || true
31
31
  ```
32
32
 
@@ -213,7 +213,7 @@ Lead: Wait for all three researchers to complete. Then synthesize:
213
213
  ```
214
214
 
215
215
  After team completes — run via Bash:
216
- `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.js'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
216
+ `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.cjs'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
217
217
  Append to `.gsd-t/token-log.md`:
218
218
  `| {DT_START} | {DT_END} | gsd-t-debug | Step 1.5 | sonnet | {DURATION}s | deep research loop break: {issue summary} | {CTX_PCT} |`
219
219
 
@@ -506,7 +506,7 @@ Spawn Task subagent (general-purpose, model: opus):
506
506
  After subagent returns — run via Bash:
507
507
  ```
508
508
  T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START))
509
- CTX_PCT=$(node -e "try{const tb=require('./bin/token-budget.js'); process.stdout.write(String(tb.getSessionStatus('.').pct))}catch(_){process.stdout.write('N/A')}")
509
+ CTX_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs'); process.stdout.write(String(tb.getSessionStatus('.').pct))}catch(_){process.stdout.write('N/A')}")
510
510
  ```
511
511
  Append to `.gsd-t/token-log.md`:
512
512
  `| {DT_START} | {DT_END} | gsd-t-debug | Red Team | opus | {DURATION}s | {VERDICT} — {N} bugs found | | | {CTX_PCT} |`
@@ -79,7 +79,7 @@ Lead: Synthesize into decisions and update contracts.
79
79
  ```
80
80
 
81
81
  After team completes — run via Bash:
82
- `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.js'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
82
+ `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.cjs'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
83
83
  Append to `.gsd-t/token-log.md` (create with header `| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Ctx% |` if missing):
84
84
  `| {DT_START} | {DT_END} | gsd-t-discuss | Step 3 | sonnet | {DURATION}s | team discuss: {topic summary} | {CTX_PCT} |`
85
85
 
@@ -17,14 +17,14 @@ Per `.gsd-t/contracts/token-telemetry-contract.md` v1.0.0. Every Task subagent s
17
17
 
18
18
  ```bash
19
19
  T0_TOKENS=$(node -e "try{const s=require('fs').readFileSync('.gsd-t/.context-meter-state.json','utf8');process.stdout.write(String(JSON.parse(s).inputTokens||0))}catch(_){process.stdout.write('0')}")
20
- T0_PCT=$(node -e "try{const tb=require('./bin/token-budget.js');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
20
+ T0_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
21
21
  ```
22
22
 
23
23
  **After each spawn — record the bracket:**
24
24
 
25
25
  ```bash
26
26
  T1_TOKENS=$(node -e "try{const s=require('fs').readFileSync('.gsd-t/.context-meter-state.json','utf8');process.stdout.write(String(JSON.parse(s).inputTokens||0))}catch(_){process.stdout.write('0')}")
27
- T1_PCT=$(node -e "try{const tb=require('./bin/token-budget.js');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
27
+ T1_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
28
28
  node -e "require('./bin/token-telemetry.js').recordSpawn({timestamp:new Date().toISOString(),milestone:process.env.GSD_T_MILESTONE||'',command:'gsd-t-doc-ripple',phase:'doc-ripple',step:'${STEP:-}',domain:'${DOMAIN:-}',domain_type:'${DOMAIN_TYPE:-}',task:'${TASK:-}',model:'${MODEL:-sonnet}',duration_s:${DURATION:-0},input_tokens_before:${T0_TOKENS},input_tokens_after:${T1_TOKENS},tokens_consumed:${T1_TOKENS}-${T0_TOKENS},context_window_pct_before:${T0_PCT},context_window_pct_after:${T1_PCT},outcome:'${OUTCOME:-success}',halt_type:${HALT_TYPE:-null},escalated_via_advisor:${ESCALATED_VIA_ADVISOR:-false}})" 2>/dev/null || true
29
29
  ```
30
30
 
@@ -127,7 +127,7 @@ After subagent returns — run via Bash:
127
127
  `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START))`
128
128
 
129
129
  Read the real context% from the Context Meter state file:
130
- `CTX_PCT=$(node -e "try{const tb=require('./bin/token-budget.js'); process.stdout.write(String(tb.getSessionStatus('.').pct))}catch(_){process.stdout.write('N/A')}")`
130
+ `CTX_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs'); process.stdout.write(String(tb.getSessionStatus('.').pct))}catch(_){process.stdout.write('N/A')}")`
131
131
 
132
132
  Append to `.gsd-t/token-log.md` (create with header `| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Domain | Task | Ctx% |` if missing):
133
133
  `| {DT_START} | {DT_END} | gsd-t-doc-ripple | Step 5 | {model} | {DURATION}s | update:{document} | doc-ripple | — | {CTX_PCT} |`
@@ -23,14 +23,14 @@ Per `.gsd-t/contracts/token-telemetry-contract.md` v1.0.0. Every Task subagent s
23
23
 
24
24
  ```bash
25
25
  T0_TOKENS=$(node -e "try{const s=require('fs').readFileSync('.gsd-t/.context-meter-state.json','utf8');process.stdout.write(String(JSON.parse(s).inputTokens||0))}catch(_){process.stdout.write('0')}")
26
- T0_PCT=$(node -e "try{const tb=require('./bin/token-budget.js');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
26
+ T0_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
27
27
  ```
28
28
 
29
29
  **After each spawn — record the bracket:**
30
30
 
31
31
  ```bash
32
32
  T1_TOKENS=$(node -e "try{const s=require('fs').readFileSync('.gsd-t/.context-meter-state.json','utf8');process.stdout.write(String(JSON.parse(s).inputTokens||0))}catch(_){process.stdout.write('0')}")
33
- T1_PCT=$(node -e "try{const tb=require('./bin/token-budget.js');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
33
+ T1_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
34
34
  node -e "require('./bin/token-telemetry.js').recordSpawn({timestamp:new Date().toISOString(),milestone:process.env.GSD_T_MILESTONE||'',command:'gsd-t-execute',phase:'${PHASE:-execute}',step:'${STEP:-}',domain:'${DOMAIN:-}',domain_type:'${DOMAIN_TYPE:-}',task:'${TASK:-}',model:'${MODEL:-sonnet}',duration_s:${DURATION:-0},input_tokens_before:${T0_TOKENS},input_tokens_after:${T1_TOKENS},tokens_consumed:${T1_TOKENS}-${T0_TOKENS},context_window_pct_before:${T0_PCT},context_window_pct_after:${T1_PCT},outcome:'${OUTCOME:-success}',halt_type:${HALT_TYPE:-null},escalated_via_advisor:${ESCALATED_VIA_ADVISOR:-false}})" 2>/dev/null || true
35
35
  ```
36
36
 
@@ -74,7 +74,7 @@ If `can_start === false`, the Step 0 block above has already spawned the headles
74
74
  Run via Bash:
75
75
 
76
76
  ```bash
77
- node -e "const tb = require('./bin/token-budget.js'); const s = tb.getSessionStatus('.'); console.log(JSON.stringify(s));"
77
+ node -e "const tb = require('./bin/token-budget.cjs'); const s = tb.getSessionStatus('.'); console.log(JSON.stringify(s));"
78
78
  ```
79
79
 
80
80
  This calls `getSessionStatus()` (v2.0.0) which reads `.gsd-t/.context-meter-state.json` produced by the Context Meter PostToolUse hook. If the state file is fresh (timestamp within 5 min), you get real `pct` and `threshold` values; if missing or stale, the call falls back to the historical heuristic from `.gsd-t/token-log.md`.
@@ -191,7 +191,7 @@ Where `{CTX_PCT}` is the current `pct` value returned by `getSessionStatus()` (S
191
191
  **Token Budget Check (before dispatching each domain's tasks):**
192
192
 
193
193
  Run via Bash:
194
- `node -e "const tb = require('./bin/token-budget.js'); const s = tb.getSessionStatus('.'); const d = tb.getDegradationActions(s.threshold, '.'); process.stdout.write(JSON.stringify({band: d.band, pct: d.pct, message: d.message}));" 2>/dev/null`
194
+ `node -e "const tb = require('./bin/token-budget.cjs'); const s = tb.getSessionStatus('.'); const d = tb.getDegradationActions(s.threshold, '.'); process.stdout.write(JSON.stringify({band: d.band, pct: d.pct, message: d.message}));" 2>/dev/null`
195
195
 
196
196
  Apply the result (three-band model per `token-budget-contract.md` v3.0.0 — never silently degrade quality):
197
197
  - `band: 'normal'` or file missing → proceed with standard model assignments
@@ -529,7 +529,7 @@ Report back:
529
529
 
530
530
  6. **Per-domain Red Team** — invoke Step 5.5 (Red Team) NOW for this domain. This is the first place Red Team runs in v2.74.12 — there is no global post-execute Red Team anymore. If Red Team returns FAIL, fix bugs and re-run before proceeding to the next domain (max 2 fix-and-verify cycles); if bugs persist, log to `.gsd-t/deferred-items.md` and present to user.
531
531
 
532
- 7. **Context gate re-check** — run `node -e "const tb=require('./bin/token-budget.js'); const s=tb.getSessionStatus('.'); if(s.threshold==='stop'||s.threshold==='stale')process.exit(10); if(s.threshold==='warn')process.exit(13);"`. If exit code is `10`, follow the Step 3.5 STOP procedure now (do NOT spawn the next domain). `stale` means the context meter is dead (usually missing `ANTHROPIC_API_KEY`) and is treated as STOP — print `⚠ Context meter DEAD — run 'gsd-t doctor' and fix before continuing` and halt. If exit code is `13`, log the warning and proceed at full quality for the next domain (no model overrides, no phase skips — quality is never silently degraded).
532
+ 7. **Context gate re-check** — run `node -e "const tb=require('./bin/token-budget.cjs'); const s=tb.getSessionStatus('.'); if(s.threshold==='stop'||s.threshold==='stale')process.exit(10); if(s.threshold==='warn')process.exit(13);"`. If exit code is `10`, follow the Step 3.5 STOP procedure now (do NOT spawn the next domain). `stale` means the context meter is dead (usually missing `ANTHROPIC_API_KEY`) and is treated as STOP — print `⚠ Context meter DEAD — run 'gsd-t doctor' and fix before continuing` and halt. If exit code is `13`, log the warning and proceed at full quality for the next domain (no model overrides, no phase skips — quality is never silently degraded).
533
533
 
534
534
  ### Team Mode (when agent teams are enabled)
535
535
  Spawn teammates for domains within the same wave. Only domains in the same wave can run in parallel — do not spawn teammates for domains in different waves simultaneously. Each teammate uses the **domain task-dispatcher pattern** — one subagent per task within their domain (same as solo mode).
@@ -675,12 +675,12 @@ Cleanup is not optional — orphaned worktrees waste disk space and can confuse
675
675
 
676
676
  ## Step 3.5: Orchestrator Context Gate (MANDATORY)
677
677
 
678
- The orchestrator MUST check `getSessionStatus()` BEFORE every task subagent spawn AND immediately AFTER every domain completes. This is the real context-burn guardrail. As of v2.0.0 (M34), `bin/token-budget.js` reads `.gsd-t/.context-meter-state.json` — the live count_tokens-based `input_tokens` measurement produced by the Context Meter PostToolUse hook. When the state file is fresh (timestamp within 5 min), thresholds reflect the ACTUAL context window utilization; when absent or stale, the call falls back to the historical heuristic from `.gsd-t/token-log.md`.
678
+ The orchestrator MUST check `getSessionStatus()` BEFORE every task subagent spawn AND immediately AFTER every domain completes. This is the real context-burn guardrail. As of v2.0.0 (M34), `bin/token-budget.cjs` reads `.gsd-t/.context-meter-state.json` — the live count_tokens-based `input_tokens` measurement produced by the Context Meter PostToolUse hook. When the state file is fresh (timestamp within 5 min), thresholds reflect the ACTUAL context window utilization; when absent or stale, the call falls back to the historical heuristic from `.gsd-t/token-log.md`.
679
679
 
680
680
  **Before each task spawn — gate check:**
681
681
 
682
682
  ```bash
683
- node -e "const tb=require('./bin/token-budget.js'); const s=tb.getSessionStatus('.'); process.stdout.write(JSON.stringify(s)); if(s.threshold==='stop'||s.threshold==='stale')process.exit(10); if(s.threshold==='warn')process.exit(13);"
683
+ node -e "const tb=require('./bin/token-budget.cjs'); const s=tb.getSessionStatus('.'); process.stdout.write(JSON.stringify(s)); if(s.threshold==='stop'||s.threshold==='stale')process.exit(10); if(s.threshold==='warn')process.exit(13);"
684
684
  ```
685
685
 
686
686
  Exit code semantics (three-band model per `token-budget-contract.md` v3.0.0, extended with a fourth `stale` guard in v3.10.12):
@@ -709,13 +709,13 @@ Run the same command again. The fresh reading reflects post-task consumption (th
709
709
 
710
710
  **Configuring threshold bands:**
711
711
 
712
- Band boundaries (`warn=70`, `stop=85`) are defined in `bin/token-budget.js` (`WARN_THRESHOLD_PCT` / `STOP_THRESHOLD_PCT` constants) and documented in `.gsd-t/contracts/token-budget-contract.md` v3.0.0. The `modelWindowSize` used for the denominator comes from `.gsd-t/context-meter-config.json` (default `200000`). Override the window size there if running against a different model. There is no per-session env-var override — the real-time measurement supersedes the need for one.
712
+ Band boundaries (`warn=70`, `stop=85`) are defined in `bin/token-budget.cjs` (`WARN_THRESHOLD_PCT` / `STOP_THRESHOLD_PCT` constants) and documented in `.gsd-t/contracts/token-budget-contract.md` v3.0.0. The `modelWindowSize` used for the denominator comes from `.gsd-t/context-meter-config.json` (default `200000`). Override the window size there if running against a different model. There is no per-session env-var override — the real-time measurement supersedes the need for one.
713
713
 
714
714
  **On resume (Step 0 — first thing the orchestrator does in a fresh session):**
715
715
 
716
716
  Step 0 runs `getSessionStatus()` once for readiness confirmation. The reading should be fresh (the Context Meter hook fires on every tool call), so the gate immediately reflects the new session's starting pct — typically near 0 since `/clear` resets the conversation.
717
717
 
718
- This gate replaces the v2.74.12 task counter proxy and the (never-functional) v1.x env-var check. It is fail-safe: if `bin/token-budget.js` or the state file is unreadable for any reason, `getSessionStatus()` throws and the gate exits non-zero (treated as STOP) rather than silently allowing unlimited spawns.
718
+ This gate replaces the v2.74.12 task counter proxy and the (never-functional) v1.x env-var check. It is fail-safe: if `bin/token-budget.cjs` or the state file is unreadable for any reason, `getSessionStatus()` throws and the gate exits non-zero (treated as STOP) rather than silently allowing unlimited spawns.
719
719
 
720
720
  ## Step 4: Checkpoint Handling
721
721
 
@@ -791,7 +791,7 @@ and summary, and the full comparison table per the protocol's Step 7."
791
791
  ```
792
792
 
793
793
  After subagent returns — run via Bash:
794
- `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "try{const tb=require('./bin/token-budget.js'); process.stdout.write(String(tb.getSessionStatus('.').pct))}catch(_){process.stdout.write('N/A')}")`
794
+ `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs'); process.stdout.write(String(tb.getSessionStatus('.').pct))}catch(_){process.stdout.write('N/A')}")`
795
795
  Append to `.gsd-t/token-log.md`:
796
796
  `| {DT_START} | {DT_END} | gsd-t-execute | Design Verify | opus | {DURATION}s | {VERDICT} — {MATCH}/{TOTAL} elements for {domain-name} | | | {CTX_PCT} |`
797
797
 
@@ -844,7 +844,7 @@ attack categories exhausted, and the path to the written
844
844
  ```
845
845
 
846
846
  After subagent returns — run via Bash:
847
- `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "try{const tb=require('./bin/token-budget.js'); process.stdout.write(String(tb.getSessionStatus('.').pct))}catch(_){process.stdout.write('N/A')}")`
847
+ `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs'); process.stdout.write(String(tb.getSessionStatus('.').pct))}catch(_){process.stdout.write('N/A')}")`
848
848
  Append to `.gsd-t/token-log.md`:
849
849
  `| {DT_START} | {DT_END} | gsd-t-execute | Red Team | opus | {DURATION}s | {VERDICT} — {N} bugs found in {domain-name} | | | {CTX_PCT} |`
850
850
 
@@ -369,7 +369,7 @@ Use these when user asks for help on a specific command:
369
369
  - **Summary**: Harness self-audit — analyze cost and benefit of GSD-T enforcement components (QA, Red Team, doc-ripple, token budget), with optional shadow mode to measure overhead
370
370
  - **Auto-invoked**: No
371
371
  - **Creates**: `.gsd-t/audit-report.md`
372
- - **Reads**: `.gsd-t/metrics/`, `.gsd-t/token-log.md`, `bin/component-registry.js`, `bin/qa-calibrator.js`, `bin/token-budget.js`
372
+ - **Reads**: `.gsd-t/metrics/`, `.gsd-t/token-log.md`, `bin/component-registry.js`, `bin/qa-calibrator.js`, `bin/token-budget.cjs`
373
373
  - **Use when**: Reviewing whether enforcement components are adding value or overhead; shadow mode measures impact without disabling components; `--disable {component}` temporarily disables one component for comparison
374
374
 
375
375
  ### headless --debug-loop
@@ -19,14 +19,14 @@ Per `.gsd-t/contracts/token-telemetry-contract.md` v1.0.0. Every Task subagent s
19
19
 
20
20
  ```bash
21
21
  T0_TOKENS=$(node -e "try{const s=require('fs').readFileSync('.gsd-t/.context-meter-state.json','utf8');process.stdout.write(String(JSON.parse(s).inputTokens||0))}catch(_){process.stdout.write('0')}")
22
- T0_PCT=$(node -e "try{const tb=require('./bin/token-budget.js');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
22
+ T0_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
23
23
  ```
24
24
 
25
25
  **After each spawn — record the bracket:**
26
26
 
27
27
  ```bash
28
28
  T1_TOKENS=$(node -e "try{const s=require('fs').readFileSync('.gsd-t/.context-meter-state.json','utf8');process.stdout.write(String(JSON.parse(s).inputTokens||0))}catch(_){process.stdout.write('0')}")
29
- T1_PCT=$(node -e "try{const tb=require('./bin/token-budget.js');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
29
+ T1_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
30
30
  node -e "require('./bin/token-telemetry.js').recordSpawn({timestamp:new Date().toISOString(),milestone:process.env.GSD_T_MILESTONE||'',command:'gsd-t-integrate',phase:'integrate',step:'${STEP:-}',domain:'${DOMAIN:-}',domain_type:'${DOMAIN_TYPE:-}',task:'${TASK:-}',model:'${MODEL:-sonnet}',duration_s:${DURATION:-0},input_tokens_before:${T0_TOKENS},input_tokens_after:${T1_TOKENS},tokens_consumed:${T1_TOKENS}-${T0_TOKENS},context_window_pct_before:${T0_PCT},context_window_pct_after:${T1_PCT},outcome:'${OUTCOME:-success}',halt_type:${HALT_TYPE:-null},escalated_via_advisor:${ESCALATED_VIA_ADVISOR:-false}})" 2>/dev/null || true
31
31
  ```
32
32
 
@@ -292,7 +292,7 @@ Spawn Task subagent (general-purpose, model: opus):
292
292
  After subagent returns — run via Bash:
293
293
  ```
294
294
  T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START))
295
- CTX_PCT=$(node -e "try{const tb=require('./bin/token-budget.js'); process.stdout.write(String(tb.getSessionStatus('.').pct))}catch(_){process.stdout.write('N/A')}")
295
+ CTX_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs'); process.stdout.write(String(tb.getSessionStatus('.').pct))}catch(_){process.stdout.write('N/A')}")
296
296
  ```
297
297
  Append to `.gsd-t/token-log.md`:
298
298
  `| {DT_START} | {DT_END} | gsd-t-integrate | Red Team | opus | {DURATION}s | {VERDICT} — {N} bugs found | | | {CTX_PCT} |`
@@ -383,7 +383,7 @@ Report: PASS (all checks pass) or FAIL with specific gaps listed."
383
383
  Before spawning — run via Bash:
384
384
  `T_START=$(date +%s) && DT_START=$(date +"%Y-%m-%d %H:%M")`
385
385
  After subagent returns — run via Bash:
386
- `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.js'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
386
+ `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.cjs'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
387
387
  Append to `.gsd-t/token-log.md` (create with header `| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Domain | Task | Ctx% |` if missing):
388
388
  `| {DT_START} | {DT_END} | gsd-t-plan | Step 7 | haiku | {DURATION}s | {PASS/FAIL}, iteration {N} | | | {CTX_PCT} |`
389
389
  If validation FAIL, append each gap to `.gsd-t/qa-issues.md` (create with header `| Date | Command | Step | Model | Duration(s) | Severity | Finding |` if missing):
@@ -23,7 +23,7 @@ Read CLAUDE.md and .gsd-t/progress.md for project context, then execute gsd-t-pr
23
23
  ```
24
24
 
25
25
  After subagent returns — run via Bash:
26
- `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.js'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
26
+ `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.cjs'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
27
27
  Append to `.gsd-t/token-log.md` (create with header `| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Ctx% |` if missing):
28
28
  `| {DT_START} | {DT_END} | gsd-t-prd | Step 0 | sonnet | {DURATION}s | prd: {topic summary} | {CTX_PCT} |`
29
29
 
@@ -19,14 +19,14 @@ Per `.gsd-t/contracts/token-telemetry-contract.md` v1.0.0. Every Task subagent s
19
19
 
20
20
  ```bash
21
21
  T0_TOKENS=$(node -e "try{const s=require('fs').readFileSync('.gsd-t/.context-meter-state.json','utf8');process.stdout.write(String(JSON.parse(s).inputTokens||0))}catch(_){process.stdout.write('0')}")
22
- T0_PCT=$(node -e "try{const tb=require('./bin/token-budget.js');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
22
+ T0_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
23
23
  ```
24
24
 
25
25
  **After each spawn — record the bracket:**
26
26
 
27
27
  ```bash
28
28
  T1_TOKENS=$(node -e "try{const s=require('fs').readFileSync('.gsd-t/.context-meter-state.json','utf8');process.stdout.write(String(JSON.parse(s).inputTokens||0))}catch(_){process.stdout.write('0')}")
29
- T1_PCT=$(node -e "try{const tb=require('./bin/token-budget.js');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
29
+ T1_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
30
30
  node -e "require('./bin/token-telemetry.js').recordSpawn({timestamp:new Date().toISOString(),milestone:process.env.GSD_T_MILESTONE||'',command:'gsd-t-quick',phase:'quick',step:'${STEP:-}',domain:'${DOMAIN:-}',domain_type:'${DOMAIN_TYPE:-}',task:'${TASK:-}',model:'${MODEL:-sonnet}',duration_s:${DURATION:-0},input_tokens_before:${T0_TOKENS},input_tokens_after:${T1_TOKENS},tokens_consumed:${T1_TOKENS}-${T0_TOKENS},context_window_pct_before:${T0_PCT},context_window_pct_after:${T1_PCT},outcome:'${OUTCOME:-success}',halt_type:${HALT_TYPE:-null},escalated_via_advisor:${ESCALATED_VIA_ADVISOR:-false}})" 2>/dev/null || true
31
31
  ```
32
32
 
@@ -78,7 +78,7 @@ Before spawning — run via Bash:
78
78
  **Token Budget Check (before spawning subagent):**
79
79
 
80
80
  Run via Bash:
81
- `node -e "const tb = require('./bin/token-budget.js'); const s = tb.getSessionStatus('.'); process.stdout.write(s.threshold);" 2>/dev/null`
81
+ `node -e "const tb = require('./bin/token-budget.cjs'); const s = tb.getSessionStatus('.'); process.stdout.write(s.threshold);" 2>/dev/null`
82
82
 
83
83
  Apply the result (three-band model per `token-budget-contract.md` v3.0.0 — never silently degrade quality):
84
84
  - `normal` or file missing → proceed with default model (sonnet)
@@ -403,7 +403,7 @@ Spawn Task subagent (general-purpose, model: opus):
403
403
  After subagent returns — run via Bash:
404
404
  ```
405
405
  T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START))
406
- CTX_PCT=$(node -e "try{const tb=require('./bin/token-budget.js'); process.stdout.write(String(tb.getSessionStatus('.').pct))}catch(_){process.stdout.write('N/A')}")
406
+ CTX_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs'); process.stdout.write(String(tb.getSessionStatus('.').pct))}catch(_){process.stdout.write('N/A')}")
407
407
  ```
408
408
  Append to `.gsd-t/token-log.md`:
409
409
  `| {DT_START} | {DT_END} | gsd-t-quick | Red Team | opus | {DURATION}s | {VERDICT} — {N} bugs found | | | {CTX_PCT} |`
@@ -21,7 +21,7 @@ Skip Step 0 — you are already the subagent."
21
21
  **OBSERVABILITY LOGGING — after subagent returns:**
22
22
 
23
23
  Run via Bash:
24
- `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.js'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
24
+ `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.cjs'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
25
25
 
26
26
  Append to `.gsd-t/token-log.md` (create with header `| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Ctx% |` if missing):
27
27
  `| {DT_START} | {DT_END} | gsd-t-reflect | Step 0 | sonnet | {DURATION}s | retrospective generated | {CTX_PCT} |`
@@ -80,7 +80,7 @@ Run via Bash:
80
80
 
81
81
  ```bash
82
82
  node -e "
83
- const tb=require('./bin/token-budget.js');
83
+ const tb=require('./bin/token-budget.cjs');
84
84
  const s=tb.getSessionStatus('.');
85
85
  if (s.threshold === 'stale') {
86
86
  console.error('⚠ Context meter is DEAD — reason: ' + (s.deadReason || 'unknown'));
@@ -223,7 +223,7 @@ If `WARNINGS` were emitted, print them as a non-blocking advisory before proceed
223
223
  ```bash
224
224
  T_START=$(date +%s) && DT_START=$(date +"%Y-%m-%d %H:%M")
225
225
  T0_TOKENS=$(node -e "try{const s=require('fs').readFileSync('.gsd-t/.context-meter-state.json','utf8');process.stdout.write(String(JSON.parse(s).inputTokens||0))}catch(_){process.stdout.write('0')}")
226
- T0_PCT=$(node -e "try{const tb=require('./bin/token-budget.js');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
226
+ T0_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
227
227
  ```
228
228
 
229
229
  If `--dry-run` was specified, print:
@@ -273,7 +273,7 @@ Capture `SPAWNED_PID` from the output.
273
273
  ```bash
274
274
  T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START))
275
275
  T1_TOKENS=$(node -e "try{const s=require('fs').readFileSync('.gsd-t/.context-meter-state.json','utf8');process.stdout.write(String(JSON.parse(s).inputTokens||0))}catch(_){process.stdout.write('0')}")
276
- T1_PCT=$(node -e "try{const tb=require('./bin/token-budget.js');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
276
+ T1_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
277
277
  COUNTER=$(node bin/task-counter.cjs status 2>/dev/null | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{try{process.stdout.write(String(JSON.parse(s).count||''))}catch(_){process.stdout.write('')}})")
278
278
  ```
279
279
 
@@ -168,7 +168,7 @@ Before spawning — run via Bash:
168
168
  `T_START=$(date +%s) && DT_START=$(date +"%Y-%m-%d %H:%M")`
169
169
  Spawn a Task subagent to run the full test suite and contract audit.
170
170
  After subagent returns — run via Bash:
171
- `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.js'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
171
+ `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.cjs'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
172
172
  Append to `.gsd-t/token-log.md` (create with header `| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Ctx% |` if missing):
173
173
  `| {DT_START} | {DT_END} | gsd-t-verify | Step 4 | haiku | {DURATION}s | test audit + contract review | {CTX_PCT} |`
174
174
  Collect all reports, synthesize, create remediation plan.
@@ -374,7 +374,7 @@ Report back: one-line status summary."
374
374
  ```
375
375
 
376
376
  After subagent returns — run via Bash:
377
- `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.js'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
377
+ `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.cjs'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
378
378
  Append to `.gsd-t/token-log.md`:
379
379
  `| {DT_START} | {DT_END} | gsd-t-verify | Step 8 | sonnet | {DURATION}s | auto-complete-milestone | | | {CTX_PCT} |`
380
380
 
@@ -21,7 +21,7 @@ Skip Step 0 — you are already the subagent."
21
21
  **OBSERVABILITY LOGGING — after subagent returns:**
22
22
 
23
23
  Run via Bash:
24
- `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.js'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
24
+ `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START)) && CTX_PCT=$(node -e "const tb=require('./bin/token-budget.cjs'); process.stdout.write(String(tb.getSessionStatus('.').pct||'N/A'))" 2>/dev/null || echo "N/A")`
25
25
 
26
26
  Append to `.gsd-t/token-log.md` (create with header `| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Ctx% |` if missing):
27
27
  `| {DT_START} | {DT_END} | gsd-t-visualize | Step 0 | sonnet | {DURATION}s | dashboard launched | {CTX_PCT} |`
@@ -18,14 +18,14 @@ Per `.gsd-t/contracts/token-telemetry-contract.md` v1.0.0. Every phase agent spa
18
18
 
19
19
  ```bash
20
20
  T0_TOKENS=$(node -e "try{const s=require('fs').readFileSync('.gsd-t/.context-meter-state.json','utf8');process.stdout.write(String(JSON.parse(s).inputTokens||0))}catch(_){process.stdout.write('0')}")
21
- T0_PCT=$(node -e "try{const tb=require('./bin/token-budget.js');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
21
+ T0_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
22
22
  ```
23
23
 
24
24
  **After each phase spawn — record the bracket:**
25
25
 
26
26
  ```bash
27
27
  T1_TOKENS=$(node -e "try{const s=require('fs').readFileSync('.gsd-t/.context-meter-state.json','utf8');process.stdout.write(String(JSON.parse(s).inputTokens||0))}catch(_){process.stdout.write('0')}")
28
- T1_PCT=$(node -e "try{const tb=require('./bin/token-budget.js');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
28
+ T1_PCT=$(node -e "try{const tb=require('./bin/token-budget.cjs');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
29
29
  node -e "require('./bin/token-telemetry.js').recordSpawn({timestamp:new Date().toISOString(),milestone:process.env.GSD_T_MILESTONE||'',command:'gsd-t-wave',phase:'${PHASE:-}',step:'${STEP:-}',domain:'${DOMAIN:-}',domain_type:'${DOMAIN_TYPE:-}',task:'${TASK:-}',model:'${MODEL:-sonnet}',duration_s:${DURATION:-0},input_tokens_before:${T0_TOKENS},input_tokens_after:${T1_TOKENS},tokens_consumed:${T1_TOKENS}-${T0_TOKENS},context_window_pct_before:${T0_PCT},context_window_pct_after:${T1_PCT},outcome:'${OUTCOME:-success}',halt_type:${HALT_TYPE:-null},escalated_via_advisor:${ESCALATED_VIA_ADVISOR:-false}})" 2>/dev/null || true
30
30
  ```
31
31
 
@@ -69,10 +69,10 @@ If `can_start === false`, the headless continuation has already been spawned and
69
69
  Run via Bash:
70
70
 
71
71
  ```bash
72
- node -e "const tb = require('./bin/token-budget.js'); const s = tb.getSessionStatus('.'); console.log(JSON.stringify(s));"
72
+ node -e "const tb = require('./bin/token-budget.cjs'); const s = tb.getSessionStatus('.'); console.log(JSON.stringify(s));"
73
73
  ```
74
74
 
75
- This calls `getSessionStatus()` (v2.0.0) which reads `.gsd-t/.context-meter-state.json` produced by the Context Meter PostToolUse hook. The returned `threshold` drives the gate logic in the Phase Agent Spawn Pattern below — it enforces the three-band stop boundary (85%) so the wave orchestrator itself never runs out of context mid-wave. When the state file is absent or stale, the call falls back to a historical heuristic from `.gsd-t/token-log.md`. Band boundaries and `modelWindowSize` are configured in `.gsd-t/context-meter-config.json` and `bin/token-budget.js` (THRESHOLDS constant).
75
+ This calls `getSessionStatus()` (v2.0.0) which reads `.gsd-t/.context-meter-state.json` produced by the Context Meter PostToolUse hook. The returned `threshold` drives the gate logic in the Phase Agent Spawn Pattern below — it enforces the three-band stop boundary (85%) so the wave orchestrator itself never runs out of context mid-wave. When the state file is absent or stale, the call falls back to a historical heuristic from `.gsd-t/token-log.md`. Band boundaries and `modelWindowSize` are configured in `.gsd-t/context-meter-config.json` and `bin/token-budget.cjs` (THRESHOLDS constant).
76
76
 
77
77
  ## Step 1: Load State (Lightweight)
78
78
 
@@ -116,7 +116,7 @@ From progress.md status, determine which phase to start from:
116
116
  Before starting the phase loop, check the projected token cost for this milestone:
117
117
 
118
118
  Run via Bash:
119
- `node -e "const tb = require('./bin/token-budget.js'); const est = tb.estimateMilestoneCost('.'); if(est) process.stdout.write(JSON.stringify(est));" 2>/dev/null`
119
+ `node -e "const tb = require('./bin/token-budget.cjs'); const est = tb.estimateMilestoneCost('.'); if(est) process.stdout.write(JSON.stringify(est));" 2>/dev/null`
120
120
 
121
121
  If the command returns data, display to user:
122
122
  - `estimated_tokens`: projected total tokens for this milestone
@@ -156,7 +156,7 @@ If STACK_RULES is empty (no templates/stacks/ dir or no matches), skip silently.
156
156
  **Per-Phase Token Budget Check (before each phase spawn):**
157
157
 
158
158
  Run via Bash:
159
- `node -e "const tb = require('./bin/token-budget.js'); const s = tb.getSessionStatus('.'); process.stdout.write(s.threshold);" 2>/dev/null`
159
+ `node -e "const tb = require('./bin/token-budget.cjs'); const s = tb.getSessionStatus('.'); process.stdout.write(s.threshold);" 2>/dev/null`
160
160
 
161
161
  Three-band model per `token-budget-contract.md` v3.0.0 (never silently degrade quality):
162
162
  - `normal` or file missing → proceed normally
@@ -193,7 +193,7 @@ After phase agent returns — run via Bash:
193
193
  Run via Bash AFTER each phase agent returns:
194
194
 
195
195
  ```bash
196
- node -e "const tb=require('./bin/token-budget.js'); const s=tb.getSessionStatus('.'); process.stdout.write(JSON.stringify(s)); if(s.threshold==='stop'||s.threshold==='stale')process.exit(10); if(s.threshold==='warn')process.exit(13);"
196
+ node -e "const tb=require('./bin/token-budget.cjs'); const s=tb.getSessionStatus('.'); process.stdout.write(JSON.stringify(s)); if(s.threshold==='stop'||s.threshold==='stale')process.exit(10); if(s.threshold==='warn')process.exit(13);"
197
197
  ```
198
198
 
199
199
  The JSON on stdout contains `{consumed, estimated_remaining, pct, threshold}` — capture `pct` as `{CTX_PCT}` for the token-log row.
@@ -204,7 +204,7 @@ Exit-code handling (three-band model per `token-budget-contract.md` v3.0.0, exte
204
204
  - `10` (stop, ≥85%) → STOP the wave loop. Save checkpoint to `.gsd-t/progress.md` — record which phases are complete, which remain. Call `autoSpawnHeadless({command: 'gsd-t-wave', args, projectDir})` — this spawns a fresh headless session that auto-resumes via `/gsd-t-resume` without any manual `/clear`. Output: `⏸️ Wave orchestrator context gate reached ({pct}% of model window) — handing off to a fresh headless session (ID: {id}). Progress saved.` Return cleanly. Do NOT spawn the next phase agent, do NOT exit with a special code — the handoff is the success path.
205
205
  - `10` (stale, meter dead) → **not resumable**. The context meter is dead (usually missing `ANTHROPIC_API_KEY`). Do NOT auto-spawn a fresh session — a fresh session would have the same broken guardrail. Instead, halt the wave and print `⚠ Context meter DEAD — run 'gsd-t doctor' and fix the cause before resuming`.
206
206
 
207
- As of v2.0.0 (M34), the wave orchestrator reads the SAME `bin/token-budget.js` real-source measurement as the execute orchestrator — both trace back to `.gsd-t/.context-meter-state.json` produced by the Context Meter PostToolUse hook. Each phase spawn (PARTITION, DISCUSS, PLAN, IMPACT, EXECUTE, TEST-SYNC, INTEGRATE, VERIFY+COMPLETE, DOC-RIPPLE) causes post-call updates to the state file, so each subsequent gate check reflects the real context consumption trajectory. When the state file is absent or stale, the call falls back to the historical heuristic.
207
+ As of v2.0.0 (M34), the wave orchestrator reads the SAME `bin/token-budget.cjs` real-source measurement as the execute orchestrator — both trace back to `.gsd-t/.context-meter-state.json` produced by the Context Meter PostToolUse hook. Each phase spawn (PARTITION, DISCUSS, PLAN, IMPACT, EXECUTE, TEST-SYNC, INTEGRATE, VERIFY+COMPLETE, DOC-RIPPLE) causes post-call updates to the state file, so each subsequent gate check reflects the real context consumption trajectory. When the state file is absent or stale, the call falls back to the historical heuristic.
208
208
 
209
209
  The previous v1.x version relied on an environment-variable-based context check which Claude Code never populated; v2.74.12 stood in a proxy task counter; v2.0.0 (M34) retires both and uses the real count_tokens measurement.
210
210
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "3.10.12",
3
+ "version": "3.10.14",
4
4
  "description": "GSD-T: Contract-Driven Development for Claude Code — 61 slash commands with unattended supervisor relay, headless CI/CD mode, graph-powered code analysis, real-time agent dashboard, execution intelligence, 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",
@@ -152,7 +152,69 @@ async function parseTranscript(transcriptPath) {
152
152
  return null;
153
153
  }
154
154
 
155
- return { system, messages };
155
+ return { system, messages: sanitizeToolPairs(messages) };
156
+ }
157
+
158
+ /**
159
+ * Enforce the count_tokens adjacency constraint: every assistant tool_use
160
+ * must be immediately followed by a user message whose tool_result ids match
161
+ * ALL tool_use ids from the preceding assistant message. Walk the message
162
+ * list and strip tool_use / tool_result blocks from any pair that violates
163
+ * this rule. Drop messages that become empty after stripping.
164
+ */
165
+ function sanitizeToolPairs(messages) {
166
+ const out = [];
167
+ for (let i = 0; i < messages.length; i++) {
168
+ const m = messages[i];
169
+ if (!Array.isArray(m.content)) { out.push(m); continue; }
170
+
171
+ if (m.role === "assistant") {
172
+ const toolUseIds = new Set();
173
+ for (const b of m.content) {
174
+ if (b.type === "tool_use" && typeof b.id === "string") toolUseIds.add(b.id);
175
+ }
176
+
177
+ if (toolUseIds.size === 0) { out.push(m); continue; }
178
+
179
+ const next = messages[i + 1];
180
+ const nextResultIds = new Set();
181
+ if (next && next.role === "user" && Array.isArray(next.content)) {
182
+ for (const b of next.content) {
183
+ if (b.type === "tool_result" && typeof b.tool_use_id === "string") {
184
+ nextResultIds.add(b.tool_use_id);
185
+ }
186
+ }
187
+ }
188
+
189
+ const validIds = new Set([...toolUseIds].filter((id) => nextResultIds.has(id)));
190
+ const filtered = m.content.filter((b) => {
191
+ if (b.type === "tool_use") return validIds.has(b.id);
192
+ return true;
193
+ });
194
+ if (filtered.length > 0) out.push({ role: m.role, content: filtered });
195
+ continue;
196
+ }
197
+
198
+ if (m.role === "user") {
199
+ const prev = out[out.length - 1];
200
+ const prevUseIds = new Set();
201
+ if (prev && prev.role === "assistant" && Array.isArray(prev.content)) {
202
+ for (const b of prev.content) {
203
+ if (b.type === "tool_use" && typeof b.id === "string") prevUseIds.add(b.id);
204
+ }
205
+ }
206
+
207
+ const filtered = m.content.filter((b) => {
208
+ if (b.type === "tool_result") return prevUseIds.has(b.tool_use_id);
209
+ return true;
210
+ });
211
+ if (filtered.length > 0) out.push({ role: m.role, content: filtered });
212
+ continue;
213
+ }
214
+
215
+ out.push(m);
216
+ }
217
+ return out;
156
218
  }
157
219
 
158
220
  /**
@@ -176,6 +176,15 @@ test("tool_use / tool_result pairing by tool_use_id preserved in order", async (
176
176
 
177
177
  test("tool_result with array content (text blocks) is normalized", async () => {
178
178
  const { dir, file } = mkTmpFile([
179
+ {
180
+ type: "assistant",
181
+ message: {
182
+ role: "assistant",
183
+ content: [
184
+ { type: "tool_use", id: "toolu_02", name: "Read", input: { file_path: "/tmp/x" } },
185
+ ],
186
+ },
187
+ },
179
188
  {
180
189
  type: "user",
181
190
  message: {
@@ -196,8 +205,8 @@ test("tool_result with array content (text blocks) is normalized", async () => {
196
205
  ]);
197
206
  try {
198
207
  const got = await parseTranscript(file);
199
- assert.equal(got.messages.length, 1);
200
- const tr = got.messages[0].content[0];
208
+ const userMsg = got.messages.find(m => m.role === "user");
209
+ const tr = userMsg.content[0];
201
210
  assert.equal(tr.type, "tool_result");
202
211
  assert.deepEqual(tr.content, [
203
212
  { type: "text", text: "line 1" },
@@ -210,6 +219,15 @@ test("tool_result with array content (text blocks) is normalized", async () => {
210
219
 
211
220
  test("tool_result with is_error:true preserved", async () => {
212
221
  const { dir, file } = mkTmpFile([
222
+ {
223
+ type: "assistant",
224
+ message: {
225
+ role: "assistant",
226
+ content: [
227
+ { type: "tool_use", id: "toolu_03", name: "Bash", input: { command: "false" } },
228
+ ],
229
+ },
230
+ },
213
231
  {
214
232
  type: "user",
215
233
  message: {
@@ -222,7 +240,8 @@ test("tool_result with is_error:true preserved", async () => {
222
240
  ]);
223
241
  try {
224
242
  const got = await parseTranscript(file);
225
- assert.equal(got.messages[0].content[0].is_error, true);
243
+ const userMsg = got.messages.find(m => m.role === "user");
244
+ assert.equal(userMsg.content[0].is_error, true);
226
245
  } finally {
227
246
  cleanup(dir);
228
247
  }
@@ -318,3 +337,34 @@ test("message with content array but no recognized blocks → message skipped",
318
337
  cleanup(dir);
319
338
  }
320
339
  });
340
+
341
+ test("orphaned tool_use without matching tool_result is stripped", async () => {
342
+ const { dir, file } = mkTmpFile([
343
+ { type: "user", message: { role: "user", content: "hello" } },
344
+ {
345
+ type: "assistant",
346
+ message: {
347
+ role: "assistant",
348
+ content: [
349
+ { type: "text", text: "I will read a file" },
350
+ { type: "tool_use", id: "toolu_orphan", name: "Read", input: { file_path: "/tmp/x" } },
351
+ ],
352
+ },
353
+ },
354
+ { type: "user", message: { role: "user", content: "thanks" } },
355
+ ]);
356
+ try {
357
+ const got = await parseTranscript(file);
358
+ const assistantMsg = got.messages.find(
359
+ (m) => m.role === "assistant" && m.content.some((b) => b.type === "text")
360
+ );
361
+ assert.ok(assistantMsg, "assistant message preserved");
362
+ assert.ok(
363
+ !assistantMsg.content.some((b) => b.type === "tool_use"),
364
+ "orphaned tool_use stripped"
365
+ );
366
+ assert.equal(assistantMsg.content[0].text, "I will read a file");
367
+ } finally {
368
+ cleanup(dir);
369
+ }
370
+ });
File without changes