@tekyzinc/gsd-t 2.74.13 → 2.76.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 (61) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/README.md +71 -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.js +709 -16
  8. package/bin/headless-auto-spawn.js +290 -0
  9. package/bin/model-selector.js +224 -0
  10. package/bin/runway-estimator.js +242 -0
  11. package/bin/token-budget.js +96 -89
  12. package/bin/token-optimizer.js +471 -0
  13. package/bin/token-telemetry.js +246 -0
  14. package/commands/gsd-t-audit.md +3 -3
  15. package/commands/gsd-t-backlog-list.md +38 -0
  16. package/commands/gsd-t-brainstorm.md +3 -3
  17. package/commands/gsd-t-complete-milestone.md +24 -0
  18. package/commands/gsd-t-debug.md +124 -7
  19. package/commands/gsd-t-discuss.md +10 -3
  20. package/commands/gsd-t-doc-ripple.md +32 -4
  21. package/commands/gsd-t-execute.md +107 -52
  22. package/commands/gsd-t-help.md +19 -0
  23. package/commands/gsd-t-integrate.md +67 -4
  24. package/commands/gsd-t-optimization-apply.md +91 -0
  25. package/commands/gsd-t-optimization-reject.md +94 -0
  26. package/commands/gsd-t-partition.md +7 -0
  27. package/commands/gsd-t-pause.md +3 -0
  28. package/commands/gsd-t-plan.md +10 -3
  29. package/commands/gsd-t-prd.md +3 -3
  30. package/commands/gsd-t-quick.md +71 -9
  31. package/commands/gsd-t-reflect.md +3 -7
  32. package/commands/gsd-t-resume.md +36 -0
  33. package/commands/gsd-t-status.md +31 -0
  34. package/commands/gsd-t-test-sync.md +7 -0
  35. package/commands/gsd-t-verify.md +12 -5
  36. package/commands/gsd-t-visualize.md +3 -7
  37. package/commands/gsd-t-wave.md +82 -18
  38. package/docs/GSD-T-README.md +52 -0
  39. package/docs/architecture.md +95 -0
  40. package/docs/infrastructure.md +117 -0
  41. package/docs/methodology.md +36 -0
  42. package/docs/prd-harness-evolution.md +51 -37
  43. package/docs/requirements.md +66 -0
  44. package/package.json +1 -1
  45. package/scripts/context-meter/count-tokens-client.js +221 -0
  46. package/scripts/context-meter/count-tokens-client.test.js +308 -0
  47. package/scripts/context-meter/test-injector.js +55 -0
  48. package/scripts/context-meter/threshold.js +88 -0
  49. package/scripts/context-meter/threshold.test.js +255 -0
  50. package/scripts/context-meter/transcript-parser.js +252 -0
  51. package/scripts/context-meter/transcript-parser.test.js +320 -0
  52. package/scripts/gsd-t-context-meter.e2e.test.js +415 -0
  53. package/scripts/gsd-t-context-meter.js +350 -0
  54. package/scripts/gsd-t-context-meter.test.js +417 -0
  55. package/scripts/gsd-t-heartbeat.js +2 -2
  56. package/scripts/gsd-t-statusline.js +23 -8
  57. package/templates/CLAUDE-global.md +5 -1
  58. package/templates/CLAUDE-project.md +26 -6
  59. package/templates/context-meter-config.json +10 -0
  60. package/templates/prompts/README.md +1 -1
  61. package/bin/task-counter.cjs +0 -161
