@hegemonart/get-design-done 1.46.0 → 1.48.0

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 (35) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +94 -0
  4. package/README.md +4 -0
  5. package/SKILL.md +2 -1
  6. package/agents/brief-auditor.md +147 -0
  7. package/agents/copy-auditor.md +215 -0
  8. package/agents/design-auditor.md +13 -3
  9. package/agents/design-debt-crawler.md +269 -0
  10. package/agents/design-fixer.md +2 -0
  11. package/agents/quality-gate-runner.md +11 -10
  12. package/dist/claude-code/.claude/skills/brief/SKILL.md +17 -0
  13. package/dist/claude-code/.claude/skills/live/SKILL.md +98 -0
  14. package/dist/claude-code/.claude/skills/quality-gate/SKILL.md +2 -2
  15. package/hooks/gdd-a11y-gate.js +119 -0
  16. package/hooks/hooks.json +8 -0
  17. package/package.json +1 -1
  18. package/reference/brief-quality-rubric.md +98 -0
  19. package/reference/copy-quality.md +135 -0
  20. package/reference/debt-categories.md +148 -0
  21. package/reference/live-mode-integration.md +80 -0
  22. package/reference/registry.json +28 -0
  23. package/reference/schemas/events.schema.json +1 -1
  24. package/reference/schemas/live-session.schema.json +64 -0
  25. package/scripts/lib/live/bandit-feed.cjs +64 -0
  26. package/scripts/lib/live/events.cjs +86 -0
  27. package/scripts/lib/live/harness-mode.cjs +93 -0
  28. package/scripts/lib/live/postcheck.cjs +158 -0
  29. package/scripts/lib/live/runtime.cjs +233 -0
  30. package/scripts/lib/live/scope-guard.cjs +145 -0
  31. package/scripts/lib/live/session-store.cjs +364 -0
  32. package/scripts/lib/manifest/skills.json +8 -0
  33. package/skills/brief/SKILL.md +17 -0
  34. package/skills/live/SKILL.md +98 -0
  35. package/skills/quality-gate/SKILL.md +2 -2
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+ /**
3
+ * scripts/lib/live/scope-guard.cjs — Phase 47 (Live Mode) write-scope guard.
4
+ *
5
+ * `/gdd:live` lets the agent rewrite the source files behind a picked DOM
6
+ * element. To keep that powerful loop safe, EVERY write the live session makes
7
+ * must be inside an explicitly enumerated allow-set:
8
+ *
9
+ * (a) the session's own bookkeeping: anything under `.design/live-sessions/`
10
+ * and `.design/telemetry/` (relative to projectRoot), and
11
+ * (b) the `implicated` source files — the concrete files the picked element
12
+ * maps to (passed in by the caller from the element→source mapping).
13
+ *
14
+ * Anything else — a random repo file, a `package.json`, a `..` escape out of the
15
+ * project — is rejected. This is the runtime backstop for the same intent the
16
+ * Editor/Write protected-paths list expresses at author time.
17
+ *
18
+ * Resolution rules:
19
+ * - All paths are resolved to absolute with path.resolve so `..` segments are
20
+ * collapsed BEFORE comparison (a `../../etc/passwd` can never sneak past a
21
+ * string-prefix check).
22
+ * - Containment uses a normalized, separator-terminated prefix so that
23
+ * `.design/live-sessions-evil/x` does NOT count as inside
24
+ * `.design/live-sessions/` (sibling-directory false positive).
25
+ * - On case-insensitive / Windows filesystems we still compare case-sensitive
26
+ * prefixes after resolve; the implicated set is matched by exact resolved
27
+ * path, which is the conservative choice (a guard that is too strict fails
28
+ * closed, never open).
29
+ *
30
+ * Pure, dependency-free CommonJS (`path` only — no `fs`, no network, no clock).
31
+ * Cross-platform via path.resolve / path.join.
32
+ */
33
+
34
+ const path = require('path');
35
+
36
+ /** The two project-relative directory roots always writable by a live session. */
37
+ const ALWAYS_ALLOWED_SUBDIRS = Object.freeze([
38
+ path.join('.design', 'live-sessions'),
39
+ path.join('.design', 'telemetry'),
40
+ ]);
41
+
42
+ /**
43
+ * True when `child` is the same path as, or nested inside, `parentDir`.
44
+ * Both inputs must already be absolute + resolved. Uses a separator-terminated
45
+ * prefix so sibling dirs that share a name prefix are NOT treated as inside.
46
+ */
47
+ function isWithin(parentDir, child) {
48
+ if (child === parentDir) return true;
49
+ const withSep = parentDir.endsWith(path.sep) ? parentDir : parentDir + path.sep;
50
+ return child.startsWith(withSep);
51
+ }
52
+
53
+ /**
54
+ * Normalize the allowed write-set for a live session into a flat, resolved
55
+ * structure the predicates can test against.
56
+ *
57
+ * @param {object} args
58
+ * @param {string} args.projectRoot project root (required to anchor the always-allowed dirs)
59
+ * @param {Array<string>} [args.implicated] source files the picked element maps to
60
+ * @returns {{ dirs: string[], files: Set<string> }}
61
+ * `dirs` — resolved directory prefixes any descendant of which is allowed.
62
+ * `files` — resolved exact file paths that are allowed.
63
+ */
64
+ function enumerateScope(args = {}) {
65
+ const { projectRoot } = args;
66
+ if (!projectRoot) throw new TypeError('enumerateScope: projectRoot is required');
67
+ const root = path.resolve(projectRoot);
68
+
69
+ const dirs = ALWAYS_ALLOWED_SUBDIRS.map((rel) => path.resolve(root, rel));
70
+
71
+ const files = new Set();
72
+ const implicated = Array.isArray(args.implicated) ? args.implicated : [];
73
+ for (const f of implicated) {
74
+ if (f == null || !String(f).length) continue;
75
+ // Implicated paths may be absolute or relative-to-projectRoot; resolve both
76
+ // against the root so callers can pass either form.
77
+ files.add(path.resolve(root, String(f)));
78
+ }
79
+ return { dirs, files };
80
+ }
81
+
82
+ /**
83
+ * Whether `targetPath` is inside the enumerated scope.
84
+ *
85
+ * @param {object} args
86
+ * @param {string} args.projectRoot
87
+ * @param {string} args.targetPath the path about to be written
88
+ * @param {Array<string>} [args.implicated] element→source files
89
+ * @returns {boolean}
90
+ */
91
+ function isInScope(args = {}) {
92
+ const { projectRoot, targetPath } = args;
93
+ if (!projectRoot) throw new TypeError('isInScope: projectRoot is required');
94
+ if (targetPath == null || !String(targetPath).length) {
95
+ throw new TypeError('isInScope: targetPath is required');
96
+ }
97
+ const root = path.resolve(projectRoot);
98
+ const target = path.resolve(root, String(targetPath));
99
+ const { dirs, files } = enumerateScope({ projectRoot, implicated: args.implicated });
100
+
101
+ if (files.has(target)) return true;
102
+ for (const d of dirs) {
103
+ if (isWithin(d, target)) return true;
104
+ }
105
+ return false;
106
+ }
107
+
108
+ /**
109
+ * Throw unless `targetPath` is inside the enumerated scope. The error message
110
+ * enumerates the allowed set so a violation is diagnosable.
111
+ *
112
+ * @param {object} args same shape as isInScope
113
+ * @returns {{ targetPath: string, resolved: string }} on success
114
+ */
115
+ function assertInScope(args = {}) {
116
+ const { projectRoot, targetPath } = args;
117
+ if (!projectRoot) throw new TypeError('assertInScope: projectRoot is required');
118
+ if (targetPath == null || !String(targetPath).length) {
119
+ throw new TypeError('assertInScope: targetPath is required');
120
+ }
121
+ if (isInScope(args)) {
122
+ const root = path.resolve(projectRoot);
123
+ return { targetPath: String(targetPath), resolved: path.resolve(root, String(targetPath)) };
124
+ }
125
+ const root = path.resolve(projectRoot);
126
+ const resolved = path.resolve(root, String(targetPath));
127
+ const { dirs, files } = enumerateScope({ projectRoot, implicated: args.implicated });
128
+ const allowed = [
129
+ ...dirs.map((d) => `${d}${path.sep}* (always-allowed)`),
130
+ ...[...files].map((f) => `${f} (implicated)`),
131
+ ];
132
+ throw new Error(
133
+ `scope-guard: refusing to write "${resolved}" — outside the live-session write scope.\n` +
134
+ `Allowed:\n ${allowed.length ? allowed.join('\n ') : '(none)'}`,
135
+ );
136
+ }
137
+
138
+ module.exports = {
139
+ assertInScope,
140
+ isInScope,
141
+ enumerateScope,
142
+ // exported for callers + tests
143
+ isWithin,
144
+ ALWAYS_ALLOWED_SUBDIRS,
145
+ };
@@ -0,0 +1,364 @@
1
+ 'use strict';
2
+ /**
3
+ * scripts/lib/live/session-store.cjs — Phase 47 (Live Mode) session persistence.
4
+ *
5
+ * The substrate behind `/gdd:live`: the user picks a DOM element on a dev
6
+ * server, the agent generates N design variants, the user accepts/discards, and
7
+ * the session survives a crash / `--resume`. This module owns the per-session
8
+ * record on disk at:
9
+ *
10
+ * <projectRoot>/.design/live-sessions/<session-id>.json
11
+ *
12
+ * Each record is a single JSON document conforming to
13
+ * reference/schemas/live-session.schema.json — `{schema_version, session_id,
14
+ * started_at, ended_at|null, status, url?, dev_server?, events:[...]}` — where
15
+ * `events` is an append-only log of `{kind:'pick'|'generate'|'accept'|'discard',
16
+ * at, ...}` entries.
17
+ *
18
+ * Design constraints (mirrors scripts/lib/pin/store.cjs + ds-arms store):
19
+ * - Pure, dependency-free CommonJS. Only `fs` + `path`. No network.
20
+ * - NO top-level Date.now()/Math.random()/new Date(). Time + id are injected
21
+ * (`now` / `id` / `sessionId`) so every behaviour is deterministic and
22
+ * hermetically testable. The clock is only ever read from caller input.
23
+ * - Atomic writes: contents go to `<dest>.tmp` then fs.renameSync into place,
24
+ * so an interrupted write never leaves a half-written session (and never a
25
+ * stray `.tmp`).
26
+ * - Every entry point takes an explicit `projectRoot`; all paths are built
27
+ * with path.join so the module is cross-platform.
28
+ *
29
+ * Ships in the npm package (scripts/lib/ is in package.json `files`), so it must
30
+ * stay runtime-safe — no dev-only requires.
31
+ */
32
+
33
+ const fs = require('fs');
34
+ const path = require('path');
35
+
36
+ const SCHEMA_VERSION = '47.0';
37
+
38
+ /** Statuses an active session can hold. */
39
+ const STATUS_IN_PROGRESS = 'in_progress';
40
+ const STATUS_COMPLETED = 'completed';
41
+ const STATUS_ABANDONED = 'abandoned';
42
+
43
+ /** Event kinds recognised in the append-only log. */
44
+ const EVENT_KINDS = Object.freeze(['pick', 'generate', 'accept', 'discard']);
45
+
46
+ /** Directory (relative to projectRoot) that holds every session record. */
47
+ const SESSIONS_SUBDIR = path.join('.design', 'live-sessions');
48
+
49
+ /**
50
+ * Absolute path to the live-sessions directory for a project.
51
+ * @param {string} projectRoot
52
+ * @returns {string}
53
+ */
54
+ function sessionsDir(projectRoot) {
55
+ if (!projectRoot) throw new TypeError('session-store: projectRoot is required');
56
+ return path.join(projectRoot, SESSIONS_SUBDIR);
57
+ }
58
+
59
+ /**
60
+ * Absolute path to a single session record.
61
+ * @param {string} projectRoot
62
+ * @param {string} sessionId
63
+ * @returns {string}
64
+ */
65
+ function sessionPath(projectRoot, sessionId) {
66
+ if (!sessionId) throw new TypeError('session-store: sessionId is required');
67
+ // Guard against path traversal in the id — a session id is a flat token, never
68
+ // a path. We reject any separator or `..` rather than silently joining it.
69
+ const id = String(sessionId);
70
+ if (id.includes('/') || id.includes('\\') || id === '.' || id === '..') {
71
+ throw new Error(`session-store: invalid sessionId "${id}" (must not contain path separators)`);
72
+ }
73
+ return path.join(sessionsDir(projectRoot), `${id}.json`);
74
+ }
75
+
76
+ /** Atomic write: write to `<dest>.tmp` then rename into place. */
77
+ function atomicWriteJson(dest, value) {
78
+ const dir = path.dirname(dest);
79
+ fs.mkdirSync(dir, { recursive: true });
80
+ const tmp = `${dest}.tmp`;
81
+ fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
82
+ try {
83
+ fs.renameSync(tmp, dest);
84
+ } catch (e) {
85
+ // Never leave a stray .tmp behind on a failed rename.
86
+ try { fs.unlinkSync(tmp); } catch { /* ignore */ }
87
+ throw e;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Derive a stable session id WITHOUT touching a global clock or RNG. The id is
93
+ * built purely from caller-supplied inputs so it is deterministic in tests.
94
+ *
95
+ * Preference order:
96
+ * 1. explicit `id` (the injectable)
97
+ * 2. a slug of `now` (the injected timestamp) — e.g. "2026-06-03T01:02:03Z"
98
+ * becomes "session-2026-06-03t01-02-03z"
99
+ *
100
+ * @param {{ id?: string, now?: string }} args
101
+ * @returns {string}
102
+ */
103
+ function deriveSessionId(args = {}) {
104
+ if (args.id != null && String(args.id).length) return String(args.id);
105
+ if (args.now != null && String(args.now).length) {
106
+ const slug = String(args.now)
107
+ .toLowerCase()
108
+ .replace(/[^a-z0-9]+/g, '-')
109
+ .replace(/^-+|-+$/g, '');
110
+ if (slug.length) return `session-${slug}`;
111
+ }
112
+ throw new TypeError(
113
+ 'session-store.newSession: provide an explicit sessionId, or inject `id`/`now` to derive one ' +
114
+ '(no internal clock/RNG by design)',
115
+ );
116
+ }
117
+
118
+ /**
119
+ * Create a new session record on disk.
120
+ *
121
+ * @param {object} args
122
+ * @param {string} args.projectRoot
123
+ * @param {string} [args.sessionId] explicit id; if omitted, derived from `id`/`now`
124
+ * @param {string} [args.id] injectable id source (used when sessionId omitted)
125
+ * @param {string} [args.now] ISO timestamp for started_at + id derivation
126
+ * @param {string} [args.url] the page the element was picked from
127
+ * @param {string|object} [args.devServer] dev-server descriptor (url/port/command)
128
+ * @returns {{ sessionId: string, path: string, session: object }}
129
+ */
130
+ function newSession(args = {}) {
131
+ const { projectRoot } = args;
132
+ if (!projectRoot) throw new TypeError('newSession: projectRoot is required');
133
+
134
+ const sessionId = args.sessionId != null && String(args.sessionId).length
135
+ ? String(args.sessionId)
136
+ : deriveSessionId({ id: args.id, now: args.now });
137
+
138
+ const startedAt = args.now != null && String(args.now).length ? String(args.now) : null;
139
+ if (!startedAt) {
140
+ throw new TypeError(
141
+ 'newSession: `now` (ISO timestamp) is required for started_at (no internal clock by design)',
142
+ );
143
+ }
144
+
145
+ const session = {
146
+ schema_version: SCHEMA_VERSION,
147
+ session_id: sessionId,
148
+ status: STATUS_IN_PROGRESS,
149
+ started_at: startedAt,
150
+ ended_at: null,
151
+ events: [],
152
+ };
153
+ if (args.url != null) session.url = String(args.url);
154
+ if (args.devServer != null) session.dev_server = args.devServer;
155
+
156
+ const dest = sessionPath(projectRoot, sessionId);
157
+ atomicWriteJson(dest, session);
158
+ return { sessionId, path: dest, session };
159
+ }
160
+
161
+ /**
162
+ * Load a session record, or null if it does not exist / is unparseable.
163
+ * @param {{ projectRoot: string, sessionId: string }} args
164
+ * @returns {object|null}
165
+ */
166
+ function loadSession(args = {}) {
167
+ const { projectRoot, sessionId } = args;
168
+ if (!projectRoot) throw new TypeError('loadSession: projectRoot is required');
169
+ if (!sessionId) throw new TypeError('loadSession: sessionId is required');
170
+ const file = sessionPath(projectRoot, sessionId);
171
+ let raw;
172
+ try {
173
+ raw = fs.readFileSync(file, 'utf8');
174
+ } catch {
175
+ return null;
176
+ }
177
+ try {
178
+ const data = JSON.parse(raw);
179
+ if (!Array.isArray(data.events)) data.events = [];
180
+ return data;
181
+ } catch {
182
+ return null;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Append an event to a session's `events` log and atomically rewrite the record.
188
+ *
189
+ * The event MUST carry a recognised `kind` and an `at` timestamp — the `at`
190
+ * comes from the caller (no internal clock). Any extra payload fields (e.g.
191
+ * a `generate` event's variant list, a `pick` event's selector + implicated
192
+ * files) are preserved verbatim.
193
+ *
194
+ * @param {object} args
195
+ * @param {string} args.projectRoot
196
+ * @param {string} args.sessionId
197
+ * @param {object} args.event { kind, at, ... }
198
+ * @returns {{ sessionId: string, path: string, session: object, eventIndex: number }}
199
+ */
200
+ function appendEvent(args = {}) {
201
+ const { projectRoot, sessionId, event } = args;
202
+ if (!projectRoot) throw new TypeError('appendEvent: projectRoot is required');
203
+ if (!sessionId) throw new TypeError('appendEvent: sessionId is required');
204
+ if (!event || typeof event !== 'object') throw new TypeError('appendEvent: event object is required');
205
+ if (!EVENT_KINDS.includes(event.kind)) {
206
+ throw new Error(
207
+ `appendEvent: unknown event.kind "${event.kind}" (expected one of ${EVENT_KINDS.join(', ')})`,
208
+ );
209
+ }
210
+ if (event.at == null || !String(event.at).length) {
211
+ throw new TypeError('appendEvent: event.at (ISO timestamp) is required (no internal clock by design)');
212
+ }
213
+
214
+ const session = loadSession({ projectRoot, sessionId });
215
+ if (!session) {
216
+ throw new Error(`appendEvent: no session "${sessionId}" under ${sessionsDir(projectRoot)}`);
217
+ }
218
+ // Normalise the stored event so `kind`/`at` lead, then spread the rest.
219
+ const stored = { kind: event.kind, at: String(event.at) };
220
+ for (const [k, v] of Object.entries(event)) {
221
+ if (k === 'kind' || k === 'at') continue;
222
+ stored[k] = v;
223
+ }
224
+ session.events.push(stored);
225
+
226
+ const dest = sessionPath(projectRoot, sessionId);
227
+ atomicWriteJson(dest, session);
228
+ return { sessionId, path: dest, session, eventIndex: session.events.length - 1 };
229
+ }
230
+
231
+ /** The last event in a session, or null if none. */
232
+ function lastEventOf(session) {
233
+ if (!session || !Array.isArray(session.events) || session.events.length === 0) return null;
234
+ return session.events[session.events.length - 1];
235
+ }
236
+
237
+ /**
238
+ * List every session for a project, newest activity first, for resume offers.
239
+ *
240
+ * Each entry: `{ sessionId, status, started_at, lastEvent }`. Unparseable /
241
+ * non-session files are skipped. Returns [] when the directory is absent.
242
+ *
243
+ * @param {string} projectRoot
244
+ * @returns {Array<{ sessionId: string, status: string, started_at: string|null, lastEvent: object|null }>}
245
+ */
246
+ function listSessions(projectRoot) {
247
+ if (!projectRoot) throw new TypeError('listSessions: projectRoot is required');
248
+ const dir = sessionsDir(projectRoot);
249
+ let entries;
250
+ try {
251
+ entries = fs.readdirSync(dir, { withFileTypes: true });
252
+ } catch {
253
+ return [];
254
+ }
255
+ const out = [];
256
+ for (const e of entries) {
257
+ if (!e.isFile()) continue;
258
+ if (!e.name.endsWith('.json')) continue;
259
+ if (e.name.endsWith('.tmp')) continue;
260
+ const sessionId = e.name.slice(0, -'.json'.length);
261
+ const session = loadSession({ projectRoot, sessionId });
262
+ if (!session || session.session_id == null) continue;
263
+ out.push({
264
+ sessionId: session.session_id,
265
+ status: session.status || null,
266
+ started_at: session.started_at || null,
267
+ lastEvent: lastEventOf(session),
268
+ });
269
+ }
270
+ // Stable order: most recently started first; ties broken by id for determinism.
271
+ out.sort((a, b) => {
272
+ const sa = a.started_at || '';
273
+ const sb = b.started_at || '';
274
+ if (sa === sb) return a.sessionId.localeCompare(b.sessionId);
275
+ return sa < sb ? 1 : -1;
276
+ });
277
+ return out;
278
+ }
279
+
280
+ /**
281
+ * Resume information for a single session, so the skill can offer
282
+ * "continue from <last_event>" vs "start fresh".
283
+ *
284
+ * @param {{ projectRoot: string, sessionId: string }} args
285
+ * @returns {{ canResume: boolean, lastEvent: object|null, summary: string }}
286
+ */
287
+ function resumeInfo(args = {}) {
288
+ const { projectRoot, sessionId } = args;
289
+ if (!projectRoot) throw new TypeError('resumeInfo: projectRoot is required');
290
+ if (!sessionId) throw new TypeError('resumeInfo: sessionId is required');
291
+
292
+ const session = loadSession({ projectRoot, sessionId });
293
+ if (!session) {
294
+ return { canResume: false, lastEvent: null, summary: `no session "${sessionId}"` };
295
+ }
296
+ const last = lastEventOf(session);
297
+ const eventCount = Array.isArray(session.events) ? session.events.length : 0;
298
+ // Only in-progress sessions can be resumed; completed/abandoned ones are done.
299
+ const canResume = session.status === STATUS_IN_PROGRESS;
300
+
301
+ let summary;
302
+ if (!canResume) {
303
+ summary = `session "${sessionId}" is ${session.status} — start fresh`;
304
+ } else if (last) {
305
+ summary = `continue from "${last.kind}" (${eventCount} event${eventCount === 1 ? '' : 's'} recorded)`;
306
+ } else {
307
+ summary = `session "${sessionId}" started but has no events yet — continue or start fresh`;
308
+ }
309
+ return { canResume, lastEvent: last, summary };
310
+ }
311
+
312
+ /**
313
+ * Close a session: set `ended_at` and a terminal status.
314
+ *
315
+ * @param {object} args
316
+ * @param {string} args.projectRoot
317
+ * @param {string} args.sessionId
318
+ * @param {('completed'|'abandoned')} args.status
319
+ * @param {string} args.now ISO timestamp for ended_at (no internal clock)
320
+ * @returns {{ sessionId: string, path: string, session: object }}
321
+ */
322
+ function endSession(args = {}) {
323
+ const { projectRoot, sessionId } = args;
324
+ if (!projectRoot) throw new TypeError('endSession: projectRoot is required');
325
+ if (!sessionId) throw new TypeError('endSession: sessionId is required');
326
+ const status = args.status || STATUS_COMPLETED;
327
+ if (status !== STATUS_COMPLETED && status !== STATUS_ABANDONED) {
328
+ throw new Error(`endSession: status must be "${STATUS_COMPLETED}" or "${STATUS_ABANDONED}", got "${status}"`);
329
+ }
330
+ if (args.now == null || !String(args.now).length) {
331
+ throw new TypeError('endSession: `now` (ISO timestamp) is required for ended_at (no internal clock by design)');
332
+ }
333
+
334
+ const session = loadSession({ projectRoot, sessionId });
335
+ if (!session) {
336
+ throw new Error(`endSession: no session "${sessionId}" under ${sessionsDir(projectRoot)}`);
337
+ }
338
+ session.status = status;
339
+ session.ended_at = String(args.now);
340
+
341
+ const dest = sessionPath(projectRoot, sessionId);
342
+ atomicWriteJson(dest, session);
343
+ return { sessionId, path: dest, session };
344
+ }
345
+
346
+ module.exports = {
347
+ newSession,
348
+ appendEvent,
349
+ loadSession,
350
+ listSessions,
351
+ resumeInfo,
352
+ endSession,
353
+ // exported for callers + tests
354
+ sessionsDir,
355
+ sessionPath,
356
+ deriveSessionId,
357
+ lastEventOf,
358
+ SCHEMA_VERSION,
359
+ SESSIONS_SUBDIR,
360
+ EVENT_KINDS,
361
+ STATUS_IN_PROGRESS,
362
+ STATUS_COMPLETED,
363
+ STATUS_ABANDONED,
364
+ };
@@ -198,6 +198,14 @@
198
198
  "tools": "Read, Bash",
199
199
  "registered_in_phase": "46"
200
200
  },
201
+ {
202
+ "name": "live",
203
+ "description": "Live in-browser design mode. The user picks a DOM element on a running dev server (via the Claude Preview MCP), the agent generates N design variants in one batch, they hot-swap in place through HMR or preview_eval using a data-gdd-variant marker, the user accepts or discards, and the whole pick-generate-accept loop persists to .design/live-sessions so it survives a crash or resume. Use when the user wants to iterate on the look of a live component against a real running server, asks to try variants on a page, or runs the live command with a url; falls back to a screenshot-only degraded mode on harnesses without MCP support.",
204
+ "argument_hint": "[--variants N] [--resume <session-id>] [url]",
205
+ "tools": "Read, Write, Edit, Bash, Glob, Grep, Task",
206
+ "user_invocable": true,
207
+ "registered_in_phase": "47"
208
+ },
201
209
  {
202
210
  "name": "locale",
203
211
  "description": "Inspects or sets the GDD CLI locale for this project. With no argument, reports the resolved locale (config.locale > env LANG > en), the fallback chain, and per-locale coverage (which message tables are complete vs placeholder). With a <code> (en/ru/uk/de/fr/zh/ja), sets .design/config.json#locale after previewing the change. Localizes --help, common error messages, and skill prompt headers via scripts/lib/i18n/; missing keys fall back to English, so a partial locale never breaks the CLI. Use to switch GDD's own output language.",
@@ -108,6 +108,23 @@ Run this final spec-quality pass over `.design/BRIEF.md` before the brief→expl
108
108
  - Scope check: nothing in the artifact exceeds (or silently drops) the agreed scope.
109
109
  - Ambiguity check: every requirement/decision is specific enough to act on without a follow-up question.
110
110
 
111
+ ## Optional brief audit (non-blocking)
112
+
113
+ Before the gate, you MAY spawn `agents/brief-auditor.md` via `Task` to grade the brief against the five
114
+ brief anti-patterns (vague verbs, missing audience, immeasurable success criteria, scope creep, missing
115
+ anti-goals). The auditor reads `.design/BRIEF.md` plus `reference/brief-quality-rubric.md` and writes
116
+ advisory findings to `.design/BRIEF-AUDIT.md`. This step is advisory and MUST NOT block the brief to
117
+ explore transition.
118
+
119
+ If the auditor reports one or more fired anti-patterns, surface a single-line pointer to the user:
120
+
121
+ ```
122
+ Brief audit flagged N issue(s) - run /gdd:discuss brief to refine, or proceed to explore.
123
+ ```
124
+
125
+ The user decides. Proceeding to explore with a flagged brief is allowed; the pointer is a nudge, not a gate.
126
+ If the auditor reports no fired anti-patterns, or you skip the audit, continue to the gate unchanged.
127
+
111
128
  <HARD-GATE>
112
129
  Do NOT transition to explore (or invoke `/gdd:explore`) until the brief artifact (default `.design/BRIEF.md`) is committed AND the user has approved it. If this project uses a custom `.design` location, read the artifact path from `.design/STATE.md` rather than assuming the default.
113
130
  </HARD-GATE>
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: gdd-live
3
+ description: "Live in-browser design mode. The user picks a DOM element on a running dev server (via the Claude Preview MCP), the agent generates N design variants in one batch, they hot-swap in place through HMR or preview_eval using a data-gdd-variant marker, the user accepts or discards, and the whole pick-generate-accept loop persists to .design/live-sessions so it survives a crash or resume. Use when the user wants to iterate on the look of a live component against a real running server, asks to try variants on a page, or runs the live command with a url; falls back to a screenshot-only degraded mode on harnesses without MCP support."
4
+ argument-hint: "[--variants N] [--resume <session-id>] [url]"
5
+ tools: Read, Write, Edit, Bash, Glob, Grep, Task
6
+ user-invocable: true
7
+ ---
8
+
9
+ # gdd-live - Live In-Browser Design Mode
10
+
11
+ Pick a DOM element on a running dev server, generate competing design variants, hot-swap them in place, and accept the winner as a real source edit. Every step persists to `.design/live-sessions/<id>.json` so the session survives a crash or a later resume.
12
+
13
+ The browser-side runtime, the harness-mode gate, the session store, the events feed, the post-check, the scope guard, and the bandit feed are all separate modules under `scripts/lib/live/`. This skill describes the loop and names the module that owns each step; it does not import them.
14
+
15
+ For the full surface (the Preview MCP tools, the six `live_*` events, the session file, the bandit feed, degraded mode, the scope guard), see `../../reference/live-mode-integration.md`. For the SKILL.md structural contract, see `../../reference/skill-authoring-contract.md`.
16
+
17
+ ---
18
+
19
+ ## Arguments
20
+
21
+ - `[url]` - the page to drive. Optional. When omitted, detect the dev server and use its root.
22
+ - `--variants N` - how many variants to generate per pick. Default 3.
23
+ - `--resume <session-id>` - reattach to an in-progress session in `.design/live-sessions/`.
24
+
25
+ ---
26
+
27
+ ## BOOT
28
+
29
+ 1. Probe the Preview MCP per `../../connections/preview.md`: `ToolSearch({ query: "Claude_Preview" })`, then `mcp__Claude_Preview__preview_list`. Empty ToolSearch means the MCP is not loaded.
30
+ 2. Resolve the harness live mode. The capability signal is `capability_matrix.mcp_support` in `scripts/lib/manifest/harnesses.json`, projected by `scripts/lib/live/harness-mode.cjs` (`liveModeFor(harnessId)`). A `puppeteer` result means full live mode; a `degraded` result means screenshot-only.
31
+ 3. If `mcp_support` is false for this harness, or Preview is unavailable, enter DEGRADED mode and say so plainly: variants are generated and captured as static screenshots, with no in-page hot-swap. Skip the INJECT and PICK steps; generate against the file the user names instead.
32
+ 4. Detect the dev server. Look for Vite, Next, Bun, or a static server (check `package.json` scripts plus a `preview_list` entry). Record the server descriptor on the session.
33
+ 5. Open or create the session via `scripts/lib/live/session-store.cjs` (`.design/live-sessions/<id>.json`). On `--resume`, load the named session (see RESUME).
34
+
35
+ ---
36
+
37
+ ## INJECT
38
+
39
+ Inject the browser runtime once. Read `RUNTIME_JS` from `scripts/lib/live/runtime.cjs` and evaluate it in the page with `mcp__Claude_Preview__preview_eval`. The runtime is an idempotent IIFE bound to `window.__gddLive`, so a re-inject after navigation rebinds the same singleton rather than stacking listeners. It installs the pick handler and the variant-swap helpers, and stamps the live variant on the element via the `data-gdd-variant` attribute.
40
+
41
+ ---
42
+
43
+ ## PICK
44
+
45
+ 1. Arm the picker (`window.__gddLive.pick()`), then guide the user to click the target element. Use `preview_click` and `preview_inspect` to confirm the element and read its computed styles and bounding box.
46
+ 2. Read the pick report back. Its fields are documented in `pickReportShape` (selector, tagName, classList, boundingRect, computedStyle subset, current variant). The selector strategy prefers id, then a data-testid, then a tag plus class plus nth-of-type path.
47
+ 3. Emit a `live_pick` event through `scripts/lib/live/events.cjs` and append a `pick` entry to the session.
48
+
49
+ ---
50
+
51
+ ## GENERATE (one batch)
52
+
53
+ 1. Load the relevant Phase 45 canonical reference index FIRST, so variants are grounded in real guidance: the domain index that matches the picked element (for example `../../reference/spatial.md` for layout, `../../reference/interaction.md` for components and a11y, `../../reference/color.md` for color, `../../reference/typography.md` for type, `../../reference/motion.md` for animation).
54
+ 2. Generate all N variants in ONE batch (default 3), each a distinct, hypothesis-tagged design direction for the picked element. Do not generate them one at a time.
55
+ 3. For each variant: write the change atomically to the implicated source file, then make it live. With HMR running, the file write is enough; otherwise apply the variant in place with `window.__gddLive.swapVariant({ n, style, html })`, which sets `data-gdd-variant="n"` and applies the variant's style or markup.
56
+
57
+ ---
58
+
59
+ ## POST-CHECK
60
+
61
+ Run the post-check on each variant via `scripts/lib/live/postcheck.cjs`, which invokes `gdd-detect`. Show the findings inline next to each variant. A variant that trips a finding is flagged, NOT auto-rejected: the user still decides. Append a `live_postcheck` event per variant.
62
+
63
+ ---
64
+
65
+ ## ACCEPT / DISCARD
66
+
67
+ - ACCEPT one variant: apply the chosen variant as the canonical source edit, and revert the others in the page (`window.__gddLive.revert()` on each non-chosen element). Emit a `live_accept` event and feed the outcome to the design-variants bandit via `scripts/lib/live/bandit-feed.cjs` (a dev-time signal). Append an `accept` entry.
68
+ - DISCARD: revert every variant in the page back to its captured original and leave the source untouched. Emit a `live_discard` event and append a `discard` entry.
69
+
70
+ Either way, persist the result through `scripts/lib/live/session-store.cjs` before continuing.
71
+
72
+ ---
73
+
74
+ ## PERSIST
75
+
76
+ Every step (boot, pick, generate, post-check, accept, discard) is written to the session file through `scripts/lib/live/session-store.cjs` as it happens. The on-disk event log uses the `pick`, `generate`, `accept`, `discard` kinds; the telemetry stream uses the six `live_*` event types. Writes are atomic, so an interrupted step never leaves a half-written session.
77
+
78
+ ---
79
+
80
+ ## RESUME
81
+
82
+ With `--resume <session-id>`, load the named session from `.design/live-sessions/`. Only an `in_progress` session is resumable. Offer the user two choices: continue from the last recorded event (report what that was, for example "last pick was the primary button"), or start fresh (open a new session and leave the old one intact). Never silently replay completed events.
83
+
84
+ ---
85
+
86
+ ## SCOPE GUARD
87
+
88
+ Never write outside the source files implicated by the picked element. Run every proposed write through `scripts/lib/live/scope-guard.cjs`, which maps the picked selector to its owning source files and rejects edits that fall outside them. If a variant would need a change beyond that scope (a shared token, a parent layout, a new dependency), stop and surface it to the user rather than widening the blast radius.
89
+
90
+ ## Constraints
91
+
92
+ - Do NOT edit files outside the picked element's implicated sources (enforced by the scope guard).
93
+ - Do NOT generate variants one at a time; generate the full batch, then swap.
94
+ - Do NOT auto-reject a variant on a post-check finding; flag it and let the user decide.
95
+ - In DEGRADED mode, state up front that hot-swap is unavailable and fall back to screenshots.
96
+ - Persist before every user-facing prompt so a crash never loses accepted work.
97
+
98
+ ## LIVE COMPLETE
@@ -39,7 +39,7 @@ Read once at start from `.design/config.json` (all optional; defaults in parens)
39
39
  Stop at the first tier that produces ≥ 1 command:
40
40
 
41
41
  1. **Authoritative config.** If `.design/config.json` has `quality_gate.commands` non-empty, use verbatim.
42
- 2. **Auto-detect from `package.json#scripts`** - match against allowlist: `lint`, `typecheck`, `tsc` (only if `typecheck` absent), `test`, `chromatic`, `test:visual`, `lint:design` (Phase 41 - the `gdd-detect` deterministic anti-pattern gate, alongside `axe`/`pa11y`/`lighthouse`). Exclude by name: `test:e2e`, `test:integration` (if separate `test`), anything starting `dev:`, `build:`, `start:`. Run via `npm run <name>` unless `quality_gate.package_manager` overrides.
42
+ 2. **Auto-detect from `package.json#scripts`** - match against allowlist: `lint`, `typecheck`, `tsc` (only if `typecheck` absent), `test`, `chromatic`, `test:visual`, `lint:design` (Phase 41 - the `gdd-detect` deterministic anti-pattern gate), and the accessibility scripts `axe`, `pa11y`, `lighthouse`, `eslint-plugin-jsx-a11y` (or a script named `jsx-a11y`) which classify into the `a11y` bucket. Exclude by name: `test:e2e`, `test:integration` (if separate `test`), anything starting `dev:`, `build:`, `start:`. Run via `npm run <name>` unless `quality_gate.package_manager` overrides.
43
43
  3. **Skip with notice.** Emit `quality_gate_skipped` (Step 6) and write a `<run/>` with `status="skipped"`. Verify treats skipped as non-blocking.
44
44
 
45
45
  ## Step 2 - Parallel run
@@ -48,7 +48,7 @@ Emit `quality_gate_started`. Spawn each command in a separate `Bash`; collect `{
48
48
 
49
49
  ## Step 3 - Classification
50
50
 
51
- Spawn `quality-gate-runner` agent via `Task` with payload `{outputs: [{command, exit_code, stderr}, ...]}`. Agent returns `{status: "pass"|"fail", classified_failures: {lint, type, test, visual}}`. `pass` → Step 5. `fail` → Step 4.
51
+ Spawn `quality-gate-runner` agent via `Task` with payload `{outputs: [{command, exit_code, stderr}, ...]}`. Agent returns `{status: "pass"|"fail", classified_failures: {lint, type, test, visual, a11y}}`. The `a11y` bucket groups accessibility failures from axe / pa11y / lighthouse / jsx-a11y. `pass` → Step 5. `fail` → Step 4.
52
52
 
53
53
  ## Step 4 - Fix loop (D-08)
54
54