@hegemonart/get-design-done 1.27.5 → 1.27.7

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 (67) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +6 -3
  3. package/CHANGELOG.md +99 -0
  4. package/agents/perf-analyzer.md +166 -0
  5. package/hooks/gdd-precompact-snapshot.js +334 -0
  6. package/hooks/gdd-sessionstart-recap.js +281 -0
  7. package/hooks/hooks.json +18 -0
  8. package/package.json +6 -5
  9. package/reference/perf-budget.md +142 -0
  10. package/reference/registry.json +14 -0
  11. package/reference/retrieval-contract.md +16 -0
  12. package/reference/schemas/mcp-gdd-tools.schema.json +381 -0
  13. package/scripts/install.cjs +42 -0
  14. package/scripts/lib/cache/gdd-cache-manager.cjs +292 -0
  15. package/scripts/lib/discuss-parallel-runner/index.ts +5 -1
  16. package/scripts/lib/explore-parallel-runner/index.ts +5 -1
  17. package/scripts/lib/gsd-health-mirror/index.cjs +105 -0
  18. package/scripts/lib/gsd-health-mirror/index.d.cts +14 -0
  19. package/scripts/lib/install/mcp-register.cjs +235 -0
  20. package/scripts/lib/install/mcp-register.d.cts +64 -0
  21. package/scripts/lib/intel-store/index.cjs +55 -0
  22. package/scripts/lib/intel-store/index.d.cts +11 -0
  23. package/scripts/lib/mcp-tools-lint/index.cjs +216 -0
  24. package/scripts/lib/mcp-tools-lint/index.d.cts +74 -0
  25. package/scripts/lib/parallelism-engine/concurrency-tuner.cjs +259 -0
  26. package/scripts/lib/parallelism-engine/concurrency-tuner.d.cts +53 -0
  27. package/scripts/lib/perf-analyzer/cost-regression.cjs +299 -0
  28. package/scripts/lib/perf-analyzer/index.cjs +139 -0
  29. package/scripts/lib/prompt-dedup/index.cjs +161 -0
  30. package/scripts/lib/reflections-reader/index.cjs +107 -0
  31. package/scripts/lib/reflections-reader/index.d.cts +18 -0
  32. package/scripts/lib/roadmap-reader/index.cjs +81 -0
  33. package/scripts/lib/roadmap-reader/index.d.cts +13 -0
  34. package/scripts/lib/snapshot-reader/index.cjs +70 -0
  35. package/scripts/lib/snapshot-reader/index.d.cts +28 -0
  36. package/scripts/mcp-servers/gdd-mcp/README.md +66 -0
  37. package/scripts/mcp-servers/gdd-mcp/schemas/gdd_cycle_recap.schema.json +30 -0
  38. package/scripts/mcp-servers/gdd-mcp/schemas/gdd_decisions_list.schema.json +32 -0
  39. package/scripts/mcp-servers/gdd-mcp/schemas/gdd_events_tail.schema.json +22 -0
  40. package/scripts/mcp-servers/gdd-mcp/schemas/gdd_health.schema.json +30 -0
  41. package/scripts/mcp-servers/gdd-mcp/schemas/gdd_intel_get.schema.json +24 -0
  42. package/scripts/mcp-servers/gdd-mcp/schemas/gdd_learnings_digest.schema.json +22 -0
  43. package/scripts/mcp-servers/gdd-mcp/schemas/gdd_phase_current.schema.json +22 -0
  44. package/scripts/mcp-servers/gdd-mcp/schemas/gdd_phases_list.schema.json +31 -0
  45. package/scripts/mcp-servers/gdd-mcp/schemas/gdd_plans_list.schema.json +33 -0
  46. package/scripts/mcp-servers/gdd-mcp/schemas/gdd_reflections_latest.schema.json +21 -0
  47. package/scripts/mcp-servers/gdd-mcp/schemas/gdd_status.schema.json +23 -0
  48. package/scripts/mcp-servers/gdd-mcp/schemas/gdd_telemetry_query.schema.json +23 -0
  49. package/scripts/mcp-servers/gdd-mcp/server.ts +317 -0
  50. package/scripts/mcp-servers/gdd-mcp/tools/gdd_cycle_recap.ts +37 -0
  51. package/scripts/mcp-servers/gdd-mcp/tools/gdd_decisions_list.ts +33 -0
  52. package/scripts/mcp-servers/gdd-mcp/tools/gdd_events_tail.ts +26 -0
  53. package/scripts/mcp-servers/gdd-mcp/tools/gdd_health.ts +19 -0
  54. package/scripts/mcp-servers/gdd-mcp/tools/gdd_intel_get.ts +32 -0
  55. package/scripts/mcp-servers/gdd-mcp/tools/gdd_learnings_digest.ts +23 -0
  56. package/scripts/mcp-servers/gdd-mcp/tools/gdd_phase_current.ts +29 -0
  57. package/scripts/mcp-servers/gdd-mcp/tools/gdd_phases_list.ts +26 -0
  58. package/scripts/mcp-servers/gdd-mcp/tools/gdd_plans_list.ts +39 -0
  59. package/scripts/mcp-servers/gdd-mcp/tools/gdd_reflections_latest.ts +25 -0
  60. package/scripts/mcp-servers/gdd-mcp/tools/gdd_status.ts +31 -0
  61. package/scripts/mcp-servers/gdd-mcp/tools/gdd_telemetry_query.ts +27 -0
  62. package/scripts/mcp-servers/gdd-mcp/tools/index.ts +75 -0
  63. package/scripts/mcp-servers/gdd-mcp/tools/shared.ts +134 -0
  64. package/skills/health/SKILL.md +36 -0
  65. package/skills/next/SKILL.md +28 -3
  66. package/skills/progress/SKILL.md +21 -6
  67. package/skills/resume/SKILL.md +26 -1
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Get Design Done — 5-stage agent-orchestrated design pipeline with 9 connections, handoff-first workflow, bidirectional Figma write-back, 22+ specialized agents, queryable knowledge layer (intel store, dependency analysis, learnings extraction), and a self-improvement loop (reflector, frontmatter + budget feedback, global-skills layer). v1.20.0 ships the SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream, and resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) for rate-limit + 429 + context-overflow recovery. Full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows) and release automation (auto-tag + GitHub Release + release-time smoke test).",
8
- "version": "1.27.5"
8
+ "version": "1.27.7"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "get-design-done",
13
13
  "source": "./",