package/CHANGELOG.md CHANGED
@@ -2,6 +2,122 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [2.76.10] - 2026-04-15
6
+
7
+ ### M35: Runway-Protected Execution — Aggressive Pause-Resume Replaces Graduated Degradation
8
+
9
+ **Background**: Between v2.74 and v2.75, GSD-T coped with context pressure via graduated degradation — `downgrade` and `conserve` bands that silently demoted opus→sonnet→haiku and skipped Red Team / doc-ripple / Design Verify phases. This made quality **conditional on context pressure**, a load-bearing invariant the user could neither see nor control. M35 removes graduated degradation entirely and replaces it with: surgical per-phase model selection (plan-time, never runtime), a pre-flight runway estimator that refuses runs projected to cross 85% and auto-spawns a detached headless continuation, frozen 18-field per-spawn token telemetry, and a detect-only optimization backlog the user explicitly promotes or rejects. The user never types `/clear` under normal operation.
10
+
11
+ ### Added
12
+
13
+ - **`bin/model-selector.js`** — declarative phase→tier mapping (≥13 phase mappings) with complexity-signal escalation (`cross_module_refactor`, `security_boundary`, `data_loss_risk`, `contract_design`) that escalates sonnet→opus at plan time. Each command file carries a `## Model Assignment` block.
14
+ - **`bin/runway-estimator.js`** — `estimateRunway({command, domain_type, remaining_tasks})` reads `.gsd-t/token-metrics.jsonl` via a three-tier query fallback (exact → command+phase → command) and returns `{can_start, projected_end_pct, confidence, recommendation}`. Confidence grading: high ≥50 records, medium ≥10, low <10 (+1.25× skew).
15
+ - **`bin/headless-auto-spawn.js`** — detached `child_process.spawn({detached:true, stdio:['ignore', fd, fd]}) + child.unref()`. Writes `.gsd-t/headless-sessions/{session-id}.json`, polls with `process.kill(pid, 0)` (timer `.unref()`-ed), marks `status: completed`, posts a macOS `osascript` notification on exit (graceful no-op on non-darwin).
16
+ - **`bin/check-headless-sessions.js`** — scans `.gsd-t/headless-sessions/` for `status === 'completed' && surfaced !== true` and renders the read-back banner on `/user:gsd-t-resume` and `/user:gsd-t-status`. Exports `checkCompletedSessions`, `markSurfaced`, `formatBanner`, `printBannerIfAny`, `computeDurationLabel`.
17
+ - **`bin/token-telemetry.js`** — per-spawn token bracket writes one frozen 18-field JSONL record per subagent spawn to `.gsd-t/token-metrics.jsonl`. Fields: `timestamp, session_id, command, phase, domain, task_id, model, complexity_signals[], input_tokens, output_tokens, duration_seconds, start_pct, end_pct, halt_type, halt_reason, exit_code, run_type, projection_variance`. `halt_type` values: `clean`, `stop-band`, `runway-refuse`, `native-compact` (defect), `crash`.
18
+ - **`bin/token-optimizer.js`** — at `complete-milestone`, scans the last 3 milestones and appends recalibration recommendations to `.gsd-t/optimization-backlog.md`. Four detection rules: `demote` (opus phase ≥90% success, ≥3 volume), `escalate` (sonnet phase ≥30% failure rate, ≥5 volume), `runway-tune` (projection vs. actual divergence >15%), `investigate` (per-phase p95 > 2× median, ≥10 volume). Fingerprint-based 5-milestone cooldown on rejected items. Exports `detectRecommendations`, `appendToBacklog`, `readBacklog`, `writeBacklog`, `parseBacklog`, `setRecommendationStatus`, `DETECTION_RULES`, `REJECTION_COOLDOWN_MILESTONES`.
19
+ - **`bin/advisor-integration.js`** — `/advisor` escalation hook; convention-based fallback if no programmable API.
20
+ - **`.gsd-t/contracts/token-budget-contract.md` v3.0.0 ACTIVE** — clean-break rewrite. Three bands only: `normal` <70%, `warn` 70–85%, `stop` ≥85%. Response shape `{band, pct, message}`. `downgrade`, `conserve`, `modelOverrides`, `skipPhases` all deleted — no compat shim.
21
+ - **`.gsd-t/contracts/model-selection-contract.md` v1.0.0 ACTIVE** — declarative phase→tier rules, complexity-signal escalation semantics, `/advisor` hook schema.
22
+ - **`.gsd-t/contracts/token-telemetry-contract.md` v1.0.0 ACTIVE** — frozen 18-field per-spawn JSONL schema, `halt_type` enum, `run_type` enum.
23
+ - **`.gsd-t/contracts/runway-estimator-contract.md` v1.0.0 ACTIVE** — pre-flight projection, three-tier query fallback, confidence grading, refusal + headless handoff contract.
24
+ - **`.gsd-t/contracts/headless-auto-spawn-contract.md` v1.0.0 ACTIVE** — detached continuation, session file schema, macOS notification channel, read-back banner.
25
+ - **`commands/gsd-t-optimization-apply.md`** — promotes a backlog recommendation by ID, auto-routes to `/user:gsd-t-quick` or `/user:gsd-t-backlog-promote` based on recommendation type.
26
+ - **`commands/gsd-t-optimization-reject.md`** — rejects a recommendation with optional `--reason`, sets 5-milestone cooldown. Reason captured in token-log.md + Decision Log.
27
+ - **`gsd-t metrics` flags** — `--tokens` (per-command/phase token summary), `--halts` (halt-type breakdown; flags any `native-compact` as defect), `--context-window` (trailing 20-run `end_pct` with runway headroom).
28
+ - **Test coverage**: `test/headless-auto-spawn.test.js` (16 tests — session file schema, completion watcher, read-back banner, non-darwin degradation, E2E shim smoke), `test/token-optimizer.test.js` (19 tests — each rule triggers/skips, parseBacklog round-trip, cooldown filter, OB-T1+OB-T4 integration roundtrip), plus rewrites of `test/token-budget.test.js` around v3.0.0. **~1011/1011 total tests green through Wave 4**.
29
+
30
+ ### Changed
31
+
32
+ - **`bin/token-budget.js`** — `getSessionStatus()` now returns `{band, pct, message}` with only three bands. `applyModelOverride`, `skipPhases`, `getDegradationActions` band-branching for `downgrade`/`conserve` — all deleted.
33
+ - **`bin/orchestrator.js`** — gate semantics: `normal` proceed, `warn` log + proceed at **full quality**, `stop` halt cleanly and hand off to runway estimator → headless-auto-spawn. No model swaps. No phase skips.
34
+ - **Command files** (`gsd-t-execute.md`, `gsd-t-wave.md`, `gsd-t-quick.md`, `gsd-t-integrate.md`, `gsd-t-debug.md`, `gsd-t-doc-ripple.md`) — Step 0 runway gate; `## Model Assignment` blocks documenting per-phase tier choices; per-spawn token telemetry brackets around every subagent spawn.
35
+ - **`commands/gsd-t-resume.md`** — Step 0.5 Headless Read-Back Banner (MANDATORY) invokes `node bin/check-headless-sessions.js . 2>/dev/null || true`.
36
+ - **`commands/gsd-t-status.md`** — Step 0 Headless Read-Back Banner + Step 0.5 Optimization Backlog Pending Count (one-liner, suppressed when N=0).
37
+ - **`commands/gsd-t-complete-milestone.md`** — Step 14 non-blocking optimizer invocation: `detectRecommendations({lookbackMilestones: 3})` → `appendToBacklog`. Wrapped in try/catch; optimizer failure logged but not re-thrown.
38
+ - **`commands/gsd-t-backlog-list.md`** — `--file` flag supports rendering `optimization-backlog.md` via `bin/token-optimizer.js` parseBacklog, with optional `--status {pending|promoted|rejected}` filter.
39
+ - **`commands/gsd-t-help.md`** — new OPTIMIZATION section in summary table; detailed entries for `optimization-apply` and `optimization-reject`.
40
+ - **Documentation ripple**:
41
+ - `README.md` — "Runway-Protected Execution (M35, v2.76.10)" section replacing "Token-Aware Orchestration"; threshold description updated to "85% = stop band; 70% = warn band — cue for explicit pause/resume; no silent degradation".
42
+ - `docs/GSD-T-README.md` — 3-band table replacing 5-band table, "Zero silent quality degradation" explanation, per-phase model selection, `/advisor` escalation, `gsd-t metrics` flags, optimization apply/reject.
43
+ - `docs/methodology.md` — new "From Silent Degradation to Aggressive Pause-Resume (M35)" section with five principles (quality non-negotiable, explicit per-phase model selection, user never types `/clear`, data before optimization, clean break no compat shim) + "Structural guarantee" closing paragraph.
44
+ - `docs/architecture.md` — dataflow updated for runway estimator + headless auto-spawn + v3.0.0 band semantics; M35 supporting components section (model-selector, token-optimizer, check-headless-sessions).
45
+ - `docs/infrastructure.md` — 3-band threshold table replacing 5-band; new Runway-Protected Execution section covering all 5 components; `gsd-t metrics` CLI table; `/advisor` convention.
46
+ - `docs/requirements.md` — REQ-069 through REQ-078 M35 traceability; REQ-076/077 marked complete.
47
+ - `docs/prd-harness-evolution.md` — §3.7 rewritten as "Context Gate + Surgical Model Escalation"; risk-table + session-cost mitigations updated to reference runway estimator + headless handoff (no graduated degradation).
48
+ - `templates/CLAUDE-global.md` + `templates/CLAUDE-project.md` — Token-Aware Orchestration section rewritten around M35 semantics.
49
+
50
+ ### Removed
51
+
52
+ - **Graduated degradation** — `downgrade` and `conserve` bands are deleted from `bin/token-budget.js`, the v3.0.0 `token-budget-contract.md`, and every command file. `applyModelOverride`, `skipPhases`, and all related runtime machinery are gone.
53
+ - **Runtime model downgrade** — there is no code path that swaps opus→sonnet or sonnet→haiku under context pressure. Model choice is a plan-time decision made by `bin/model-selector.js`, full stop.
54
+ - **Phase-skipping under pressure** — Red Team, doc-ripple, and Design Verify always run at their designated tier regardless of context %. No "non-essential" phase exists.
55
+ - **Manual `/clear` prompts** under normal operation — the user only sees a `/clear` prompt when the headless handoff itself fails, which is an explicit degradation path, not a silent one.
56
+
57
+ ### Migration
58
+
59
+ - **No user migration required** for v2.75.10 → v2.76.10 — `gsd-t update-all` rewrites command files in place and the new contracts ship with the package. Existing projects inherit the three-band gate automatically.
60
+ - **Projects with custom wrappers calling `getSessionStatus()`** — the return shape changed from `{band, pct, modelOverrides, skipPhases, message}` to `{band, pct, message}`. `modelOverrides` and `skipPhases` consumers must delete their handling code (they never had a quality-reducing role in v3.0.0 anyway).
61
+ - **Historical note**: `halt_type: native-compact` entries in `.gsd-t/token-metrics.jsonl` are defect signals — if they appear after upgrade, the runway estimator thresholds need re-tuning. The structural guarantee is that with `STOP_THRESHOLD_PCT = 85` and pre-flight refusal, the runtime's 95% native compact is unreachable under healthy operation.
62
+
63
+ ### Propagation
64
+
65
+ Run `/user:gsd-t-version-update-all` from any registered GSD-T project to propagate v2.76.10 to all projects. The command files, templates, and `bin/` scripts are rewritten in place; project state in `.gsd-t/` is preserved.
66
+
67
+ ---
68
+
69
+ ## [2.75.10] - 2026-04-14
70
+
71
+ ### M34: Context Meter — Real Context-Window Measurement Replaces Task-Counter Proxy
72
+
73
+ **Background**: v2.74.12/v2.74.13 introduced `bin/task-counter.cjs` as a deterministic session-burn gate after the env-var-based context self-check (`CLAUDE_CONTEXT_TOKENS_USED`) was found to be permanently inert. The task counter fixed the immediate bleeding, but it was always a proxy — 5 tasks ≠ N tokens, and Opus-primary sessions burn context faster than Sonnet-primary sessions for the same task count. M34 replaces the proxy with real measurement via the Anthropic `count_tokens` API, re-exposed through a PostToolUse hook.
74
+
75
+ ### Added
76
+
77
+ - **`scripts/gsd-t-context-meter.js`** — PostToolUse hook that measures the active Claude Code session's context window after every tool call. Writes a snapshot to `.gsd-t/.context-meter-state.json` (`{pct, consumed, limit, timestamp, model}`) and, when `pct >= warn_threshold`, injects `additionalContext` into the Claude Code response so the orchestrator sees real burn in real time. Fails open (silent no-op) when `ANTHROPIC_API_KEY` is missing or the API is unreachable — never blocks the user's session.
78
+ - **`scripts/context-meter/`** — helper modules: `parser.js` (extract recent turns from transcript), `client.js` (count_tokens API wrapper with retry), `threshold.js` (warn/degrade/conserve/stop bands), `test-injector.js` (deterministic fixtures for unit tests).
79
+ - **`bin/context-meter-config.cjs`** — config loader with defaults and schema validation.
80
+ - **`templates/context-meter-config.json`** — default config (thresholds: warn 0.65, degrade 0.75, conserve 0.85, stop 0.92; staleness window 5 min).
81
+ - **`.gsd-t/contracts/context-meter-contract.md`** v1.0.0 ACTIVE — hook I/O contract, state file schema, threshold semantics, fail-open guarantees.
82
+ - **`.gsd-t/contracts/token-budget-contract.md`** v2.0.0 ACTIVE — rewritten around real measurement; public `getSessionStatus()` API surface preserved but semantics now reflect actual context % instead of task count.
83
+ - **Installer extensions (`bin/gsd-t.js`)**:
84
+ - `install`/`update` registers `scripts/gsd-t-context-meter.js` as a PostToolUse hook in `~/.claude/settings.json` (idempotent).
85
+ - First-run prompt for `ANTHROPIC_API_KEY` (skippable — doctor will later fail-red if unset).
86
+ - `doctor` adds hard-gate checks for API key presence, hook registration, config file, and a dry-run smoke test of the hook entry point.
87
+ - `status` displays real context % read from `.gsd-t/.context-meter-state.json` (falls back to heuristic when state is missing/stale).
88
+ - **Test coverage**: `scripts/gsd-t-context-meter.e2e.test.js` (90 tests — parser, client, threshold, hook entry, injection); `test/token-budget.test.js` fully rewritten around real measurement; `test/installer-m34.test.js` covers hook install, API key prompt, doctor gate, status line; **941/941 total tests green**.
89
+
90
+ ### Changed
91
+
92
+ - **`bin/token-budget.js`** — `getSessionStatus()` now reads `.gsd-t/.context-meter-state.json` (with a 5-minute staleness window) and falls back to a heuristic based on `.gsd-t/token-log.md` row count when state is unavailable. Graduated degradation (`warn`/`downgrade`/`conserve`/`stop`) fires on real context % instead of task count. Public API unchanged so `bin/orchestrator.js` and every command that calls it keeps working.
93
+ - **`bin/orchestrator.js`** — task-budget gate now calls `token-budget.getSessionStatus()` for the real signal; checkpoint-and-stop behavior preserved.
94
+ - **Command files** (`gsd-t-execute.md`, `gsd-t-wave.md`, `gsd-t-quick.md`, `gsd-t-integrate.md`, `gsd-t-debug.md`) — every `node bin/task-counter.cjs …` invocation replaced with a `CTX_PCT` bash shim that sources the context meter state file. Observability logging updated.
95
+ - **Token log schema** — `Tasks-Since-Reset` column renamed to `Ctx%`. All command files and templates updated.
96
+ - **Documentation ripple**:
97
+ - `README.md` — Context Meter feature bullet + full "Context Meter Setup" section.
98
+ - `docs/GSD-T-README.md` — Configuration → Context Meter subsection with data-flow, threshold bands, upgrade notes.
99
+ - `docs/architecture.md` — Context Meter Architecture with full data-flow diagram.
100
+ - `docs/infrastructure.md` — Context Meter Setup section with API key instructions, doctor verification, threshold table, upgrade migration.
101
+ - `docs/methodology.md` — "Context Awareness: From Proxy to Real Measurement" narrative explaining why proxies failed and how real measurement restores gate integrity.
102
+ - `docs/requirements.md` — M34 REQ-063 through REQ-068 traceability table with functional and non-functional requirements.
103
+ - `templates/CLAUDE-global.md` — Context Meter Gate subsection + historical note about the task-counter era.
104
+ - `templates/CLAUDE-project.md` — new Context Meter section for per-project setup.
105
+
106
+ ### Removed
107
+
108
+ - **`bin/task-counter.cjs`** — deleted. The entire proxy gate retires. `.gsd-t/.task-counter`, `.gsd-t/task-counter-config.json`, and the `Tasks-Since-Reset` column are no longer read by any code.
109
+ - All `CLAUDE_CONTEXT_TOKENS_USED` / `CLAUDE_CONTEXT_TOKENS_MAX` references across `commands/`, `bin/`, `scripts/`, and `templates/` — the last vestiges of the original broken env-var self-check.
110
+
111
+ ### Migration
112
+
113
+ - **`gsd-t update-all`** runs a one-shot task-counter retirement migration: deletes `bin/task-counter.cjs`, `.gsd-t/.task-counter`, `.gsd-t/task-counter-config.json` from each registered project; writes `.gsd-t/.task-counter-retired-v1` marker so the migration is idempotent. Projects that had the proxy gate wired in come out the other side on the real context meter with zero manual intervention.
114
+ - **Users MUST set `ANTHROPIC_API_KEY`** in their shell environment (or accept the install-time prompt) for the context meter to produce real readings. Without the key, the hook fails open and `doctor` reports RED on the API key check — the gate falls back to the `token-log.md` row-count heuristic, which is safer than the old env-var vaporware but less accurate than real measurement.
115
+ - Both `install` and `update-all` register the hook in `~/.claude/settings.json` and copy the default config template. Existing `.claude/settings.json` is preserved; only the hook entry is appended.
116
+
117
+ ### Propagation
118
+
119
+ After publishing, run `/user:gsd-t-version-update-all` to propagate M34 (hook, config, installer, rewritten token-budget, command file updates, retirement migration) to every registered GSD-T project in a single sweep.
120
+
5
121
  ## [2.74.13] - 2026-04-14
