@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
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* get-design-done — update check (Phase 13.3) — Node port
|
|
4
|
+
*
|
|
5
|
+
* Original: hooks/update-check.sh
|
|
6
|
+
* SessionStart hook. Silent-on-failure by policy (D-04): exits 0 on every error path.
|
|
7
|
+
* 24h-cached unauthenticated GET of /releases/latest. Renders .design/update-available.md
|
|
8
|
+
* only when a newer version exists AND it is not dismissed AND stage-guard allows.
|
|
9
|
+
*
|
|
10
|
+
* Sourcing guard (Node equivalent): main() runs only when require.main === module.
|
|
11
|
+
* Helpers are exported for tests. This mirrors the bash `[ "${BASH_SOURCE[0]}" = "$0" ]`
|
|
12
|
+
* pattern — sourcing the .sh in tests loads functions without side effects; requiring
|
|
13
|
+
* this .cjs likewise loads exports without running main.
|
|
14
|
+
*
|
|
15
|
+
* Non-obvious behaviors preserved:
|
|
16
|
+
* - 4-segment semver tuple comparison (handles "v1.0.7.3" off-cadence builds).
|
|
17
|
+
* - LATEST_TAG safety regex /^v?\d+\.\d+(\.\d+)*$/ before trusting fetched data.
|
|
18
|
+
* - Body excerpt: stripped of control chars 0x00-0x08, 0x0B, 0x0C, 0x0E-0x1F and
|
|
19
|
+
* double-quotes (prevents JSON read-back injection — body is display-only).
|
|
20
|
+
* - C_DELTA allowlist gate (major|minor|patch|off-cadence|none → else "unknown").
|
|
21
|
+
* - Atomic writes via .tmp.<pid> + rename for both cache and banner files.
|
|
22
|
+
* - State stage suppression: plan|design|verify silences the banner.
|
|
23
|
+
* - --refresh flag forces fresh fetch regardless of cache age.
|
|
24
|
+
* - GDD_UPDATE_DEBUG=1 enables '[gdd update-check]' prefixed stderr logging.
|
|
25
|
+
* - Cache freshness: mtime < 24h ago (86400s).
|
|
26
|
+
* - Plugin root: CLAUDE_PLUGIN_ROOT env override, else dirname(__dirname).
|
|
27
|
+
* - Windows backslashes in PLUGIN_ROOT normalized to forward slashes.
|
|
28
|
+
*/
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
const fs = require('node:fs');
|
|
32
|
+
const path = require('node:path');
|
|
33
|
+
const https = require('node:https');
|
|
34
|
+
const process = require('node:process');
|
|
35
|
+
|
|
36
|
+
const CACHE_TTL_SECONDS = 86400; // 24h
|
|
37
|
+
|
|
38
|
+
// ---- Logger (silent unless GDD_UPDATE_DEBUG=1) ----
|
|
39
|
+
function log(...args) {
|
|
40
|
+
if (process.env.GDD_UPDATE_DEBUG === '1') {
|
|
41
|
+
process.stderr.write('[gdd update-check] ' + args.join(' ') + '\n');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---- Path helpers — derive paths from cwd + plugin root ----
|
|
46
|
+
function getPluginRoot() {
|
|
47
|
+
let root = process.env.CLAUDE_PLUGIN_ROOT;
|
|
48
|
+
if (!root || root.length === 0) {
|
|
49
|
+
// dirname of __dirname == project root (hooks/.. == plugin root)
|
|
50
|
+
root = path.resolve(__dirname, '..');
|
|
51
|
+
}
|
|
52
|
+
return root.replace(/\\/g, '/');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getPaths(cwd) {
|
|
56
|
+
const designDir = path.join(cwd || process.cwd(), '.design');
|
|
57
|
+
return {
|
|
58
|
+
designDir,
|
|
59
|
+
cache: path.join(designDir, 'update-cache.json'),
|
|
60
|
+
banner: path.join(designDir, 'update-available.md'),
|
|
61
|
+
config: path.join(designDir, 'config.json'),
|
|
62
|
+
state: path.join(designDir, 'STATE.md'),
|
|
63
|
+
pluginJson: path.join(getPluginRoot(), '.claude-plugin', 'plugin.json'),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---- Read current plugin version from .claude-plugin/plugin.json ----
|
|
68
|
+
function readCurrentTag(pluginJsonPath) {
|
|
69
|
+
const p = pluginJsonPath || getPaths().pluginJson;
|
|
70
|
+
try {
|
|
71
|
+
if (!fs.existsSync(p)) return '';
|
|
72
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
73
|
+
const obj = JSON.parse(raw);
|
|
74
|
+
const v = obj && typeof obj.version === 'string' ? obj.version : '';
|
|
75
|
+
return v;
|
|
76
|
+
} catch (e) {
|
|
77
|
+
log('readCurrentTag failed:', e && e.message);
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---- Semver normalizer: "v1.0.7" → [1,0,7,0]; "v1.0.7.3" → [1,0,7,3] ----
|
|
83
|
+
// Returns 4-element array of non-negative integers. Sanitizes each segment to digits only.
|
|
84
|
+
function normalizeSemver(input) {
|
|
85
|
+
if (input == null) return [0, 0, 0, 0];
|
|
86
|
+
let t = String(input);
|
|
87
|
+
if (t.startsWith('v')) t = t.slice(1);
|
|
88
|
+
// strip any -pre/-beta suffix after first hyphen
|
|
89
|
+
const hyphenIdx = t.indexOf('-');
|
|
90
|
+
if (hyphenIdx >= 0) t = t.slice(0, hyphenIdx);
|
|
91
|
+
const parts = t.split('.');
|
|
92
|
+
const out = [0, 0, 0, 0];
|
|
93
|
+
for (let i = 0; i < 4; i++) {
|
|
94
|
+
const seg = parts[i] != null ? String(parts[i]).replace(/[^0-9]/g, '') : '';
|
|
95
|
+
out[i] = seg.length === 0 ? 0 : parseInt(seg, 10);
|
|
96
|
+
if (!Number.isFinite(out[i])) out[i] = 0;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---- Classify delta: compare 4-segment tuples ----
|
|
102
|
+
// Returns { state: 'newer'|'older'|'same', kind: 'major'|'minor'|'patch'|'off-cadence'|'none' }
|
|
103
|
+
function classifyDelta(currentTag, latestTag) {
|
|
104
|
+
const cur = normalizeSemver(currentTag);
|
|
105
|
+
const lat = normalizeSemver(latestTag);
|
|
106
|
+
const kinds = ['major', 'minor', 'patch', 'off-cadence'];
|
|
107
|
+
for (let i = 0; i < 4; i++) {
|
|
108
|
+
if (lat[i] > cur[i]) return { state: 'newer', kind: kinds[i] };
|
|
109
|
+
if (lat[i] < cur[i]) return { state: 'older', kind: kinds[i] };
|
|
110
|
+
}
|
|
111
|
+
return { state: 'same', kind: 'none' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---- Cache freshness: returns true if cache exists and mtime is < 24h ago ----
|
|
115
|
+
function isCacheFresh(cachePath) {
|
|
116
|
+
try {
|
|
117
|
+
const st = fs.statSync(cachePath);
|
|
118
|
+
if (!st || !st.isFile()) return false;
|
|
119
|
+
const now = Math.floor(Date.now() / 1000);
|
|
120
|
+
const mtime = Math.floor(st.mtimeMs / 1000);
|
|
121
|
+
const age = now - mtime;
|
|
122
|
+
return age < CACHE_TTL_SECONDS;
|
|
123
|
+
} catch (e) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---- Fetch latest release. Returns Promise<string> with raw body, or '' on failure. ----
|
|
129
|
+
function fetchLatest() {
|
|
130
|
+
const url = 'https://api.github.com/repos/hegemonart/get-design-done/releases/latest';
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
let done = false;
|
|
133
|
+
const finish = (val) => {
|
|
134
|
+
if (done) return;
|
|
135
|
+
done = true;
|
|
136
|
+
resolve(val);
|
|
137
|
+
};
|
|
138
|
+
try {
|
|
139
|
+
const req = https.get(
|
|
140
|
+
url,
|
|
141
|
+
{
|
|
142
|
+
headers: {
|
|
143
|
+
Accept: 'application/vnd.github+json',
|
|
144
|
+
'User-Agent': 'gdd-update-check',
|
|
145
|
+
},
|
|
146
|
+
timeout: 3000,
|
|
147
|
+
},
|
|
148
|
+
(res) => {
|
|
149
|
+
// Follow one redirect (3xx). GitHub API rarely redirects but be defensive.
|
|
150
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
151
|
+
res.resume();
|
|
152
|
+
const redirected = https.get(
|
|
153
|
+
res.headers.location,
|
|
154
|
+
{
|
|
155
|
+
headers: {
|
|
156
|
+
Accept: 'application/vnd.github+json',
|
|
157
|
+
'User-Agent': 'gdd-update-check',
|
|
158
|
+
},
|
|
159
|
+
timeout: 3000,
|
|
160
|
+
},
|
|
161
|
+
(r2) => {
|
|
162
|
+
if (r2.statusCode < 200 || r2.statusCode >= 300) {
|
|
163
|
+
r2.resume();
|
|
164
|
+
log('fetch redirect status', r2.statusCode);
|
|
165
|
+
return finish('');
|
|
166
|
+
}
|
|
167
|
+
const chunks = [];
|
|
168
|
+
r2.on('data', (c) => chunks.push(c));
|
|
169
|
+
r2.on('end', () => finish(Buffer.concat(chunks).toString('utf8')));
|
|
170
|
+
r2.on('error', (e) => {
|
|
171
|
+
log('redirect read error', e && e.message);
|
|
172
|
+
finish('');
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
redirected.on('error', (e) => {
|
|
177
|
+
log('redirect request error', e && e.message);
|
|
178
|
+
finish('');
|
|
179
|
+
});
|
|
180
|
+
redirected.on('timeout', () => {
|
|
181
|
+
try { redirected.destroy(); } catch (_) {}
|
|
182
|
+
finish('');
|
|
183
|
+
});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
187
|
+
res.resume();
|
|
188
|
+
log('fetch status', res.statusCode);
|
|
189
|
+
return finish('');
|
|
190
|
+
}
|
|
191
|
+
const chunks = [];
|
|
192
|
+
res.on('data', (c) => chunks.push(c));
|
|
193
|
+
res.on('end', () => finish(Buffer.concat(chunks).toString('utf8')));
|
|
194
|
+
res.on('error', (e) => {
|
|
195
|
+
log('read error', e && e.message);
|
|
196
|
+
finish('');
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
req.on('timeout', () => {
|
|
201
|
+
try { req.destroy(); } catch (_) {}
|
|
202
|
+
log('timeout');
|
|
203
|
+
finish('');
|
|
204
|
+
});
|
|
205
|
+
req.on('error', (e) => {
|
|
206
|
+
log('request error', e && e.message);
|
|
207
|
+
finish('');
|
|
208
|
+
});
|
|
209
|
+
} catch (e) {
|
|
210
|
+
log('fetch threw', e && e.message);
|
|
211
|
+
finish('');
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---- Extract tag_name from release JSON. Returns '' on failure. ----
|
|
217
|
+
function extractTag(raw) {
|
|
218
|
+
if (!raw || typeof raw !== 'string') return '';
|
|
219
|
+
try {
|
|
220
|
+
const obj = JSON.parse(raw);
|
|
221
|
+
return obj && typeof obj.tag_name === 'string' ? obj.tag_name : '';
|
|
222
|
+
} catch (e) {
|
|
223
|
+
log('extractTag parse error:', e && e.message);
|
|
224
|
+
return '';
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---- Extract body from release JSON. Slices to 500 chars, strips ctrl chars + double-quotes. ----
|
|
229
|
+
function extractBody(raw) {
|
|
230
|
+
if (!raw || typeof raw !== 'string') return '';
|
|
231
|
+
try {
|
|
232
|
+
const obj = JSON.parse(raw);
|
|
233
|
+
let body = obj && typeof obj.body === 'string' ? obj.body : '';
|
|
234
|
+
body = body.slice(0, 500);
|
|
235
|
+
// Strip control chars: 0x00-0x08, 0x0B, 0x0C, 0x0E-0x1F
|
|
236
|
+
// eslint-disable-next-line no-control-regex
|
|
237
|
+
body = body.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
|
|
238
|
+
// Strip double-quotes (display-only, prevents JSON read-back injection)
|
|
239
|
+
body = body.replace(/"/g, '');
|
|
240
|
+
return body;
|
|
241
|
+
} catch (e) {
|
|
242
|
+
log('extractBody parse error:', e && e.message);
|
|
243
|
+
return '';
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---- Read .design/STATE.md stage field. Returns "brief"|"explore"|"plan"|"design"|"verify"|"" ----
|
|
248
|
+
function readStateStage(statePath) {
|
|
249
|
+
const p = statePath || getPaths().state;
|
|
250
|
+
try {
|
|
251
|
+
if (!fs.existsSync(p)) return '';
|
|
252
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
253
|
+
const lines = raw.split(/\r?\n/);
|
|
254
|
+
for (const line of lines) {
|
|
255
|
+
const m = line.match(/^stage:\s*"?([^"\s]+)"?/);
|
|
256
|
+
if (m) return m[1];
|
|
257
|
+
}
|
|
258
|
+
return '';
|
|
259
|
+
} catch (e) {
|
|
260
|
+
log('readStateStage failed:', e && e.message);
|
|
261
|
+
return '';
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---- Read .design/config.json#update_dismissed. Returns tag string or ''. ----
|
|
266
|
+
function readDismissed(configPath) {
|
|
267
|
+
const p = configPath || getPaths().config;
|
|
268
|
+
try {
|
|
269
|
+
if (!fs.existsSync(p)) return '';
|
|
270
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
271
|
+
try {
|
|
272
|
+
const obj = JSON.parse(raw);
|
|
273
|
+
const v = obj && typeof obj.update_dismissed === 'string' ? obj.update_dismissed : '';
|
|
274
|
+
return v;
|
|
275
|
+
} catch (_) {
|
|
276
|
+
// Fall back to regex extraction if JSON is malformed — matches bash grep behavior.
|
|
277
|
+
const m = raw.match(/"update_dismissed"\s*:\s*"([^"]+)"/);
|
|
278
|
+
return m ? m[1] : '';
|
|
279
|
+
}
|
|
280
|
+
} catch (e) {
|
|
281
|
+
log('readDismissed failed:', e && e.message);
|
|
282
|
+
return '';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---- Validate that a tag string is a safe semver before trusting it (CR-02). ----
|
|
287
|
+
function isSafeSemverTag(tag) {
|
|
288
|
+
if (!tag || typeof tag !== 'string') return false;
|
|
289
|
+
return /^v?\d+\.\d+(\.\d+)*$/.test(tag);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---- Atomic write: write to .tmp.<pid> then rename. On any error, attempt cleanup. ----
|
|
293
|
+
function atomicWrite(targetPath, contents) {
|
|
294
|
+
const tmp = `${targetPath}.tmp.${process.pid}`;
|
|
295
|
+
try {
|
|
296
|
+
fs.writeFileSync(tmp, contents);
|
|
297
|
+
fs.renameSync(tmp, targetPath);
|
|
298
|
+
return true;
|
|
299
|
+
} catch (e) {
|
|
300
|
+
log('atomicWrite failed:', e && e.message);
|
|
301
|
+
try { fs.unlinkSync(tmp); } catch (_) {}
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ---- Build the JSON cache contents. Body excerpt is JSON-string-escaped. ----
|
|
307
|
+
function buildCacheJson({ checkedAt, currentTag, latestTag, deltaKind, isNewer, bodyExcerpt }) {
|
|
308
|
+
// Match the bash output format closely: newlines and 2-space indent.
|
|
309
|
+
// JSON.stringify the body to handle escape sequences cleanly.
|
|
310
|
+
const escapedBody = JSON.stringify(bodyExcerpt || '').slice(1, -1); // strip outer quotes
|
|
311
|
+
const lines = [
|
|
312
|
+
'{',
|
|
313
|
+
` "checked_at": ${checkedAt},`,
|
|
314
|
+
` "current_tag": "${currentTag}",`,
|
|
315
|
+
` "latest_tag": "${latestTag}",`,
|
|
316
|
+
` "delta": "${deltaKind}",`,
|
|
317
|
+
` "is_newer": ${isNewer ? 'true' : 'false'},`,
|
|
318
|
+
` "changelog_excerpt": "${escapedBody}"`,
|
|
319
|
+
'}',
|
|
320
|
+
'',
|
|
321
|
+
];
|
|
322
|
+
return lines.join('\n');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ---- Read cache and extract the four fields the main flow consumes. ----
|
|
326
|
+
function readCache(cachePath) {
|
|
327
|
+
try {
|
|
328
|
+
const raw = fs.readFileSync(cachePath, 'utf8');
|
|
329
|
+
try {
|
|
330
|
+
const obj = JSON.parse(raw);
|
|
331
|
+
return {
|
|
332
|
+
latest_tag: typeof obj.latest_tag === 'string' ? obj.latest_tag : '',
|
|
333
|
+
delta: typeof obj.delta === 'string' ? obj.delta : '',
|
|
334
|
+
is_newer: obj.is_newer === true,
|
|
335
|
+
changelog_excerpt:
|
|
336
|
+
typeof obj.changelog_excerpt === 'string'
|
|
337
|
+
? obj.changelog_excerpt.replace(/\\n/g, '\n')
|
|
338
|
+
: '',
|
|
339
|
+
};
|
|
340
|
+
} catch (_) {
|
|
341
|
+
// Regex fallback to mirror the bash grep+sed pipeline (handles slightly malformed caches).
|
|
342
|
+
const get = (key) => {
|
|
343
|
+
const m = raw.match(new RegExp(`"${key}"\\s*:\\s*"([^"]*)"`));
|
|
344
|
+
return m ? m[1] : '';
|
|
345
|
+
};
|
|
346
|
+
const newerMatch = raw.match(/"is_newer"\s*:\s*(true|false)/);
|
|
347
|
+
return {
|
|
348
|
+
latest_tag: get('latest_tag'),
|
|
349
|
+
delta: get('delta'),
|
|
350
|
+
is_newer: newerMatch ? newerMatch[1] === 'true' : false,
|
|
351
|
+
changelog_excerpt: get('changelog_excerpt').replace(/\\n/g, '\n'),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
} catch (e) {
|
|
355
|
+
log('readCache failed:', e && e.message);
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ---- Banner renderer ----
|
|
361
|
+
function buildBanner({ displayCurrent, latestTag, deltaKind, body }) {
|
|
362
|
+
const bar = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━';
|
|
363
|
+
const lines = [];
|
|
364
|
+
lines.push(bar);
|
|
365
|
+
lines.push(` 📦 Plugin update: ${displayCurrent} → ${latestTag} (${deltaKind})`);
|
|
366
|
+
if (body && body.length > 0) {
|
|
367
|
+
lines.push(body);
|
|
368
|
+
}
|
|
369
|
+
lines.push(' Install: /gdd:update Dismiss: /gdd:check-update --dismiss');
|
|
370
|
+
lines.push(bar);
|
|
371
|
+
lines.push('');
|
|
372
|
+
return lines.join('\n');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ---- Main control flow. argv is process.argv-style; defaults to process.argv. ----
|
|
376
|
+
async function main(argv) {
|
|
377
|
+
argv = argv || process.argv.slice(2);
|
|
378
|
+
const paths = getPaths();
|
|
379
|
+
|
|
380
|
+
// Ensure .design/ exists (belt+suspenders — bootstrap normally creates it).
|
|
381
|
+
try {
|
|
382
|
+
fs.mkdirSync(paths.designDir, { recursive: true });
|
|
383
|
+
} catch (_) {
|
|
384
|
+
return 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const currentTag = readCurrentTag(paths.pluginJson);
|
|
388
|
+
if (!currentTag) {
|
|
389
|
+
log('no plugin.json or no current version parsed');
|
|
390
|
+
return 0;
|
|
391
|
+
}
|
|
392
|
+
const displayCurrent = 'v' + currentTag.replace(/^v/, '');
|
|
393
|
+
|
|
394
|
+
let forceRefresh = false;
|
|
395
|
+
for (const arg of argv) {
|
|
396
|
+
if (arg === '--refresh') forceRefresh = true;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// 1. Populate cache if missing/stale or forced.
|
|
400
|
+
if (forceRefresh || !isCacheFresh(paths.cache)) {
|
|
401
|
+
let raw = '';
|
|
402
|
+
try {
|
|
403
|
+
raw = await fetchLatest();
|
|
404
|
+
} catch (_) {
|
|
405
|
+
raw = '';
|
|
406
|
+
}
|
|
407
|
+
if (raw && raw.length > 0) {
|
|
408
|
+
const latestTagRaw = extractTag(raw);
|
|
409
|
+
const bodyExcerpt = extractBody(raw);
|
|
410
|
+
let latestTag = latestTagRaw;
|
|
411
|
+
if (!isSafeSemverTag(latestTag)) {
|
|
412
|
+
log(`LATEST_TAG '${latestTag}' failed semver safety check — aborting cache write`);
|
|
413
|
+
latestTag = '';
|
|
414
|
+
}
|
|
415
|
+
if (latestTag) {
|
|
416
|
+
const delta = classifyDelta(displayCurrent, latestTag);
|
|
417
|
+
const isNewer = delta.state === 'newer';
|
|
418
|
+
const checkedAt = Math.floor(Date.now() / 1000);
|
|
419
|
+
const json = buildCacheJson({
|
|
420
|
+
checkedAt,
|
|
421
|
+
currentTag: displayCurrent,
|
|
422
|
+
latestTag,
|
|
423
|
+
deltaKind: delta.kind,
|
|
424
|
+
isNewer,
|
|
425
|
+
bodyExcerpt,
|
|
426
|
+
});
|
|
427
|
+
atomicWrite(paths.cache, json);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// 2. Read cache (whether freshly written or still valid).
|
|
433
|
+
if (!fs.existsSync(paths.cache)) {
|
|
434
|
+
return 0; // no cache, nothing to do — silent exit
|
|
435
|
+
}
|
|
436
|
+
const cache = readCache(paths.cache);
|
|
437
|
+
if (!cache) return 0;
|
|
438
|
+
|
|
439
|
+
const cLatest = cache.latest_tag;
|
|
440
|
+
let cDelta = cache.delta;
|
|
441
|
+
// Allowlist-gate cDelta before it reaches any banner context (WR-04).
|
|
442
|
+
const allowedDeltas = new Set(['major', 'minor', 'patch', 'off-cadence', 'none']);
|
|
443
|
+
if (!allowedDeltas.has(cDelta)) cDelta = 'unknown';
|
|
444
|
+
const cNewer = cache.is_newer === true;
|
|
445
|
+
const cBody = cache.changelog_excerpt || '';
|
|
446
|
+
|
|
447
|
+
// 3. Gate: if cache says not newer, remove any stale banner and exit.
|
|
448
|
+
if (!cNewer) {
|
|
449
|
+
try { fs.unlinkSync(paths.banner); } catch (_) {}
|
|
450
|
+
return 0;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// 4. Dismissal gate (D-13): if user already dismissed this exact tag, suppress.
|
|
454
|
+
const dismissed = readDismissed(paths.config);
|
|
455
|
+
if (dismissed && dismissed === cLatest) {
|
|
456
|
+
try { fs.unlinkSync(paths.banner); } catch (_) {}
|
|
457
|
+
return 0;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// 5. State-machine guard (D-11/D-12): suppress during plan|design|verify.
|
|
461
|
+
const stage = readStateStage(paths.state);
|
|
462
|
+
if (stage === 'plan' || stage === 'design' || stage === 'verify') {
|
|
463
|
+
try { fs.unlinkSync(paths.banner); } catch (_) {}
|
|
464
|
+
return 0;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 6. All gates passed — render the banner atomically.
|
|
468
|
+
const banner = buildBanner({
|
|
469
|
+
displayCurrent,
|
|
470
|
+
latestTag: cLatest,
|
|
471
|
+
deltaKind: cDelta,
|
|
472
|
+
body: cBody,
|
|
473
|
+
});
|
|
474
|
+
atomicWrite(paths.banner, banner);
|
|
475
|
+
|
|
476
|
+
return 0;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ---- Exports for tests (mirrors bash sourcing pattern) ----
|
|
480
|
+
module.exports = {
|
|
481
|
+
// Public helpers (named to match the bash function names where reasonable).
|
|
482
|
+
normalizeSemver,
|
|
483
|
+
classifyDelta,
|
|
484
|
+
isCacheFresh,
|
|
485
|
+
readCurrentTag,
|
|
486
|
+
readStateStage,
|
|
487
|
+
readDismissed,
|
|
488
|
+
fetchLatest,
|
|
489
|
+
extractTag,
|
|
490
|
+
extractBody,
|
|
491
|
+
// Additional helpers useful for tests / orchestrator.
|
|
492
|
+
isSafeSemverTag,
|
|
493
|
+
atomicWrite,
|
|
494
|
+
buildCacheJson,
|
|
495
|
+
buildBanner,
|
|
496
|
+
readCache,
|
|
497
|
+
getPaths,
|
|
498
|
+
getPluginRoot,
|
|
499
|
+
main,
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
// ---- Entry-point guard: only run main when invoked directly (not when required). ----
|
|
503
|
+
if (require.main === module) {
|
|
504
|
+
// Always exit 0 (silent-on-failure). Promise rejections also exit 0.
|
|
505
|
+
main(process.argv.slice(2))
|
|
506
|
+
.then(() => process.exit(0))
|
|
507
|
+
.catch((e) => {
|
|
508
|
+
log('main threw:', e && e.message);
|
|
509
|
+
process.exit(0);
|
|
510
|
+
});
|
|
511
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hegemonart/get-design-done",
|
|
3
|
-
"version": "1.57.
|
|
4
|
-
"description": "A design-quality pipeline for AI coding agents: brief, plan,
|
|
3
|
+
"version": "1.57.2",
|
|
4
|
+
"description": "A design-quality pipeline for AI coding agents: brief, explore, plan, design, and verify UI work against your design system.",
|
|
5
5
|
"author": "Hegemon",
|
|
6
6
|
"homepage": "https://github.com/hegemonart/get-design-done",
|
|
7
7
|
"repository": {
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"scripts/lib/",
|
|
28
28
|
"scripts/cli/",
|
|
29
29
|
"scripts/install.cjs",
|
|
30
|
+
"scripts/injection-patterns.cjs",
|
|
31
|
+
"scripts/bootstrap.cjs",
|
|
30
32
|
"SKILL.md",
|
|
31
33
|
"README.md",
|
|
32
34
|
"CHANGELOG.md",
|
|
@@ -84,10 +86,15 @@
|
|
|
84
86
|
"validate:schemas": "node --experimental-strip-types scripts/validate-schemas.ts",
|
|
85
87
|
"validate:frontmatter": "node --experimental-strip-types scripts/validate-frontmatter.ts agents/",
|
|
86
88
|
"detect:stale-refs": "node scripts/detect-stale-refs.cjs",
|
|
89
|
+
"validate:feature-counts": "node scripts/check-feature-counts.cjs",
|
|
90
|
+
"validate:registry-tiers": "node scripts/validate-registry-tiers.cjs",
|
|
91
|
+
"validate:no-internal-refs": "node scripts/validate-no-internal-refs.cjs",
|
|
92
|
+
"validate:cache-tiers": "node scripts/check-cache-tiers.cjs",
|
|
87
93
|
"scan:injection": "node scripts/run-injection-scanner-ci.cjs",
|
|
88
94
|
"scan:outbound": "node scripts/scan-outbound-network.cjs",
|
|
89
95
|
"scan:ws-bind": "node scripts/scan-ws-bind.cjs",
|
|
90
96
|
"test:size-budget": "node --test test/suite/agent-size-budget.test.cjs",
|
|
97
|
+
"validate:skill-surface": "node --test test/suite/skill-surface-sync.test.cjs",
|
|
91
98
|
"release:extract-changelog": "node scripts/extract-changelog-section.cjs",
|
|
92
99
|
"verify:version-sync": "node scripts/verify-version-sync.cjs",
|
|
93
100
|
"typecheck:session-runner": "tsc --noEmit",
|
|
@@ -7,10 +7,7 @@ This is the canonical template for the design pipeline's runtime state file.
|
|
|
7
7
|
- Every subsequent stage (discover, plan, design, verify) reads `.design/STATE.md` at entry and updates it at completion, per the Write Contract below.
|
|
8
8
|
- `.design/` is gitignored (not distributed with the plugin); only this template ships.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
- `.planning/STATE.md` is GSD development state - used by the developers building this plugin.
|
|
12
|
-
- `.design/STATE.md` is pipeline runtime state - used by the pipeline when it runs in a user's project.
|
|
13
|
-
- Keep them strictly separate. Cross-references between them are deferred to Phase 6 per CONTEXT.md.
|
|
10
|
+
`.design/` is the runtime state for the design pipeline as it runs in a user's project. The plugin's own internal development workspace lives outside this directory and never reaches a user install.
|
|
14
11
|
|
|
15
12
|
---
|
|
16
13
|
|
|
@@ -47,7 +44,7 @@ skipped_stages: ""
|
|
|
47
44
|
<decisions>
|
|
48
45
|
<!-- Filled by discover stage. Format: -->
|
|
49
46
|
<!-- D-01: [decision text] (locked | tentative) -->
|
|
50
|
-
<!--
|
|
47
|
+
<!-- In team mode, an optional attribution suffix records provenance for multi-author merges: -->
|
|
51
48
|
<!-- D-01: [decision text] (locked | tentative) [author=<git-user> co-author=<gdd-instance-id>] -->
|
|
52
49
|
<!-- The suffix is optional + backward-compatible; see reference/multi-author-model.md. -->
|
|
53
50
|
</decisions>
|
|
@@ -59,7 +56,7 @@ skipped_stages: ""
|
|
|
59
56
|
</must_haves>
|
|
60
57
|
|
|
61
58
|
<prototyping>
|
|
62
|
-
<!--
|
|
59
|
+
<!-- Appended by sketch-wrap-up / spike-wrap-up + the prototype-gate. -->
|
|
63
60
|
<!-- Three child element types, each on its own line: -->
|
|
64
61
|
<!-- <sketch slug="…" cycle="…" decision="D-XX" status="resolved"/> -->
|
|
65
62
|
<!-- <spike slug="…" cycle="…" decision="D-XX" verdict="yes|no|partial" status="resolved"/> -->
|
|
@@ -69,7 +66,7 @@ skipped_stages: ""
|
|
|
69
66
|
</prototyping>
|
|
70
67
|
|
|
71
68
|
<quality_gate>
|
|
72
|
-
<!--
|
|
69
|
+
<!-- Written by the quality-gate skill (Stage 4.5). -->
|
|
73
70
|
<!-- Houses a single most-recent <run/> entry — append-mode would be overkill. -->
|
|
74
71
|
<!-- Format: -->
|
|
75
72
|
<!-- <run started_at="…" completed_at="…" status="pass|fail|timeout|skipped" iteration="N" commands_run="lint,typecheck,test"/> -->
|
|
@@ -173,22 +170,22 @@ Discover stage populates with observable behaviors. Verify stage updates status.
|
|
|
173
170
|
|
|
174
171
|
### `<prototyping>`
|
|
175
172
|
|
|
176
|
-
|
|
173
|
+
A checkpoint log - NOT a stage. Tracks sketch and spike outcomes plus cycle-scoped skip suppressions for the prototype gate.
|
|
177
174
|
|
|
178
175
|
- `<sketch slug=… cycle=… decision=D-XX status=resolved/>` - written by `sketch-wrap-up` after a sketch resolves into a D-XX decision.
|
|
179
176
|
- `<spike slug=… cycle=… decision=D-XX verdict=yes|no|partial status=resolved/>` - written by `spike-wrap-up` after a spike resolves; `verdict` captures the answer.
|
|
180
|
-
- `<skipped at=… cycle=… reason=…/>` - written by the prototype gate when the user declines to sketch/spike at a firing point.
|
|
177
|
+
- `<skipped at=… cycle=… reason=…/>` - written by the prototype gate when the user declines to sketch/spike at a firing point. The entry provides cycle-scoped suppression: it suppresses re-asking for the rest of the named cycle.
|
|
181
178
|
|
|
182
179
|
The block is **optional** - fresh STATE.md files do not carry it. The serializer omits the block entirely when no entries exist; appending the first entry is what materializes the block.
|
|
183
180
|
|
|
184
181
|
### `<quality_gate>`
|
|
185
182
|
|
|
186
|
-
|
|
183
|
+
Captures the most recent run of the Stage 4.5 quality gate (lint / typecheck / test / visual-regression) between Design and Verify. The block houses a single self-closing `<run/>` element - append-mode is overkill, so each gate completion overwrites the entry.
|
|
187
184
|
|
|
188
185
|
- `started_at` - ISO 8601 at which the parallel command run entered.
|
|
189
186
|
- `completed_at` - ISO 8601 at which the gate produced its terminal status.
|
|
190
|
-
- `status` - `pass | fail | timeout | skipped`. `pass` clears the verify-entry gate; `fail` blocks; `timeout` warns + proceeds
|
|
191
|
-
- `iteration` - non-negative integer fix-loop count
|
|
187
|
+
- `status` - `pass | fail | timeout | skipped`. `pass` clears the verify-entry gate; `fail` blocks; `timeout` warns + proceeds; `skipped` indicates the detection chain resolved zero commands.
|
|
188
|
+
- `iteration` - non-negative integer fix-loop count. `1` = single clean pass; `N === max_iters` with `status === 'fail'` = bounded exhaustion.
|
|
192
189
|
- `commands_run` - comma-separated names of the commands actually executed in Step 2 (e.g., `lint,typecheck,test`). Empty string when `status === 'skipped'`.
|
|
193
190
|
|
|
194
191
|
The block is **optional** - fresh STATE.md files do not carry it. The serializer omits the block entirely when `quality_gate === null`; the SKILL writes the first `<run/>` to materialize it.
|
|
@@ -245,7 +242,7 @@ Every stage that runs in the pipeline MUST follow this contract when reading and
|
|
|
245
242
|
|
|
246
243
|
---
|
|
247
244
|
|
|
248
|
-
## Notes for
|
|
245
|
+
## Notes for implementors
|
|
249
246
|
|
|
250
247
|
- Do not add new top-level XML sections without updating this template.
|
|
251
248
|
- The write contract is non-negotiable - stages that skip the read-at-entry step break resume.
|
|
@@ -247,7 +247,7 @@ Attach to findings under the Visual Hierarchy pillar that relate to compositiona
|
|
|
247
247
|
|
|
248
248
|
### `i18n_readiness`
|
|
249
249
|
|
|
250
|
-
Attach to findings under the Accessibility pillar (for WCAG 3.1.1 / 3.1.2 violations) or under the Anti-Pattern Compliance pillar (for hardcoded-string / overflow-at-+40% defects). Emitted by the `agents/design-verifier.md` §i18n probes section
|
|
250
|
+
Attach to findings under the Accessibility pillar (for WCAG 3.1.1 / 3.1.2 violations) or under the Anti-Pattern Compliance pillar (for hardcoded-string / overflow-at-+40% defects). Emitted by the `agents/design-verifier.md` §i18n probes section. See [`./i18n.md`](./i18n.md) §WCAG i18n + §Verifier Integration Spec. Does NOT change pillar weights or scores.
|
|
251
251
|
|
|
252
252
|
### `verb_axes` (anti-slop)
|
|
253
253
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Cache-Tier Doctrine
|
|
2
|
+
|
|
3
|
+
The Anthropic prompt cache has a 5-minute TTL and is invalidated by a single byte change in the prefix. Every agent body imports `reference/shared-preamble.md` as the first thing it reads; that import expands to a flat byte block at the top of every agent's prompt. Stable bytes there mean stable cache; churning bytes there mean every agent's cache misses on every spawn.
|
|
4
|
+
|
|
5
|
+
This doctrine pins the bytes that have to stay stable.
|
|
6
|
+
|
|
7
|
+
## The four tiers
|
|
8
|
+
|
|
9
|
+
| Tier | Lifetime | Files | Edit cost |
|
|
10
|
+
|------|----------|-------|-----------|
|
|
11
|
+
| **L0** | Framework-invariant | `reference/meta-rules.md` + `reference/shared-preamble.md` | Invalidates cache for every agent simultaneously. CI gate enforces SHA-256 stability; ratchet via `--rebaseline` only when the framework genuinely changes. |
|
|
12
|
+
| **L1** | Per-agent stable | Agent body itself (role, tools contract, output format) | Invalidates cache for the one agent. Edits are routine. |
|
|
13
|
+
| **L2** | Per-spawn dynamic | `<required_reading>` block + per-invocation prompt | Never caches. Edits are free. |
|
|
14
|
+
| **L3** | Reference-only | `reference/*.md` other than L0 | Loaded per-agent on demand; not part of the cache prefix. Edits do not affect cache. |
|
|
15
|
+
|
|
16
|
+
## The L0 contract
|
|
17
|
+
|
|
18
|
+
1. **Two files, no more.** L0 = `reference/meta-rules.md` (framework-invariant rules: required reading, writes protocol, deviation handling, completion markers, context-exhaustion + budget) + `reference/shared-preamble.md` (the aggregator that imports meta-rules and contributes the design-family pillar lists). Any third L0 file requires a doctrine update.
|
|
19
|
+
2. **Byte-stable across cycles.** Section heading text, prose order, ordering of bulleted lists, even whitespace runs; all are essential. An edit that "just rewords a sentence" still invalidates cache for every agent for one session per agent.
|
|
20
|
+
3. **CI gate enforces stability.** `scripts/check-cache-tiers.cjs` computes SHA-256 of each L0 file and compares to `test/fixtures/baselines/l0-hashes.json`. Drift fails the build. A real edit requires `--rebaseline` and a baseline-hash commit alongside the L0 edit.
|
|
21
|
+
4. **Pre-warming exists for legitimate L0 edits.** Run `/gdd:warm-cache` after an L0 edit lands to pre-load the new prefix so the next real spawn is a hit rather than a miss.
|
|
22
|
+
|
|
23
|
+
## What goes where
|
|
24
|
+
|
|
25
|
+
- A new design-pillar list shared by 5 design-family skills → `shared-preamble.md` (L0). Costs cache miss; ratchet the L0 baseline.
|
|
26
|
+
- A new validator rule for a single agent → that agent body (L1). No L0 impact.
|
|
27
|
+
- A per-invocation context block; the brief, the must-haves, the user's specific request → `<required_reading>` (L2). No cache implications either way.
|
|
28
|
+
- A reference catalog of heuristics, anti-patterns, WCAG thresholds → `reference/*.md` (L3). No cache implications.
|
|
29
|
+
|
|
30
|
+
## Verification
|
|
31
|
+
|
|
32
|
+
- `npm run validate:registry-tiers` - confirms `registry.json` entries' `tier` field is one of L0/L1/L2/L3.
|
|
33
|
+
- `npm run validate:cache-tiers` - confirms L0 file SHA-256 matches the baseline.
|
|
34
|
+
- Both run in CI as part of the default test suite.
|
|
35
|
+
|
|
36
|
+
## When to ratchet
|
|
37
|
+
|
|
38
|
+
Ratchet (`--rebaseline`) only when:
|
|
39
|
+
|
|
40
|
+
1. Framework invariant changes (rare).
|
|
41
|
+
2. A design-pillar list shared by ≥3 skills needs updating.
|
|
42
|
+
3. A factual claim that EVERY agent must know becomes false.
|
|
43
|
+
|
|
44
|
+
If any of those three is true: edit the L0 file, run `node scripts/check-cache-tiers.cjs --rebaseline`, commit both the L0 edit and the updated baseline. Reviewers see the L0-hash diff and understand the cache cost.
|
|
45
|
+
|
|
46
|
+
If none of the three is true: the edit belongs in L1 (an agent body), L2 (a per-spawn block), or L3 (a reference doc). Reject the L0 edit; relocate.
|