@hegemonart/get-design-done 1.28.8 → 1.30.5

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 (58) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +116 -0
  4. package/README.de.md +25 -0
  5. package/README.fr.md +25 -0
  6. package/README.it.md +25 -0
  7. package/README.ja.md +25 -0
  8. package/README.ko.md +25 -0
  9. package/README.md +30 -0
  10. package/README.zh-CN.md +25 -0
  11. package/SKILL.md +2 -0
  12. package/agents/design-authority-watcher.md +42 -1
  13. package/agents/design-reflector.md +50 -0
  14. package/package.json +1 -1
  15. package/reference/capability-gap-stage-gate.md +261 -0
  16. package/reference/known-failure-modes.md +521 -0
  17. package/reference/pseudonymization-rules.md +189 -0
  18. package/reference/registry.json +22 -1
  19. package/reference/schemas/events.schema.json +158 -3
  20. package/reference/schemas/generated.d.ts +319 -4
  21. package/scripts/cli/gdd-events.mjs +35 -2
  22. package/scripts/gsd-cleanup-incubator.cjs +367 -0
  23. package/scripts/lib/apply-reflections/incubator-proposals.cjs +455 -0
  24. package/scripts/lib/authority-watcher/index.cjs +201 -0
  25. package/scripts/lib/bandit-router.cjs +92 -9
  26. package/scripts/lib/failure-mode-matcher.cjs +460 -0
  27. package/scripts/lib/gsd-health-mirror/index.cjs +37 -1
  28. package/scripts/lib/incubator-author.cjs +845 -0
  29. package/scripts/lib/install/interactive.cjs +27 -2
  30. package/scripts/lib/issue-reporter/cli-flag-report.cjs +153 -0
  31. package/scripts/lib/issue-reporter/consent-prompt.cjs +231 -0
  32. package/scripts/lib/issue-reporter/dedup.cjs +458 -0
  33. package/scripts/lib/issue-reporter/destination.cjs +37 -0
  34. package/scripts/lib/issue-reporter/draft-writer.cjs +157 -0
  35. package/scripts/lib/issue-reporter/gh-absent-fallback.cjs +220 -0
  36. package/scripts/lib/issue-reporter/gh-submit.cjs +114 -0
  37. package/scripts/lib/issue-reporter/kill-switch.cjs +122 -0
  38. package/scripts/lib/issue-reporter/payload-assembly.cjs +367 -0
  39. package/scripts/lib/issue-reporter/privacy-diff.cjs +385 -0
  40. package/scripts/lib/issue-reporter/report-flow.cjs +269 -0
  41. package/scripts/lib/issue-reporter/triage-matcher.cjs +270 -0
  42. package/scripts/lib/pseudonymize.cjs +444 -0
  43. package/scripts/lib/reflections-cycle-writer.cjs +172 -0
  44. package/scripts/lib/reflector/capability-gap-scan.cjs +751 -0
  45. package/scripts/lib/reflector-capability-gap-aggregator.cjs +352 -0
  46. package/scripts/lib/reflector-kfm-proposer.cjs +468 -0
  47. package/scripts/release-smoke-test.cjs +33 -2
  48. package/scripts/validate-incubator-scope.cjs +133 -0
  49. package/skills/apply-reflections/SKILL.md +20 -1
  50. package/skills/apply-reflections/apply-reflections-procedure.md +106 -4
  51. package/skills/fast/SKILL.md +46 -0
  52. package/skills/reflect/SKILL.md +9 -0
  53. package/skills/reflect/procedures/capability-gap-scan.md +120 -0
  54. package/skills/report-issue/SKILL.md +53 -0
  55. package/skills/report-issue/report-issue-procedure.md +120 -0
  56. package/skills/router/SKILL.md +5 -0
  57. package/skills/router/capability-gap-emitter.md +65 -0
  58. package/skills/update/SKILL.md +3 -2
@@ -40,16 +40,40 @@ function isCancel(p, value) {
40
40
  return typeof p.isCancel === 'function' ? p.isCancel(value) : false;
41
41
  }
42
42
 