6
122
 
7
123
  ### Fixed — v2.74.12 task-counter distribution gap (P0)
package/README.md CHANGED
@@ -14,7 +14,13 @@ A methodology for reliable, parallelizable development using Claude Code with op
14
14
  **Cross-project learning** — proven rules propagate to `~/.claude/metrics/` and sync across all registered projects via `update-all`. Rules validated in 3+ projects become universal; 5+ projects qualify for npm distribution. Cross-project signal comparison and global ELO rankings available via `gsd-t-metrics --cross-project` and `gsd-t-status`.
15
15
  **Stack Rules Engine** — auto-detects project tech stack (React, TypeScript, Node API, Python, Go, Rust) from manifest files and injects mandatory best-practice rules into subagent prompts at execute-time. Universal security rules always apply; stack-specific rules layer on top. Includes **design-to-code** rules for pixel-perfect frontend implementation from Figma, screenshots, or design images — with Figma MCP integration, design token extraction, stack capability evaluation, and mandatory visual verification: every screen is rendered in a real browser, screenshotted at mobile/tablet/desktop, and compared pixel-by-pixel against the Figma design. Auto-bootstraps during partition when design references are detected. Extensible: drop a `.md` file in `templates/stacks/` to add a new stack.
16
16
  **Self-Calibrating QA** — `qa-calibrator.js` tracks QA miss-rates across milestones, detects weak-spot categories (error paths, boundaries, state transitions), and automatically injects targeted guidance into QA subagent prompts. Projects on the same stack share miss-rate data for faster calibration.
