@hegemonart/get-design-done 1.27.5 → 1.27.6

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.
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * hooks/gdd-sessionstart-recap.js — Plan 27.6-05
4
+ *
5
+ * Claude Code SessionStart hook. Emits a "what changed while you were
6
+ * away" diff between the most-recent PreCompact snapshot and the
7
+ * current STATE.md.
8
+ *
9
+ * Phase 27.6 D-09: markdown summary to stderr + structured JSON to
10
+ * `.design/snapshots/last-recap.json` (the JSON is a sidecar for
11
+ * downstream tools: progress dashboard, resume skill).
12
+ * Phase 27.6 D-10: harness-aware Codex no-op (Phase 45 dep for full
13
+ * pre-large-context recap integration).
14
+ *
15
+ * Silent-on-failure: tolerable errors exit 0 with breadcrumb.
16
+ * Emits `recap.emitted` event via lazy appendEvent (best-effort).
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const fs = require('node:fs');
22
+ const path = require('node:path');
23
+
24
+ const SNAPSHOT_DIR = path.resolve(process.cwd(), '.design', 'snapshots');
25
+ const STATE_MD_PATH = path.resolve(process.cwd(), '.design', 'STATE.md');
26
+ const EVENTS_PATH = path.resolve(process.cwd(), '.design', 'telemetry', 'events.jsonl');
27
+ const RECAP_JSON_PATH = path.join(SNAPSHOT_DIR, 'last-recap.json');
28
+ const SCHEMA_VERSION = '1.0.0';
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Harness detection (D-10) — mirrors gdd-precompact-snapshot.js
32
+ // ---------------------------------------------------------------------------
33
+
34
+ function detectHarness() {
35
+ const explicit = (process.env.CLAUDE_HARNESS || process.env.GDD_HARNESS || '')
36
+ .toLowerCase()
37
+ .trim();
38
+ if (explicit === 'codex' || explicit === 'codex-cli') return 'codex';
39
+ return 'claude-code';
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Lazy event-stream emit (best-effort)
44
+ // ---------------------------------------------------------------------------
45
+
46
+ function getAppendEvent() {
47
+ try {
48
+ const m = require('../scripts/lib/event-stream');
49
+ if (m && typeof m.appendEvent === 'function') return m.appendEvent;
50
+ } catch {
51
+ /* swallow — event-stream is optional infrastructure */
52
+ }
53
+ return function noopAppend(_ev) {
54
+ /* no-op */
55
+ };
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // STATE.md tolerant parser (lighter than the PreCompact version — only
60
+ // needs frontmatter + a flat decisions list for the diff)
61
+ // ---------------------------------------------------------------------------
62
+
63
+ function readStateMd() {
64
+ if (!fs.existsSync(STATE_MD_PATH)) return { frontmatter: {}, decisions: [] };
65
+ let body;
66
+ try {
67
+ body = fs.readFileSync(STATE_MD_PATH, 'utf8');
68
+ } catch {
69
+ return { frontmatter: {}, decisions: [] };
70
+ }
71
+
72
+ const frontmatter = {};
73
+ const fmMatch = body.match(/^---\n([\s\S]*?)\n---\n/);
74
+ if (fmMatch) {
75
+ for (const line of fmMatch[1].split('\n')) {
76
+ const m = line.match(/^(\w+):\s*(.+)$/);
77
+ if (m) frontmatter[m[1]] = m[2].trim();
78
+ }
79
+ }
80
+
81
+ // All D-XX entries anywhere in the body — broad sweep is fine for diff.
82
+ const decisions = [];
83
+ const dRe = /D-\d+:[^\n]+/g;
84
+ let m2;
85
+ while ((m2 = dRe.exec(body)) !== null) {
86
+ decisions.push(m2[0].trim());
87
+ }
88
+ return { frontmatter, decisions };
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Snapshot discovery — highest-mtime *.json (excluding last-recap.json)
93
+ // ---------------------------------------------------------------------------
94
+
95
+ function findLatestSnapshot() {
96
+ if (!fs.existsSync(SNAPSHOT_DIR)) return null;
97
+ let files;
98
+ try {
99
+ files = fs.readdirSync(SNAPSHOT_DIR);
100
+ } catch {
101
+ return null;
102
+ }
103
+ const candidates = files.filter(
104
+ (f) => f.endsWith('.json') && f !== 'last-recap.json' && !f.endsWith('.tmp'),
105
+ );
106
+ if (candidates.length === 0) return null;
107
+
108
+ let best = null;
109
+ let bestMtime = -1;
110
+ for (const name of candidates) {
111
+ const full = path.join(SNAPSHOT_DIR, name);
112
+ try {
113
+ const mtime = fs.statSync(full).mtimeMs;
114
+ if (mtime > bestMtime) {
115
+ best = full;
116
+ bestMtime = mtime;
117
+ }
118
+ } catch {
119
+ /* swallow */
120
+ }
121
+ }
122
+ return best;
123
+ }
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Event count since snapshot timestamp (JSONL-tolerant)
127
+ // ---------------------------------------------------------------------------
128
+
129
+ function countEventsSince(isoTimestamp) {
130
+ if (!fs.existsSync(EVENTS_PATH)) return 0;
131
+ let body;
132
+ try {
133
+ body = fs.readFileSync(EVENTS_PATH, 'utf8');
134
+ } catch {
135
+ return 0;
136
+ }
137
+ let count = 0;
138
+ for (const line of body.split(/\r?\n/)) {
139
+ const t = line.trim();
140
+ if (t.length === 0) continue;
141
+ try {
142
+ const ev = JSON.parse(t);
143
+ if (typeof ev.timestamp === 'string' && ev.timestamp > isoTimestamp) {
144
+ count++;
145
+ }
146
+ } catch {
147
+ /* tolerate malformed */
148
+ }
149
+ }
150
+ return count;
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Main
155
+ // ---------------------------------------------------------------------------
156
+
157
+ function main() {
158
+ const harness = detectHarness();
159
+ if (harness === 'codex') {
160
+ // D-10: SessionStart on Codex skips recap; Phase 45 dep for full
161
+ // pre-large-context-action integration.
162
+ process.stderr.write('[gdd-sessionstart-recap] codex harness no-op (Phase 45 dep)\n');
163
+ process.exit(0);
164
+ }
165
+
166
+ const snapshotPath = findLatestSnapshot();
167
+ if (!snapshotPath) {
168
+ process.stderr.write('[gdd-sessionstart-recap] no prior snapshot\n');
169
+ process.exit(0);
170
+ }
171
+
172
+ let snapshot;
173
+ try {
174
+ snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf8'));
175
+ } catch (err) {
176
+ process.stderr.write(
177
+ '[gdd-sessionstart-recap] snapshot unreadable: ' +
178
+ (err && err.message ? err.message : String(err)) +
179
+ '\n',
180
+ );
181
+ process.exit(0);
182
+ }
183
+
184
+ const current = readStateMd();
185
+ const priorDecisions = Array.isArray(snapshot.last_n_decisions)
186
+ ? snapshot.last_n_decisions
187
+ : [];
188
+ const priorSet = new Set(priorDecisions);
189
+ const newDecisions = current.decisions.filter((d) => !priorSet.has(d));
190
+ const newEventCount = countEventsSince(snapshot.timestamp || '1970-01-01T00:00:00.000Z');
191
+
192
+ const priorCycle = snapshot.cycle_id || 'unknown';
193
+ const currentCycle = current.frontmatter.milestone || 'unknown';
194
+ const cycleChanged = priorCycle !== currentCycle ? `${priorCycle} → ${currentCycle}` : null;
195
+
196
+ const snapshotTime = snapshot.timestamp ? new Date(snapshot.timestamp).getTime() : 0;
197
+ const timeElapsedMs =
198
+ snapshotTime > 0 && Number.isFinite(snapshotTime) ? Date.now() - snapshotTime : 0;
199
+
200
+ // Markdown summary to stderr (D-09).
201
+ const md = [
202
+ '## Session Recap',
203
+ `Snapshot taken: ${snapshot.timestamp || 'unknown'}`,
204
+ `Time elapsed: ${(timeElapsedMs / 60000).toFixed(1)} min`,
205
+ cycleChanged ? `Cycle: ${cycleChanged}` : `Cycle: ${currentCycle} (unchanged)`,
206
+ `New decisions: ${newDecisions.length}`,
207
+ ...newDecisions.slice(0, 5).map((d) => ` - ${d}`),
208
+ `New events since snapshot: ${newEventCount}`,
209
+ '',
210
+ ].join('\n');
211
+ process.stderr.write(md + '\n');
212
+
213
+ // JSON sidecar (D-09) — atomic .tmp + rename for consistency.
214
+ const recap = {
215
+ schema_version: SCHEMA_VERSION,
216
+ previous_snapshot: snapshotPath,
217
+ current_timestamp: new Date().toISOString(),
218
+ diff: {
219
+ new_decisions: newDecisions,
220
+ new_events_since_snapshot: newEventCount,
221
+ cycle_changed: cycleChanged,
222
+ time_elapsed_ms: timeElapsedMs,
223
+ },
224
+ };
225
+
226
+ try {
227
+ // mkdir -p for safety — directory should exist if snapshotPath was found,
228
+ // but defensive ensure for race conditions.
229
+ fs.mkdirSync(SNAPSHOT_DIR, { recursive: true });
230
+ fs.writeFileSync(RECAP_JSON_PATH + '.tmp', JSON.stringify(recap, null, 2), 'utf8');
231
+ fs.renameSync(RECAP_JSON_PATH + '.tmp', RECAP_JSON_PATH);
232
+ } catch (err) {
233
+ process.stderr.write(
234
+ '[gdd-sessionstart-recap] sidecar write failed: ' +
235
+ (err && err.message ? err.message : String(err)) +
236
+ '\n',
237
+ );
238
+ }
239
+
240
+ // Best-effort event emit.
241
+ const appendEvent = getAppendEvent();
242
+ try {
243
+ appendEvent({
244
+ type: 'recap.emitted',
245
+ timestamp: new Date().toISOString(),
246
+ sessionId: process.env.GDD_SESSION_ID || 'sessionstart-hook',
247
+ payload: {
248
+ new_decisions: newDecisions.length,
249
+ new_events: newEventCount,
250
+ time_elapsed_ms: timeElapsedMs,
251
+ harness,
252
+ },
253
+ });
254
+ } catch {
255
+ /* swallow */
256
+ }
257
+
258
+ // Emit non-blocking continue verdict on stdout.
259
+ try {
260
+ process.stdout.write(JSON.stringify({ continue: true, suppressOutput: true }));
261
+ } catch {
262
+ /* swallow */
263
+ }
264
+
265
+ process.exit(0);
266
+ }
267
+
268
+ try {
269
+ main();
270
+ } catch (err) {
271
+ try {
272
+ process.stderr.write(
273
+ '[gdd-sessionstart-recap] uncaught: ' +
274
+ (err && err.message ? err.message : String(err)) +
275
+ '\n',
276
+ );
277
+ } catch {
278
+ /* swallow */
279
+ }
280
+ process.exit(0);
281
+ }
package/hooks/hooks.json CHANGED
@@ -24,6 +24,14 @@
24
24
  "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/first-run-nudge.sh\""
25
25
  }
26
26
  ]
27
+ },
28
+ {
29
+ "hooks": [
30
+ {
31
+ "type": "command",
32
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gdd-sessionstart-recap.js\""
33
+ }
34
+ ]
27
35
  }
28
36
  ],
