@hegemonart/get-design-done 1.28.8 → 1.30.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 (53) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +81 -0
  4. package/README.de.md +23 -0
  5. package/README.fr.md +23 -0
  6. package/README.it.md +23 -0
  7. package/README.ja.md +23 -0
  8. package/README.ko.md +23 -0
  9. package/README.md +28 -0
  10. package/README.zh-CN.md +23 -0
  11. package/SKILL.md +2 -0
  12. package/agents/design-reflector.md +50 -0
  13. package/package.json +1 -1
  14. package/reference/capability-gap-stage-gate.md +261 -0
  15. package/reference/known-failure-modes.md +185 -0
  16. package/reference/pseudonymization-rules.md +189 -0
  17. package/reference/registry.json +22 -1
  18. package/reference/schemas/events.schema.json +97 -3
  19. package/reference/schemas/generated.d.ts +319 -4
  20. package/scripts/cli/gdd-events.mjs +35 -2
  21. package/scripts/gsd-cleanup-incubator.cjs +367 -0
  22. package/scripts/lib/apply-reflections/incubator-proposals.cjs +448 -0
  23. package/scripts/lib/bandit-router.cjs +92 -9
  24. package/scripts/lib/gsd-health-mirror/index.cjs +37 -1
  25. package/scripts/lib/incubator-author.cjs +845 -0
  26. package/scripts/lib/issue-reporter/cli-flag-report.cjs +153 -0
  27. package/scripts/lib/issue-reporter/consent-prompt.cjs +231 -0
  28. package/scripts/lib/issue-reporter/dedup.cjs +458 -0
  29. package/scripts/lib/issue-reporter/destination.cjs +37 -0
  30. package/scripts/lib/issue-reporter/draft-writer.cjs +157 -0
  31. package/scripts/lib/issue-reporter/gh-absent-fallback.cjs +220 -0
  32. package/scripts/lib/issue-reporter/gh-submit.cjs +114 -0
  33. package/scripts/lib/issue-reporter/kill-switch.cjs +122 -0
  34. package/scripts/lib/issue-reporter/payload-assembly.cjs +367 -0
  35. package/scripts/lib/issue-reporter/privacy-diff.cjs +385 -0
  36. package/scripts/lib/issue-reporter/report-flow.cjs +269 -0
  37. package/scripts/lib/issue-reporter/triage-matcher.cjs +270 -0
  38. package/scripts/lib/pseudonymize.cjs +444 -0
  39. package/scripts/lib/reflections-cycle-writer.cjs +172 -0
  40. package/scripts/lib/reflector/capability-gap-scan.cjs +751 -0
  41. package/scripts/lib/reflector-capability-gap-aggregator.cjs +320 -0
  42. package/scripts/release-smoke-test.cjs +33 -2
  43. package/scripts/validate-incubator-scope.cjs +133 -0
  44. package/skills/apply-reflections/SKILL.md +16 -1
  45. package/skills/apply-reflections/apply-reflections-procedure.md +71 -3
  46. package/skills/fast/SKILL.md +46 -0
  47. package/skills/reflect/SKILL.md +9 -0
  48. package/skills/reflect/procedures/capability-gap-scan.md +120 -0
  49. package/skills/report-issue/SKILL.md +53 -0
  50. package/skills/report-issue/report-issue-procedure.md +120 -0
  51. package/skills/router/SKILL.md +5 -0
  52. package/skills/router/capability-gap-emitter.md +65 -0
  53. package/skills/update/SKILL.md +3 -2
