@tekyzinc/gsd-t 2.74.13 → 3.10.10

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/README.md +117 -1
  3. package/bin/advisor-integration.js +93 -0
  4. package/bin/check-headless-sessions.js +140 -0
  5. package/bin/context-meter-config.cjs +101 -0
  6. package/bin/context-meter-config.test.cjs +101 -0
  7. package/bin/gsd-t-unattended-platform.js +381 -0
  8. package/bin/gsd-t-unattended-safety.js +766 -0
  9. package/bin/gsd-t-unattended.js +1259 -0
  10. package/bin/gsd-t.js +723 -19
  11. package/bin/handoff-lock.js +249 -0
  12. package/bin/headless-auto-spawn.js +328 -0
  13. package/bin/model-selector.js +224 -0
  14. package/bin/runway-estimator.js +242 -0
  15. package/bin/token-budget.js +96 -89
  16. package/bin/token-optimizer.js +471 -0
  17. package/bin/token-telemetry.js +246 -0
  18. package/commands/gsd-t-audit.md +3 -3
  19. package/commands/gsd-t-backlog-list.md +38 -0
  20. package/commands/gsd-t-brainstorm.md +3 -3
  21. package/commands/gsd-t-complete-milestone.md +24 -0
  22. package/commands/gsd-t-debug.md +124 -7
  23. package/commands/gsd-t-discuss.md +10 -3
  24. package/commands/gsd-t-doc-ripple.md +32 -4
  25. package/commands/gsd-t-execute.md +107 -52
  26. package/commands/gsd-t-help.md +22 -0
  27. package/commands/gsd-t-integrate.md +67 -4
  28. package/commands/gsd-t-optimization-apply.md +91 -0
  29. package/commands/gsd-t-optimization-reject.md +94 -0
  30. package/commands/gsd-t-partition.md +7 -0
  31. package/commands/gsd-t-pause.md +3 -0
  32. package/commands/gsd-t-plan.md +10 -3
  33. package/commands/gsd-t-prd.md +3 -3
  34. package/commands/gsd-t-quick.md +71 -9
  35. package/commands/gsd-t-reflect.md +3 -7
  36. package/commands/gsd-t-resume.md +86 -1
  37. package/commands/gsd-t-status.md +31 -0
  38. package/commands/gsd-t-test-sync.md +7 -0
  39. package/commands/gsd-t-unattended-stop.md +83 -0
  40. package/commands/gsd-t-unattended-watch.md +290 -0
  41. package/commands/gsd-t-unattended.md +414 -0
  42. package/commands/gsd-t-verify.md +12 -5
  43. package/commands/gsd-t-visualize.md +3 -7
  44. package/commands/gsd-t-wave.md +82 -18
  45. package/docs/GSD-T-README.md +69 -0
  46. package/docs/architecture.md +176 -4
  47. package/docs/infrastructure.md +221 -0
  48. package/docs/methodology.md +44 -0
  49. package/docs/prd-harness-evolution.md +51 -37
  50. package/docs/requirements.md +95 -0
  51. package/docs/unattended-windows-caveats.md +245 -0
  52. package/package.json +2 -2
  53. package/scripts/context-meter/count-tokens-client.js +221 -0
  54. package/scripts/context-meter/count-tokens-client.test.js +308 -0
  55. package/scripts/context-meter/test-injector.js +55 -0
  56. package/scripts/context-meter/threshold.js +88 -0
  57. package/scripts/context-meter/threshold.test.js +255 -0
  58. package/scripts/context-meter/transcript-parser.js +252 -0
  59. package/scripts/context-meter/transcript-parser.test.js +320 -0
  60. package/scripts/gsd-t-context-meter.e2e.test.js +415 -0
  61. package/scripts/gsd-t-context-meter.js +350 -0
  62. package/scripts/gsd-t-context-meter.test.js +417 -0
  63. package/scripts/gsd-t-heartbeat.js +2 -2
  64. package/scripts/gsd-t-statusline.js +23 -8
  65. package/templates/CLAUDE-global.md +17 -1
  66. package/templates/CLAUDE-project.md +26 -6
  67. package/templates/context-meter-config.json +10 -0
  68. package/templates/prompts/README.md +1 -1
  69. package/bin/task-counter.cjs +0 -161
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * GSD-T Token Telemetry — per-subagent-spawn granular telemetry recorder
5
+ *
6
+ * Records one JSON object per line to .gsd-t/token-metrics.jsonl for every
7
+ * subagent spawn across every command file. Feeds:
8
+ * - bin/runway-estimator.js (M35 Wave 3) — pre-flight runway projection
9
+ * - bin/token-optimizer.js (M35 Wave 4) — optimization backlog detector
10
+ * - gsd-t metrics --tokens / --halts / --tokens --context-window CLI
11
+ *
12
+ * Zero external dependencies (Node.js built-ins only).
13
+ * Zero API calls (reads .gsd-t/.context-meter-state.json written by M34 hook).
14
+ * Single-writer assumption — no lockfile; fs.appendFileSync is atomic for
15
+ * writes under PIPE_BUF (4096 bytes on POSIX), and a single record is well
16
+ * under that limit.
17
+ *
18
+ * Contract: .gsd-t/contracts/token-telemetry-contract.md v1.0.0
19
+ * Schema is frozen for v1.x — fields can be added in minor bumps but never
20
+ * removed or renamed.
21
+ */
22
+
23
+ const fs = require("fs");
24
+ const path = require("path");
25
+
26
+ // ── Frozen schema (matches token-telemetry-contract.md v1.0.0) ──────────────
27
+
28
+ /**
29
+ * The 18 required fields. Order is not significant on disk (parsers use keys),
30
+ * but this array is the canonical list for validation error messages and
31
+ * downstream tooling that needs a stable field enumeration.
32
+ */
33
+ const REQUIRED_FIELDS = Object.freeze([
34
+ "timestamp",
35
+ "milestone",
36
+ "command",
37
+ "phase",
38
+ "step",
39
+ "domain",
40
+ "domain_type",
41
+ "task",
42
+ "model",
43
+ "duration_s",
44
+ "input_tokens_before",
45
+ "input_tokens_after",
46
+ "tokens_consumed",
47
+ "context_window_pct_before",
48
+ "context_window_pct_after",
49
+ "outcome",
50
+ "halt_type",
51
+ "escalated_via_advisor",
52
+ ]);
53
+
54
+ /**
55
+ * Type enforcement map. Keys are field names; values are either "string",
56
+ * "number", "boolean", "nullable-string", or a Set of valid string enum values.
57
+ * halt_type is the only nullable field in v1.0.0 per the contract.
58
+ */
59
+ const FIELD_TYPES = Object.freeze({
60
+ timestamp: "string",
61
+ milestone: "string",
62
+ command: "string",
63
+ phase: "string",
64
+ step: "string",
65
+ domain: "string",
66
+ domain_type: "string",
67
+ task: "string",
68
+ model: new Set(["haiku", "sonnet", "opus"]),
69
+ duration_s: "number",
70
+ input_tokens_before: "number",
71
+ input_tokens_after: "number",
72
+ tokens_consumed: "number",
73
+ context_window_pct_before: "number",
74
+ context_window_pct_after: "number",
75
+ outcome: new Set(["success", "failure", "blocked", "escalated"]),
76
+ halt_type: "nullable-string", // null OR one of the halt_type enum values
77
+ escalated_via_advisor: "boolean",
78
+ });
79
+
80
+ const HALT_TYPE_ENUM = Object.freeze(
81
+ new Set(["clean", "runway-refusal", "headless-handoff", "native-compact"]),
82
+ );
83
+
84
+ // ── Exports ─────────────────────────────────────────────────────────────────
85
+
86
+ module.exports = {
87
+ recordSpawn,
88
+ readAll,
89
+ aggregate,
90
+ REQUIRED_FIELDS,
91
+ };
92
+
93
+ // ── recordSpawn ─────────────────────────────────────────────────────────────
94
+
95
+ /**
96
+ * Append one telemetry record to .gsd-t/token-metrics.jsonl.
97
+ *
98
+ * @param {object} record - A record matching the v1.0.0 schema. All 18
99
+ * required fields must be present and of the correct type.
100
+ * @param {string} [projectDir] - Optional project root. Defaults to cwd.
101
+ * @throws {Error} on missing required field, wrong type, or I/O failure.
102
+ * @returns {void}
103
+ */
104
+ function recordSpawn(record, projectDir) {
105
+ validateRecord(record);
106
+ const dir = projectDir || process.cwd();
107
+ const gsdDir = path.join(dir, ".gsd-t");
108
+ ensureDir(gsdDir);
109
+ const fp = path.join(gsdDir, "token-metrics.jsonl");
110
+ const line = JSON.stringify(record) + "\n";
111
+ fs.appendFileSync(fp, line);
112
+ }
113
+
114
+ // ── readAll ─────────────────────────────────────────────────────────────────
115
+
116
+ /**
117
+ * Read and parse every record from .gsd-t/token-metrics.jsonl.
118
+ *
119
+ * @param {string} [projectDir] - Optional project root. Defaults to cwd.
120
+ * @returns {Array<object>} - Array of parsed records. Returns [] if the file
121
+ * does not exist. Malformed lines are skipped with a console.warn (does
122
+ * not abort the read).
123
+ */
124
+ function readAll(projectDir) {
125
+ const dir = projectDir || process.cwd();
126
+ const fp = path.join(dir, ".gsd-t", "token-metrics.jsonl");
127
+ if (!fs.existsSync(fp)) return [];
128
+ const raw = fs.readFileSync(fp, "utf8");
129
+ const lines = raw.split("\n").filter((l) => l.trim().length > 0);
130
+ const records = [];
131
+ for (const line of lines) {
132
+ try {
133
+ records.push(JSON.parse(line));
134
+ } catch (e) {
135
+ // eslint-disable-next-line no-console
136
+ console.warn(`token-telemetry.readAll: skipping malformed line: ${e.message}`);
137
+ }
138
+ }
139
+ return records;
140
+ }
141
+
142
+ // ── aggregate ───────────────────────────────────────────────────────────────
143
+
144
+ /**
145
+ * Group records by one or more fields and compute per-group statistics.
146
+ *
147
+ * @param {Array<object>} records
148
+ * @param {{ by: Array<string> }} options - Array of field names to group by.
149
+ * Unknown fields yield empty-string values in the group key.
150
+ * @returns {Array<{ key: object, count: number, total_tokens: number,
151
+ * mean: number, median: number, p95: number }>}
152
+ */
153
+ function aggregate(records, options) {
154
+ const by = (options && Array.isArray(options.by)) ? options.by : [];
155
+ if (!Array.isArray(records) || records.length === 0) return [];
156
+
157
+ // Build groups keyed on a stable string (JSON of the key object).
158
+ const groups = new Map();
159
+ for (const r of records) {
160
+ const key = {};
161
+ for (const field of by) key[field] = r[field] != null ? r[field] : "";
162
+ const keyStr = JSON.stringify(key);
163
+ if (!groups.has(keyStr)) groups.set(keyStr, { key, tokens: [] });
164
+ const tokens = typeof r.tokens_consumed === "number" ? r.tokens_consumed : 0;
165
+ groups.get(keyStr).tokens.push(tokens);
166
+ }
167
+
168
+ const result = [];
169
+ for (const { key, tokens } of groups.values()) {
170
+ const count = tokens.length;
171
+ const total_tokens = tokens.reduce((s, v) => s + v, 0);
172
+ const mean = count > 0 ? total_tokens / count : 0;
173
+ const sorted = tokens.slice().sort((a, b) => a - b);
174
+ const median = count > 0 ? sorted[Math.floor(count / 2)] : 0;
175
+ const p95idx = count > 0 ? Math.min(count - 1, Math.floor(count * 0.95)) : 0;
176
+ const p95 = count > 0 ? sorted[p95idx] : 0;
177
+ result.push({ key, count, total_tokens, mean, median, p95 });
178
+ }
179
+ return result;
180
+ }
181
+
182
+ // ── Internal: schema validation ─────────────────────────────────────────────
183
+
184
+ function validateRecord(record) {
185
+ if (record == null || typeof record !== "object" || Array.isArray(record)) {
186
+ throw new Error(
187
+ `recordSpawn: record must be a plain object, got ${Array.isArray(record) ? "array" : typeof record}`,
188
+ );
189
+ }
190
+ for (const field of REQUIRED_FIELDS) {
191
+ if (!(field in record)) {
192
+ throw new Error(`recordSpawn: missing required field: ${field}`);
193
+ }
194
+ }
195
+ for (const field of REQUIRED_FIELDS) {
196
+ const expected = FIELD_TYPES[field];
197
+ const value = record[field];
198
+ if (expected === "string") {
199
+ if (typeof value !== "string") {
200
+ throw new Error(
201
+ `recordSpawn: field ${field} has wrong type: expected string, got ${typeName(value)}`,
202
+ );
203
+ }
204
+ } else if (expected === "number") {
205
+ if (typeof value !== "number" || !Number.isFinite(value)) {
206
+ throw new Error(
207
+ `recordSpawn: field ${field} has wrong type: expected finite number, got ${typeName(value)}`,
208
+ );
209
+ }
210
+ } else if (expected === "boolean") {
211
+ if (typeof value !== "boolean") {
212
+ throw new Error(
213
+ `recordSpawn: field ${field} has wrong type: expected boolean, got ${typeName(value)}`,
214
+ );
215
+ }
216
+ } else if (expected === "nullable-string") {
217
+ // halt_type: null OR one of the halt_type enum values
218
+ if (value !== null) {
219
+ if (typeof value !== "string" || !HALT_TYPE_ENUM.has(value)) {
220
+ throw new Error(
221
+ `recordSpawn: field ${field} has wrong value: expected null or one of ${Array.from(HALT_TYPE_ENUM).join("|")}, got ${JSON.stringify(value)}`,
222
+ );
223
+ }
224
+ }
225
+ } else if (expected instanceof Set) {
226
+ // string enum
227
+ if (typeof value !== "string" || !expected.has(value)) {
228
+ throw new Error(
229
+ `recordSpawn: field ${field} has wrong value: expected one of ${Array.from(expected).join("|")}, got ${JSON.stringify(value)}`,
230
+ );
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+ function typeName(v) {
237
+ if (v === null) return "null";
238
+ if (Array.isArray(v)) return "array";
239
+ return typeof v;
240
+ }
241
+
242
+ // ── Internal: fs helpers ────────────────────────────────────────────────────
243
+
244
+ function ensureDir(d) {
245
+ if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
246
+ }
@@ -22,9 +22,9 @@ 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))`
26
- Append to `.gsd-t/token-log.md` (create with header if missing):
27
- `| {DT_START} | {DT_END} | gsd-t-audit | Step 0 | sonnet | {DURATION}s | audit: {args summary} | | | {COUNTER} |`
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")`
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
+ `| {DT_START} | {DT_END} | gsd-t-audit | Step 0 | sonnet | {DURATION}s | audit: {args summary} | | | {CTX_PCT} |`
28
28
 
29
29
  Relay the subagent's summary to the user. **Do not execute Steps 1–5 yourself.**
30
30
 
@@ -2,6 +2,44 @@
2
2
 
3
3
  You are displaying the project backlog with optional filtering and limiting.
4
4
 
5
+ ## Step 0: Parse --file flag
6
+
7
+ If `$ARGUMENTS` contains `--file {path}`, read from `.gsd-t/{path}` instead of the default `.gsd-t/backlog.md`. This enables listing alternate backlog files such as `.gsd-t/optimization-backlog.md` (produced by the token optimizer at complete-milestone):
8
+
9
+ ```
10
+ /user:gsd-t-backlog-list --file optimization-backlog.md
11
+ /user:gsd-t-backlog-list --file optimization-backlog.md --status pending
12
+ ```
13
+
14
+ Also support `--status {pending|promoted|rejected}` when listing the optimization backlog — filters by the `**Status**:` field inside each H2 block.
15
+
16
+ If `--file optimization-backlog.md` is supplied, use `bin/token-optimizer.js` parseBacklog() to parse entries, then render a simplified table with columns: ID, Type, Status, Evidence (truncated to 80 chars). Example:
17
+
18
+ ```bash
19
+ node -e "
20
+ const opt = require('./bin/token-optimizer.js');
21
+ const entries = opt.parseBacklog(opt.readBacklog('.'));
22
+ const statusFilter = process.argv[1] || '';
23
+ const filtered = statusFilter
24
+ ? entries.filter(e => e.status === statusFilter)
25
+ : entries;
26
+ if (filtered.length === 0) {
27
+ console.log('No recommendations' + (statusFilter ? ' with status=' + statusFilter : '') + '.');
28
+ process.exit(0);
29
+ }
30
+ console.log('# Optimization Backlog (' + filtered.length + ' entries' + (statusFilter ? ', status=' + statusFilter : '') + ')');
31
+ console.log('');
32
+ console.log('| ID | Type | Status | Evidence |');
33
+ console.log('|---|---|---|---|');
34
+ for (const e of filtered) {
35
+ const ev = (e.evidence || '').slice(0, 80);
36
+ console.log('| ' + e.id + ' | ' + (e.type || '') + ' | ' + (e.status || '') + ' | ' + ev + ' |');
37
+ }
38
+ " "$STATUS_FILTER"
39
+ ```
40
+
41
+ Exit after rendering when `--file` is present — skip the default steps below.
42
+
5
43
  ## Step 1: Read Backlog
6
44
 
7
45
  Read `.gsd-t/backlog.md` and parse all entries.
@@ -123,9 +123,9 @@ 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))`
127
- Append to `.gsd-t/token-log.md` (create with header `| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Tasks-Since-Reset |` if missing):
128
- `| {DT_START} | {DT_END} | gsd-t-brainstorm | Step 3 | sonnet | {DURATION}s | deep research: {topic summary} | {COUNTER} |`
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")`
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
+ `| {DT_START} | {DT_END} | gsd-t-brainstorm | Step 3 | sonnet | {DURATION}s | deep research: {topic summary} | {CTX_PCT} |`
129
129
 