17
- **Token-Aware Orchestration** — `token-budget.js` tracks session token consumption and applies graduated degradation: downgrade model assignments when approaching limits, checkpoint and skip non-essential operations to conserve budget, and halt cleanly with a resume instruction at the ceiling. Wave and execute phases check budget before each subagent spawn.
17
+ **Runway-Protected Execution (M35, v2.76.10)** — three-band token budget (`normal` < 70%, `warn` 70–85%, `stop` 85%) with **zero silent quality degradation**. No model downgrades, no phase skips under pressure. Instead:
18
+ - **Surgical model selection** — `bin/model-selector.js` assigns haiku/sonnet/opus per phase via a declarative rules table; `/advisor` escalation path with convention-based fallback.
19
+ - **Pre-flight runway estimator** — `bin/runway-estimator.js` reads per-spawn token telemetry and projects whether a long-running command would cross 85%. If yes, the run is refused *before burning any tokens*.
20
+ - **Headless auto-spawn on refusal** — `bin/headless-auto-spawn.js` detaches a child process to continue the work in a fresh context. The interactive session never sees a `/clear` prompt.
21
+ - **Per-spawn token telemetry** — `.gsd-t/token-metrics.jsonl` records one 18-field row per Task subagent spawn. Feeds the runway estimator and the retrospective optimization backlog.
22
+ - **Optimization backlog** — `bin/token-optimizer.js` runs at `complete-milestone` and appends model-tier recalibration recommendations (`demote`, `escalate`, `runway-tune`, `investigate`) to `.gsd-t/optimization-backlog.md`. **Never auto-applies.** User promotes via `/user:gsd-t-optimization-apply {ID}` or dismisses via `/user:gsd-t-optimization-reject {ID} [--reason "..."]` with a 5-milestone cooldown.
23
+ **Context Meter (M34)** — real-time context window measurement via the Anthropic `count_tokens` API. A PostToolUse hook streams the current transcript to `count_tokens`, writes the exact input-token count and threshold band to `.gsd-t/.context-meter-state.json`, and `token-budget.getSessionStatus()` reads that state file as the authoritative context-burn signal. Replaces the v2.74.12 task-counter proxy. Requires an `ANTHROPIC_API_KEY` — `gsd-t doctor` hard-gates on it. See the Context Meter Setup section below.
18
24
  **Quality North Star** — projects define a `## Quality North Star` section in CLAUDE.md (1–3 sentences, e.g., "This is a published npm library. Every public API must be intuitive and backward-compatible."). `gsd-t-init` auto-detects preset (library/web-app/cli) from package.json signals; `gsd-t-setup` configures it for existing projects. Subagents read it as a quality lens; absent = silent skip (backward compatible).
