@hegemonart/get-design-done 1.57.1 → 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.
- package/.claude-plugin/marketplace.json +26 -41
- package/.claude-plugin/plugin.json +23 -48
- package/CHANGELOG.md +91 -0
- package/README.md +166 -511
- package/SKILL.md +2 -0
- package/agents/README.md +33 -36
- package/agents/a11y-mapper.md +3 -3
- package/agents/component-benchmark-harvester.md +6 -6
- package/agents/component-benchmark-synthesizer.md +3 -3
- package/agents/compose-executor.md +3 -3
- package/agents/cost-forecaster.md +2 -2
- package/agents/design-auditor.md +7 -7
- package/agents/design-authority-watcher.md +15 -15
- package/agents/design-context-builder.md +4 -4
- package/agents/design-context-checker-gate.md +1 -1
- package/agents/design-discussant.md +2 -2
- package/agents/design-doc-writer.md +1 -1
- package/agents/design-executor.md +2 -2
- package/agents/design-figma-writer.md +2 -2
- package/agents/design-fixer.md +7 -7
- package/agents/design-integration-checker-gate.md +1 -1
- package/agents/design-integration-checker.md +1 -1
- package/agents/design-paper-writer.md +3 -3
- package/agents/design-pencil-writer.md +1 -1
- package/agents/design-planner.md +21 -0
- package/agents/design-reflector.md +39 -39
- package/agents/design-research-synthesizer.md +1 -0
- package/agents/design-start-writer.md +1 -1
- package/agents/design-update-checker.md +5 -5
- package/agents/design-verifier-gate.md +1 -1
- package/agents/design-verifier.md +52 -48
- package/agents/ds-generator.md +2 -2
- package/agents/ds-migration-planner.md +4 -4
- package/agents/email-executor.md +9 -9
- package/agents/experiment-result-ingester.md +3 -3
- package/agents/flutter-executor.md +5 -5
- package/agents/gdd-graph-refresh.md +3 -3
- package/agents/gdd-intel-updater.md +2 -2
- package/agents/motion-mapper.md +2 -2
- package/agents/motion-verifier.md +4 -4
- package/agents/pdf-executor.md +8 -8
- package/agents/perf-analyzer.md +17 -17
- package/agents/pr-commenter.md +9 -9
- package/agents/prototype-gate.md +2 -2
- package/agents/quality-gate-runner.md +1 -1
- package/agents/rollout-coordinator.md +3 -3
- package/agents/swift-executor.md +4 -4
- package/agents/ticket-sync-agent.md +6 -6
- package/agents/user-research-synthesizer.md +2 -2
- package/connections/connections.md +44 -45
- package/connections/cursor.md +73 -0
- package/connections/preview.md +3 -3
- package/dist/claude-code/.claude/skills/cache-manager/SKILL.md +3 -3
- package/dist/claude-code/.claude/skills/cache-manager/cache-policy.md +1 -1
- package/dist/claude-code/.claude/skills/design/SKILL.md +19 -0
- package/dist/claude-code/.claude/skills/explore/SKILL.md +11 -0
- package/dist/claude-code/.claude/skills/figma-write/SKILL.md +13 -2
- package/dist/claude-code/.claude/skills/paper-write/SKILL.md +54 -0
- package/dist/claude-code/.claude/skills/pencil-write/SKILL.md +54 -0
- package/dist/claude-code/.claude/skills/report-issue/SKILL.md +2 -2
- package/dist/claude-code/.claude/skills/router/SKILL.md +2 -2
- package/dist/claude-code/.claude/skills/verify/verify-procedure.md +10 -11
- package/dist/claude-code/.claude/skills/warm-cache/SKILL.md +1 -1
- package/hooks/first-run-nudge.cjs +171 -0
- package/hooks/gdd-intel-trigger.js +243 -0
- package/hooks/gdd-mcp-circuit-breaker.js +62 -7
- package/hooks/gdd-precompact-snapshot.js +50 -29
- package/hooks/gdd-protected-paths.js +150 -18
- package/hooks/gdd-risk-gate.js +93 -1
- package/hooks/gdd-sessionstart-recap.js +59 -24
- package/hooks/hooks.json +13 -4
- package/hooks/inject-using-gdd.cjs +188 -0
- package/hooks/update-check.cjs +511 -0
- package/package.json +9 -2
- package/reference/STATE-TEMPLATE.md +10 -13
- package/reference/audit-scoring.md +1 -1
- package/reference/cache-tier-doctrine.md +46 -0
- package/reference/config-schema.md +9 -9
- package/reference/i18n.md +1 -1
- package/reference/intel-schema.md +37 -2
- package/reference/meta-rules.md +4 -4
- package/reference/model-tiers.md +2 -2
- package/reference/registry.json +101 -94
- package/reference/runtime-models.md +11 -1
- package/reference/shared-preamble.md +13 -14
- package/reference/skill-graph.md +24 -1
- package/scripts/bootstrap.cjs +373 -0
- package/scripts/injection-patterns.cjs +58 -0
- package/scripts/lib/apply-reflections/incubator-proposals.cjs +57 -26
- package/scripts/lib/install/converters/codex-plugin.cjs +5 -2
- package/scripts/lib/install/converters/cursor.cjs +20 -0
- package/scripts/lib/issue-reporter/report-flow.cjs +1 -1
- package/scripts/lib/manifest/skills.json +80 -13
- package/scripts/lib/state/query-surface.cjs +67 -9
- package/scripts/lib/state/state-store.cjs +68 -26
- package/sdk/cli/commands/stage.ts +17 -0
- package/sdk/cli/index.js +14 -0
- package/skills/cache-manager/SKILL.md +3 -3
- package/skills/cache-manager/cache-policy.md +1 -1
- package/skills/design/SKILL.md +19 -0
- package/skills/explore/SKILL.md +11 -0
- package/skills/figma-write/SKILL.md +13 -2
- package/skills/paper-write/SKILL.md +54 -0
- package/skills/pencil-write/SKILL.md +54 -0
- package/skills/report-issue/SKILL.md +2 -2
- package/skills/router/SKILL.md +2 -2
- package/skills/verify/verify-procedure.md +10 -11
- package/skills/warm-cache/SKILL.md +1 -1
- package/hooks/first-run-nudge.sh +0 -82
- package/hooks/inject-using-gdd.sh +0 -72
- package/hooks/update-check.sh +0 -251
- package/scripts/lib/audit-aggregator/index.cjs +0 -219
- package/scripts/lib/hedge-ensemble.cjs +0 -217
package/hooks/gdd-risk-gate.js
CHANGED
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Quantifies the confidence/risk of a writer action with the PURE scorer
|
|
10
10
|
* `scripts/lib/risk/compute-risk.cjs` (executor A), emits a `risk_assessment`
|
|
11
|
-
* telemetry event,
|
|
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`:
|
|
12
15
|
*
|
|
13
16
|
* allow -> { continue: true } (silent)
|
|
14
17
|
* review -> { continue: true, hookSpecificOutput: { … } } (advisory, non-blocking)
|
|
@@ -83,6 +86,85 @@ let _riskLoadError = null;
|
|
|
83
86
|
}
|
|
84
87
|
})();
|
|
85
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
|
+
|
|
86
168
|
// ── Best-effort `risk_assessment` event emit ────────────────────────────────
|
|
87
169
|
// The firehose (`appendEvent`, sdk/event-stream) is the sink the wire-in tests
|
|
88
170
|
// read via GDD_EVENTS_PATH. `type` is free-form on the envelope, so emitting
|
|
@@ -358,6 +440,14 @@ async function main() {
|
|
|
358
440
|
sessionId,
|
|
359
441
|
);
|
|
360
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
|
+
|
|
361
451
|
// Mirror the decision onto the hook.fired row (allow|review|confirm|block).
|
|
362
452
|
const firedDecision = action === 'block' ? 'block' : 'allow';
|
|
363
453
|
emitHookFired(firedDecision, { suggested_action: action, score: assessment.score });
|
|
@@ -404,6 +494,8 @@ if (require.main === module) {
|
|
|
404
494
|
// Exported for tests — pure helpers + the resolver. main() owns the I/O + contract.
|
|
405
495
|
module.exports = {
|
|
406
496
|
findRiskModule,
|
|
497
|
+
findCalibrationModule,
|
|
498
|
+
recordCalibration,
|
|
407
499
|
buildMergedTables,
|
|
408
500
|
compileFileSensitivityExtra,
|
|
409
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(
|
|
80
|
+
function readStateMd(paths) {
|
|
81
|
+
if (!fs.existsSync(paths.stateMdPath)) return { frontmatter: {}, decisions: [] };
|
|
65
82
|
let body;
|
|
66
83
|
try {
|
|
67
|
-
body = fs.readFileSync(
|
|
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(
|
|
112
|
+
function findLatestSnapshot(paths) {
|
|
113
|
+
if (!fs.existsSync(paths.snapshotDir)) return null;
|
|
97
114
|
let files;
|
|
98
115
|
try {
|
|
99
|
-
files = fs.readdirSync(
|
|
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(
|
|
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(
|
|
146
|
+
function countEventsSince(paths, isoTimestamp) {
|
|
147
|
+
if (!fs.existsSync(paths.eventsPath)) return 0;
|
|
131
148
|
let body;
|
|
132
149
|
try {
|
|
133
|
-
body = fs.readFileSync(
|
|
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
|
|
161
|
-
// pre-large-context-action integration.
|
|
162
|
-
process.stderr.write('[gdd-sessionstart-recap] codex harness no-op
|
|
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
|
-
|
|
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(
|
|
230
|
-
fs.writeFileSync(
|
|
231
|
-
fs.renameSync(
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": [
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* hooks/inject-using-gdd.cjs — SessionStart per-harness context injector (D-07).
|
|
4
|
+
*
|
|
5
|
+
* Node CommonJS port of hooks/inject-using-gdd.sh. Written so it runs natively on
|
|
6
|
+
* Windows (where bash is often absent or stubbed) without spawning a subshell.
|
|
7
|
+
*
|
|
8
|
+
* The forcing function GDD lacked: on every session start / /clear / compact this
|
|
9
|
+
* reads skills/using-gdd/SKILL.md (the bootstrap discipline contract) and emits
|
|
10
|
+
* it as the host harness's SessionStart "additionalContext" shape so the agent is
|
|
11
|
+
* primed with the 1%-rule + red-flags + skill-priority before it acts.
|
|
12
|
+
*
|
|
13
|
+
* Three emitted shapes (ONE JSON object on stdout, terminated by "\n"):
|
|
14
|
+
* Cursor (CURSOR_PLUGIN_ROOT set) -> {"additional_context": "<escaped>"}
|
|
15
|
+
* Claude Code (CLAUDE_PLUGIN_ROOT set, no Cursor)
|
|
16
|
+
* -> {"hookSpecificOutput":
|
|
17
|
+
* {"hookEventName": "SessionStart",
|
|
18
|
+
* "additionalContext": "<escaped>"}}
|
|
19
|
+
* SDK-standard (neither; e.g. COPILOT_CLI) -> {"additionalContext": "<escaped>"}
|
|
20
|
+
*
|
|
21
|
+
* Branch order: check Cursor BEFORE Claude Code — a Cursor session may also export
|
|
22
|
+
* CLAUDE_PLUGIN_ROOT, and Cursor's own var must win.
|
|
23
|
+
*
|
|
24
|
+
* Byte-format preserved: the original bash uses `printf '{"key": %s}\n' "$ESCAPED"`,
|
|
25
|
+
* which yields a single space after each colon and a trailing newline. We build the
|
|
26
|
+
* envelope by hand (using JSON.stringify only for the escaped string value) so the
|
|
27
|
+
* stdout bytes match the bash original. JSON.parse is whitespace-insensitive, so
|
|
28
|
+
* tests pass either way, but matching the wire format keeps any downstream
|
|
29
|
+
* byte-sensitive consumer happy.
|
|
30
|
+
*
|
|
31
|
+
* Silent-on-failure: every error path exits 0 (matching the bash defensive contract
|
|
32
|
+
* — a partial install or missing SKILL.md must still produce a syntactically valid
|
|
33
|
+
* JSON envelope). The bash never had non-zero exits, so neither do we.
|
|
34
|
+
*
|
|
35
|
+
* Sourcing guard: helpers are exported via module.exports; main() runs only when
|
|
36
|
+
* this file is the entry point (require.main === module). Mirrors the bash
|
|
37
|
+
* `[ "${BASH_SOURCE[0]}" = "$0" ]` pattern so tests can require this module and
|
|
38
|
+
* exercise helpers without firing the emit side-effect.
|
|
39
|
+
*
|
|
40
|
+
* NO-CASCADE (D-06): wired ONLY under SessionStart in hooks/hooks.json. Subagent
|
|
41
|
+
* spawns do not fire SessionStart, so the inject cannot cascade into a subagent's
|
|
42
|
+
* context. (Structural guarantee; behavioral proof = P33.)
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
'use strict';
|
|
46
|
+
|
|
47
|
+
const fs = require('node:fs');
|
|
48
|
+
const path = require('node:path');
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the plugin root the same way the bash original does:
|
|
52
|
+
* CURSOR_PLUGIN_ROOT -> CLAUDE_PLUGIN_ROOT -> dirname(__file__)/..
|
|
53
|
+
* Then normalize Windows backslashes to forward slashes (matching the bash
|
|
54
|
+
* `"${ROOT//\\//}"` parameter substitution) so downstream path joins are stable.
|
|
55
|
+
*
|
|
56
|
+
* @param {NodeJS.ProcessEnv} env Environment (defaults to process.env).
|
|
57
|
+
* @param {string} selfDir Directory of this script (defaults to __dirname).
|
|
58
|
+
* @returns {string} Plugin root, forward-slash normalized.
|
|
59
|
+
*/
|
|
60
|
+
function resolveRoot(env, selfDir) {
|
|
61
|
+
const e = env || process.env;
|
|
62
|
+
const sd = selfDir || __dirname;
|
|
63
|
+
// Bash: ROOT="${CURSOR_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-${SELF_DIR}/..}}"
|
|
64
|
+
// The :- operator treats both unset AND empty as "use the fallback". Match that
|
|
65
|
+
// by treating empty strings as absent here too.
|
|
66
|
+
const cursor = e.CURSOR_PLUGIN_ROOT;
|
|
67
|
+
const claude = e.CLAUDE_PLUGIN_ROOT;
|
|
68
|
+
let root;
|
|
69
|
+
if (cursor && cursor.length > 0) {
|
|
70
|
+
root = cursor;
|
|
71
|
+
} else if (claude && claude.length > 0) {
|
|
72
|
+
root = claude;
|
|
73
|
+
} else {
|
|
74
|
+
root = path.join(sd, '..');
|
|
75
|
+
}
|
|
76
|
+
// Normalize backslashes -> forward slashes (Windows). Matches bash ${ROOT//\\//}.
|
|
77
|
+
return root.replace(/\\/g, '/');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read the SKILL.md bootstrap contract. Defensive: never throw — return '' on any
|
|
82
|
+
* read failure so the emitted JSON envelope is still well-formed. Matches the
|
|
83
|
+
* bash `[[ -r "${SKILL}" ]]` guard (returns "" on missing / unreadable).
|
|
84
|
+
*
|
|
85
|
+
* Bash `CONTENT="$(cat "${SKILL}")"` strips ALL trailing newlines from the file —
|
|
86
|
+
* that's a fundamental POSIX command-substitution rule. To stay byte-identical
|
|
87
|
+
* with the original on every emitted JSON envelope, we replicate that here: trim
|
|
88
|
+
* the trailing run of LF/CRLF after reading. Interior newlines are untouched (so
|
|
89
|
+
* the multi-line round-trip the tests check still works).
|
|
90
|
+
*
|
|
91
|
+
* @param {string} root Plugin root (already normalized).
|
|
92
|
+
* @returns {string} File contents, or '' if missing/unreadable.
|
|
93
|
+
*/
|
|
94
|
+
function readSkill(root) {
|
|
95
|
+
const skillPath = `${root}/skills/using-gdd/SKILL.md`;
|
|
96
|
+
let raw;
|
|
97
|
+
try {
|
|
98
|
+
raw = fs.readFileSync(skillPath, 'utf8');
|
|
99
|
+
} catch {
|
|
100
|
+
return '';
|
|
101
|
+
}
|
|
102
|
+
// Match bash $() trailing-newline stripping. /(\r?\n)+$/ also handles CRLF
|
|
103
|
+
// line endings on Windows checkouts so a single CRLF tail strips to ''.
|
|
104
|
+
return raw.replace(/(?:\r?\n)+$/, '');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* JSON-escape a string and wrap it in double-quotes — the bash original was
|
|
109
|
+
* hand-rolled in pure parameter substitution because the script must run with no
|
|
110
|
+
* jq/python dependency. In Node, JSON.stringify handles every code-point the bash
|
|
111
|
+
* version handled (backslash, double-quote, \t, \r, \n) AND the ones it didn't
|
|
112
|
+
* (other C0 controls, lone surrogates) more correctly. Output includes the
|
|
113
|
+
* surrounding quotes so callers can splice the value directly into a JSON object
|
|
114
|
+
* literal — matching the bash `printf '"%s"' "$s"` contract.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} s
|
|
117
|
+
* @returns {string} e.g. `"hello\nworld"`
|
|
118
|
+
*/
|
|
119
|
+
function escapeForJson(s) {
|
|
120
|
+
return JSON.stringify(String(s == null ? '' : s));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build the harness-specific JSON envelope as the exact byte sequence the bash
|
|
125
|
+
* `printf` produces — single space after each colon, trailing `\n`. Branch order
|
|
126
|
+
* matches the bash: Cursor BEFORE Claude Code so a Cursor session that also
|
|
127
|
+
* exports CLAUDE_PLUGIN_ROOT still gets the Cursor shape.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} escapedJsonValue Already JSON-encoded string including quotes.
|
|
130
|
+
* @param {NodeJS.ProcessEnv} env Environment (defaults to process.env).
|
|
131
|
+
* @returns {string} The full stdout payload, terminated by '\n'.
|
|
132
|
+
*/
|
|
133
|
+
function buildEnvelope(escapedJsonValue, env) {
|
|
134
|
+
const e = env || process.env;
|
|
135
|
+
const cursor = e.CURSOR_PLUGIN_ROOT;
|
|
136
|
+
const claude = e.CLAUDE_PLUGIN_ROOT;
|
|
137
|
+
if (cursor && cursor.length > 0) {
|
|
138
|
+
// Cursor: top-level additional_context.
|
|
139
|
+
return `{"additional_context": ${escapedJsonValue}}\n`;
|
|
140
|
+
}
|
|
141
|
+
if (claude && claude.length > 0) {
|
|
142
|
+
// Claude Code: hookSpecificOutput envelope (mirrors gdd-decision-injector.js).
|
|
143
|
+
return `{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": ${escapedJsonValue}}}\n`;
|
|
144
|
+
}
|
|
145
|
+
// SDK-standard (COPILOT_CLI or none): top-level additionalContext.
|
|
146
|
+
return `{"additionalContext": ${escapedJsonValue}}\n`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* End-to-end main: resolve root, read skill, escape, emit, exit 0. All error
|
|
151
|
+
* paths swallow and still emit a well-formed envelope (silent-on-failure).
|
|
152
|
+
*
|
|
153
|
+
* @returns {number} Always 0.
|
|
154
|
+
*/
|
|
155
|
+
function main() {
|
|
156
|
+
let payload;
|
|
157
|
+
try {
|
|
158
|
+
const root = resolveRoot(process.env, __dirname);
|
|
159
|
+
const content = readSkill(root);
|
|
160
|
+
const escaped = escapeForJson(content);
|
|
161
|
+
payload = buildEnvelope(escaped, process.env);
|
|
162
|
+
} catch {
|
|
163
|
+
// Belt-and-braces: if anything above unexpectedly throws (e.g. JSON.stringify
|
|
164
|
+
// on a hostile input — shouldn't happen with a string), still emit a valid
|
|
165
|
+
// empty envelope so the SessionStart pipeline never breaks.
|
|
166
|
+
payload = buildEnvelope('""', process.env);
|
|
167
|
+
}
|
|
168
|
+
// Use process.stdout.write so we don't get a console.log-added newline on top
|
|
169
|
+
// of the one already in the payload. Matches bash printf '...\n' exactly.
|
|
170
|
+
process.stdout.write(payload);
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
resolveRoot,
|
|
176
|
+
readSkill,
|
|
177
|
+
escapeForJson,
|
|
178
|
+
buildEnvelope,
|
|
179
|
+
main,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Sourcing guard: only run main() when invoked as the entry point. Tests can
|
|
183
|
+
// `require('./inject-using-gdd.cjs')` to exercise helpers in isolation without
|
|
184
|
+
// firing the emit side-effect — mirrors the bash `[ "${BASH_SOURCE[0]}" = "$0" ]`
|
|
185
|
+
// pattern.
|
|
186
|
+
if (require.main === module) {
|
|
187
|
+
process.exit(main());
|
|
188
|
+
}
|