@hegemonart/get-design-done 1.57.0 → 1.57.2

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 (123) hide show
  1. package/.claude-plugin/marketplace.json +26 -41
  2. package/.claude-plugin/plugin.json +23 -48
  3. package/CHANGELOG.md +119 -0
  4. package/README.md +166 -511
  5. package/SKILL.md +2 -0
  6. package/agents/README.md +33 -36
  7. package/agents/a11y-mapper.md +3 -3
  8. package/agents/component-benchmark-harvester.md +6 -6
  9. package/agents/component-benchmark-synthesizer.md +3 -3
  10. package/agents/compose-executor.md +3 -3
  11. package/agents/cost-forecaster.md +2 -2
  12. package/agents/design-auditor.md +7 -7
  13. package/agents/design-authority-watcher.md +15 -15
  14. package/agents/design-context-builder.md +4 -4
  15. package/agents/design-context-checker-gate.md +1 -1
  16. package/agents/design-discussant.md +2 -2
  17. package/agents/design-doc-writer.md +1 -1
  18. package/agents/design-executor.md +2 -2
  19. package/agents/design-figma-writer.md +2 -2
  20. package/agents/design-fixer.md +7 -7
  21. package/agents/design-integration-checker-gate.md +1 -1
  22. package/agents/design-integration-checker.md +1 -1
  23. package/agents/design-paper-writer.md +3 -3
  24. package/agents/design-pencil-writer.md +1 -1
  25. package/agents/design-planner.md +21 -0
  26. package/agents/design-reflector.md +39 -39
  27. package/agents/design-research-synthesizer.md +1 -0
  28. package/agents/design-start-writer.md +1 -1
  29. package/agents/design-update-checker.md +5 -5
  30. package/agents/design-verifier-gate.md +1 -1
  31. package/agents/design-verifier.md +52 -48
  32. package/agents/ds-generator.md +2 -2
  33. package/agents/ds-migration-planner.md +4 -4
  34. package/agents/email-executor.md +9 -9
  35. package/agents/experiment-result-ingester.md +3 -3
  36. package/agents/flutter-executor.md +5 -5
  37. package/agents/gdd-graph-refresh.md +3 -3
  38. package/agents/gdd-intel-updater.md +2 -2
  39. package/agents/motion-mapper.md +2 -2
  40. package/agents/motion-verifier.md +4 -4
  41. package/agents/pdf-executor.md +8 -8
  42. package/agents/perf-analyzer.md +17 -17
  43. package/agents/pr-commenter.md +9 -9
  44. package/agents/prototype-gate.md +2 -2
  45. package/agents/quality-gate-runner.md +1 -1
  46. package/agents/rollout-coordinator.md +3 -3
  47. package/agents/swift-executor.md +4 -4
  48. package/agents/ticket-sync-agent.md +6 -6
  49. package/agents/user-research-synthesizer.md +2 -2
  50. package/connections/connections.md +44 -45
  51. package/connections/cursor.md +73 -0
  52. package/connections/preview.md +3 -3
  53. package/dist/claude-code/.claude/skills/cache-manager/SKILL.md +3 -3
  54. package/dist/claude-code/.claude/skills/cache-manager/cache-policy.md +1 -1
  55. package/dist/claude-code/.claude/skills/design/SKILL.md +19 -0
  56. package/dist/claude-code/.claude/skills/explore/SKILL.md +11 -0
  57. package/dist/claude-code/.claude/skills/figma-write/SKILL.md +13 -2
  58. package/dist/claude-code/.claude/skills/paper-write/SKILL.md +54 -0
  59. package/dist/claude-code/.claude/skills/pencil-write/SKILL.md +54 -0
  60. package/dist/claude-code/.claude/skills/report-issue/SKILL.md +2 -2
  61. package/dist/claude-code/.claude/skills/router/SKILL.md +2 -2
  62. package/dist/claude-code/.claude/skills/verify/verify-procedure.md +10 -11
  63. package/dist/claude-code/.claude/skills/warm-cache/SKILL.md +1 -1
  64. package/hooks/budget-enforcer.ts +5 -4
  65. package/hooks/first-run-nudge.cjs +171 -0
  66. package/hooks/gdd-intel-trigger.js +243 -0
  67. package/hooks/gdd-mcp-circuit-breaker.js +62 -7
  68. package/hooks/gdd-precompact-snapshot.js +50 -29
  69. package/hooks/gdd-protected-paths.js +175 -20
  70. package/hooks/gdd-read-injection-scanner.ts +9 -1
  71. package/hooks/gdd-risk-gate.js +110 -8
  72. package/hooks/gdd-sessionstart-recap.js +59 -24
  73. package/hooks/hooks.json +13 -4
  74. package/hooks/inject-using-gdd.cjs +188 -0
  75. package/hooks/update-check.cjs +511 -0
  76. package/package.json +9 -2
  77. package/reference/STATE-TEMPLATE.md +10 -13
  78. package/reference/audit-scoring.md +1 -1
  79. package/reference/cache-tier-doctrine.md +46 -0
  80. package/reference/config-schema.md +9 -9
  81. package/reference/i18n.md +1 -1
  82. package/reference/intel-schema.md +37 -2
  83. package/reference/meta-rules.md +4 -4
  84. package/reference/model-tiers.md +2 -2
  85. package/reference/registry.json +101 -94
  86. package/reference/runtime-models.md +11 -1
  87. package/reference/shared-preamble.md +13 -14
  88. package/reference/skill-graph.md +24 -1
  89. package/scripts/bootstrap.cjs +373 -0
  90. package/scripts/injection-patterns.cjs +58 -0
  91. package/scripts/lib/apply-reflections/incubator-proposals.cjs +57 -26
  92. package/scripts/lib/design-search.cjs +20 -2
  93. package/scripts/lib/install/converters/codex-plugin.cjs +5 -2
  94. package/scripts/lib/install/converters/cursor.cjs +20 -0
  95. package/scripts/lib/issue-reporter/report-flow.cjs +1 -1
  96. package/scripts/lib/manifest/skills.json +80 -13
  97. package/scripts/lib/state/migrate-to-sqlite.cjs +23 -7
  98. package/scripts/lib/state/query-surface.cjs +86 -16
  99. package/scripts/lib/state/render-markdown.cjs +26 -14
  100. package/scripts/lib/state/state-store.cjs +141 -68
  101. package/sdk/cli/commands/stage.ts +17 -0
  102. package/sdk/cli/index.js +21 -1
  103. package/sdk/dashboard/data/_pkg-root.cjs +4 -4
  104. package/sdk/dashboard/data/risk-surface.cjs +54 -19
  105. package/sdk/dashboard/tui/index.cjs +28 -2
  106. package/sdk/mcp/gdd-state/server.js +7 -1
  107. package/sdk/state/index.ts +11 -1
  108. package/skills/cache-manager/SKILL.md +3 -3
  109. package/skills/cache-manager/cache-policy.md +1 -1
  110. package/skills/design/SKILL.md +19 -0
  111. package/skills/explore/SKILL.md +11 -0
  112. package/skills/figma-write/SKILL.md +13 -2
  113. package/skills/paper-write/SKILL.md +54 -0
  114. package/skills/pencil-write/SKILL.md +54 -0
  115. package/skills/report-issue/SKILL.md +2 -2
  116. package/skills/router/SKILL.md +2 -2
  117. package/skills/verify/verify-procedure.md +10 -11
  118. package/skills/warm-cache/SKILL.md +1 -1
  119. package/hooks/first-run-nudge.sh +0 -82
  120. package/hooks/inject-using-gdd.sh +0 -72
  121. package/hooks/update-check.sh +0 -251
  122. package/scripts/lib/audit-aggregator/index.cjs +0 -219
  123. package/scripts/lib/hedge-ensemble.cjs +0 -217