130
130
  ## Step 4: Capture the Sparks
131
131
 
@@ -508,6 +508,30 @@ If `.gsd-t/roadmap.md` exists:
508
508
  - Update any dependent milestones
509
509
  - Highlight next recommended milestone
510
510
 
511
+ ## Step 14: Token Optimization Recommendations (non-blocking)
512
+
513
+ After all quality gates pass and the milestone is archived, run the token optimizer to detect model-tier miscalibration signals from the milestone's telemetry. This appends recommendations to `.gsd-t/optimization-backlog.md`. **Never blocks, never prompts, never auto-applies.** Optimizer failure is caught and logged, not re-thrown.
514
+
515
+ ```bash
516
+ node -e "
517
+ try {
518
+ const opt = require('./bin/token-optimizer.js');
519
+ const recs = opt.detectRecommendations({projectDir: '.', lookbackMilestones: 3});
520
+ opt.appendToBacklog(recs, '.');
521
+ if (recs.length === 0) {
522
+ console.log('Token optimizer: no new recommendations.');
523
+ } else {
524
+ console.log('Token optimizer: ' + recs.length + ' new recommendation(s) → .gsd-t/optimization-backlog.md');
525
+ console.log('Review with: /user:gsd-t-backlog-list --file optimization-backlog.md');
526
+ }
527
+ } catch (e) {
528
+ console.error('Token optimizer error (non-blocking): ' + e.message);
529
+ }
530
+ "
531
+ ```
532
+
533
+ Contract: `.gsd-t/contracts/token-telemetry-contract.md` v1.0.0
534
+
511
535
  ## Error Handling