19
25
  **Design Brief Artifact** — during partition, UI/frontend projects (React, Vue, Svelte, Flutter, Tailwind) automatically get `.gsd-t/contracts/design-brief.md` with color palette, typography, spacing system, component patterns, and tone/voice. Non-UI projects skip silently. User-customized briefs are preserved. Referenced in plan phase for visual consistency.
20
26
  **Design Verification Agent** — after QA passes on design-to-code projects, a dedicated verification agent opens a browser with both the built frontend AND the original design (Figma page, design image, or MCP screenshot) side-by-side for direct visual comparison. Produces a structured element-by-element comparison table (30+ rows) with specific design values vs. implementation values and MATCH/DEVIATION verdicts. An artifact gate enforces that the comparison table exists — missing it blocks completion. Separation of concerns: coding agents code, verification agents verify. Wired into execute (Step 5.25) and quick (Step 5.25). Only fires when `.gsd-t/contracts/design-contract.md` exists — non-design projects are unaffected.
@@ -305,6 +311,70 @@ your-project/
305
311
 
306
312
  ---
307
313
 
314
+ ## Context Meter Setup (M34 — v2.75.10+)
315
+
316
+ The Context Meter replaces the v2.74.12 task-counter proxy with real context-window measurement via the Anthropic `count_tokens` API. This is the authoritative signal for session-stop gates in `gsd-t-execute`, `gsd-t-wave`, `gsd-t-quick`, `gsd-t-integrate`, and `gsd-t-debug`.
317
+
318
+ ### 1. Set your API key
319
+
320
+ Create a key at [console.anthropic.com](https://console.anthropic.com) (free tier is sufficient — `count_tokens` calls are inexpensive) and export it in your shell profile:
321
+
322
+ ```bash
323
+ export ANTHROPIC_API_KEY="sk-ant-..."
324
+ ```
325
+
326
+ The env var name is configurable in `.gsd-t/context-meter-config.json` (default: `ANTHROPIC_API_KEY`).
327
+
328
+ ### 2. Verify with `gsd-t doctor`
329
+
330
+ ```bash
331
+ npx @tekyzinc/gsd-t doctor
332
+ ```
333
+
334
+ Doctor checks:
335
+ - `ANTHROPIC_API_KEY` is set (RED if missing)
336
+ - PostToolUse hook is registered in `~/.claude/settings.json`
337
+ - `scripts/gsd-t-context-meter.js` exists in the project
338
+ - `.gsd-t/context-meter-config.json` parses cleanly
339
+ - A live `count_tokens` dry-run succeeds (RED on 401/403/network failure)
340
+
341
+ ### 3. Adjust thresholds (optional)
342
+
343
+ Edit `.gsd-t/context-meter-config.json`:
344
+
345
+ ```json
346
+ {
347
+ "enabled": true,
348
+ "apiKeyEnvVar": "ANTHROPIC_API_KEY",
349
+ "modelWindowSize": 200000,
350
+ "thresholdPct": 85,
351
+ "checkFrequency": 1
352
+ }
353
+ ```
354
+
355
+ - `modelWindowSize` — total context window (200K for Opus/Sonnet)
356
+ - `thresholdPct` — percentage at which the orchestrator halts (85% = stop band; 70% = warn band — cue for explicit pause/resume; no silent degradation)
357
+ - `checkFrequency` — run `count_tokens` every N tool calls (1 = every call; higher = cheaper + slightly delayed signal)
358
+
359
+ ### 4. Live status
360
+
361
+ ```bash
362
+ npx @tekyzinc/gsd-t status
363
+ ```
364
+
365
+ Displays a Context line with `{pct}% of {window} tokens ({band}) — last check {time ago}`. Missing state file shows `N/A (meter hook not run this session)`.
366
+
367
+ ### Upgrading from pre-M34
368
+
369
+ Running `gsd-t update-all` handles the migration automatically:
370
+ - Copies the new hook script, runtime files, config template, and `context-meter-config.cjs` loader into every registered project
371
+ - Runs a one-time task-counter retirement — deletes `bin/task-counter.cjs`, `.gsd-t/task-counter-config.json`, `.gsd-t/.task-counter-state.json`, and the `.gsd-t/.task-counter` state file
372
+ - Writes `.gsd-t/.task-counter-retired-v1` marker (subsequent runs are no-op)
373
+
374
+ After upgrading, **you must set `ANTHROPIC_API_KEY`** — `gsd-t doctor` will fail otherwise.
375
+
376
+ ---
377
+
308
378
  ## Enabling Agent Teams
309
379
 
310
380
  ```json
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * GSD-T Advisor Integration — convention-based /advisor escalation fallback
5
+ *
6
+ * Per `.gsd-t/M35-advisor-findings.md`: Claude Code's native /advisor tool has
7
+ * NO programmable API at subagent scope. This module exists as a seam so that
8
+ * when Anthropic ships a programmable advisor endpoint, the function body can
9
+ * be rewritten without touching callers.
10
+ *
11
+ * Current (M35 v1.0.0) behavior:
12
+ * - invokeAdvisor() always returns {available: false, guidance: null, loggedMiss: true}
13
+ * - The call appends a single `missed_escalation` record to
14
+ * `.gsd-t/token-log.md` so that token-telemetry's aggregate view can
15
+ * report how many escalation points occurred without a programmable path
16
+ * - Graceful degradation: if the log write fails, return loggedMiss: false
17
+ * but never throw — callers proceed at their assigned model either way
18
+ *
19
+ * Contract: `.gsd-t/contracts/model-selection-contract.md` v1.0.0 (M35 T4)
20
+ * Zero external dependencies.
21
+ */
22
+
23
+ const fs = require("fs");
24
+ const path = require("path");
25
+
26
+ const TOKEN_LOG_RELATIVE = ".gsd-t/token-log.md";
27
+
28
+ /**
29
+ * Invoke the /advisor escalation hook.
30
+ *
31
+ * @param {object} args
32
+ * @param {string} args.question — the escalation question being asked
33
+ * @param {object} [args.context] — optional structured context (phase, domain, task)
34
+ * @param {string} [args.projectDir] — project root; defaults to cwd
35
+ * @returns {{available: boolean, guidance: string|null, loggedMiss: boolean}}
36
+ */
37
+ function invokeAdvisor(args) {
38
+ const { question, context, projectDir } = args || {};
39
+ const dir = projectDir || process.cwd();
40
+
41
+ // There is no programmable path to try. Record the miss and return.
42
+ const loggedMiss = logMissedEscalation(dir, question, context);
43
+
44
+ return {
45
+ available: false,
46
+ guidance: null,
47
+ loggedMiss,
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Append a single missed-escalation record to `.gsd-t/token-log.md`.
53
+ * Returns true on successful append, false on any filesystem error
54
+ * (non-throwing — this is a best-effort audit trail).
55
+ *
56
+ * @param {string} projectDir
57
+ * @param {string} [question]
58
+ * @param {object} [context]
59
+ * @returns {boolean}
60
+ */
61
+ function logMissedEscalation(projectDir, question, context) {
62
+ try {
63
+ const logPath = path.join(projectDir, TOKEN_LOG_RELATIVE);
64
+ const logDir = path.dirname(logPath);
65
+ if (!fs.existsSync(logDir)) return false;
66
+
67
+ const ts = new Date().toISOString();
68
+ const q = sanitizeOneLine(question || "(no question provided)");
69
+ const ctxPhase = (context && context.phase) ? sanitizeOneLine(String(context.phase)) : "";
70
+ const ctxDomain = (context && context.domain) ? sanitizeOneLine(String(context.domain)) : "";
71
+ const ctxTask = (context && context.task) ? sanitizeOneLine(String(context.task)) : "";
72
+
73
+ const line = `<!-- missed_escalation ${ts} phase=${ctxPhase} domain=${ctxDomain} task=${ctxTask} q="${q}" -->\n`;
74
+
75
+ fs.appendFileSync(logPath, line, "utf8");
76
+ return true;
77
+ } catch (_err) {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Strip newlines and trim whitespace so the record stays on one line.
84
+ */
85
+ function sanitizeOneLine(s) {
86
+ return String(s).replace(/\s+/g, " ").trim().slice(0, 500);
87
+ }
88
+
89
+ module.exports = {
90
+ invokeAdvisor,
91
+ logMissedEscalation,
92
+ TOKEN_LOG_RELATIVE,
93
+ };
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * GSD-T Check Headless Sessions — Read-back banner helper
5
+ *
6
+ * Scans .gsd-t/headless-sessions/ for completed sessions that have not yet
7
+ * been surfaced to the user. Consumed by `gsd-t-resume` and `gsd-t-status`
8
+ * to print a "Headless runs since you left" banner at the start of their
9
+ * output. After surfacing, marks the session file with `surfaced: true`
10
+ * so the banner never re-appears for the same session.
11
+ *
12
+ * Zero external dependencies (Node.js built-ins only).
13
+ *
14
+ * Contract: .gsd-t/contracts/headless-auto-spawn-contract.md v1.0.0
15
+ * Consumers: commands/gsd-t-resume.md, commands/gsd-t-status.md
16
+ */
17
+
18
+ const fs = require("fs");
19
+ const path = require("path");
20
+
21
+ const SESSIONS_DIR_REL = path.join(".gsd-t", "headless-sessions");
22
+
23
+ module.exports = {
24
+ checkCompletedSessions,
25
+ markSurfaced,
26
+ formatBanner,
27
+ printBannerIfAny,
28
+ };
29
+
30
+ /**
31
+ * @param {string} [projectDir]
32
+ * @returns {Array<object>} unsurfaced completed sessions, oldest first
33
+ */
34
+ function checkCompletedSessions(projectDir) {
35
+ const dir = path.join(projectDir || process.cwd(), SESSIONS_DIR_REL);
36
+ if (!fs.existsSync(dir)) return [];
37
+
38
+ const entries = [];
39
+ let files;
40
+ try {
41
+ files = fs.readdirSync(dir);
42
+ } catch (_) {
43
+ return [];
44
+ }
45
+
46
+ for (const f of files) {
47
+ if (!f.endsWith(".json")) continue;
48
+ if (f.endsWith("-context.json")) continue;
49
+ const fp = path.join(dir, f);
50
+ try {
51
+ const s = JSON.parse(fs.readFileSync(fp, "utf8"));
52
+ if (s && s.status === "completed" && s.surfaced !== true) {
53
+ entries.push(s);
54
+ }
55
+ } catch (_) {
56
+ // skip malformed session files silently
57
+ }
58
+ }
59
+
60
+ entries.sort((a, b) => {
61
+ const ta = a.endTimestamp || a.startTimestamp || "";
62
+ const tb = b.endTimestamp || b.startTimestamp || "";
63
+ return ta.localeCompare(tb);
64
+ });
65
+
66
+ return entries;
67
+ }
68
+
69
+ /**
70
+ * Mark a session as surfaced so the banner won't re-appear for it.
71
+ * @param {string} projectDir
72
+ * @param {string} id
73
+ */
74
+ function markSurfaced(projectDir, id) {
75
+ const fp = path.join(projectDir || process.cwd(), SESSIONS_DIR_REL, `${id}.json`);
76
+ if (!fs.existsSync(fp)) return;
77
+ try {
78
+ const s = JSON.parse(fs.readFileSync(fp, "utf8"));
79
+ s.surfaced = true;
80
+ fs.writeFileSync(fp, JSON.stringify(s, null, 2) + "\n");
81
+ } catch (_) {
82
+ /* ignore */
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Format a human-readable banner for the given sessions. Does not print.
88
+ * @param {Array<object>} sessions
89
+ * @returns {string}
90
+ */
91
+ function formatBanner(sessions) {
92
+ if (!sessions || sessions.length === 0) return "";
93
+ const lines = [];
94
+ lines.push("## Headless runs since you left");
95
+ lines.push("");
96
+ for (const s of sessions) {
97
+ const duration = computeDurationLabel(s.startTimestamp, s.endTimestamp);
98
+ const outcome = s.exitCode === 0 ? "success" : `exit ${s.exitCode}`;
99
+ const cmd = s.command || "(unknown)";
100
+ lines.push(`- **${s.id}** — ${cmd} — ${duration} — ${outcome}`);
101
+ if (s.logPath) lines.push(` Log: \`${s.logPath}\``);
102
+ }
103
+ lines.push("");
104
+ return lines.join("\n");
105
+ }
106
+
107
+ /**
108
+ * Convenience wrapper: check, print to stdout if any, mark surfaced.
109
+ * Returns number of sessions surfaced.
110
+ */
111
+ function printBannerIfAny(projectDir) {
112
+ const sessions = checkCompletedSessions(projectDir);
113
+ if (sessions.length === 0) return 0;
114
+ process.stdout.write(formatBanner(sessions) + "\n");
115
+ for (const s of sessions) markSurfaced(projectDir, s.id);
116
+ return sessions.length;
117
+ }
118
+
119
+ function computeDurationLabel(startIso, endIso) {
120
+ if (!startIso || !endIso) return "unknown duration";
121
+ const start = Date.parse(startIso);
122
+ const end = Date.parse(endIso);
123
+ if (!isFinite(start) || !isFinite(end) || end < start) return "unknown duration";
124
+ const secs = Math.round((end - start) / 1000);
125
+ if (secs < 60) return `${secs}s`;
126
+ const mins = Math.floor(secs / 60);
127
+ const rem = secs % 60;
128
+ if (mins < 60) return `${mins}m ${rem}s`;
129
+ const hrs = Math.floor(mins / 60);
130
+ const remMins = mins % 60;
131
+ return `${hrs}h ${remMins}m`;
132
+ }
133
+
134
+ // ── CLI entry point ─────────────────────────────────────────────────────────
135
+
136
+ if (require.main === module) {
137
+ const projectDir = process.argv[2] || process.cwd();
138
+ const n = printBannerIfAny(projectDir);
139
+ process.exit(n > 0 ? 0 : 0);
140
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Context Meter config loader (M34).
3
+ *
4
+ * Reads .gsd-t/context-meter-config.json, merges over defaults, validates,
5
+ * and returns the resolved config. Missing file → defaults. Unknown schema
6
+ * version or API-key leak → throws with a clear message.
7
+ *
8
+ * See .gsd-t/contracts/context-meter-contract.md for the schema, validation
9
+ * rules, and the API-key-never-stored invariant.
10
+ */
11
+
12
+ const fs = require("fs");
13
+ const path = require("path");
14
+
15
+ const DEFAULTS = Object.freeze({
16
+ version: 1,
17
+ thresholdPct: 75,
18
+ modelWindowSize: 200000,
19
+ checkFrequency: 5,
20
+ apiKeyEnvVar: "ANTHROPIC_API_KEY",
21
+ statePath: ".gsd-t/.context-meter-state.json",
22
+ logPath: ".gsd-t/context-meter.log",
23
+ timeoutMs: 2000,
24
+ });
25
+
26
+ const SUPPORTED_VERSION = 1;
27
+ const API_KEY_FIELD_RE = /api.?key/i;
28
+ const HEX_LOOKALIKE_RE = /^[a-zA-Z0-9_-]{64,}$/;
29
+
30
+ function loadConfig(projectRoot) {
31
+ const root = projectRoot || process.cwd();
32
+ const configPath = path.join(root, ".gsd-t", "context-meter-config.json");
33
+
34
+ let userConfig = {};
35
+ if (fs.existsSync(configPath)) {
36
+ const raw = fs.readFileSync(configPath, "utf8");
37
+ try {
38
+ userConfig = JSON.parse(raw);
39
+ } catch (e) {
40
+ throw new Error(
41
+ `context-meter-config: invalid JSON in ${configPath}: ${e.message}`
42
+ );
43
+ }
44
+ if (!userConfig || typeof userConfig !== "object" || Array.isArray(userConfig)) {
45
+ throw new Error(`context-meter-config: ${configPath} must contain a JSON object`);
46
+ }
47
+ }
48
+
49
+ validateNoKeyLeak(userConfig);
50
+
51
+ if (userConfig.version !== undefined && userConfig.version !== SUPPORTED_VERSION) {
52
+ throw new Error(
53
+ `context-meter-config: unsupported schema version ${userConfig.version} ` +
54
+ `(expected ${SUPPORTED_VERSION}). See .gsd-t/contracts/context-meter-contract.md#breaking-changes for migration.`
55
+ );
56
+ }
57
+
58
+ const merged = { ...DEFAULTS, ...userConfig, version: SUPPORTED_VERSION };
59
+ validateRanges(merged);
60
+ return merged;
61
+ }
62
+
63
+ function validateNoKeyLeak(obj) {
64
+ for (const key of Object.keys(obj)) {
65
+ if (key === "apiKeyEnvVar") continue;
66
+ if (API_KEY_FIELD_RE.test(key)) {
67
+ throw new Error(
68
+ `context-meter-config: field "${key}" looks like an API key storage field. ` +
69
+ `API keys must only be read from the env var named in apiKeyEnvVar.`
70
+ );
71
+ }
72
+ const val = obj[key];
73
+ if (typeof val === "string" && val.length > 100 && HEX_LOOKALIKE_RE.test(val)) {
74
+ throw new Error(
75
+ `context-meter-config: field "${key}" contains a long token-like string. ` +
76
+ `Do not store API keys in config — use apiKeyEnvVar to name the env var.`
77
+ );
78
+ }
79
+ }
80
+ }
81
+
82
+ function validateRanges(c) {
83
+ const assert = (cond, msg) => { if (!cond) throw new Error(`context-meter-config: ${msg}`); };
84
+
85
+ assert(Number.isFinite(c.thresholdPct) && c.thresholdPct > 0 && c.thresholdPct < 100,
86
+ `thresholdPct must be a number in (0, 100), got ${c.thresholdPct}`);
87
+ assert(Number.isInteger(c.modelWindowSize) && c.modelWindowSize > 0,
88
+ `modelWindowSize must be a positive integer, got ${c.modelWindowSize}`);
89
+ assert(Number.isInteger(c.checkFrequency) && c.checkFrequency >= 1,
90
+ `checkFrequency must be an integer >= 1, got ${c.checkFrequency}`);
91
+ assert(typeof c.apiKeyEnvVar === "string" && c.apiKeyEnvVar.length > 0,
92
+ `apiKeyEnvVar must be a non-empty string, got ${JSON.stringify(c.apiKeyEnvVar)}`);
93
+ assert(typeof c.statePath === "string" && c.statePath.length > 0,
94
+ `statePath must be a non-empty string`);
95
+ assert(typeof c.logPath === "string" && c.logPath.length > 0,
96
+ `logPath must be a non-empty string`);
97
+ assert(Number.isInteger(c.timeoutMs) && c.timeoutMs > 0,
98
+ `timeoutMs must be a positive integer, got ${c.timeoutMs}`);
99
+ }
100
+
101
+ module.exports = { loadConfig, DEFAULTS };
@@ -0,0 +1,101 @@
1
+ const { test } = require("node:test");
2
+ const assert = require("node:assert/strict");
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ const { loadConfig, DEFAULTS } = require("./context-meter-config.cjs");
8
+
9
+ function makeProject(config) {
10
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "cmcfg-"));
11
+ fs.mkdirSync(path.join(root, ".gsd-t"), { recursive: true });
12
+ if (config !== undefined) {
13
+ fs.writeFileSync(
14
+ path.join(root, ".gsd-t", "context-meter-config.json"),
15
+ typeof config === "string" ? config : JSON.stringify(config)
16
+ );
17
+ }
18
+ return root;
19
+ }
20
+
21
+ test("missing config file → returns defaults", () => {
22
+ const root = makeProject();
23
+ assert.deepEqual(loadConfig(root), DEFAULTS);
24
+ });
25
+
26
+ test("valid full config → returns exact values", () => {
27
+ const custom = {
28
+ version: 1,
29
+ thresholdPct: 80,
30
+ modelWindowSize: 400000,
31
+ checkFrequency: 10,
32
+ apiKeyEnvVar: "CLAUDE_KEY",
33
+ statePath: ".gsd-t/my-state.json",
34
+ logPath: ".gsd-t/my.log",
35
+ timeoutMs: 5000,
36
+ };
37
+ const root = makeProject(custom);
38
+ assert.deepEqual(loadConfig(root), custom);
39
+ });
40
+
41
+ test("partial config → missing fields filled with defaults", () => {
42
+ const root = makeProject({ thresholdPct: 60, timeoutMs: 1000 });
43
+ const cfg = loadConfig(root);
44
+ assert.equal(cfg.thresholdPct, 60);
45
+ assert.equal(cfg.timeoutMs, 1000);
46
+ assert.equal(cfg.modelWindowSize, DEFAULTS.modelWindowSize);
47
+ assert.equal(cfg.checkFrequency, DEFAULTS.checkFrequency);
48
+ assert.equal(cfg.apiKeyEnvVar, DEFAULTS.apiKeyEnvVar);
49
+ });
50
+
51
+ test("thresholdPct out of range throws", () => {
52
+ for (const bad of [0, 100, -5, 150, "80"]) {
53
+ const root = makeProject({ thresholdPct: bad });
54
+ assert.throws(() => loadConfig(root), /thresholdPct/);
55
+ }
56
+ });
57
+
58
+ test("modelWindowSize <= 0 throws", () => {
59
+ for (const bad of [0, -1, 1.5, "100"]) {
60
+ const root = makeProject({ modelWindowSize: bad });
61
+ assert.throws(() => loadConfig(root), /modelWindowSize/);
62
+ }
63
+ });
64
+
65
+ test("checkFrequency < 1 throws", () => {
66
+ for (const bad of [0, -1, 0.5]) {
67
+ const root = makeProject({ checkFrequency: bad });
68
+ assert.throws(() => loadConfig(root), /checkFrequency/);
69
+ }
70
+ });
71
+
72
+ test("empty apiKeyEnvVar throws", () => {
73
+ const root = makeProject({ apiKeyEnvVar: "" });
74
+ assert.throws(() => loadConfig(root), /apiKeyEnvVar/);
75
+ });
76
+
77
+ test("unknown version throws with migration pointer", () => {
78
+ const root = makeProject({ version: 2, thresholdPct: 75 });
79
+ assert.throws(() => loadConfig(root), /version 2|migration/i);
80
+ });
81
+
82
+ test("config containing an apiKey field is rejected as leak", () => {
83
+ const root = makeProject({ apiKey: "sk-ant-abc123" });
84
+ assert.throws(() => loadConfig(root), /api.?key/i);
85
+ });
86
+
87
+ test("config containing a long hex-like string value is rejected as leak", () => {
88
+ const longHex = "a".repeat(120);
89
+ const root = makeProject({ customField: longHex });
90
+ assert.throws(() => loadConfig(root), /api.?key|token-like/i);
91
+ });
92
+
93
+ test("invalid JSON throws with clear message", () => {
94
+ const root = makeProject("{ not valid json");
95
+ assert.throws(() => loadConfig(root), /invalid JSON/);
96
+ });
97
+
98
+ test("non-object JSON throws", () => {
99
+ const root = makeProject("[1, 2, 3]");
100
+ assert.throws(() => loadConfig(root), /JSON object/);
101
+ });