@@ -13,9 +13,32 @@
13
13
 
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
- const { matches } = require(path.join(__dirname, '..', 'scripts', 'lib', 'glob-match.cjs'));
17
16
 
18
- const REPO_ROOT = path.resolve(__dirname, '..');
17
+ /**
18
+ * Walk up from startDir to find the package root by looking for a
19
+ * package.json with name '@hegemonart/get-design-done'. Returns null
20
+ * when the root cannot be found (e.g. in unusual installed layouts).
21
+ * Mirrors the pattern used by gdd-fact-force.js / gdd-risk-gate.js
22
+ * (Phase 56+) to be robust against esbuild/installed layouts that
23
+ * may relocate or rewrite __dirname.
24
+ */
25
+ function findPackageRoot(startDir) {
26
+ let dir = startDir;
27
+ for (let i = 0; i < 12; i++) {
28
+ try {
29
+ const pkg = require(path.join(dir, 'package.json'));
30
+ if (pkg && pkg.name === '@hegemonart/get-design-done') return dir;
31
+ } catch { /* not this level */ }
32
+ const parent = path.dirname(dir);
33
+ if (parent === dir) break;
34
+ dir = parent;
35
+ }
36
+ return null;
37
+ }
38
+
39
+ const REPO_ROOT = findPackageRoot(__dirname) || path.resolve(__dirname, '..');
40
+
41
+ const { matches } = require(path.join(REPO_ROOT, 'scripts', 'lib', 'glob-match.cjs'));
19
42
 