29
37
  "PreToolUse": [
@@ -110,6 +118,16 @@
110
118
  }
111
119
  ]
112
120
  }
121
+ ],
122
+ "PreCompact": [
123
+ {
124
+ "hooks": [
125
+ {
126
+ "type": "command",
127
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gdd-precompact-snapshot.js\""
128
+ }
129
+ ]
130
+ }
113
131
  ]
114
132
  }
115
133
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hegemonart/get-design-done",
3
- "version": "1.27.5",
3
+ "version": "1.27.6",
4
4
  "description": "A design-quality pipeline for AI coding agents: brief, plan, implement, and verify UI work against your design system.",
5
5
  "author": "Hegemon",
6
6
  "homepage": "https://github.com/hegemonart/get-design-done",
@@ -83,7 +83,7 @@
83
83
  ],
84
84
  "hooks": "hooks/hooks.json",
85
85
  "dependencies": {
86
- "@anthropic-ai/claude-agent-sdk": "^0.2.119",
86
+ "@anthropic-ai/claude-agent-sdk": "^0.3.143",
87
87
  "@clack/prompts": "^1.2.0",
88
88
  "@modelcontextprotocol/sdk": "^1.0.0"
89
89
  },
@@ -0,0 +1,142 @@
1
+ ---
2
+ name: perf-budget
3
+ phase: 27.6
4
+ version: 1.0.0
5
+ type: meta-rules
6
+ description: Per-agent token-cost budget reference and CI regression-gate documentation. Budgets sourced from current p50 + 25% buffer (Phase 27.6 D-05); CI gate fails on >25% regression vs baseline across 3 cycles (D-01); thresholds configurable via .design/budget.json.
7
+ ---
8
+
9
+ # Per-Agent Performance Budgets — Phase 27.6
10
+
11
+ This reference documents the token-cost budgets that the pipeline measures itself against. Two surfaces consume this document:
12
+
13
+ 1. `tests/perf-budget.test.cjs` — CI regression gate. Fails the build when any agent's p50 USD-cost has regressed > 25% vs baseline across the last 3 cycles.
14
+ 2. `agents/perf-analyzer.md` — cross-cycle reflector. Reads the same budget + telemetry; surfaces top-3 regressions as `[REGRESSION]` proposals.
15
+
16
+ Phase 27.5 (v1.27.5, shipped 2026-05-17) made production telemetry real by wiring the bandit into routing. Phase 27.6 reads what 27.5 writes.
17
+
18
+ ---
19
+
20
+ ## How budgets are derived
21
+
22
+ Per **D-05**, each per-agent budget is the agent's current p50 USD-cost plus a 25% buffer (`p50 × 1.25`). The buffer absorbs natural cycle-to-cycle variance without firing the gate, while still flagging genuine cost growth.
23
+
24
+ Per **D-03**, the v1.27.6 baseline data lives at `test-fixture/baselines/phase-27-6/perf-baseline.json` and is built from **synthetic cycle replay**. Real-cycle calibration ships as a follow-up after 1-2 production cycles accumulate, via the commit:
25
+
26
+ ```
27
+ chore(27.6): recalibrate perf-budget against measured cycles
28
+ ```
29
+
30
+ Per **D-01**, the regression-gate threshold is **25%**, configurable via `.design/budget.json#perf_regression_threshold`. A minimum of **3 distinct cycles** must be observed per agent before that agent is evaluated for regression. Agents with fewer than 3 cycles are silently skipped (cold-start tolerance).
31
+
32
+ This conservative-then-tighten discipline matches Phase 23.5 `PRIOR_STRENGTH` calibration — start wide to avoid noise, tighten once enough samples accumulate to compute realistic p95 bounds.
33
+
34
+ ---
35
+
36
+ ## Per-agent budget table
37
+
38
+ | Agent | p50 budget (USD) | Buffer | Hit-rate baseline | p95 wall (ms) | Notes |
39
+ |---|---|---|---|---|---|
40
+ | design-verifier | 0.04 | 0.05 (+25%) | 0.55 | 12000 | Stage 5; reads DESIGN-VERIFICATION.md scoring rubric |
41
+ | design-planner | 0.08 | 0.10 (+25%) | 0.40 | 18000 | Stage 3; opus default |
42
+ | design-executor | 0.06 | 0.075 (+25%) | 0.50 | 15000 | Stage 4 |
43
+ | design-context-checker | 0.02 | 0.025 (+25%) | 0.65 | 6000 | Gate; pre-stage validator |
44
+ | design-reflector | 0.10 | 0.125 (+25%) | 0.35 | 22000 | XL reflector tier |
45
+ | design-discussant | 0.05 | 0.0625 (+25%) | 0.45 | 11000 | Spawned by `/gdd:discuss` |
46
+ | perf-analyzer | 0.10 | 0.125 (+25%) | 0.30 | 22000 | XL reflector tier (this phase) |
47
+
48
+ These values are **seed numbers**, re-calibrated after 1-2 real production cycles. The authoritative numbers live in `test-fixture/baselines/phase-27-6/perf-baseline.json` (created at Phase 27.6 closeout in Plan 27.6-06). The CI gate reads that file at runtime, not this table.
49
+
50
+ When the baseline JSON is **absent** (first run after this plan lands but before 27.6-06), the gate passes silently with a stderr notice — it does NOT block Wave A from shipping.
51
+
52
+ ---
53
+
54
+ ## CI Regression Gate
55
+
56
+ File: `tests/perf-budget.test.cjs`
57
+
58
+ Algorithm (single source of truth — re-uses `detectCostRegressions` from `scripts/lib/perf-analyzer/cost-regression.cjs`):
59
+
60
+ 1. Load `test-fixture/baselines/phase-27-6/perf-baseline.json`. If absent, exit early — gate passes with stderr notice. (Phase 27.6-06 creates this file at closeout.)
61
+ 2. Load `.design/telemetry/costs.jsonl` via `loadCosts`. If absent or empty, exit early — no data to regress against.
62
+ 3. Read `perf_regression_threshold` from `.design/budget.json` (default 25 per D-01).
63
+ 4. Call `detectCostRegressions({rows, baseline: parsedBaseline.agents, thresholdPct, cyclesRequired: 3})`.
64
+ 5. If `result.regressions.length === 0`, gate passes.
65
+ 6. Otherwise, fail the test with the regression details (agent, baseline_p50_usd, current_p50_usd, delta_pct, cycles_observed).
66
+
67
+ The gate is intentionally **low-noise**:
68
+
69
+ - Skips agents with fewer than 3 distinct cycles of data (avoids false positives during cold-start).
70
+ - Only fires on the **regression rule** — NOT on cache-hit-rate drops or p95 latency spikes; those surface as `agents/perf-analyzer.md` proposals only.
71
+ - Top-3 cap on the regressions list — a "noisy day" can flag at most three agents, never the entire roster.
72
+
73
+ The gate runs as a regular `node --test` entry under the `tests/**/*.test.cjs` glob — no special CI wiring required. If you can run `npm test`, you run the gate.
74
+
75
+ ---
76
+
77
+ ## Tuning the Gate
78
+
79
+ Override the regression threshold by adding to `.design/budget.json`:
80
+
81
+ ```json
82
+ {
83
+ "perf_regression_threshold": 30
84
+ }
85
+ ```
86
+
87
+ Override the cache-warming false-positive tolerance (used by Phase 27.6-03):
88
+
89
+ ```json
90
+ {
91
+ "cache_warming_falsepositive_threshold": 25
92
+ }
93
+ ```
94
+
95
+ **Defaults** (per Phase 27.6 D-01 + D-02):
96
+
97
+ - `perf_regression_threshold: 25`
98
+ - `cache_warming_falsepositive_threshold: 20`
99
+
100
+ After 5 measured cycles accumulate, re-tune based on observed natural variance. The 25%-default is conservative — likely too loose once real telemetry stabilizes. The first tightening pass belongs to a measurement-gated follow-up, not v1.27.6 itself.
101
+
102
+ ---
103
+
104
+ ## Recalibration (Phase 27.6 D-03 follow-up)
105
+
106
+ v1.27.6 ships with synthetic-cycle-replay baselines. After 1-2 real production cycles accumulate, re-lock the baseline:
107
+
108
+ ```
109
+ chore(27.6): recalibrate perf-budget against measured cycles
110
+ ```
111
+
112
+ That commit:
113
+
114
+ 1. Re-runs the baseline-fixture build against real telemetry.
115
+ 2. Updates `test-fixture/baselines/phase-27-6/perf-baseline.json` with the measured p50, hit_rate, and p95_ms per agent.
116
+ 3. Bumps the budget numbers in this document to match.
117
+ 4. Optionally tightens `perf_regression_threshold` from 25 toward 15-20 if measured variance permits.
118
+
119
+ The synthetic baseline is **not a hack** — it's the documented v1 path per spec Success Criterion #7. Real-cycle data simply doesn't exist yet at v1.27.6 cut, because Phase 27.5 only shipped 2026-05-17.
120
+
121
+ ---
122
+
123
+ ## Cross-references
124
+
125
+ - `agents/perf-analyzer.md` — cross-cycle reflector that reads the same baseline. Surfaces top-3 cost regressions, hit-rate deltas, and p95 spikes as `[REGRESSION]` proposals per `/gdd:reflect`.
126
+ - `scripts/lib/perf-analyzer/cost-regression.cjs` — **single source of truth** for the regression rule. The CI gate re-uses `detectCostRegressions` from this module; it does NOT re-implement the rule.
127
+ - `scripts/lib/perf-analyzer/index.cjs` — telemetry loader (`loadCosts`, `loadTrajectories`). JSONL-tolerant; blank lines silently ignored, malformed lines counted in `skipped_count`.
128
+ - `tests/perf-budget.test.cjs` — the CI gate itself. Always-green when no baseline + no data; fails on >25% regression vs baseline once both exist.
129
+ - `reference/bandit-integration.md` — Phase 27.5 routing reference (precursor; the bandit picks tier **within** the budget — the gate evaluates whether the picked tier behaved within budget).
130
+ - `.design/budget.json` — operator-tunable thresholds. Optional file; absent file means defaults (`perf_regression_threshold: 25`, `cache_warming_falsepositive_threshold: 20`).
131
+ - `test-fixture/baselines/phase-27-6/perf-baseline.json` — authoritative per-agent p50 / hit_rate / p95_ms values. Created in Plan 27.6-06 closeout.
132
+
133
+ ---
134
+
135
+ ## Boundary semantics (matching detectCostRegressions)
136
+
137
+ - **>= threshold** is a regression. A current p50 exactly 25% above baseline (e.g., baseline 0.05, current 0.0625) fires the gate. This matches the Phase 27.6-01 test contract.
138
+ - **base = 0 + current > 0** → flagged as `delta_pct: Infinity`. A previously-zero-cost agent becoming non-zero is always a regression.
139
+ - **base = 0 + current = 0** → NOT a regression (both `delta_pct = 0`).
140
+ - **Missing baseline entry** → agent silently skipped (no false positive on new agents that haven't been calibrated yet).
141
+
142
+ The gate's "fail loud, false-positive rare" character comes from these boundary choices plus the 3-cycle minimum — together they make the gate safe to wire into CI without flaking on first-run noise.
@@ -659,6 +659,13 @@
659
659
  "phase": 27,
660
660
  "description": "Phase 27 peer-CLI delegation capability matrix — which peer (codex/copilot/cursor/gemini/qwen) claims which agent role, protocol (ACP/ASP), tie-break order, and opt-in gating semantics"
661
661
  },