@@ -0,0 +1,367 @@
1
+ /**
2
+ * payload-assembly.cjs — Phase 30 Plan 30-02 issue payload assembler.
3
+ *
4
+ * Single source of truth for what a reported issue payload looks like
5
+ * BEFORE it ever hits disk (D-04) or a clipboard. Pure module: no I/O,
6
+ * no globals consumed, no env reads, no clock reads. Deterministic for
7
+ * fixed inputs (this is what enables the golden snapshot test).
8
+ *
9
+ * Two-layer scrub pipeline (order is non-negotiable):
10
+ * Step 1: Phase 22 redact.cjs → strips secrets to `[REDACTED:type]`
11
+ * Step 2: Phase 30 pseudonymize → rewrites identity (user/path/host)
12
+ *
13
+ * The order matters: if pseudonymize ran first, the username PORTION of
14
+ * a token like `sk-ant-aliceUser-…` would be rewritten before the
15
+ * redact pattern got a chance to match the whole token, leaving a
16
+ * half-mangled secret hint in the payload. Case 9 of the test suite
17
+ * locks this order with a negative test (see threat T-30-02-01).
18
+ *
19
+ * Pseudonymize (Plan 30-01) is late-bound INSIDE assemble() — NOT at
20
+ * module-scope. This makes 30-02 parallel-safe with 30-01 at planning
21
+ * time. If 30-01 hasn't shipped yet when assemble() is first called,
22
+ * a clear remediation error is thrown.
23
+ *
24
+ * D-01: Disclaimer text is hardcoded as module constants. No template
25
+ * files, no i18n indirection, no env override.
26
+ * D-04: Returns a STRING. Persistence is a separate concern (Plan 30-04).
27
+ * D-14: capability_gap inclusion iterates EXACTLY the 7 Phase 29 D-02
28
+ * fields by name; extra keys on the input object are silently
29
+ * dropped. This is the enforcement mechanism for D-14.
30
+ *
31
+ * hostOsClass contract: the caller (30-03 collector) is responsible for
32
+ * narrowing the OS string to one of "linux" | "darwin" | "windows".
33
+ * Full uname / kernel version is OUT OF SCOPE and must not be passed in
34
+ * (threat T-30-02-03).
35
+ */
36
+
37
+ 'use strict';
38
+
39
+ const crypto = require('node:crypto');
40
+ const { redact } = require('../redact.cjs');
41
+
42
+ /**
43
+ * D-01 disclaimer constants. Hardcoded, verbatim, bilingual.
44
+ * Tests Cases 2 + 3 assert these exact substrings appear in the output;
45
+ * any change to the prose must update the tests in lockstep.
46
+ */
47
+ const DISCLAIMER_RU =
48
+ 'Это псевдонимизация, не анонимизация. Содержимое промптов и кода может косвенно идентифицировать. Финальный ревью — на тебе.';
49
+ const DISCLAIMER_EN =
50
+ 'This is pseudonymization, not anonymization. Prompt and code contents can still indirectly identify. Final review is on you.';
51
+
52
+ /**
53
+ * The seven Phase 29 D-02 capability_gap fields, in fixed render order.
54
+ * D-14: iteration is BY THIS LIST. Extra keys on the input event object
55
+ * are intentionally dropped — never rendered. Adding fields here would
56
+ * leak fields that don't exist in the Phase 29 source contract.
57
+ */
58
+ const CAPABILITY_GAP_FIELDS = [
59
+ 'event_type',
60
+ 'command_name',
61
+ 'capability_id',
62
+ 'expected_outcome',
63
+ 'observed_outcome',
64
+ 'runtime',
65
+ 'timestamp',
66
+ ];
67
+
68
+ /**
69
+ * Normalize a stack-trace string for stable fingerprinting.
70
+ *
71
+ * Strips:
72
+ * - line:col offsets (`:42:18` → '')
73
+ * - absolute path prefixes (POSIX `/.../` and Windows `\...\\` → '')
74
+ *
75
+ * Keeps:
76
+ * - frame leading text (`at Object.<anonymous> (`)
77
+ * - basename of the file
78
+ * - trailing characters after the location (e.g., closing paren)
79
+ *
80
+ * This is what makes fingerprints stable across machines and across
81
+ * runs from different working directories. Two users hitting the same
82
+ * bug from different cwd's get the same fingerprint → dedup works.
83
+ *
84
+ * @param {string} stack
85
+ * @returns {string}
86
+ */
87
+ function normalizeStack(stack) {
88
+ if (typeof stack !== 'string' || stack.length === 0) return '';
89
+ const lines = stack.split('\n');
90
+ const normalized = lines.map((rawLine) => {
91
+ let line = rawLine;
92
+ // Strip :line:col offsets (handle one or both; line:col is the common
93
+ // Node format; line-only also appears for some runtimes).
94
+ line = line.replace(/:\d+:\d+/g, '');
95
+ line = line.replace(/:\d+(?=\)|\s|$)/g, '');
96
+ // Strip absolute path prefixes — keep basename only. Match both
97
+ // POSIX (`/`) and Windows (`\\`) separators. The regex is greedy:
98
+ // remove everything up through the last path separator.
99
+ line = line.replace(/[A-Za-z]?:?[/\\][^()\s]*[/\\]/g, '');
100
+ return line.trim();
101
+ });
102
+ return normalized.join('\n');
103
+ }
104
+
105
+ /**
106
+ * Compute a deterministic fingerprint for dedup grouping.
107
+ *
108
+ * Formula: sha256(normalize(stack) + '|' + command_name + '|' + runtime + '|' + plugin_version)
109
+ *
110
+ * Locked by Cases 5 (determinism), 6 (cross-cwd stability), 7+8 (changes
111
+ * when the inputs change). See threat T-30-02-05.
112
+ *
113
+ * @param {object} args
114
+ * @param {string} [args.stack] — error stack trace string
115
+ * @param {string} [args.commandName] — e.g., "gsd:plan-phase"
116
+ * @param {string} [args.runtime] — e.g., "claude-code"
117
+ * @param {string} [args.pluginVersion] — e.g., "1.30.0"
118
+ * @returns {string} — 64-char hex digest
119
+ */
120
+ function computeFingerprint(args) {
121
+ const stack = args && args.stack != null ? args.stack : '';
122
+ const commandName = args && args.commandName != null ? args.commandName : '';
123
+ const runtime = args && args.runtime != null ? args.runtime : '';
124
+ const pluginVersion = args && args.pluginVersion != null ? args.pluginVersion : '';
125
+ const material =
126
+ normalizeStack(String(stack)) +
127
+ '|' +
128
+ String(commandName) +
129
+ '|' +
130
+ String(runtime) +
131
+ '|' +
132
+ String(pluginVersion);
133
+ return crypto.createHash('sha256').update(material).digest('hex');
134
+ }
135
+
136
+ /**
137
+ * Late-bound require of Plan 30-01's pseudonymize. Called INSIDE
138
+ * assemble() rather than at module scope so that 30-02 can be planned
139
+ * and reviewed before 30-01 has landed. If 30-01 hasn't shipped, the
140
+ * call throws a clear remediation error instead of crashing at require.
141
+ *
142
+ * The real 30-01 API is `pseudonymize(payload, opts) -> { payload, replacements }`
143
+ * (see scripts/lib/pseudonymize.cjs). Caller supplies identity/hostname/
144
+ * repoOrigin via opts; this module unwraps `.payload` from the return.
145
+ *
146
+ * @returns {(payload: unknown, opts?: object) => { payload: unknown, replacements: Array<object> }}
147
+ */
148
+ function loadPseudonymize() {
149
+ try {
150
+ // eslint-disable-next-line global-require
151
+ const mod = require('../pseudonymize.cjs');
152
+ if (!mod || typeof mod.pseudonymize !== 'function') {
153
+ throw new Error(
154
+ 'pseudonymize.cjs loaded but does not export a `pseudonymize` function.'
155
+ );
156
+ }
157
+ return mod.pseudonymize;
158
+ } catch (err) {
159
+ throw new Error(
160
+ 'Phase 30 payload assembly requires scripts/lib/pseudonymize.cjs ' +
161
+ '(Plan 30-01). Run Plan 30-01 first. Underlying error: ' +
162
+ (err && err.message ? err.message : String(err))
163
+ );
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Build the `opts` object passed to Plan 30-01's pseudonymize() from the
169
+ * fields the caller stuffed onto errorContext. All fields are optional —
170
+ * pseudonymize handles missing inputs gracefully (rule helpers no-op on
171
+ * empty identity/hostname).
172
+ *
173
+ * @param {object} errorContext
174
+ * @returns {object} opts compatible with scripts/lib/pseudonymize.cjs
175
+ */
176
+ function buildPseudonymizeOpts(errorContext) {
177
+ const ctx = errorContext || {};
178
+ return {
179
+ identity: ctx.identity && typeof ctx.identity === 'object' ? ctx.identity : {},
180
+ hostname: typeof ctx.hostname === 'string' ? ctx.hostname : '',
181
+ repoOrigin: typeof ctx.repoOrigin === 'string' ? ctx.repoOrigin : '',
182
+ repoVisibility: ctx.repoVisibility,
183
+ envSnapshot:
184
+ ctx.envSnapshot && typeof ctx.envSnapshot === 'object' ? ctx.envSnapshot : {},
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Render the bilingual disclaimer block. RU above EN, both inside a
190
+ * single GitHub-flavored markdown blockquote with an [!IMPORTANT] alert.
191
+ * D-01 mandates this block be the FIRST thing in the payload, before any
192
+ * technical content. Locked by Cases 2, 3, 4.
193
+ *
194
+ * @returns {string}
195
+ */
196
+ function renderDisclaimer() {
197
+ return (
198
+ '> [!IMPORTANT] Disclaimer / Дисклеймер\n' +
199
+ '> ' +
200
+ DISCLAIMER_RU +
201
+ '\n' +
202
+ '>\n' +
203
+ '> ' +
204
+ DISCLAIMER_EN
205
+ );
206
+ }
207
+
208
+ /**
209
+ * Render the optional capability_gap section. D-14: iterate the 7 D-02
210
+ * fields explicitly by name. Extra keys on `event` are dropped — this
211
+ * is the leak prevention. Returns null when event is null/undefined so
212
+ * the caller can omit the section header entirely.
213
+ *
214
+ * @param {object|null|undefined} event
215
+ * @returns {string|null}
216
+ */
217
+ function renderCapabilityGap(event) {
218
+ if (event == null) return null;
219
+ const lines = ['## Capability Gap'];
220
+ // D-14: only the 7 D-02 fields are rendered; extra keys on the event
221
+ // are intentionally dropped.
222
+ for (const field of CAPABILITY_GAP_FIELDS) {
223
+ const raw = event[field];
224
+ const value = raw == null ? '' : String(raw);
225
+ lines.push('- ' + field + ': ' + value);
226
+ }
227
+ return lines.join('\n');
228
+ }
229
+
230
+ /**
231
+ * Render trajectory reference. When provided, prints verbatim (the path
232
+ * is NOT dereferenced — that's the caller's concern). When omitted,
233
+ * renders the italic placeholder `_not provided_`.
234
+ *
235
+ * @param {string|null|undefined} ref
236
+ * @returns {string}
237
+ */
238
+ function renderTrajectoryRef(ref) {
239
+ if (ref == null || ref === '') return '_not provided_';
240
+ return String(ref);
241
+ }
242
+
243
+ /**
244
+ * Assemble a deterministic, scrubbed, bilingual-disclaimer issue payload.
245
+ *
246
+ * Pure: no I/O, no globals consumed, deterministic for fixed inputs.
247
+ *
248
+ * errorContext.identity / .hostname / .repoOrigin / .repoVisibility /
249
+ * .envSnapshot are passed to 30-01 pseudonymize() via opts. Any of those
250
+ * may be omitted — pseudonymize no-ops on empty inputs.
251
+ *
252
+ * @param {string} commandName e.g., "gsd:plan-phase"
253
+ * @param {object} errorContext { message, stack, runtime, pluginVersion, nodeVersion, hostOsClass, identity?, hostname?, repoOrigin?, repoVisibility?, envSnapshot? }
254
+ * @param {string} [trajectoryRef] relative path or ID; printed verbatim
255
+ * @param {object} [capabilityGapEvent] full D-02 event; only its 7 fields rendered
256
+ * @returns {string} markdown payload
257
+ */
258
+ function assemble(commandName, errorContext, trajectoryRef, capabilityGapEvent) {
259
+ // Late-bind 30-01's pseudonymize at call-time. Keeps 30-02 parallel-
260
+ // safe with 30-01 at planning time. If 30-01 hasn't shipped, this
261
+ // throws an informative error instead of crashing at module load.
262
+ const pseudonymize = loadPseudonymize();
263
+
264
+ // Step 1: redact secrets (Phase 22). MUST run BEFORE pseudonymize.
265
+ // See header comment + threat T-30-02-01 + Case 9 negative test.
266
+ const ctxRedacted = redact(errorContext == null ? {} : errorContext);
267
+ const gapRedacted =
268
+ capabilityGapEvent == null ? null : redact(capabilityGapEvent);
269
+
270
+ // Step 2: pseudonymize identity (Phase 30 Plan 30-01).
271
+ // 30-01 API: pseudonymize(payload, opts) -> { payload, replacements }
272
+ const pseudoOpts = buildPseudonymizeOpts(ctxRedacted);
273
+ const ctxResult = pseudonymize(ctxRedacted, pseudoOpts);
274
+ const ctxScrubbed = ctxResult && ctxResult.payload != null ? ctxResult.payload : ctxRedacted;
275
+
276
+ let gapScrubbed = null;
277
+ if (gapRedacted != null) {
278
+ const gapResult = pseudonymize(gapRedacted, pseudoOpts);
279
+ gapScrubbed = gapResult && gapResult.payload != null ? gapResult.payload : gapRedacted;
280
+ }
281
+
282
+ // Pull scrubbed fields out for rendering. Default to '' so the markdown
283
+ // shape remains stable even when the caller passes a sparse object.
284
+ const scrubbedMessage =
285
+ ctxScrubbed && ctxScrubbed.message != null ? String(ctxScrubbed.message) : '';
286
+ const scrubbedStack =
287
+ ctxScrubbed && ctxScrubbed.stack != null ? String(ctxScrubbed.stack) : '';
288
+ const runtime =
289
+ ctxScrubbed && ctxScrubbed.runtime != null ? String(ctxScrubbed.runtime) : '';
290
+ const pluginVersion =
291
+ ctxScrubbed && ctxScrubbed.pluginVersion != null
292
+ ? String(ctxScrubbed.pluginVersion)
293
+ : '';
294
+ const nodeVersion =
295
+ ctxScrubbed && ctxScrubbed.nodeVersion != null
296
+ ? String(ctxScrubbed.nodeVersion)
297
+ : '';
298
+ const hostOsClass =
299
+ ctxScrubbed && ctxScrubbed.hostOsClass != null
300
+ ? String(ctxScrubbed.hostOsClass)
301
+ : '';
302
+
303
+ // Step 3: fingerprint. Use SCRUBBED stack so pseudonymized identifiers
304
+ // are baked into the fingerprint — same bug from two different users
305
+ // still hashes the same after Plan 30-01's identity rewrite.
306
+ const fingerprint = computeFingerprint({
307
+ stack: scrubbedStack,
308
+ commandName: String(commandName == null ? '' : commandName),
309
+ runtime,
310
+ pluginVersion,
311
+ });
312
+
313
+ // Step 4: render markdown. Disclaimer FIRST (D-01), then command,
314
+ // then fingerprint, then runtime metadata, then error + stack, then
315
+ // trajectory, then optional capability_gap.
316
+ const sections = [];
317
+
318
+ sections.push(renderDisclaimer());
319
+
320
+ sections.push('## Command\n`' + String(commandName == null ? '' : commandName) + '`');
321
+
322
+ sections.push(
323
+ '## Fingerprint\n`' +
324
+ fingerprint +
325
+ '` — derived from normalized stack + command + runtime + version'
326
+ );
327
+
328
+ sections.push(
329
+ '## Runtime\n' +
330
+ '- Node: ' +
331
+ nodeVersion +
332
+ '\n' +
333
+ '- Plugin: ' +
334
+ pluginVersion +
335
+ '\n' +
336
+ '- OS class: ' +
337
+ hostOsClass
338
+ );
339
+
340
+ sections.push('## Error\n```\n' + scrubbedMessage + '\n```');
341
+
342
+ sections.push('### Stack (normalized)\n```\n' + scrubbedStack + '\n```');
343
+
344
+ sections.push('## Trajectory\n' + renderTrajectoryRef(trajectoryRef));
345
+
346
+ const capGapSection = renderCapabilityGap(gapScrubbed);
347
+ if (capGapSection !== null) {
348
+ sections.push(capGapSection);
349
+ }
350
+
351
+ // Join with blank lines between sections. Trailing newline keeps the
352
+ // file POSIX-friendly and makes `cat`/`git diff` happy.
353
+ return sections.join('\n\n') + '\n';
354
+ }
355
+
356
+ module.exports = {
357
+ assemble,
358
+ computeFingerprint,
359
+ DISCLAIMER_RU,
360
+ DISCLAIMER_EN,
361
+ // Internal helpers — exported only because tests want stable hooks.
362
+ // Treat as private API; downstream plans must not import these.
363
+ _internal: {
364
+ normalizeStack,
365
+ CAPABILITY_GAP_FIELDS,
366
+ },
367
+ };