512
536
 
513
537
  ### If verify failed:
@@ -2,7 +2,70 @@
2
2
 
3
3
  You are debugging an issue in a contract-driven project. Your approach should identify whether the bug is within a domain or at a contract boundary.
4
4
 
5
- ## Step 0: Launch via Subagent
5
+ ## Model Assignment
6
+
7
+ Per `.gsd-t/contracts/model-selection-contract.md` v1.0.0.
8
+
9
+ - **Default**: `opus` (`selectModel({phase: "debug"})`) — debugging is high-stakes reasoning by default.
10
+ - **Root-cause analysis**: `opus` (`selectModel({phase: "debug", task_type: "root_cause"})`).
11
+ - **Fix-apply**: `sonnet` (`selectModel({phase: "debug", task_type: "fix_apply"})`) — applying a known fix is routine code work.
12
+ - **Escalation**: already at opus for judgment work; `/advisor` fallback applies if a fix crosses a contract boundary or schema. Never silently downgrade the model under context pressure — M35 removed that behavior.
13
+
14
+ ## Per-Spawn Token Bracket (MANDATORY — wrap EVERY Task subagent spawn)
15
+
16
+ Per `.gsd-t/contracts/token-telemetry-contract.md` v1.0.0. Every Task subagent spawn below **MUST** be wrapped in this token bracket so `.gsd-t/token-metrics.jsonl` gets one record per spawn. This is additive — the existing OBSERVABILITY LOGGING blocks in each spawn site are preserved unmodified alongside this bracket.
17
+
18
+ **Before each spawn — read starting context tokens:**
19
+
20
+ ```bash
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')}")
23
+ ```
24
+
25
+ **After each spawn — record the bracket:**
26
+
27
+ ```bash
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')}")
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
+ ```
32
+
33
+ The bracket is additive to the existing `.gsd-t/token-log.md` OBSERVABILITY LOGGING rows. Both sinks coexist.
34
+
35
+ ## Step 0: Runway Check (MANDATORY — before any other work in a fresh session)
36
+
37
+ Debug uses conservative per-iteration cost (opus-default fallback = 8%/task). Run with `remaining_tasks=1` for a single pass; the mid-loop check (below, added by HAS-T3) re-runs this gate between iterations. Run via Bash:
38
+
39
+ ```bash
40
+ node -e "
41
+ const r = require('./bin/runway-estimator.js').estimateRunway({
42
+ command: 'gsd-t-debug',
43
+ domain_type: '',
44
+ remaining_tasks: 1,
45
+ projectDir: '.'
46
+ });
47
+ console.log(JSON.stringify(r, null, 2));
48
+ if (!r.can_start) {
49
+ console.log('⛔ Insufficient runway — projected ' + r.projected_end_pct + '% (current ' + r.current_pct + '%, ' + r.pct_per_task + '%/task, ' + r.confidence + ' confidence, ' + r.confidence_basis + ' records)');
50
+ console.log('Auto-spawning headless to continue in a fresh context.');
51
+ const s = require('./bin/headless-auto-spawn.js').autoSpawnHeadless({
52
+ command: 'gsd-t-debug', args: [], continue_from: '.'
53
+ });
54
+ console.log('Session ID: ' + s.id);
55
+ console.log('Status: tail ' + s.logPath);
56
+ console.log('');
57
+ console.log('Your interactive session remains idle — you can use it for other work.');
58
+ console.log('You will be notified when the headless run completes.');
59
+ process.exit(0);
60
+ }
61
+ "
62
+ ```
63
+
64
+ If `can_start === false`, the headless continuation has been spawned and the interactive session must stop here. Do NOT proceed to Step 0.1.
65
+
66
+ **Contract**: `.gsd-t/contracts/runway-estimator-contract.md` v1.0.0; stop threshold (85%) mirrors `.gsd-t/contracts/token-budget-contract.md` v3.0.0.
67
+
68
+ ## Step 0.1: Launch via Subagent
6
69
 