43
+ // Build a one-line picker hint per runtime kind. Multi-artifact entries
44
+ // intentionally lack a `files` field (see runtimes.cjs header) — destinations
45
+ // are computed by runtime-artifact-layout.cjs from the runtime's configDir.
46
+ function hintForRuntime(r) {
47
+ if (r.kind === 'claude-marketplace') return 'marketplace registration';
48
+ if (r.kind === 'multi-artifact') {
49
+ return r.configDirFallback
50
+ ? `installs into ~/${r.configDirFallback}`
51
+ : 'installs skills/commands/agents';
52
+ }
53
+ // Future-proof fallback for any new kind: prefer files[0] when present,
54
+ // then configDirFallback, then a neutral label. Never crash on missing
55
+ // optional fields.
56
+ if (Array.isArray(r.files) && r.files.length > 0) return `drops ${r.files[0]}`;
57
+ if (r.configDirFallback) return `installs into ~/${r.configDirFallback}`;
58
+ return r.kind || 'install target';
59
+ }
60
+
43
61
  async function runInteractiveInstall() {
44
62
  const p = loadClack();
45
63
 
46
64
  p.intro('get-design-done — multi-runtime installer');
47
65
 
48
- const runtimes = listRuntimes();
66
+ // Tier-2 distribution channels (cursor-marketplace, codex-plugin) carry
67
+ // `configDir: null` per Phase 28.8 — they're out-of-band bundles, not
68
+ // per-user install targets. The interactive picker should hide them; the
69
+ // regular install pipeline already skips them because configDir is null.
70
+ const runtimes = listRuntimes().filter(
71
+ (r) => r.configDir !== null && r.configDirFallback != null,
72
+ );
49
73
  const options = runtimes.map((r) => ({
50
74
  value: r.id,
51
75
  label: r.displayName,
52
- hint: r.kind === 'claude-marketplace' ? 'marketplace registration' : `drops ${r.files[0] || 'AGENTS.md'}`,
76
+ hint: hintForRuntime(r),
53
77
  }));
54
78
 
55
79
  const picked = await p.multiselect({
@@ -139,4 +163,5 @@ async function runInteractiveUninstall(opts) {
139
163
  module.exports = {
140
164
  runInteractiveInstall,
141
165
  runInteractiveUninstall,
166
+ hintForRuntime,
142
167
  };
@@ -0,0 +1,153 @@
1
+ 'use strict';
2
+ /**
3
+ * cli-flag-report.cjs — Plan 30-04 D-11 `--report` flag plumbing.
4
+ *
5
+ * The `--report` flag is intentionally NOT available on every command.
6
+ * Per D-11 it is whitelisted to specific failure modes catalogued in
7
+ * `reference/known-failure-modes.md` with `propose_report: true`. The
8
+ * matcher (30-03) does not act on this flag; this module is where it
9
+ * gates which commands get the flag at all.
10
+ *
11
+ * The schema for known-failure-modes.md does NOT include a per-mode
12
+ * `command` field — the catalogue is regex-based, not command-keyed.
13
+ * The whitelist therefore lives here as an explicit set of command
14
+ * names whose plausible failures map to one or more propose_report=true
15
+ * entries. Adding a command to the whitelist is a deliberate maintainer
16
+ * choice; we do not auto-derive it to avoid surprising users with new
17
+ * `--report` flags appearing as the catalogue grows.
18
+ *
19
+ * Today (matching KFM-008 MCP unreachable + KFM-009 plugin file missing):
20
+ * - `gdd:plan-phase` — typically the first MCP-touching command
21
+ * - `gdd:execute-phase` — typically the first plugin-file-touching command
22
+ * - `gdd:report-issue` — the flag is meaningful on itself (force flow)
23
+ *
24
+ * Commands NOT on the whitelist silently do not see the flag —
25
+ * non-whitelisted argv parsing returns `{ report: false }` regardless
26
+ * of whether the user typed --report.
27
+ */
28
+
29
+ const { listProposeReportModes } = require('./triage-matcher.cjs');
30
+
31
+ /**
32
+ * Command whitelist — derived from CONTEXT D-11 + KFM-008/KFM-009 modes.
33
+ * Frozen to prevent accidental mutation at runtime.
34
+ *
35
+ * The intersection with listProposeReportModes() is checked at
36
+ * isReportFlagWhitelisted call time: if the catalogue is missing
37
+ * propose_report=true entries entirely, the flag is unavailable
38
+ * everywhere (defensive default).
39
+ */
40
+ const COMMAND_WHITELIST = Object.freeze(new Set([
41
+ 'gdd:plan-phase',
42
+ 'gdd:execute-phase',
43
+ 'gdd:report-issue',
44
+ ]));
45
+
46
+ /**
47
+ * @param {string} commandName
48
+ * @param {{ listFn?: typeof listProposeReportModes }} [opts]
49
+ * @returns {boolean}
50
+ */
51
+ function isReportFlagWhitelisted(commandName, opts) {
52
+ if (typeof commandName !== 'string' || commandName.length === 0) return false;
53
+ if (!COMMAND_WHITELIST.has(commandName)) return false;
54
+ const listFn = (opts && opts.listFn) || listProposeReportModes;
55
+ let modes;
56
+ try {
57
+ modes = listFn();
58
+ } catch {
59
+ modes = [];
60
+ }
61
+ // Defensive: if the catalogue lost all propose_report=true entries,
62
+ // the flag is unavailable everywhere.
63
+ return Array.isArray(modes) && modes.length > 0;
64
+ }
65
+
66
+ /**
67
+ * Install --report on the given command-line parser ONLY if the command
68
+ * is whitelisted. The function adapts to two parser shapes:
69
+ *
70
+ * yargs-style: parser.option('report', { type: 'boolean', describe: ... })
71
+ * commander-style: parser.option('--report', '...', false)
72
+ *
73
+ * Non-whitelisted commands → no-op. Returns true if the flag was
74
+ * installed, false otherwise.
75
+ *
76
+ * @param {object} parser
77
+ * @param {string} commandName
78
+ * @param {{ listFn?: typeof listProposeReportModes }} [opts]
79
+ * @returns {boolean}
80
+ */
81
+ function installReportFlagOn(parser, commandName, opts) {
82
+ if (!isReportFlagWhitelisted(commandName, opts)) {
83
+ return false;
84
+ }
85
+ if (parser == null) return false;
86
+ // Try yargs-style first (named option with a config object).
87
+ if (typeof parser.option === 'function') {
88
+ try {
89
+ parser.option('report', {
90
+ type: 'boolean',
91
+ default: false,
92
+ describe:
93
+ 'Propose a GitHub issue draft after a failure (D-11 whitelisted; consent required — no auto-submit).',
94
+ });
95
+ return true;
96
+ } catch {
97
+ // fall through to commander-style
98
+ }
99
+ try {
100
+ parser.option(
101
+ '--report',
102
+ 'Propose a GitHub issue draft after a failure (D-11 whitelisted; consent required — no auto-submit).',
103
+ false
104
+ );
105
+ return true;
106
+ } catch {
107
+ // fall through
108
+ }
109
+ }
110
+ return false;
111
+ }
112
+
113
+ /**
114
+ * Parse `--report` out of an argv array.
115
+ *
116
+ * Cheap parser used by tests and by any caller that just wants to know
117
+ * whether the flag was passed. Non-whitelisted commands ALWAYS return
118
+ * `false` regardless of what's in argv — the flag is unavailable to them.
119
+ *
120
+ * @param {string} commandName
121
+ * @param {string[]} argv
122
+ * @param {{ listFn?: typeof listProposeReportModes }} [opts]
123
+ * @returns {{ report: boolean, forceReport: boolean }}
124
+ */
125
+ function parseReportFlag(commandName, argv, opts) {
126
+ const whitelisted = isReportFlagWhitelisted(commandName, opts);
127
+ let report = false;
128
+ let forceReport = false;
129
+ if (Array.isArray(argv)) {
130
+ for (const a of argv) {
131
+ if (a === '--report') report = true;
132
+ else if (a === '--force-report') forceReport = true;
133
+ else if (typeof a === 'string' && a.startsWith('--report=')) {
134
+ const v = a.slice('--report='.length).toLowerCase();
135
+ report = v === 'true' || v === '1' || v === 'yes';
136
+ }
137
+ }
138
+ }
139
+ if (!whitelisted) {
140
+ // Whitelist gate: even if the user typed --report, the command does
141
+ // not expose it. Force-report is independent: it's a global modifier
142
+ // for the explicit /gdd:report-issue command path.
143
+ return { report: false, forceReport: forceReport && commandName === 'gdd:report-issue' };
144
+ }
145
+ return { report, forceReport };
146
+ }
147
+
148
+ module.exports = {
149
+ installReportFlagOn,
150
+ isReportFlagWhitelisted,
151
+ parseReportFlag,
152
+ COMMAND_WHITELIST,
153
+ };
@@ -0,0 +1,231 @@
1
+ 'use strict';
2
+ /**
3
+ * consent-prompt.cjs — Plan 30-04 D-03 consent gate.
4
+ *
5
+ * The ONE place where the user explicitly says "yes" before /gdd:report-issue
6
+ * submits anything. There is no auto-mode and no env-var bypass:
7
+ *
8
+ * - Refuses (throws) if stdin is not a TTY (no piped 'y' from CI, no
9
+ * non-interactive shells).
10
+ * - Refuses (throws) if any process.env key matches /REPORT|ISSUE|AUTO_REPORT/i
11
+ * AND has a truthy value (runtime belt + suspenders for the static test
12
+ * in tests/report-issue-no-auto-submit-static.test.cjs).
13
+ * - Treats any answer other than literal `y`/`yes` (case-insensitive) as
14
+ * decline.
15
+ *
16
+ * After (optionally) opening the user's $EDITOR on the draft, RE-READS the
17
+ * draft from disk and returns the re-read title + body. This is what makes
18
+ * "edit before submit" work: the editor exit handler does not bind the
19
+ * content; the content is freshly loaded from the file path on every entry.
20
+ *
21
+ * EDITOR is a POSIX-standard user env (used by git, crontab, gh itself). It
22
+ * is intentionally NOT in the forbidden list — the static test only blocks
23
+ * names matching /REPORT|ISSUE|AUTO_REPORT/i.
24
+ *
25
+ * Pure dependencies: readline, child_process.spawnSync. No `fetch`, no
26
+ * `https`, no third-party packages.
27
+ */
28
+
29
+ const fs = require('node:fs');
30
+ const readline = require('node:readline');
31
+ const { spawnSync } = require('node:child_process');
32
+
33
+ const { DESTINATION_REPO } = require('./destination.cjs');
34
+ const { readDraft } = require('./draft-writer.cjs');
35
+
36
+ const FORBIDDEN_ENV_RE = /(REPORT|ISSUE|AUTO_REPORT)/i;
37
+
38
+ /**
39
+ * Scan process.env for any auto-submit bypass env var with a truthy value.
40
+ * If found, throws — D-03 runtime gate counterpart to the static test.
41
+ *
42
+ * Implementation note: we iterate Object.keys(process.env) so we can match
43
+ * substrings (REPORT, ISSUE, AUTO_REPORT). We never directly read a
44
+ * specific named env var here — that would itself fail the static-grep
45
+ * test in tests/report-issue-no-auto-submit-static.test.cjs (which only
46
+ * forbids `process.env.NAME` patterns).
47
+ *
48
+ * Truthy means: present, non-empty, not literal "0" / "false" / "no".
49
+ *
50
+ * @param {NodeJS.ProcessEnv} [env] — injection point for tests
51
+ */
52
+ function rejectBypassEnv(env) {
53
+ const source = env || process.env;
54
+ const offenders = [];
55
+ for (const key of Object.keys(source)) {
56
+ if (!FORBIDDEN_ENV_RE.test(key)) continue;
57
+ const value = source[key];
58
+ if (value == null) continue;
59
+ const s = String(value).trim().toLowerCase();
60
+ if (s === '' || s === '0' || s === 'false' || s === 'no') continue;
61
+ offenders.push(key);
62
+ }
63
+ if (offenders.length > 0) {
64
+ throw new Error(
65
+ `refused: env var ${offenders.join(', ')} detected; /gdd:report-issue has no auto-mode by design (D-03)`
66
+ );
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Open the user's $EDITOR on the draft file, blocking until it exits.
72
+ *
73
+ * @param {string} draftPath
74
+ * @param {NodeJS.ProcessEnv} [env]
75
+ * @returns {boolean} — true if an editor was spawned, false if EDITOR unset
76
+ */
77
+ function openInEditor(draftPath, env) {
78
+ const source = env || process.env;
79
+ const editor = source.EDITOR;
80
+ if (typeof editor !== 'string' || editor.trim().length === 0) {
81
+ return false;
82
+ }
83
+ // Whole-string command: split on first space for editor + args.
84
+ const parts = editor.trim().split(/\s+/);
85
+ const cmd = parts[0];
86
+ const args = parts.slice(1).concat([draftPath]);
87
+ spawnSync(cmd, args, { stdio: 'inherit' });
88
+ return true;
89
+ }
90
+
91
+ /**
92
+ * Ask the user the single y/N question via readline. EOF / empty input
93
+ * counts as decline.
94
+ *
95
+ * @param {NodeJS.ReadableStream} input
96
+ * @param {NodeJS.WritableStream} output
97
+ * @returns {Promise<string>} — raw answer string
98
+ */
99
+ function askYesNo(input, output) {
100
+ return new Promise((resolve) => {
101
+ const rl = readline.createInterface({ input, output, terminal: false });
102
+ let answered = false;
103
+ const finish = (value) => {
104
+ if (answered) return;
105
+ answered = true;
106
+ try { rl.close(); } catch { /* noop */ }
107
+ resolve(value);
108
+ };
109
+ rl.question(
110
+ `Submit this issue to ${DESTINATION_REPO}? [y/N] `,
111
+ (answer) => finish(typeof answer === 'string' ? answer.trim() : '')
112
+ );
113
+ rl.on('close', () => finish(''));
114
+ });
115
+ }
116
+
117
+ function isAffirmative(answer) {
118
+ if (typeof answer !== 'string') return false;
119
+ const a = answer.trim().toLowerCase();
120
+ return a === 'y' || a === 'yes';
121
+ }
122
+
123
+ /**
124
+ * Build the human-facing summary printed before the y/N question.
125
+ *
126
+ * @param {{ title: string, body: string }} draft
127
+ * @param {string} draftPath
128
+ * @returns {string}
129
+ */
130
+ function buildSummary(draft, draftPath) {
131
+ const head = String(draft.body || '').split(/\r?\n/).slice(0, 10).join('\n');
132
+ return [
133
+ '',
134
+ '--- /gdd:report-issue draft summary ---',
135
+ `Destination: ${DESTINATION_REPO}`,
136
+ `Draft path: ${draftPath}`,
137
+ `Title: ${draft.title}`,
138
+ 'Body (first 10 lines):',
139
+ head,
140
+ '---',
141
+ '',
142
+ ].join('\n');
143
+ }
144
+
145
+ /**
146
+ * The full consent flow.
147
+ *
148
+ * 1. Reject bypass env vars (throw).
149
+ * 2. Reject non-TTY stdin (throw).
150
+ * 3. (Optional) open $EDITOR on the draft and wait.
151
+ * 4. Re-read the draft from disk → final {title, body}.
152
+ * 5. Print summary.
153
+ * 6. Ask y/N via readline.
154
+ * 7. Return { consented, finalTitle, finalBody }.
155
+ *
156
+ * @param {{
157
+ * draftPath: string,
158
+ * openEditor?: boolean,
159
+ * stdin?: NodeJS.ReadableStream,
160
+ * stdout?: NodeJS.WritableStream,
161
+ * env?: NodeJS.ProcessEnv,
162
+ * askYesNo?: typeof askYesNo,
163
+ * openInEditor?: typeof openInEditor
164
+ * }} opts
165
+ * @returns {Promise<{ consented: boolean, finalTitle: string, finalBody: string }>}
166
+ */
167
+ async function promptConsent(opts) {
168
+ if (opts == null || typeof opts !== 'object') {
169
+ throw new Error('promptConsent: opts object required');
170
+ }
171
+ const draftPath = opts.draftPath;
172
+ if (typeof draftPath !== 'string' || draftPath.length === 0) {
173
+ throw new Error('promptConsent: draftPath required');
174
+ }
175
+
176
+ // Step 1: runtime env-var bypass gate (D-03).
177
+ rejectBypassEnv(opts.env);
178
+
179
+ // Step 2: TTY gate (D-03).
180
+ const stdin = opts.stdin || process.stdin;
181
+ const stdout = opts.stdout || process.stdout;
182
+ // Allow tests to bypass the TTY check by injecting a stream that sets
183
+ // `isTTY = true` on itself (the public API contract is "interactive
184
+ // shell"; the underlying mechanism is the isTTY flag).
185
+ if (!stdin.isTTY) {
186
+ throw new Error(
187
+ 'refused: /gdd:report-issue requires an interactive TTY (no auto-mode by design — D-03)'
188
+ );
189
+ }
190
+
191
+ // Step 3: optional editor (skipped if openEditor === false).
192
+ const editorFn = opts.openInEditor || openInEditor;
193
+ if (opts.openEditor !== false) {
194
+ try {
195
+ editorFn(draftPath, opts.env);
196
+ } catch {
197
+ // Editor failures are non-fatal — user can edit manually + still consent.
198
+ }
199
+ }
200
+
201
+ // Step 4: re-read the (possibly edited) draft. This is the key step
202
+ // that makes E1 + E2 pass — content is freshly loaded from disk.
203
+ const draft = readDraft(draftPath);
204
+
205
+ // Step 5: summary.
206
+ try {
207
+ stdout.write(buildSummary(draft, draftPath));
208
+ } catch {
209
+ // Best-effort UI; tests can inject stdout that throws.
210
+ }
211
+
212
+ // Step 6: ask y/N.
213
+ const ask = opts.askYesNo || askYesNo;
214
+ const answer = await ask(stdin, stdout);
215
+ const consented = isAffirmative(answer);
216
+
217
+ return {
218
+ consented,
219
+ finalTitle: draft.title,
220
+ finalBody: draft.body,
221
+ };
222
+ }
223
+
224
+ module.exports = {
225
+ promptConsent,
226
+ rejectBypassEnv,
227
+ isAffirmative,
228
+ buildSummary,
229
+ openInEditor,
230
+ askYesNo,
231
+ };