14
14
  "description": "Agent-orchestrated 5-stage design pipeline: Brief → Explore → Plan → Design → Verify. 22+ specialized agents, 9 connections (Figma, Refero, Preview, Storybook, Chromatic, Figma Writer, Graphify, Pinterest, Claude Design), Claude Design handoff, bidirectional Figma write-back, and a queryable intel store (.design/intel/) for dependency and learnings queries. Standalone commands: style, darkmode, compare, figma-write, graphify, handoff, analyze-dependencies, skill-manifest, extract-learnings. Embeds NNG heuristics, WCAG thresholds, typographic systems, motion framework, and anti-pattern catalog. Ships with a full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows) and release automation. Optimization layer (v1.0.4.1, retroactive): gdd-router + gdd-cache-manager skills, PreToolUse budget-enforcer hook, tier-aware agent frontmatter, lazy checker gates, streaming synthesizer, /gdd:warm-cache + /gdd:optimize commands, and cost telemetry at .design/telemetry/costs.jsonl — targeting 50-70% per-task token-cost reduction with no quality-floor regression. v1.20.0 SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream at .design/telemetry/events.jsonl, resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) with rate-limit + 429 + context-overflow recovery, and TypeScript toolchain.",
15
- "version": "1.27.5",
15
+ "version": "1.27.7",
16
16
  "author": {
17
17
  "name": "hegemonart"
18
18
  },
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "get-design-done",
3
3
  "short_name": "gdd",
4
- "version": "1.27.5",
5
- "description": "Agent-orchestrated 5-stage design pipeline: Brief → Explore → Plan → Design → Verify. 22+ specialized agents, 9 connections (Figma, Refero, Preview, Storybook, Chromatic, Figma Writer, Graphify, Pinterest, Claude Design), handoff-first workflow via Claude Design bundles, bidirectional Figma write-back (annotations, Code Connect), queryable intel store (`.design/intel/`) for O(1) design surface lookups, and self-improvement loop (reflector agent, frontmatter + budget feedback, global-skills layer at `~/.claude/gdd/global-skills/`). Standalone commands: style, darkmode, compare, figma-write, graphify, handoff, analyze-dependencies, skill-manifest, extract-learnings, reflect, apply-reflections. Embeds NNG heuristics, WCAG thresholds, typographic systems, motion framework, and anti-pattern catalog. Ships with a full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows, lint + schema + frontmatter + stale-ref + shellcheck + gitleaks + injection-scan + blocking size-budget) and release automation (auto-tag + GitHub Release + release-time smoke test). Optimization layer (v1.0.4.1, retroactive): gdd-router + gdd-cache-manager skills, PreToolUse budget-enforcer hook, tier-aware agent frontmatter, lazy checker gates, streaming synthesizer, /gdd:warm-cache + /gdd:optimize commands, and cost telemetry at .design/telemetry/costs.jsonl — targeting 50-70% per-task token-cost reduction with no quality-floor regression. v1.20.0 SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream at .design/telemetry/events.jsonl, resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) with rate-limit + 429 + context-overflow recovery, and TypeScript toolchain.",
4
+ "version": "1.27.7",
5
+ "description": "Agent-orchestrated 5-stage design pipeline: Brief → Explore → Plan → Design → Verify. 22+ specialized agents, 9 connections (Figma, Refero, Preview, Storybook, Chromatic, Figma Writer, Graphify, Pinterest, Claude Design), handoff-first workflow via Claude Design bundles, bidirectional Figma write-back (annotations, Code Connect), queryable intel store (`.design/intel/`) for O(1) design surface lookups, and self-improvement loop (reflector agent, frontmatter + budget feedback, global-skills layer at `~/.claude/gdd/global-skills/`). Standalone commands: style, darkmode, compare, figma-write, graphify, handoff, analyze-dependencies, skill-manifest, extract-learnings, reflect, apply-reflections. Embeds NNG heuristics, WCAG thresholds, typographic systems, motion framework, and anti-pattern catalog. Ships with a full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows, lint + schema + frontmatter + stale-ref + shellcheck + gitleaks + injection-scan + blocking size-budget) and release automation (auto-tag + GitHub Release + release-time smoke test). Optimization layer (v1.0.4.1, retroactive): gdd-router + gdd-cache-manager skills, PreToolUse budget-enforcer hook, tier-aware agent frontmatter, lazy checker gates, streaming synthesizer, /gdd:warm-cache + /gdd:optimize commands, and cost telemetry at .design/telemetry/costs.jsonl — targeting 50-70% per-task token-cost reduction with no quality-floor regression. v1.20.0 SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream at .design/telemetry/events.jsonl, resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) with rate-limit + 429 + context-overflow recovery, and TypeScript toolchain. v1.27.7 ships gdd-mcp (Phase 27.7): 12 read-only MCP tools for sub-3s priming.",
6
6
  "author": {
7
7
  "name": "hegemonart",
8
8
  "url": "https://github.com/hegemonart"
@@ -59,7 +59,10 @@
59
59
  "gemini",
60
60
  "mcp",
61
61
  "parallel-agents",
62
- "agent-sdk"
62
+ "agent-sdk",
63
+ "mcp-server",
64
+ "context-loading",
65
+ "cross-session"
63
66
  ],