7
70
  To give this debug session a fresh context window and prevent compaction, always execute via a Task subagent.
8
71
 
@@ -84,8 +147,8 @@ Read CLAUDE.md and .gsd-t/progress.md for project context, then execute gsd-t-de
84
147
 
85
148
  After subagent returns — run via Bash:
86
149
  `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START))`
87
- Append to `.gsd-t/token-log.md` (create with header `| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Tasks-Since-Reset |` if missing):
88
- `| {DT_START} | {DT_END} | gsd-t-debug | Step 0 | sonnet | {DURATION}s | debug: {issue summary} | {COUNTER} |`
150
+ Append to `.gsd-t/token-log.md` (create with header `| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Ctx% |` if missing):
151
+ `| {DT_START} | {DT_END} | gsd-t-debug | Step 0 | sonnet | {DURATION}s | debug: {issue summary} | {CTX_PCT} |`
89
152
 
90
153
  Relay the subagent's summary to the user. **Do not execute Steps 1–5 yourself.**
91
154
 
@@ -150,9 +213,9 @@ Lead: Wait for all three researchers to complete. Then synthesize:
150
213
  ```
151
214
 
152
215
  After team completes — run via Bash:
153
- `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START))`
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")`
154
217
  Append to `.gsd-t/token-log.md`:
155
- `| {DT_START} | {DT_END} | gsd-t-debug | Step 1.5 | sonnet | {DURATION}s | deep research loop break: {issue summary} | {COUNTER} |`
218
+ `| {DT_START} | {DT_END} | gsd-t-debug | Step 1.5 | sonnet | {DURATION}s | deep research loop break: {issue summary} | {CTX_PCT} |`
156
219
 
157
220
  **STOP. Present findings to the user before making any changes:**
158
221
 
@@ -282,6 +345,60 @@ When you encounter unexpected situations during the fix:
282
345
 
283
346
  If the debug-loop also fails (exit 1/4), log the attempt to `.gsd-t/progress.md` Decision Log with a `[failure]` prefix, return to Step 1.5 and run Deep Research Mode before any further attempts. Present findings and options to the user before proceeding.
284
347
 
348
+ ### Between-Iteration Runway Check (MANDATORY — every iteration)
349
+
350
+ Before starting each new fix attempt (iteration N+1), re-run the runway estimator with `remaining_tasks=1`. Debug loops are the single highest-variance consumer of context, and a mid-loop halt is worse than a pre-flight halt — the user loses the current hypothesis and partial work unless we persist them first.
351
+
352
+ Run via Bash before each iteration:
353
+
354
+ ```bash
355
+ node -e "
356
+ const r = require('./bin/runway-estimator.js').estimateRunway({
357
+ command: 'gsd-t-debug',
358
+ domain_type: '',
359
+ remaining_tasks: 1,
360
+ projectDir: '.'
361
+ });
362
+ if (!r.can_start) {
363
+ // ── HAS-T3 state persistence: capture current hypothesis + fix + test output ──
364
+ const fs = require('fs');
365
+ const path = require('path');
366
+ const ledgerPath = '.gsd-t/debug-ledger.jsonl';
367
+ const snapshot = {
368
+ type: 'runway-handoff-snapshot',
369
+ timestamp: new Date().toISOString(),
370
+ hypothesis: process.env.GSD_T_DEBUG_HYPOTHESIS || '',
371
+ last_fix_diff: process.env.GSD_T_DEBUG_LAST_FIX || '',
372
+ last_test_output: process.env.GSD_T_DEBUG_LAST_TEST_OUTPUT || '',
373
+ iteration_n_plus_1: Number(process.env.GSD_T_DEBUG_NEXT_ITERATION || 0),
374
+ current_pct: r.current_pct,
375
+ projected_end_pct: r.projected_end_pct,
376
+ confidence: r.confidence
377
+ };
378
+ try { fs.mkdirSync(path.dirname(ledgerPath), { recursive: true }); } catch (_) {}
379
+ fs.appendFileSync(ledgerPath, JSON.stringify(snapshot) + '\n');
380
+
381
+ console.log('⛔ Runway exceeded mid-loop — projected ' + r.projected_end_pct + '% at iteration ' + snapshot.iteration_n_plus_1);
382
+ console.log('Persisted hypothesis + last fix + test output to ' + ledgerPath);
383
+
384
+ const s = require('./bin/headless-auto-spawn.js').autoSpawnHeadless({
385
+ command: 'gsd-t-debug',
386
+ args: ['--resume', 'iteration-' + snapshot.iteration_n_plus_1],
387
+ continue_from: ledgerPath
388
+ });
389
+ console.log('Runway exceeded mid-loop — headless debug picking up at iteration ' + snapshot.iteration_n_plus_1 + '. Session: ' + s.id + '. Log: ' + s.logPath);
390
+ process.exit(0);
391
+ }
392
+ "
393
+ ```
394
+
395
+ - On refusal: the block persists `{hypothesis, last_fix_diff, last_test_output}` as a `runway-handoff-snapshot` entry in `.gsd-t/debug-ledger.jsonl`, calls `autoSpawnHeadless` with `--resume iteration-N+1`, prints the handoff message, and exits the loop cleanly (no `/clear` prompt).
396
+ - On proceed: the block exits silently and the next iteration begins.
397
+
398
+ **Environment variables**: the calling iteration sets `GSD_T_DEBUG_HYPOTHESIS`, `GSD_T_DEBUG_LAST_FIX`, `GSD_T_DEBUG_LAST_TEST_OUTPUT`, `GSD_T_DEBUG_NEXT_ITERATION` before running the Bash block. If unset, empty strings are persisted (the ledger entry is still useful for the headless continuation).
399
+
400
+ **Contracts**: `.gsd-t/contracts/runway-estimator-contract.md` v1.0.0, `.gsd-t/contracts/headless-auto-spawn-contract.md` v1.0.0.
401
+
285
402
  ### Solo Mode