662
+ {
663
+ "name": "perf-budget",
664
+ "path": "reference/perf-budget.md",
665
+ "type": "meta-rules",
666
+ "phase": 27.6,
667
+ "description": "Phase 27.6 per-agent token-cost budget reference and CI regression-gate documentation; budgets sourced from current p50 + 25% buffer (D-05), CI gate fails on >25% regression vs baseline across 3 cycles (D-01); thresholds configurable via .design/budget.json"
668
+ },
662
669
  {
663
670
  "name": "performance",
664
671
  "path": "reference/performance.md",
@@ -25,6 +25,22 @@ A `/gdd:recall "term"` query that returns 5 Layer-1 hits ≈ 400 tokens. Opening
25
25
 
26
26
  Layer 1 becomes `scripts/lib/design-search.cjs` — same protocol, same output shape, but backed by `.design/search.db` instead of grep. Agents do not need to change anything; the backend swap is transparent.
27
27
 
28
+ ## Phase 27.6 — Shared-Context Dedup (D-11)
29
+
30
+ When >= 3 distinct agents in the same cycle read the same `reference/*.md` file, the Phase 14.5 retrieval-contract preamble is extended with a "shared context loaded once" marker — subsequent agents see a content-hash reference instead of the full file body. This reduces redundant token consumption per cycle.
31
+
32
+ The detection lives in `scripts/lib/prompt-dedup/index.cjs::detectDuplicateReferenceReads` and runs at retrieval-contract injection time. The threshold (3 agents) matches Phase 27.6 D-11 and is tunable via the `threshold` argument to `detectDuplicateReferenceReads`.
33
+
34
+ Operator opt-out: set `GDD_DEDUP_OPT_OUT=1` in the spawning agent's environment to bypass dedup for that read.
35
+
36
+ Event emission: each dedup decision emits a `dedup.injection` event via `appendEvent` so the Phase 27.6-01 perf-analyzer can surface "the same file is read N agents times per cycle" as a `[CONTEXT-WASTE]` proposal.
37
+
38
+ Cross-references:
39
+
40
+ - `scripts/lib/prompt-dedup/index.cjs` — analyzer + injection text builder.
41
+ - `tests/prompt-dedup.test.cjs` — detection rule tests.
42
+ - `agents/perf-analyzer.md` — consumes `dedup.injection` events for cross-cycle analysis.
43
+
28
44
  ---
29
45
 
30
46
  *Imported by every skill that reads `.design/` artifacts: `/gdd:progress`, `/gdd:resume`, `/gdd:reflect`, `/gdd:pause`, `/gdd:recall` (Phase 19.5+), `/gdd:timeline` (Phase 19.5+). Tier: preamble. Phase: 14.5.*