64
67
  "skills": [
65
68
  "./skills/"
package/CHANGELOG.md CHANGED
@@ -4,6 +4,105 @@ All notable changes to get-design-done are documented here. Versions follow [sem
4
4
 
5
5
  ---
6
6
 
7
+ ## [1.27.7] — 2026-05-18
8
+
9
+ ### Added
10
+
11
+ - **Phase 27.7 — GDD MCP Server** (7 plans). Ships `gdd-mcp`, a read-only Model Context Protocol server that exposes STATE.md sections, phases, decisions, plans, telemetry, intel slices, and the latest reflection as 12 typed MCP tools backed by the same `scripts/lib/*` modules the CLI uses. Sub-3-second priming target on a synthetic project (Storybloq §4.6 pattern transplant). Off-cadence v1.27.7 (CHANGELOG-only; mainline cadence resumes at v1.28.0).
12
+ - `scripts/mcp-servers/gdd-mcp/server.ts` (Plan 27.7-01) — MCP server scaffold using `@modelcontextprotocol/sdk` low-level Server + StdioServerTransport. Project-root discovery walks from `process.cwd()` looking for `.design/` OR `.planning/` OR `.claude-plugin/plugin.json`. `bin/gdd-mcp` shim added to `package.json` (alphabetized: gdd-events → gdd-mcp → gdd-sdk → gdd-state-mcp). Tests on handshake + walk-up + missing-marker behavior. (MCP-01)
13
+ - `scripts/mcp-servers/gdd-mcp/tools/` + `reference/schemas/mcp-gdd-tools.schema.json` (Plan 27.7-02) — 12 read-only tools: `gdd_status`, `gdd_cycle_recap`, `gdd_decisions_list`, `gdd_events_tail`, `gdd_health`, `gdd_intel_get`, `gdd_learnings_digest`, `gdd_phase_current`, `gdd_phases_list`, `gdd_plans_list`, `gdd_reflections_latest`, `gdd_telemetry_query`. Each tool ≤ 30 LOC (D-06), with per-tool Draft-07 JSON Schema, plus 5 helper libs (state-reader, intel-slicer, telemetry-grouper, reflection-loader, paths-resolver) and a `directory_not_found` typed projection. Tests on input schema + output shape + thin-wrapper assertion. (MCP-02)
14
+ - `scripts/lib/mcp-tools-lint/index.cjs` (Plan 27.7-03) — Static lint enforcing 4 invariants: forbid-fs-path (D-06: no direct `fs.*`/`path.*` in tool files), max-loc (≤ 30 LOC per tool), no-write-names (D-04: hard-blocks `_(create|update|delete|append|clear|write|set)` patterns), tool-count-cap (D-03: ≤ 12 files). Tests on each rule + exemptions for `index.ts`/`shared.ts`. (MCP-03)
15
+ - `scripts/lib/install/mcp-register.cjs` + `scripts/install.cjs --register-mcp` extension (Plan 27.7-04) — Idempotent registration with `claude mcp add` + `codex mcp add` (D-07 opt-in; absent-CLI fallback). `skills/health/SKILL.md` gains a `check-mcp-registration` step with 4 SKILL-row outputs (registered_with_both, not_registered, dismissed, unknown). Tests on idempotent re-run + absent-CLI paths + `--` arg-injection guard. (MCP-04)
16
+ - `skills/progress/SKILL.md` + `skills/resume/SKILL.md` + `skills/next/SKILL.md` adopted MCP-path + File-read-path fork (Plan 27.7-05). Each skill prefers `gdd-mcp` tools when registered, falls back to direct file reads when not. Structural-compliance tests via the Phase 28.5 validator. (MCP-05)
17
+ - `scripts/mcp-servers/gdd-mcp/README.md` (≤ 120 lines, Plan 27.7-06) + `test-fixture/baselines/phase-27-7/priming-benchmark.json` capturing −31.18% token reduction on a synthetic project (Storybloq's −30% floor met). Tests on README structure + benchmark fixture shape + token-reduction ≥ 30%. (MCP-06)
18
+ - `test-fixture/baselines/phase-27-7/` regression snapshots (Plan 27.7-07): `tool-registry.json` (12 tools, zero write-tools), `handshake-fixture.json` (canonical initialize response shape), `install-doctor-fixture.json` (4 SKILL-row scenarios), `manifests-version.txt` (pinned at 1.27.7). 4-manifest lockstep bump to v1.27.7. CHANGELOG entry. `OFF_CADENCE_VERSIONS.add('1.27.7')`. `reference/registry.json` gains `mcp-gdd-tools-schema` entry. `plugin.json` keywords gain `mcp-server`, `context-loading`, `cross-session`. ROADMAP scoped flip (7 plan checkboxes + 1 top-level overview entry). New `tests/phase-27-7-baseline.test.cjs` (>= 6 version-agnostic baseline tests) + new `tests/gdd-mcp-headless-e2e.test.cjs` (5 E2E tests: pack -> install -> spawn -> MCP initialize handshake -> tools/list returns 12; skip-on-Windows path documented for npm pack symlink false-negatives — Blocker #2 acceptance per ROADMAP SC #11). (MCP-07)
19
+
20
+ ### Decisions locked
21
+
22
+ - D-01: MCP server is read-mostly (read-only in v1; mutations stay in slash-skills + lockfile-safe writers — re-examine at Phase 30/41).
23
+ - D-02: Tool count capped at 12 — `TOOL_COUNT > 12` throws at module load. Adding a 13th tool requires re-scoping in a new plan.
24
+ - D-03: 12-tool cap is the hard ceiling baselined in `test-fixture/baselines/phase-27-7/tool-registry.json`.
25
+ - D-04: No write-verb tool names — `mcp-tools-lint` blocks `_(create|update|delete|append|clear|write|set)(_|$)` patterns. Baseline asserts `write_tools.length === 0`.
26
+ - D-05: stdio-only transport — no port allocation, no HTTP surface. Project-root discovery walks up from `process.cwd()` (`.design/` OR `.planning/` OR `.claude-plugin/plugin.json` marker).
27
+ - D-06: Each tool file ≤ 30 non-blank-non-comment LOC. Tools must be thin wrappers — no direct `node:fs`/`node:path` imports (enforced by `mcp-tools-lint`). All filesystem I/O routes through `scripts/lib/*` helpers.
28
+ - D-07: Installer `--register-mcp` is opt-in (default off). Absent-CLI fallback emits a non-blocking notice. Dismissable nudge via `.design/config.json#mcp_nudge: false`.
29
+ - D-08: Skill-side adoption is forked — MCP path (preferred) + File-read path (fallback). Both produce identical output shape. Skills do not hard-depend on MCP registration.
30
+ - D-09: Server name `gdd-mcp` (matches package bin); version read from `package.json#version` (single source of truth — D-12 lockstep maintains alignment).
31
+ - D-10: `bin/gdd-mcp` block in `package.json` alphabetized at scaffold time (Plan 27.7-01). Manifest bump preserves the alphabetization.
32
+ - D-11: New schema `reference/schemas/mcp-gdd-tools.schema.json` registered in `reference/registry.json` as `mcp-gdd-tools-schema`.
33
+ - D-12: 4-manifest lockstep — `package.json#version`, `.claude-plugin/plugin.json#version`, `.claude-plugin/marketplace.json#metadata.version`, `.claude-plugin/marketplace.json#plugins[0].version` all ship together at v1.27.7. `OFF_CADENCE_VERSIONS.add('1.27.7')` added to `tests/semver-compare.test.cjs`.
34
+
35
+ ### Tests added
36
+
37
+ ~69 new tests across 10 test files: 5 server scaffold (Plan 27.7-01), 27 tool + helper (Plan 27.7-02), 4 lint (Plan 27.7-03), 6 install + 4 SKILL row (Plan 27.7-04), 3 skill adoption (Plan 27.7-05), 5 README + benchmark (Plan 27.7-06), 8 baseline + 5 headless E2E (Plan 27.7-07).
38
+
39
+ ### Out of scope (deferred or rejected)
40
+
41
+ - Write tools in v1 — `gdd_decision_append`, `gdd_blocker_clear`, `gdd_plan_complete` etc. Mutation belongs to slash-skills + lockfile-safe writers (Phase 20 surface), not callable-by-any-client MCP tools. Re-examine at Phase 30 (issue reporter) or Phase 41 (team mode).
42
+ - Tool sprawl past 12 — Storybloq grew to 43 tools because read + write + autonomous + review-lens orchestration share the surface. GDD's autonomous surface is `/gdd:do`, review is `agents/*`. Re-examine when measured token-cost data justifies an additional tool.
43
+ - Live (streaming) MCP resources — Phase 48 (`/gdd:live`) handles long-lived browser sessions via `channel/`-style subprocess registry; MCP stays request/response in v1.
44
+ - Multi-project federation — one server, one project root. Two GDD projects in two terminals = two MCP servers. Federation is a Phase 41 (team mode) question.
45
+
46
+ ### Benchmark
47
+
48
+ `test-fixture/baselines/phase-27-7/priming-benchmark.json` — synthetic-fixture priming run shows −31.18% token reduction (3 MCP calls vs equivalent file-reading path), with a 34× wall-clock speedup vs file-reading. Floor target was −30% (Storybloq's measured number); GDD exceeds the floor. Real-cycle calibration follows in a patch after 1-2 production cycles.
49
+
50
+ ### Headless E2E (ROADMAP SC #11)
51
+
52
+ `tests/gdd-mcp-headless-e2e.test.cjs` — `npm pack` produces tarball -> `npm install <tarball>` into mkdtempSync prefix -> spawn `gdd-mcp` via the installed bin -> MCP initialize handshake asserts `serverInfo.name === 'gdd-mcp'` + `serverInfo.version === package.json#version` -> follow-up `tools/list` asserts `result.tools.length === 12`. Cleanup at end. Marked `skip: process.platform === 'win32'` for npm pack symlink false-negatives (Blocker #2 acceptance); POSIX CI runs all 5 E2E tests.
53
+
54
+ ---
55
+
56
+ ## [1.27.6] — 2026-05-18
57
+
58
+ ### Added
59
+
60
+ - **Phase 27.6 — Pipeline Performance + Token-Cost Optimization** (6 plans). After 27.5 wired the bandit into production routing, telemetry from `.design/telemetry/{costs,trajectories,events}.jsonl` finally measures real spawns; this phase converts that telemetry into concrete optimizations.
61
+ - `agents/perf-analyzer.md` + `scripts/lib/perf-analyzer/` (Plan 27.6-01) — reflector-tier agent that reads telemetry cross-cycle and surfaces top-3 token-cost regressions per agent + cache-hit-rate deltas + p95 latency spikes. Spawned by `/gdd:reflect` or `/gdd:audit`, NOT per-cycle (D-04).
62
+ - `reference/perf-budget.md` + `tests/perf-budget.test.cjs` (Plan 27.6-02) — per-agent budget table + CI regression gate that fails on >25% regression vs baseline across 3 cycles (D-01). Thresholds configurable via `.design/budget.json#perf_regression_threshold`.
63
+ - `scripts/lib/cache/gdd-cache-manager.cjs` (Plan 27.6-03) — cache-warming heuristic refinement: multiplicative `recency × frequency × cost` score (D-06) + top-N ranking + LRU eviction within warmed set + false-positive event emission when >20% of warmed entries evict before use (D-02).
64
+ - `scripts/lib/parallelism-engine/concurrency-tuner.cjs` (Plan 27.6-04) — data-driven concurrency resolver reading `parallelism.verdict` events; default becomes `min(cpu-1, last_observed_optimum)` capped at 8 (D-07). Both explore-parallel-runner and discuss-parallel-runner now use the resolver when `opts.concurrency` is omitted.
65
+ - `hooks/gdd-precompact-snapshot.js` + `hooks/gdd-sessionstart-recap.js` (Plan 27.6-05) — Storybloq §4.6 transplant. PreCompact hook writes atomic snapshots to `.design/snapshots/<ts>.json` (D-08; retention last-10 LRU); SessionStart recap emits markdown to stderr + JSON sidecar at `.design/snapshots/last-recap.json` (D-09). Harness-aware: Codex no-op with stderr notice (D-10, Phase 45 dep for full path).
66
+ - `scripts/lib/prompt-dedup/index.cjs` + `reference/retrieval-contract.md` extension (Plan 27.6-06) — D-11 dedup: when ≥ 3 agents in same cycle read same `reference/*.md`, the retrieval-contract preamble adds a "shared context loaded once" marker.
67
+ - `docs/PERF-OPTIMIZATION.md` (Plan 27.6-06) — operator guide covering all 6 plans, 12 D-XX decisions, the CI regression gate, perf-analyzer proposals, cache-warming tuning, concurrency resolver, snapshot/recap hooks, Codex no-op fallback, prompt-dedup, recalibration process, and troubleshooting.
68
+
69
+ ### Decisions locked
70
+
71
+ - D-01: Regression-gate threshold = 25% (configurable via `.design/budget.json#perf_regression_threshold`).
72
+ - D-02: Cache-warming false-positive tolerance = 20% (configurable via `.design/budget.json#cache_warming_falsepositive_threshold`).
73
+ - D-03: Baseline data = synthetic cycle replay; real-cycle calibration in a follow-up patch after 1-2 production cycles.
74
+ - D-04: `perf-analyzer` is reflector-tier (not per-cycle).
75
+ - D-05: Per-agent budgets = current p50 + 25% buffer initially.
76
+ - D-06: Cache-warming heuristic = multiplicative `recency × frequency × cost`.
77
+ - D-07: Parallel-mapper concurrency reads `parallelism.verdict` events; default = `min(cpu-1, last_optimum)` capped at 8.
78
+ - D-08: PreCompact snapshot uses `scripts/lib/lockfile.cjs` for atomicity (atomic `.tmp` + rename); retention last-10 LRU.
79
+ - D-09: SessionStart recap emits markdown to stderr + JSON sidecar to `.design/snapshots/last-recap.json`.
80
+ - D-10: Codex no-op fallback (stderr notice; Phase 45 dep for full path).
81
+ - D-11: Prompt-dedup injects at Phase 14.5 retrieval-contract preamble (≥ 3 agents reading same ref → shared-context marker).
82
+ - D-12: 4 manifests lockstep + CHANGELOG + OFF_CADENCE + baseline at `test-fixture/baselines/phase-27-6/`.
83
+
84
+ ### Out of scope (deferred)
85
+
86
+ - Cross-runtime cost arbitrage (Phase 26 territory).
87
+ - Per-call model substitution (Phase 23.5 bandit territory).
88
+ - Rewriting reference files (Phase 46 territory).
89
+ - Codex `pre-large-context-action` interception (Phase 45 dep).
90
+ - Cache-warming auto-tuning of heuristic weights — measurement-gated follow-up.
91
+ - Real-cycle baseline calibration — deferred to follow-up patch.
92
+
93
+ ### Test coverage
94
+
95
+ - `tests/perf-analyzer-cost-regression.test.cjs` — ≥10 tests for detection rules (Plan 27.6-01).
96
+ - `tests/perf-budget.test.cjs` — ≥6 tests for CI gate including cold-start tolerance (Plan 27.6-02).
97
+ - `tests/gdd-cache-manager-warming.test.cjs` — ≥6 tests for warming heuristic (Plan 27.6-03).
98
+ - `tests/concurrency-tuner.test.cjs` — ≥5 tests for D-07 algorithm (Plan 27.6-04).
99
+ - `tests/gdd-precompact-snapshot.test.cjs` — ≥6 tests including atomicity + harness fallback (Plan 27.6-05).
100
+ - `tests/gdd-sessionstart-recap.test.cjs` — ≥4 tests for diff + Codex no-op (Plan 27.6-05).
101
+ - `tests/prompt-dedup.test.cjs` — 12 tests for D-11 threshold + cycle scoping + alphabetic sort + malformed-event filter (Plan 27.6-06).
102
+ - `tests/phase-27-6-baseline.test.cjs` — version-agnostic regression baseline (Plan 27.6-06).
103
+
104
+ ---
105
+
7
106
  ## [1.27.5] — 2026-05-17
8
107
 
9
108
  ### Added
@@ -0,0 +1,166 @@
1
+ ---
2
+ name: perf-analyzer
3
+ description: Cross-cycle performance reflector. Reads .design/telemetry/{costs,trajectories,events}.jsonl and surfaces top-3 token-cost regressions per agent + cache-hit-rate deltas + p95 latency spikes. Spawned by /gdd:reflect or /gdd:audit (NOT per-cycle). Phase 27.6 D-04.
4
+ tools: Read, Write, Bash, Grep, Glob
5
+ color: yellow
6
+ model: inherit
7
+ default-tier: opus
8
+ tier-rationale: "Phase 27.6 reflector — analyzes cross-cycle telemetry, proposes pipeline-level perf improvements; opus matches design-reflector tier per D-04"
9
+ size_budget: XL
10
+ parallel-safe: never
11
+ typical-duration-seconds: 45
12
+ reads-only: false
13
+ writes:
14
+ - ".design/perf/*.md"
15
+ ---
16
+
17
+ @reference/shared-preamble.md
18
+
19
+ # perf-analyzer
20
+
21
+ ## Role
22
+
23
+ You are a cross-cycle performance reflector. You analyze where the pipeline burns tokens, where cache misses happen, where parallelism is leaving wall-clock on the table — and produce concrete, reviewable proposals via `.design/perf/<cycle-slug>.md`. You never auto-apply anything; the operator reviews via `/gdd:apply-reflections` (Phase 11 wiring).
24
+
25
+ You run **cross-cycle, not per-cycle** (Phase 27.6 D-04). Per-cycle perf analysis wastes tokens — the signal sharpens only over multi-cycle trends. Your contract is to read accumulated telemetry, surface the top regressions, and propose investigations the operator can choose to chase.
26
+
27
+ ## When to Run
28
+
29
+ Spawn this agent from:
30
+
31
+ - `/gdd:reflect` — on-demand reflection (Phase 11)
32
+ - `/gdd:audit` — end-of-cycle audit roll-up
33
+ - `/gdd:perf` — direct invocation (if/when added; currently the two above suffice)
34
+
35
+ **Do NOT spawn from any per-cycle stage** (brief / explore / plan / design / verify). Per-cycle invocation violates D-04 and wastes tokens — the analysis needs `>= 3` cycles of accumulated data to be meaningful (D-01). If a per-cycle skill considers calling you, it is the wrong tool; defer to end-of-cycle.
36
+
37
+ ## Required Reading
38
+
39
+ The orchestrating skill supplies a `<required_reading>` block in the prompt. Read every listed file before acting.
40
+
41
+ Minimum expected inputs (skip gracefully if absent, note what's missing in the output):
42
+
43
+ - `.design/telemetry/costs.jsonl` — per-agent-spawn cost data (Phase 10.1)
44
+ - `.design/telemetry/trajectories/*.jsonl` — agent wall-time data (Phase 22)
45
+ - `.design/telemetry/events.jsonl` — full event stream (Phase 22)
46
+ - `reference/perf-budget.md` — per-agent budgets + baseline pointers (Phase 27.6-02, may not exist yet on first run; skip gracefully)
47
+ - `test-fixture/baselines/phase-27-6/perf-baseline.json` — synthetic baseline (Phase 27.6 D-03, exists after 27.6-06 closeout)
48
+
49
+ Helper library (use Bash to require):
50
+
51
+ - `scripts/lib/perf-analyzer/index.cjs` — `loadCosts({path, sinceCycle?})`, `loadTrajectories({dir})`
52
+ - `scripts/lib/perf-analyzer/cost-regression.cjs` — `detectCostRegressions({rows, baseline, thresholdPct, cyclesRequired})`, `computeCacheHitDelta(...)`, `computeP95Spikes(...)`
53
+
54
+ The helper library is a CommonJS module with no external deps — safe to require from Bash without dragging the gdd-state MCP graph.
55
+
56
+ ## Output
57
+
58
+ Write `.design/perf/<cycle-slug>.md`. If `--dry-run` is set in the spawning prompt, print proposals to stdout only — do not write the file.
59
+
60
+ Terminate with `## PERF ANALYSIS COMPLETE`.
61
+
62
+ ## 1. Top-3 Token-Cost Regressions
63
+
64
+ Use `scripts/lib/perf-analyzer/cost-regression.cjs::detectCostRegressions` over `loadCosts({})`. Threshold = 25% (Phase 27.6 D-01 default; read `.design/budget.json#perf_regression_threshold` if present for an override). Minimum 3 distinct cycles required (D-01). Top-3 cap is enforced by the library.
65
+
66
+ For each regression, render a `[REGRESSION]` proposal:
67
+
68
+ ```
69
+ [REGRESSION] perf-analyzer-{agent}-{slug}
70
+ - agent: <agent>
71
+ - baseline_p50_usd: <number>
72
+ - current_p50_usd: <number>
73
+ - delta_pct: <number>%
74
+ - cycles_observed: <count>
75
+ - hypothesis: <one-line plausible cause; e.g., "added reference reads per spawn", "tier upgrade from sonnet→opus">
76
+ - next_action: <one-line operator action; e.g., "/gdd:perf-investigate <agent>", "consider tier_override: sonnet">
77
+ ```
78
+
79
+ For each regression, emit a `perf.regression_detected` event via `appendEvent` from the Phase 22 event stream:
80
+
81
+ ```javascript
82
+ // Pseudo-instruction for the executor — the agent runs Bash with this shape
83
+ const { appendEvent } = require('./scripts/lib/event-stream');
84
+ appendEvent({
85
+ type: 'perf.regression_detected',
86
+ timestamp: new Date().toISOString(),
87
+ sessionId: process.env.GDD_SESSION_ID ?? 'perf-analyzer',
88
+ payload: { agent, baseline_p50_usd, current_p50_usd, delta_pct, cycles_observed },
89
+ });
90
+ ```
91
+
92
+ The `perf.regression_detected` event type is additive to the Phase 22 registry — the writer accepts unknown types (per `scripts/lib/event-stream/types.ts` envelope invariant: "unknown types are allowed; validation is structural, not a closed enum").
93
+
94
+ If `detectCostRegressions` returns `summary.regressions_count === 0`, write a single line: `No token-cost regressions detected (threshold 25%, >=3 cycles).` and skip event emission for this section.
95
+
96
+ ## 2. Cache-Hit-Rate Deltas
97
+
98
+ Use `computeCacheHitDelta` over the same row set. Report agents whose `delta_pct < -20` (hit rate dropped by 20% or more) as `[CACHE-MISS]` proposals:
99
+
100
+ ```
101
+ [CACHE-MISS] perf-analyzer-{agent}-cache-{slug}
102
+ - agent: <agent>
103
+ - baseline_hit_rate: <0..1>
104
+ - current_hit_rate: <0..1>
105
+ - delta_pct: <negative number>%
106
+ - cycles_observed: <count>
107
+ - hypothesis: <one-line cause; e.g., "preamble churn invalidated prefix cache", "new reference reads broke cache key">
108
+ - next_action: <one-line; e.g., "/gdd:cache-investigate <agent>", "audit shared-preamble.md drift">
109
+ ```
110
+
111
+ If no agent crosses the -20% threshold, write a single line acknowledging that the cache hit rates are within tolerance.
112
+
113
+ ## 3. p95 Latency Spikes
114
+
115
+ Use `computeP95Spikes` over `loadTrajectories({})`. Report any agent with `multiplier >= 1.5` as a `[LATENCY-SPIKE]` proposal:
116
+
117
+ ```
118
+ [LATENCY-SPIKE] perf-analyzer-{agent}-p95-{slug}
119
+ - agent: <agent>
120
+ - baseline_p95_ms: <number>
121
+ - current_p95_ms: <number>
122
+ - multiplier: <number>x
123
+ - cycles_observed: <count>
124
+ - hypothesis: <one-line; e.g., "model upgrade increased latency", "Bash tool blocked on lock">
125
+ - next_action: <one-line; e.g., "/gdd:trace-agent <agent>", "review recent tool-args distribution">
126
+ ```
127
+
128
+ If no agent crosses the 1.5x threshold, write a single line confirming p95 wall-time is stable.
129
+
130
+ ## 4. Roll-up Summary
131
+
132
+ At the bottom, print a single table for at-a-glance cycle review:
133
+
134
+ | Metric | Value |
135
+ | ----------------------------------- | ----- |
136
+ | regressions_count | N |
137
+ | cache_miss_count | N |
138
+ | latency_spike_count | N |
139
+ | agents_evaluated | N |
140
+ | agents_skipped_insufficient_data | N |
141
+ | threshold_pct | 25 |
142
+ | cycles_required | 3 |
143
+
144
+ The numbers come straight from `detectCostRegressions().summary` and the lengths of the cache-miss / latency-spike arrays. Do not synthesize counts — read them from the library output.
145
+
146
+ ## What This Agent Does NOT Do
147
+
148
+ - Does NOT auto-tune heuristics (out of scope per CONTEXT.md "auto-tuning of heuristic weights").
149
+ - Does NOT modify model selection (Phase 23.5 bandit territory; 27.5 wired the bandit, 27.6 only measures outcomes).
150
+ - Does NOT rewrite reference files (Phase 46 territory — canonical reference index).
151
+ - Does NOT analyze cross-runtime cost arbitrage (Phase 26 territory).
152
+ - Does NOT run on every cycle. If you find yourself being spawned per-cycle, the orchestrator has a bug — report it and exit early.
153
+
154
+ Stay within the cross-cycle measurement loop. Surface proposals; the operator reviews and applies.
155
+
156
+ ## Record
157
+
158
+ At run-end, append one JSONL line to `.design/intel/insights.jsonl`:
159
+
160
+ ```json
161
+ {"ts":"<ISO-8601>","agent":"perf-analyzer","cycle":"<cycle from STATE.md>","stage":"reflection","one_line_insight":"<top regression hypothesis or 'no regressions detected'>","artifacts_written":[".design/perf/<cycle-slug>.md"]}
162
+ ```
163
+
164
+ Schema: `reference/schemas/insight-line.schema.json`. The `artifacts_written` array MUST list the per-cycle perf proposal file. If no proposals were generated (cold-start tolerance), still write the `.md` (with a "no regressions detected" body) and emit the line with the artifact path.
165
+
166
+ ## PERF ANALYSIS COMPLETE
@@ -0,0 +1,334 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * hooks/gdd-precompact-snapshot.js — Plan 27.6-05
4
+ *
5
+ * Claude Code PreCompact hook. Immediately before context compaction,
6
+ * writes an atomic snapshot of STATE.md sections + last-N event-chain
7
+ * entries + last-N decisions to `.design/snapshots/<ts>.json`.
8
+ *
9
+ * Phase 27.6 D-08: atomic .tmp + rename via scripts/lib/lockfile.cjs.
10
+ * - Lockfile serializes concurrent PreCompact writers.
11
+ * - .tmp + rename guarantees no partial file ever appears at target path
12
+ * (a SIGKILL between writeFileSync and renameSync leaves an orphan
13
+ * .tmp file, never a corrupted snapshot).
14
+ *
15
+ * Phase 27.6 D-10: harness-aware — Codex has no PreCompact, so on
16
+ * harness=codex this is a one-line stderr no-op (Phase 45 dep for
17
+ * full pre-large-context-action interception).
18
+ *
19
+ * Silent-on-failure: tolerable errors exit 0 with stderr breadcrumb.
20
+ * Emits `snapshot.written` event via lazy appendEvent (best-effort).
21
+ */
22
+
23
+ 'use strict';
24
+
25
+ const fs = require('node:fs');
26
+ const path = require('node:path');
27
+
28
+ const SNAPSHOT_DIR = path.resolve(process.cwd(), '.design', 'snapshots');
29
+ const STATE_MD_PATH = path.resolve(process.cwd(), '.design', 'STATE.md');
30
+ const EVENTS_PATH = path.resolve(process.cwd(), '.design', 'telemetry', 'events.jsonl');
31
+ const RETENTION_COUNT = 10;
32
+ const EVENTS_TAIL_COUNT = 50;
33
+ const DECISIONS_TAIL_COUNT = 10;
34
+ const SCHEMA_VERSION = '1.0.0';
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Harness detection (D-10)
38
+ // ---------------------------------------------------------------------------
39
+
40
+ function detectHarness() {
41
+ const explicit = (process.env.CLAUDE_HARNESS || process.env.GDD_HARNESS || '')
42
+ .toLowerCase()
43
+ .trim();
44
+ if (explicit === 'codex' || explicit === 'codex-cli') return 'codex';
45
+ // Default — Claude Code (only harness that emits PreCompact today).
46
+ return 'claude-code';
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Lazy event-stream emit (best-effort — never blocks the hook)
51
+ // ---------------------------------------------------------------------------
52
+
53
+ function getAppendEvent() {
54
+ try {
55
+ const m = require('../scripts/lib/event-stream');
56
+ if (m && typeof m.appendEvent === 'function') return m.appendEvent;
57
+ } catch {
58
+ /* swallow — event-stream is optional infrastructure */
59
+ }
60
+ return function noopAppend(_ev) {
61
+ /* no-op */
62
+ };
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // STATE.md tolerant parser — extracts frontmatter + decisions + blockers
67
+ // ---------------------------------------------------------------------------
68
+
69
+ function readStateSections() {
70
+ if (!fs.existsSync(STATE_MD_PATH)) {
71
+ return { frontmatter: {}, decisions: [], blockers: [], session: '' };
72
+ }
73
+ let body;
74
+ try {
75
+ body = fs.readFileSync(STATE_MD_PATH, 'utf8');
76
+ } catch {
77
+ return { frontmatter: {}, decisions: [], blockers: [], session: '' };
78
+ }
79
+
80
+ // Extract YAML frontmatter (between leading '---' delimiters)
81
+ const frontmatter = {};
82
+ const fmMatch = body.match(/^---\n([\s\S]*?)\n---\n/);
83
+ if (fmMatch) {
84
+ for (const line of fmMatch[1].split('\n')) {
85
+ const m = line.match(/^(\w+):\s*(.+)$/);
86
+ if (m) frontmatter[m[1]] = m[2].trim();
87
+ }
88
+ }
89
+
90
+ // Decisions: extract D-XX entries from a '<decisions>' or '## Decisions' section
91
+ const decisions = [];
92
+ const decisionsMatch = body.match(
93
+ /(?:<decisions>|## Decisions)([\s\S]*?)(?:<\/decisions>|^##\s|\Z)/m,
94
+ );
95
+ if (decisionsMatch) {
96
+ const dRe = /D-\d+:[^\n]+/g;
97
+ let m2;
98
+ while ((m2 = dRe.exec(decisionsMatch[1])) !== null) {
99
+ decisions.push(m2[0].trim());
100
+ }
101
+ }
102
+
103
+ // Blockers: similar to decisions
104
+ const blockers = [];
105
+ const blockersMatch = body.match(
106
+ /(?:<blockers>|## Blockers)([\s\S]*?)(?:<\/blockers>|^##\s|\Z)/m,
107
+ );
108
+ if (blockersMatch) {
109
+ const bRe = /B-\d+:[^\n]+/g;
110
+ let m3;
111
+ while ((m3 = bRe.exec(blockersMatch[1])) !== null) {
112
+ blockers.push(m3[0].trim());
113
+ }
114
+ }
115
+
116
+ // Session prefix (first ~500 chars after '## Session' or '<session>')
117
+ const sessionMatch = body.match(/(?:## Session|<session>)([\s\S]{0,500})/);
118
+ const session = sessionMatch ? sessionMatch[1].trim().slice(0, 500) : '';
119
+
120
+ return { frontmatter, decisions, blockers, session };
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Events tail reader — JSONL-tolerant (malformed lines are skipped)
125
+ // ---------------------------------------------------------------------------
126
+
127
+ function readEventsTail(count) {
128
+ if (!fs.existsSync(EVENTS_PATH)) return [];
129
+ let body;
130
+ try {
131
+ body = fs.readFileSync(EVENTS_PATH, 'utf8');
132
+ } catch {
133
+ return [];
134
+ }
135
+ const events = [];
136
+ for (const line of body.split(/\r?\n/)) {
137
+ const trimmed = line.trim();
138
+ if (trimmed.length === 0) continue;
139
+ try {
140
+ events.push(JSON.parse(trimmed));
141
+ } catch {
142
+ /* tolerate malformed line — T-27.6.05-05 mitigation */
143
+ }
144
+ }
145
+ return events.slice(-count);
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Retention prune — LRU by mtime, keep last RETENTION_COUNT (D-08)
150
+ // ---------------------------------------------------------------------------
151
+
152
+ function pruneSnapshots() {
153
+ let files;
154
+ try {
155
+ files = fs.readdirSync(SNAPSHOT_DIR);
156
+ } catch {
157
+ return;
158
+ }
159
+ const jsonFiles = files
160
+ .filter((f) => f.endsWith('.json') && f !== 'last-recap.json')
161
+ .map((f) => ({ name: f, full: path.join(SNAPSHOT_DIR, f), mtime: 0 }));
162
+
163
+ for (const entry of jsonFiles) {
164
+ try {
165
+ entry.mtime = fs.statSync(entry.full).mtimeMs;
166
+ } catch {
167
+ /* swallow */
168
+ }
169
+ }
170
+
171
+ jsonFiles.sort((a, b) => a.mtime - b.mtime);
172
+ while (jsonFiles.length > RETENTION_COUNT) {
173
+ const oldest = jsonFiles.shift();
174
+ try {
175
+ fs.unlinkSync(oldest.full);
176
+ } catch {
177
+ /* swallow — race with another writer; LRU eventually wins */
178
+ }
179
+ }
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Main — atomic write with lockfile serialization
184
+ // ---------------------------------------------------------------------------
185
+
186
+ async function main() {
187
+ const harness = detectHarness();
188
+ if (harness === 'codex') {
189
+ // D-10: Codex has no PreCompact event; emit notice + exit. Phase 45 dep
190
+ // for full `pre-large-context-action` interception.
191
+ process.stderr.write(
192
+ '[gdd-precompact-snapshot] this harness does not emit PreCompact; snapshots disabled\n',
193
+ );
194
+ process.exit(0);
195
+ }
196
+
197
+ // Drain stdin (Claude Code may pipe a hook event JSON; we don't need it
198
+ // but draining avoids EPIPE on the parent's writer side).
199
+ try {
200
+ if (!process.stdin.isTTY) {
201
+ // Best-effort, non-blocking — we have nothing time-sensitive in stdin.
202
+ process.stdin.on('error', () => {
203
+ /* swallow */
204
+ });
205
+ process.stdin.resume();
206
+ }
207
+ } catch {
208
+ /* swallow */
209
+ }
210
+
211
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
212
+ const snapshotPath = path.join(SNAPSHOT_DIR, ts + '.json');
213
+ const tmpPath = snapshotPath + '.tmp';
214
+
215
+ // Ensure snapshot dir exists (mkdir -p semantics).
216
+ try {
217
+ fs.mkdirSync(SNAPSHOT_DIR, { recursive: true });
218
+ } catch {
219
+ /* swallow — write will fail loudly below if truly missing */
220
+ }
221
+
222
+ // Acquire lockfile on the target path (T-27.6.05-02 mitigation).
223
+ // The lock file lives at <snapshotPath>.lock and serializes concurrent
224
+ // PreCompact writers; the second writer either waits or fails-silent.
225
+ let release = null;
226
+ try {
227
+ const lockfile = require('../scripts/lib/lockfile.cjs');
228
+ release = await lockfile.acquire(snapshotPath, {
229
+ staleMs: 60_000,
230
+ maxWaitMs: 10_000,
231
+ pollMs: 50,
232
+ });
233
+ } catch (err) {
234
+ process.stderr.write(
235
+ '[gdd-precompact-snapshot] lock acquire failed: ' +
236
+ (err && err.message ? err.message : String(err)) +
237
+ '\n',
238
+ );
239
+ process.exit(0);
240
+ }
241
+
242
+ try {
243
+ const sections = readStateSections();
244
+ const events = readEventsTail(EVENTS_TAIL_COUNT);
245
+ const decisions = sections.decisions.slice(-DECISIONS_TAIL_COUNT);
246
+ const cycleId =
247
+ sections.frontmatter && sections.frontmatter.milestone
248
+ ? sections.frontmatter.milestone
249
+ : 'unknown';
250
+
251
+ const snapshot = {
252
+ schema_version: SCHEMA_VERSION,
253
+ timestamp: new Date().toISOString(),
254
+ cycle_id: cycleId,
255
+ state_md_sections: sections,
256
+ last_n_events: events,
257
+ last_n_decisions: decisions,
258
+ };
259
+
260
+ const body = JSON.stringify(snapshot, null, 2);
261
+
262
+ // Atomic write: .tmp + rename (T-27.6.05-01 mitigation).
263
+ // A SIGKILL between writeFileSync and renameSync leaves <snapshotPath>.tmp
264
+ // orphaned but NEVER a partial file at <snapshotPath> itself.
265
+ try {
266
+ fs.writeFileSync(tmpPath, body, 'utf8');
267
+ fs.renameSync(tmpPath, snapshotPath);
268
+ } catch (err) {
269
+ process.stderr.write(
270
+ '[gdd-precompact-snapshot] atomic write failed: ' +
271
+ (err && err.message ? err.message : String(err)) +
272
+ '\n',
273
+ );
274
+ try {
275
+ fs.unlinkSync(tmpPath);
276
+ } catch {
277
+ /* swallow orphan cleanup */
278
+ }
279
+ process.exit(0);
280
+ }
281
+
282
+ // Retention prune (T-27.6.05-04 DoS mitigation).
283
+ pruneSnapshots();
284
+
285
+ // Best-effort event emit.
286
+ const appendEvent = getAppendEvent();
287
+ try {
288
+ appendEvent({
289
+ type: 'snapshot.written',
290
+ timestamp: new Date().toISOString(),
291
+ sessionId: process.env.GDD_SESSION_ID || 'precompact-hook',
292
+ payload: {
293
+ path: snapshotPath,
294
+ size_bytes: Buffer.byteLength(body, 'utf8'),
295
+ events_count: events.length,
296
+ decisions_count: decisions.length,
297
+ harness,
298
+ },
299
+ });
300
+ } catch {
301
+ /* swallow — telemetry never blocks */
302
+ }
303
+
304
+ // Emit non-blocking continue verdict on stdout (matches other hooks).
305
+ try {
306
+ process.stdout.write(JSON.stringify({ continue: true, suppressOutput: true }));
307
+ } catch {
308
+ /* swallow */
309
+ }
310
+
311
+ process.exit(0);
312
+ } finally {
313
+ if (release) {
314
+ try {
315
+ await release();
316
+ } catch {
317
+ /* swallow — stale-detection reclaims */
318
+ }
319
+ }
320
+ }
321
+ }
322
+
323
+ main().catch((err) => {
324
+ try {
325
+ process.stderr.write(
326
+ '[gdd-precompact-snapshot] uncaught: ' +
327
+ (err && err.message ? err.message : String(err)) +
328
+ '\n',
329
+ );
330
+ } catch {
331
+ /* swallow */
332
+ }
333
+ process.exit(0);
334
+ });