286
403
  1. Reproduce the issue — **reproduction script must exist before step 2** (see Step 2.5)
287
404
  2. Trace through the relevant domain(s)
@@ -389,10 +506,10 @@ Spawn Task subagent (general-purpose, model: opus):
389
506
  After subagent returns — run via Bash:
390
507
  ```
391
508
  T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START))
392
- 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('')}})")
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')}")
393
510
  ```
394
511
  Append to `.gsd-t/token-log.md`:
395
- `| {DT_START} | {DT_END} | gsd-t-debug | Red Team | opus | {DURATION}s | {VERDICT} — {N} bugs found | | | {COUNTER} |`
512
+ `| {DT_START} | {DT_END} | gsd-t-debug | Red Team | opus | {DURATION}s | {VERDICT} — {N} bugs found | | | {CTX_PCT} |`
396
513
 
397
514
  **If FAIL:** fix CRITICAL/HIGH bugs (≤2 cycles) → re-run. Persistent bugs → `.gsd-t/deferred-items.md`.
398
515
  **If GRUDGING PASS:** proceed to metrics and doc-ripple.
@@ -2,6 +2,13 @@
2
2
 
3
3
  You are the lead agent exploring design decisions before committing to a plan. The goal of this phase is to produce or refine **contracts** — not just recommendations.