20
43
  function loadProtectedPaths(cwd) {
21
44
  const defaultFile = path.join(REPO_ROOT, 'reference', 'protected-paths.default.json');
@@ -36,28 +59,160 @@ function loadProtectedPaths(cwd) {
36
59
  }
37
60
 
38
61
  /**
39
- * Extract a target path from a Bash command, best-effort.
40
- * Returns an array of candidate paths; empty if none parsed.
62
+ * Tokenise a string of shell-style args into individual arguments, honoring
63
+ * single/double quotes and basic backslash escapes. Used by the bash target
64
+ * extractor to get reliable arg arrays for destructive coreutils.
65
+ */
66
+ function parseShellArgs(s) {
67
+ const args = [];
68
+ let current = '';
69
+ let inQuote = null;
70
+ for (let i = 0; i < s.length; i++) {
71
+ const ch = s[i];
72
+ if (inQuote) {
73
+ if (ch === inQuote) {
74
+ inQuote = null;
75
+ args.push(current);
76
+ current = '';
77
+ } else if (ch === '\\' && i + 1 < s.length && (s[i + 1] === inQuote || s[i + 1] === '\\')) {
78
+ current += s[++i];
79
+ } else {
80
+ current += ch;
81
+ }
82
+ } else if (ch === '"' || ch === "'") {
83
+ if (current) { args.push(current); current = ''; }
84
+ inQuote = ch;
85
+ } else if (/\s/.test(ch)) {
86
+ if (current) { args.push(current); current = ''; }
87
+ } else if (ch === '\\' && i + 1 < s.length) {
88
+ current += s[++i];
89
+ } else {
90
+ current += ch;
91
+ }
92
+ }
93
+ if (current) args.push(current);
94
+ return args;
95
+ }
96
+
97
+ /**
98
+ * Extract destructive-op file targets from one shell pipeline segment
99
+ * (already split on `&&`/`||`/`;`/`|`). Catches:
100
+ * - rm / cp / mv / mkdir / touch / rmdir / chmod / chown / ln / tee
101
+ * with ALL their non-flag args (not just the first).
102
+ * - git rm / mv / restore / checkout — same treatment.
103
+ * - sed -i <args> file1 [file2 ...]
104
+ * - > file and >> file redirects appearing anywhere in the segment.
105
+ *
106
+ * `sudo ` prefix is stripped before dispatch.
107
+ */
108
+ function extractTargetsFromSegment(seg) {
109
+ const targets = [];
110
+ const cleaned = seg.replace(/^sudo\s+/, '');
111
+
112
+ // git destructive subcommands
113
+ const gitMatch = cleaned.match(/^git\s+(rm|mv|restore|checkout)\b(.*)$/);
114
+ if (gitMatch) {
115
+ const args = parseShellArgs(gitMatch[2]);
116
+ for (const arg of args) {
117
+ if (!arg.startsWith('-')) targets.push(arg);
118
+ }
119
+ }
120
+
121
+ // sed -i: only treat as destructive when -i is present
122
+ if (/^sed\b/.test(cleaned) && /(?:^|\s)-i(?:\b|=)/.test(cleaned)) {
123
+ const tokens = parseShellArgs(cleaned).slice(1); // drop 'sed'
124
+ let i = 0;
125
+ while (i < tokens.length) {
126
+ const tok = tokens[i];
127
+ // BSD `sed -i ''` consumes an extra empty-string arg
128
+ if (tok === '-i' && i + 1 < tokens.length && tokens[i + 1] === '') {
129
+ i += 2;
130
+ continue;
131
+ }
132
+ if (tok.startsWith('-')) { i++; continue; }
133
+ // First non-flag arg may be either an in-line sed script or a file;
134
+ // path matcher will simply not match a non-path. Be permissive: queue all.
135
+ targets.push(tok);
136
+ i++;
137
+ }
138
+ }
139
+
140
+ // Coreutils destructive verbs
141
+ const coreutilsMatch = cleaned.match(/^(rm|cp|mv|mkdir|touch|rmdir|chmod|chown|ln|tee)\b(.*)$/);
142
+ if (coreutilsMatch) {
143
+ const args = parseShellArgs(coreutilsMatch[2]);
144
+ for (const arg of args) {
145
+ if (!arg.startsWith('-')) targets.push(arg);
146
+ }
147
+ }
148
+
149
+ // Redirect targets: > file or >> file (appear anywhere in the segment)
150
+ const redirects = seg.matchAll(/(?:^|[^&>])>>?\s*([^\s|;&]+)/g);
151
+ for (const m of redirects) targets.push(m[1]);
152
+
153
+ return targets;
154
+ }
155
+
156
+ /**
157
+ * Extract all candidate file paths a Bash command may mutate. Walks the
158
+ * command string in three passes:
159
+ *
160
+ * 1. Recursively process every `$(...)` and `\`...\`` subshell. The
161
+ * subshell is evaluated by the shell and its OUTPUT substitutes into
162
+ * the parent command — but the inner commands themselves ALSO run,
163
+ * so anything destructive inside is a target.
164
+ * 2. Strip subshells from the outer command to simplify splitting.
165
+ * 3. Split outer command on `&&`, `||`, `;`, `|` and feed each segment
166
+ * to extractTargetsFromSegment.
167
+ *
168
+ * Previous implementation called String.prototype.match() (returns only
169
+ * the first match) and a single regex with a `[^\\s|;&>]+` capture group.
170
+ * That missed:
171
+ * - chained commands (`rm safe.txt && rm secret`)
172
+ * - multi-arg destructive verbs (`rm a b c` — only `a` was extracted)
173
+ * - subshell content (`rm $(echo secret)`)
174
+ * - backtick command substitution (`rm \`echo secret\``)
175
+ *
176
+ * xargs bypass — `find protected -print0 | xargs -0 rm` — is NOT closed
177
+ * here, because the targets come from stdin which we can't model without
178
+ * a full pipeline shape analysis. The `find` segment will be checked but
179
+ * the subsequent xargs+rm segment carries no explicit path. Project policy
180
+ * should rely on `find <protected-dir>` being blocked at the upstream
181
+ * segment via the .git/** / reference/** globs, plus general operator
182
+ * caution. Future enhancement: scan pipeline for xargs-with-destructive
183
+ * verbs and require the upstream stage to not reference protected globs.
41
184
  */
42
185
  function extractBashTargets(command) {
43
186
  if (!command) return [];
187
+
44
188
  const targets = [];
45
- // rm / cp / mv / mkdir trailing arg(s)
46
- const rmMatch = command.match(/\b(rm|cp|mv|mkdir|touch|rmdir|chmod|chown)\s+(?:-[A-Za-z]+\s+)*([^\s|;&>]+)/);
47
- if (rmMatch) targets.push(rmMatch[2]);
48
- // redirect / tee
49
- const redirectMatch = command.match(/[>|]\s*(?:tee\s+)?([^\s|;&]+)$/);
50
- if (redirectMatch) targets.push(redirectMatch[1]);
51
- // sed -i <path> (BSD and GNU variants)
52
- const sedMatch = command.match(/\bsed\s+-i(?:\s*['"][^'"]*['"])?\s+(?:-[A-Za-z]+\s+)*(?:['"][^'"]*['"]\s+)?([^\s|;&]+)/);
53
- if (sedMatch) targets.push(sedMatch[1]);
54
- // git rm / git mv
55
- const gitMatch = command.match(/\bgit\s+(rm|mv|restore|checkout)\s+(?:-[A-Za-z]+\s+)*([^\s|;&]+)/);
56
- if (gitMatch) targets.push(gitMatch[2]);
57
-
58
- return targets
59
- .filter(Boolean)
60
- .map(p => p.replace(/^['"]|['"]$/g, ''));
189
+
190
+ // 1. Recursive subshell scan.
191
+ const SUBSHELL_RE = /\$\(([^()]*)\)|`([^`]*)`/g;
192
+ let m;
193
+ while ((m = SUBSHELL_RE.exec(command)) !== null) {
194
+ targets.push(...extractBashTargets(m[1] !== undefined ? m[1] : m[2]));
195
+ }
196
+
197
+ // 2. Strip subshells from outer command.
198
+ const stripped = command.replace(SUBSHELL_RE, '');
199
+
200
+ // 3. Split on shell separators and process each segment.
201
+ const segments = stripped.split(/\s*(?:&&|\|\||;|\|)\s*/);
202
+ for (const segment of segments) {
203
+ const seg = segment.trim();
204
+ if (!seg) continue;
205
+ targets.push(...extractTargetsFromSegment(seg));
206
+ }
207
+
208
+ // Dedup + strip surrounding quotes.
209
+ return [
210
+ ...new Set(
211
+ targets
212
+ .filter(Boolean)
213
+ .map((p) => p.replace(/^['"]|['"]$/g, '')),
214
+ ),
215
+ ];
61
216
  }
62
217
 
63
218
  async function main() {
@@ -79,7 +79,15 @@ function loadPatterns(): readonly RegExp[] {
79
79
  );
80
80
  }
81
81
 
82
- const INJECTION_PATTERNS: readonly RegExp[] = loadPatterns();
82
+ // Wrapped in try/catch so a missing injection-patterns.cjs does not throw
83
+ // at import time (before main()'s catch guard is active). On failure the
84
+ // hook falls back to an empty pattern list and emits a passthrough result.
85
+ let INJECTION_PATTERNS: readonly RegExp[];
86
+ try {
87
+ INJECTION_PATTERNS = loadPatterns();
88
+ } catch {
89
+ INJECTION_PATTERNS = [];
90
+ }
83
91
 
84
92
  // ── Types ───────────────────────────────────────────────────────────────────
85
93
 
@@ -2,10 +2,16 @@
2
2
  'use strict';
3
3
  /**
4
4
  * hooks/gdd-risk-gate.js — PreToolUse:Write|Edit|MultiEdit|Bash risk gate (Phase 56, RISK-02).
5
+ * Payload shape locked to RiskAssessmentPayload (events.schema.json): event_id, tool_name,
6
+ * risk_score, suggested_action, reasons (required). Optional: agent, decision_context.
7
+ * additionalProperties:false — do NOT add breakdown/paths/score/tool to the payload.
5
8
  *
6
9
  * Quantifies the confidence/risk of a writer action with the PURE scorer
7
10
  * `scripts/lib/risk/compute-risk.cjs` (executor A), emits a `risk_assessment`
8
- * telemetry event, and routes by the scorer's `suggested_action`:
11
+ * telemetry event, writes a rolling-50 calibration row via
12
+ * `scripts/lib/risk/calibration.cjs` updateCalibration() (Phase 56 CAL-01 — this
13
+ * closes the calibration loop end-to-end: production traffic, not just tests,
14
+ * drives detectDrift), and routes by the scorer's `suggested_action`:
9
15
  *
10
16
  * allow -> { continue: true } (silent)
11
17
  * review -> { continue: true, hookSpecificOutput: { … } } (advisory, non-blocking)
@@ -39,6 +45,7 @@
39
45
 
40
46
  const fs = require('fs');
41
47
  const path = require('path');
48
+ const { randomUUID } = require('node:crypto');
42
49
 
43
50
  // ── Package-root walk-up: locate scripts/lib/risk/compute-risk.cjs ──────────
44
51
  // Start at this file's dir and climb until we find the risk module (or a
@@ -79,6 +86,85 @@ let _riskLoadError = null;
79
86
  }
80
87
  })();
81
88
 
89
+ // ── Calibration sibling resolver (same walk-up shape as the risk module) ────
90
+ // scripts/lib/risk/calibration.cjs is the rolling-50 per-agent calibration
91
+ // store (Phase 56 CAL-01). We call updateCalibration AFTER scoring so the
92
+ // store grows over time with real per-agent (risk, accepted) outcomes — that
93
+ // is what wires the calibration loop end-to-end (under_scoring / over_scoring
94
+ // drift becomes detectable from real traffic, not just from synthetic tests).
95
+ const CAL_REL = path.join('scripts', 'lib', 'risk', 'calibration.cjs');
96
+
97
+ function findCalibrationModule(startDir) {
98
+ let dir = startDir;
99
+ for (let i = 0; i < 64; i++) {
100
+ const candidate = path.join(dir, CAL_REL);
101
+ try {
102
+ if (fs.existsSync(candidate)) return candidate;
103
+ } catch { /* keep climbing */ }
104
+ const parent = path.dirname(dir);
105
+ if (parent === dir) break;
106
+ dir = parent;
107
+ }
108
+ return null;
109
+ }
110
+
111
+ let _cal = null;
112
+ let _calLoadError = null;
113
+ (function loadCal() {
114
+ try {
115
+ const modPath = findCalibrationModule(__dirname);
116
+ if (!modPath) {
117
+ _calLoadError = `calibration.cjs not found above ${__dirname}`;
118
+ return;
119
+ }
120
+ // eslint-disable-next-line global-require, import/no-dynamic-require
121
+ _cal = require(modPath);
122
+ } catch (err) {
123
+ _calLoadError = err && err.message ? err.message : String(err);
124
+ }
125
+ })();
126
+
127
+ // ── Calibration write (best-effort, never throws) ───────────────────────────
128
+ // Records one (agent, risk, accepted) outcome for the rolling-50 window.
129
+ //
130
+ // The signal we can KNOW at PreToolUse time:
131
+ // * action === 'block' -> definitive accepted:false (the hook rejected the
132
+ // call; the user never sees the tool run).
133
+ // * action ∈ {allow, review, require_confirmation} -> accepted:true at the
134
+ // PreToolUse boundary. The action proceeds past the risk gate; a later
135
+ // hook may still block, and the user may later /gdd:override or undo, but
136
+ // for THIS gate's calibration loop "the risk gate let it through" IS the
137
+ // acceptance signal. user_undo / post_apply_correct are deliberately left
138
+ // null (unresolved) — a future PostToolUse pass can resolve them later.
139
+ //
140
+ // Agent gate: a calibration row needs an agent key. When the agent is unknown
141
+ // (the common case for a generic PreToolUse hook) we skip the write rather
142
+ // than pool everything into an 'unknown' bucket that would render drift
143
+ // detection meaningless. The risk_assessment event still fires either way.
144
+ //
145
+ // Always best-effort: a calibration write must NEVER break a tool call.
146
+ function recordCalibration(agent, assessment, cwd) {
147
+ try {
148
+ if (!_cal || typeof _cal.updateCalibration !== 'function') return;
149
+ if (!agent || typeof agent !== 'string') return;
150
+ const action = assessment && assessment.suggested_action;
151
+ if (!action) return;
152
+ const score = typeof assessment.score === 'number' ? assessment.score : 0;
153
+ _cal.updateCalibration(
154
+ agent,
155
+ {
156
+ risk: score,
157
+ accepted: action !== 'block',
158
+ user_undo: false,
159
+ post_apply_correct: null,
160
+ },
161
+ { root: cwd || process.cwd() },
162
+ );
163
+ } catch {
164
+ /* swallow — calibration writes must never throw into the gate */
165
+ }
166
+ }
167
+
82
168
  // ── Best-effort `risk_assessment` event emit ────────────────────────────────
83
169
  // The firehose (`appendEvent`, sdk/event-stream) is the sink the wire-in tests
84
170
  // read via GDD_EVENTS_PATH. `type` is free-form on the envelope, so emitting
@@ -325,7 +411,14 @@ async function main() {
325
411
  );
326
412
  } catch { /* swallow */ }
327
413
  emitHookFired('allow', { reason: 'scorer-error' });
328
- emitRiskAssessment({ tool, agent: agent || undefined, error: 'scorer-error', suggested_action: 'allow' }, sessionId);
414
+ emitRiskAssessment({
415
+ event_id: randomUUID(),
416
+ tool_name: tool,
417
+ risk_score: 0,
418
+ suggested_action: 'allow',
419
+ reasons: [],
420
+ agent: agent || undefined,
421
+ }, sessionId);
329
422
  process.stdout.write(JSON.stringify(ALLOW));
330
423
  return;
331
424
  }
@@ -337,17 +430,24 @@ async function main() {
337
430
  // distribution. Best-effort.
338
431
  emitRiskAssessment(
339
432
  {
340
- tool,
341
- agent: agent || undefined,
342
- score: assessment.score,
433
+ event_id: randomUUID(),
434
+ tool_name: tool,
435
+ risk_score: assessment.score,
343
436
  suggested_action: action,
344
- reasons: assessment.reasons,
345
- breakdown: assessment.breakdown,
346
- paths: assessment.breakdown && assessment.breakdown.paths,
437
+ reasons: Array.isArray(assessment.reasons) ? assessment.reasons : [],
438
+ agent: agent || undefined,
347
439
  },
348
440
  sessionId,
349
441
  );
350
442
 
443
+ // Update the rolling-50 per-agent calibration window with this outcome
444
+ // (Phase 56 CAL-01). Best-effort; no-op when the agent is unknown. This is
445
+ // what closes the calibration loop end-to-end: the store accrues real
446
+ // (risk, accepted) pairs across the writer agent's actions, so detectDrift
447
+ // can flag under_scoring / over_scoring from production traffic rather than
448
+ // only from synthetic test calls.
449
+ recordCalibration(agent, assessment, cwd);
450
+
351
451
  // Mirror the decision onto the hook.fired row (allow|review|confirm|block).
352
452
  const firedDecision = action === 'block' ? 'block' : 'allow';
353
453
  emitHookFired(firedDecision, { suggested_action: action, score: assessment.score });
@@ -394,6 +494,8 @@ if (require.main === module) {
394
494
  // Exported for tests — pure helpers + the resolver. main() owns the I/O + contract.
395
495
  module.exports = {
396
496
  findRiskModule,
497
+ findCalibrationModule,
498
+ recordCalibration,
397
499
  buildMergedTables,
398
500
  compileFileSensitivityExtra,
399
501
  isReadOnlyAgent,
@@ -21,12 +21,29 @@
21
21
  const fs = require('node:fs');
22
22
  const path = require('node:path');
23
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
24
  const SCHEMA_VERSION = '1.0.0';
29
25
 
26
+ /**
27
+ * Resolve the bundle of paths the hook reads/writes, anchored at `cwd`.
28
+ *
29
+ * Phase 27.6 originally resolved these at module load via `process.cwd()`,
30
+ * which is the wrong anchor when Claude Code invokes the hook from a
31
+ * worktree (the harness's cwd at module load can be the parent / `.claude`
32
+ * directory, not the project root). Resolving against `payload.cwd` matches
33
+ * how 8 sibling hooks already work (gdd-protected-paths, gdd-fact-force,
34
+ * gdd-decision-injector, gdd-mcp-circuit-breaker, gdd-a11y-gate,
35
+ * gdd-design-quality-check, gdd-risk-gate, gdd-turn-closeout).
36
+ */
37
+ function computePaths(cwd) {
38
+ const snapshotDir = path.resolve(cwd, '.design', 'snapshots');
39
+ return {
40
+ snapshotDir,
41
+ stateMdPath: path.resolve(cwd, '.design', 'STATE.md'),
42
+ eventsPath: path.resolve(cwd, '.design', 'telemetry', 'events.jsonl'),
43
+ recapJsonPath: path.join(snapshotDir, 'last-recap.json'),
44
+ };
45
+ }
46
+
30
47
  // ---------------------------------------------------------------------------
31
48
  // Harness detection (D-10) — mirrors gdd-precompact-snapshot.js
32
49
  // ---------------------------------------------------------------------------
@@ -60,11 +77,11 @@ function getAppendEvent() {
60
77
  // needs frontmatter + a flat decisions list for the diff)
61
78
  // ---------------------------------------------------------------------------
62
79
 
63
- function readStateMd() {
64
- if (!fs.existsSync(STATE_MD_PATH)) return { frontmatter: {}, decisions: [] };
80
+ function readStateMd(paths) {
81
+ if (!fs.existsSync(paths.stateMdPath)) return { frontmatter: {}, decisions: [] };
65
82
  let body;
66
83
  try {
67
- body = fs.readFileSync(STATE_MD_PATH, 'utf8');
84
+ body = fs.readFileSync(paths.stateMdPath, 'utf8');
68
85
  } catch {
69
86
  return { frontmatter: {}, decisions: [] };
70
87
  }
@@ -92,11 +109,11 @@ function readStateMd() {
92
109
  // Snapshot discovery — highest-mtime *.json (excluding last-recap.json)
93
110
  // ---------------------------------------------------------------------------
94
111
 
95
- function findLatestSnapshot() {
96
- if (!fs.existsSync(SNAPSHOT_DIR)) return null;
112
+ function findLatestSnapshot(paths) {
113
+ if (!fs.existsSync(paths.snapshotDir)) return null;
97
114
  let files;
98
115
  try {
99
- files = fs.readdirSync(SNAPSHOT_DIR);
116
+ files = fs.readdirSync(paths.snapshotDir);
100
117
  } catch {
101
118
  return null;
102
119
  }
@@ -108,7 +125,7 @@ function findLatestSnapshot() {
108
125
  let best = null;
109
126
  let bestMtime = -1;
110
127
  for (const name of candidates) {
111
- const full = path.join(SNAPSHOT_DIR, name);
128
+ const full = path.join(paths.snapshotDir, name);
112
129
  try {
113
130
  const mtime = fs.statSync(full).mtimeMs;
114
131
  if (mtime > bestMtime) {
@@ -126,11 +143,11 @@ function findLatestSnapshot() {
126
143
  // Event count since snapshot timestamp (JSONL-tolerant)
127
144
  // ---------------------------------------------------------------------------
128
145
 
129
- function countEventsSince(isoTimestamp) {
130
- if (!fs.existsSync(EVENTS_PATH)) return 0;
146
+ function countEventsSince(paths, isoTimestamp) {
147
+ if (!fs.existsSync(paths.eventsPath)) return 0;
131
148
  let body;
132
149
  try {
133
- body = fs.readFileSync(EVENTS_PATH, 'utf8');
150
+ body = fs.readFileSync(paths.eventsPath, 'utf8');
134
151
  } catch {
135
152
  return 0;
136
153
  }
@@ -154,16 +171,34 @@ function countEventsSince(isoTimestamp) {
154
171
  // Main
155
172
  // ---------------------------------------------------------------------------
156
173
 
157
- function main() {
174
+ async function main() {
158
175
  const harness = detectHarness();
159
176
  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');
177
+ // D-10: SessionStart on Codex skips recap. Tracked in the runtime-parity
178
+ // matrix; full pre-large-context-action integration is on the roadmap.
179
+ process.stderr.write('[gdd-sessionstart-recap] codex harness no-op\n');
163
180
  process.exit(0);
164
181
  }
165
182
 
166
- const snapshotPath = findLatestSnapshot();
183
+ // Drain stdin and extract payload.cwd (Claude Code SessionStart pipes a JSON
184
+ // envelope). Falls back to process.cwd() when stdin is empty (unit tests,
185
+ // direct invocation).
186
+ let buf = '';
187
+ try {
188
+ for await (const chunk of process.stdin) buf += chunk;
189
+ } catch {
190
+ /* swallow — empty stdin is fine */
191
+ }
192
+ let payload = {};
193
+ try {
194
+ payload = JSON.parse(buf || '{}');
195
+ } catch {
196
+ /* malformed stdin → fall through with empty payload */
197
+ }
198
+ const cwd = (payload && typeof payload.cwd === 'string') ? payload.cwd : process.cwd();
199
+ const paths = computePaths(cwd);
200
+
201
+ const snapshotPath = findLatestSnapshot(paths);
167
202
  if (!snapshotPath) {
168
203
  process.stderr.write('[gdd-sessionstart-recap] no prior snapshot\n');
169
204
  process.exit(0);
@@ -181,13 +216,13 @@ function main() {
181
216
  process.exit(0);
182
217
  }
183
218
 
184
- const current = readStateMd();
219
+ const current = readStateMd(paths);
185
220
  const priorDecisions = Array.isArray(snapshot.last_n_decisions)
186
221
  ? snapshot.last_n_decisions
187
222
  : [];
188
223
  const priorSet = new Set(priorDecisions);
189
224
  const newDecisions = current.decisions.filter((d) => !priorSet.has(d));
190
- const newEventCount = countEventsSince(snapshot.timestamp || '1970-01-01T00:00:00.000Z');
225
+ const newEventCount = countEventsSince(paths, snapshot.timestamp || '1970-01-01T00:00:00.000Z');
191
226
 
192
227
  const priorCycle = snapshot.cycle_id || 'unknown';
193
228
  const currentCycle = current.frontmatter.milestone || 'unknown';
@@ -226,9 +261,9 @@ function main() {
226
261
  try {
227
262
  // mkdir -p for safety — directory should exist if snapshotPath was found,
228
263
  // 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);
264
+ fs.mkdirSync(paths.snapshotDir, { recursive: true });
265
+ fs.writeFileSync(paths.recapJsonPath + '.tmp', JSON.stringify(recap, null, 2), 'utf8');
266
+ fs.renameSync(paths.recapJsonPath + '.tmp', paths.recapJsonPath);
232
267
  } catch (err) {
233
268
  process.stderr.write(
234
269
  '[gdd-sessionstart-recap] sidecar write failed: ' +
package/hooks/hooks.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "hooks": [
6
6
  {
7
7
  "type": "command",
8
- "command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/bootstrap.sh\""
8
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bootstrap.cjs\""
9
9
  }
10
10
  ]
11
11
  },
@@ -13,7 +13,7 @@
13
13
  "hooks": [
14
14
  {
15
15
  "type": "command",
16
- "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/update-check.sh\""
16
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/update-check.cjs\""
17
17
  }
18
18
  ]
19
19
  },
@@ -21,7 +21,7 @@
21
21
  "hooks": [
22
22
  {
23
23
  "type": "command",
24
- "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/first-run-nudge.sh\""
24
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/first-run-nudge.cjs\""
25
25
  }
26
26
  ]
27
27
  },
@@ -38,7 +38,7 @@
38
38
  "hooks": [
39
39
  {
40
40
  "type": "command",
41
- "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/inject-using-gdd.sh\""
41
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/inject-using-gdd.cjs\""
42
42
  }
43
43
  ]
44
44
  }
@@ -151,6 +151,15 @@
151
151
  "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gdd-design-quality-check.js\""
152
152
  }
153
153
  ]
154
+ },
155
+ {
156
+ "matcher": "Edit|Write",
157
+ "hooks": [
158
+ {
159
+ "type": "command",
160
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gdd-intel-trigger.js\""
161
+ }
162
+ ]
154
163
  }
155
164
  ],
156
165
  "Stop": [