@tekyzinc/gsd-t 2.74.12 → 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.
- package/CHANGELOG.md +130 -0
- package/README.md +71 -1
- package/bin/advisor-integration.js +93 -0
- package/bin/check-headless-sessions.js +140 -0
- package/bin/context-meter-config.cjs +101 -0
- package/bin/context-meter-config.test.cjs +101 -0
- package/bin/gsd-t.js +710 -16
- package/bin/headless-auto-spawn.js +290 -0
- package/bin/model-selector.js +224 -0
- package/bin/runway-estimator.js +242 -0
- package/bin/token-budget.js +96 -89
- package/bin/token-optimizer.js +471 -0
- package/bin/token-telemetry.js +246 -0
- package/commands/gsd-t-audit.md +3 -3
- package/commands/gsd-t-backlog-list.md +38 -0
- package/commands/gsd-t-brainstorm.md +3 -3
- package/commands/gsd-t-complete-milestone.md +24 -0
- package/commands/gsd-t-debug.md +124 -7
- package/commands/gsd-t-discuss.md +10 -3
- package/commands/gsd-t-doc-ripple.md +32 -4
- package/commands/gsd-t-execute.md +107 -52
- package/commands/gsd-t-help.md +19 -0
- package/commands/gsd-t-integrate.md +67 -4
- package/commands/gsd-t-optimization-apply.md +91 -0
- package/commands/gsd-t-optimization-reject.md +94 -0
- package/commands/gsd-t-partition.md +7 -0
- package/commands/gsd-t-pause.md +3 -0
- package/commands/gsd-t-plan.md +10 -3
- package/commands/gsd-t-prd.md +3 -3
- package/commands/gsd-t-quick.md +71 -9
- package/commands/gsd-t-reflect.md +3 -7
- package/commands/gsd-t-resume.md +36 -0
- package/commands/gsd-t-status.md +31 -0
- package/commands/gsd-t-test-sync.md +7 -0
- package/commands/gsd-t-verify.md +12 -5
- package/commands/gsd-t-visualize.md +3 -7
- package/commands/gsd-t-wave.md +82 -18
- package/docs/GSD-T-README.md +52 -0
- package/docs/architecture.md +95 -0
- package/docs/infrastructure.md +117 -0
- package/docs/methodology.md +36 -0
- package/docs/prd-harness-evolution.md +51 -37
- package/docs/requirements.md +66 -0
- package/package.json +1 -1
- package/scripts/context-meter/count-tokens-client.js +221 -0
- package/scripts/context-meter/count-tokens-client.test.js +308 -0
- package/scripts/context-meter/test-injector.js +55 -0
- package/scripts/context-meter/threshold.js +88 -0
- package/scripts/context-meter/threshold.test.js +255 -0
- package/scripts/context-meter/transcript-parser.js +252 -0
- package/scripts/context-meter/transcript-parser.test.js +320 -0
- package/scripts/gsd-t-context-meter.e2e.test.js +415 -0
- package/scripts/gsd-t-context-meter.js +350 -0
- package/scripts/gsd-t-context-meter.test.js +417 -0
- package/scripts/gsd-t-heartbeat.js +2 -2
- package/scripts/gsd-t-statusline.js +23 -8
- package/templates/CLAUDE-global.md +5 -1
- package/templates/CLAUDE-project.md +26 -6
- package/templates/context-meter-config.json +10 -0
- package/templates/prompts/README.md +1 -1
- package/bin/task-counter.cjs +0 -161
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,136 @@
|
|
|
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
|
+
|
|
121
|
+
## [2.74.13] - 2026-04-14
|
|
122
|
+
|
|
123
|
+
### Fixed — v2.74.12 task-counter distribution gap (P0)
|
|
124
|
+
|
|
125
|
+
**Root cause**: v2.74.12 added `bin/task-counter.cjs` as the deterministic context-burn gate and wired every command file to call `node bin/task-counter.cjs …`, but the installer's `PROJECT_BIN_TOOLS` list (`bin/gsd-t.js:1562`) was never updated to include it. Every downstream project ran command files that referenced a file the installer never copied. In every GSD-T project, `node bin/task-counter.cjs status|should-stop|reset|increment` threw "Cannot find module" — swallowed by `2>/dev/null` — and the orchestrator silently continued with no gate. Confirmed in bee-poc: `reassign-display` 6/6 + `reassign-candidates` 2/9 executed across ~30 min while `task-counter status` stayed `{"count":0}` the entire run and `token-log.md` got zero new rows.
|
|
126
|
+
|
|
127
|
+
**Additionally**: `doInit()` (`bin/gsd-t.js:1095`) never called `copyBinToolsToProject` at all, so brand-new projects created with `gsd-t init` were born with no bin tools until the user manually ran `update`.
|
|
128
|
+
|
|
129
|
+
**Fix**:
|
|
130
|
+
- **`bin/gsd-t.js`** — `PROJECT_BIN_TOOLS` now includes `task-counter.cjs`. One-line change at `bin/gsd-t.js:1562`.
|
|
131
|
+
- **`bin/gsd-t.js`** — `doInit()` now calls `copyBinToolsToProject(projectDir, projectName)` after `initGsdtDir`, so newly-initialized projects ship bin tools immediately.
|
|
132
|
+
|
|
133
|
+
v2.74.12's entire two-layer fix (task-count gate + extracted prompts) is correct — it just needed one line to actually distribute the counter script. Running `/user:gsd-t-version-update-all` after publishing this version will propagate `task-counter.cjs` to every registered project.
|
|
134
|
+
|
|
5
135
|
## [2.74.12] - 2026-04-14
|
|
6
136
|
|
|
7
137
|
### Fixed — Context-Burn Regression (P0, affects every GSD-T project)
|
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
|
-
**
|
|
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 };
|