4
4
 
5
+ ## Model Assignment
6
+
7
+ Per `.gsd-t/contracts/model-selection-contract.md` v1.0.0.
8
+
9
+ - **Default**: `opus` (`selectModel({phase: "discuss"})`) — design exploration benefits most from top-tier reasoning.
10
+ - **Escalation**: already at opus; there is no stronger tier.
11
+
5
12
  ## IMPORTANT: Manual vs Auto-Invoked Behavior
6
13
 
7
14
  **When manually invoked** (user typed `/user:gsd-t-discuss`):
@@ -72,9 +79,9 @@ Lead: Synthesize into decisions and update contracts.
72
79
  ```
73
80
 
74
81
  After team completes — run via Bash:
75
- `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START))`
76
- Append to `.gsd-t/token-log.md` (create with header `| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Tasks-Since-Reset |` if missing):
77
- `| {DT_START} | {DT_END} | gsd-t-discuss | Step 3 | sonnet | {DURATION}s | team discuss: {topic summary} | {COUNTER} |`
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")`
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
+ `| {DT_START} | {DT_END} | gsd-t-discuss | Step 3 | sonnet | {DURATION}s | team discuss: {topic summary} | {CTX_PCT} |`
78
85
 
79
86
  Assign teammates based on the nature of the questions:
80
87
  - **Technical choice** (e.g., which database): one advocate per option + critic
@@ -2,6 +2,34 @@
2
2
 
3
3
  You are the doc-ripple agent. You identify and update all downstream documents after code changes. You are spawned by execute, integrate, quick, debug, and wave after primary work is committed.
4
4
 
5
+ ## Model Assignment
6
+
7
+ Per `.gsd-t/contracts/model-selection-contract.md` v1.0.0.
8
+
9
+ - **Default**: `sonnet` (`selectModel({phase: "doc-ripple"})`) — downstream documentation updates are routine prose editing.
10
+ - **Escalation**: `/advisor` convention-based fallback from `bin/advisor-integration.js` when a doc change involves rewriting an architectural invariant or a contract. Never silently skip doc-ripple under context pressure — M35 removed that behavior.
11
+
12
+ ## Per-Spawn Token Bracket (MANDATORY — wrap EVERY Task subagent spawn)
13
+
14
+ Per `.gsd-t/contracts/token-telemetry-contract.md` v1.0.0. Every Task subagent spawn below **MUST** be wrapped in this token bracket so `.gsd-t/token-metrics.jsonl` gets one record per spawn. This is additive — the existing OBSERVABILITY LOGGING blocks in each spawn site are preserved unmodified alongside this bracket.
15
+
16
+ **Before each spawn — read starting context tokens:**
17
+
18
+ ```bash
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')}")
21
+ ```
22
+
23
+ **After each spawn — record the bracket:**
24
+
25
+ ```bash
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')}")
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
+ ```
30
+
31
+ The bracket is additive to the existing `.gsd-t/token-log.md` OBSERVABILITY LOGGING rows. Both sinks coexist.
32
+
5
33
  ## Step 1: Load Context
6
34
 
7
35
  Read:
@@ -98,11 +126,11 @@ Before spawning — run via Bash:
98
126
  After subagent returns — run via Bash:
99
127
  `T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START))`
100
128
 
101
- Read the task counter (deterministic context-burn signal):
102
- `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('')}})")`
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')}")`
103
131
 
104
- Append to `.gsd-t/token-log.md` (create with header `| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Domain | Task | Tasks-Since-Reset |` if missing):
105
- `| {DT_START} | {DT_END} | gsd-t-doc-ripple | Step 5 | {model} | {DURATION}s | update:{document} | doc-ripple | — | {COUNTER} |`
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
+ `| {DT_START} | {DT_END} | gsd-t-doc-ripple | Step 5 | {model} | {DURATION}s | update:{document} | doc-ripple | — | {CTX_PCT} |`
106
134
 
107
135
  **Each document-update subagent prompt:**
108
136
  ```