@hegemonart/get-design-done 1.28.7 → 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 (71) 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 +37 -0
  5. package/README.fr.md +37 -0
  6. package/README.it.md +37 -0
  7. package/README.ja.md +37 -0
  8. package/README.ko.md +37 -0
  9. package/README.md +44 -0
  10. package/README.zh-CN.md +37 -0
  11. package/SKILL.md +12 -10
  12. package/agents/design-reflector.md +50 -0
  13. package/package.json +3 -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/build-distribution-bundles.cjs +549 -0
  21. package/scripts/cli/gdd-events.mjs +35 -2
  22. package/scripts/gsd-cleanup-incubator.cjs +367 -0
  23. package/scripts/install.cjs +61 -0
  24. package/scripts/lib/apply-reflections/incubator-proposals.cjs +448 -0
  25. package/scripts/lib/bandit-router.cjs +92 -9
  26. package/scripts/lib/gsd-health-mirror/index.cjs +37 -1
  27. package/scripts/lib/incubator-author.cjs +845 -0
  28. package/scripts/lib/install/config-dir.cjs +26 -0
  29. package/scripts/lib/install/converters/codex-plugin.cjs +407 -0
  30. package/scripts/lib/install/converters/cursor-marketplace.cjs +309 -0
  31. package/scripts/lib/install/doctor-codex-plugin.cjs +388 -0
  32. package/scripts/lib/install/doctor-cursor-marketplace.cjs +366 -0
  33. package/scripts/lib/install/doctor-tier2.cjs +586 -0
  34. package/scripts/lib/install/runtimes.cjs +48 -0
  35. package/scripts/lib/issue-reporter/cli-flag-report.cjs +153 -0
  36. package/scripts/lib/issue-reporter/consent-prompt.cjs +231 -0
  37. package/scripts/lib/issue-reporter/dedup.cjs +458 -0
  38. package/scripts/lib/issue-reporter/destination.cjs +37 -0
  39. package/scripts/lib/issue-reporter/draft-writer.cjs +157 -0
  40. package/scripts/lib/issue-reporter/gh-absent-fallback.cjs +220 -0
  41. package/scripts/lib/issue-reporter/gh-submit.cjs +114 -0
  42. package/scripts/lib/issue-reporter/kill-switch.cjs +122 -0
  43. package/scripts/lib/issue-reporter/payload-assembly.cjs +367 -0
  44. package/scripts/lib/issue-reporter/privacy-diff.cjs +385 -0
  45. package/scripts/lib/issue-reporter/report-flow.cjs +269 -0
  46. package/scripts/lib/issue-reporter/triage-matcher.cjs +270 -0
  47. package/scripts/lib/pseudonymize.cjs +444 -0
  48. package/scripts/lib/reflections-cycle-writer.cjs +172 -0
  49. package/scripts/lib/reflector/capability-gap-scan.cjs +751 -0
  50. package/scripts/lib/reflector-capability-gap-aggregator.cjs +320 -0
  51. package/scripts/lint-agentskills-spec.cjs +457 -0
  52. package/scripts/release-smoke-test.cjs +33 -2
  53. package/scripts/validate-incubator-scope.cjs +133 -0
  54. package/skills/apply-reflections/SKILL.md +16 -1
  55. package/skills/apply-reflections/apply-reflections-procedure.md +71 -3
  56. package/skills/compare/SKILL.md +2 -2
  57. package/skills/compare/compare-rubric.md +1 -1
  58. package/skills/darkmode/SKILL.md +2 -2
  59. package/skills/darkmode/darkmode-audit-procedure.md +1 -1
  60. package/skills/fast/SKILL.md +46 -0
  61. package/skills/figma-write/SKILL.md +2 -2
  62. package/skills/graphify/SKILL.md +2 -2
  63. package/skills/reflect/SKILL.md +9 -0
  64. package/skills/reflect/procedures/capability-gap-scan.md +120 -0
  65. package/skills/report-issue/SKILL.md +53 -0
  66. package/skills/report-issue/report-issue-procedure.md +120 -0
  67. package/skills/router/SKILL.md +5 -0
  68. package/skills/router/capability-gap-emitter.md +65 -0
  69. package/skills/style/SKILL.md +2 -2
  70. package/skills/style/style-doc-procedure.md +1 -1
  71. package/skills/update/SKILL.md +3 -2
@@ -0,0 +1,448 @@
1
+ // scripts/lib/apply-reflections/incubator-proposals.cjs — Plan 29-05
2
+ //
3
+ // Incubator-draft proposal class for /gdd:apply-reflections. Consumes drafts
4
+ // authored by scripts/lib/incubator-author.cjs (Plan 29-04) at
5
+ // `.design/reflections/incubator/<slug>/` and exposes the 7 actions surfaced
6
+ // in skills/apply-reflections/SKILL.md.
7
+ //
8
+ // Exports: discoverIncubatorDrafts, renderProposal, applyAccept, applyReject,
9
+ // applyEdit, checkStage1Gate, recordOptIn.
10
+ //
11
+ // Decisions honoured:
12
+ // * D-01 — checkStage1Gate is read-only; recordOptIn is the sole writer and
13
+ // only fires on explicit user confirmation. No auto-flip ever.
14
+ // * D-04 — applyAccept performs the full draft → final-artifact write +
15
+ // registry append in one call. No intermediate state.
16
+ // * D-05 — applyAccept calls validateScope from
17
+ // scripts/validate-incubator-scope.cjs BEFORE any filesystem
18
+ // mutation. Failure throws; registry and incubator subdir
19
+ // untouched. Non-bypassable.
20
+ // * D-12 — DRAFT.md is copied verbatim, so the drafter's `delegate_to: null`
21
+ // frontmatter survives the promotion.
22
+ //
23
+ // Style: CommonJS, zero external deps (node:fs / node:path / node:child_process /
24
+ // node:os only).
25
+
26
+ 'use strict';
27
+
28
+ const fs = require('node:fs');
29
+ const path = require('node:path');
30
+ const child_process = require('node:child_process');
31
+ const os = require('node:os');
32
+
33
+ const { validateScope } = require('../../validate-incubator-scope.cjs');
34
+
35
+ // --- Constants ---
36
+
37
+ const DEFAULT_INCUBATOR_DIR = '.design/reflections/incubator';
38
+ const DEFAULT_REGISTRY_PATH = 'reference/registry.json';
39
+ const DEFAULT_GATE_SPEC_PATH = 'reference/capability-gap-stage-gate.md';
40
+ const DEFAULT_STATE_PATH = '.planning/STATE.md';
41
+ const OPT_IN_HEADING = '## Capability-gap Stage-1 opt-in';
42
+ const OPT_IN_TOKEN_RE = /Stage-1 opt-in|capability.gap.*opt.in|confirmed_by/i;
43
+
44
+ // --- Helpers ---
45
+
46
+ function safeReadFileSync(p) {
47
+ try {
48
+ return fs.readFileSync(p, 'utf8');
49
+ } catch (_) {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ function warn(msg) {
55
+ // Single-line stderr warning. Keeps SKILL.md UX clean.
56
+ process.stderr.write(`[incubator-proposals] WARN: ${msg}\n`);
57
+ }
58
+
59
+ function quoteArg(s) {
60
+ // Cross-platform quote: wrap in double quotes and escape embedded ones.
61
+ // Sufficient for tmpdir paths (no real shell metachars expected).
62
+ return `"${String(s).replace(/"/g, '\\"')}"`;
63
+ }
64
+
65
+ // --- discoverIncubatorDrafts ---
66
+
67
+ /**
68
+ * Walk the incubator directory and return one Draft per valid slug. Malformed
69
+ * subdirs (missing/unparseable manifest, missing DRAFT.md) are skipped with a
70
+ * stderr warning; never throws.
71
+ */
72
+ function discoverIncubatorDrafts(options) {
73
+ const o = options || {};
74
+ const incubatorDir = o.incubatorDir || DEFAULT_INCUBATOR_DIR;
75
+ if (!fs.existsSync(incubatorDir)) {
76
+ return [];
77
+ }
78
+
79
+ let entries;
80
+ try {
81
+ entries = fs.readdirSync(incubatorDir, { withFileTypes: true });
82
+ } catch (err) {
83
+ warn(`cannot read incubator dir ${incubatorDir}: ${err.message}`);
84
+ return [];
85
+ }
86
+
87
+ const drafts = [];
88
+ for (const ent of entries) {
89
+ if (!ent.isDirectory()) continue;
90
+ if (ent.name === 'archive') continue; // D-06: archived drafts not surfaced
91
+
92
+ const slugDir = path.join(incubatorDir, ent.name);
93
+ const manifestPath = path.join(slugDir, 'manifest.json');
94
+ const draftPath = path.join(slugDir, 'DRAFT.md');
95
+ const originPath = path.join(slugDir, 'ORIGIN.md');
96
+
97
+ if (!fs.existsSync(manifestPath)) {
98
+ warn(`skip ${slugDir}: missing manifest.json`);
99
+ continue;
100
+ }
101
+ if (!fs.existsSync(draftPath)) {
102
+ warn(`skip ${slugDir}: missing DRAFT.md`);
103
+ continue;
104
+ }
105
+ let manifest;
106
+ try {
107
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
108
+ } catch (err) {
109
+ warn(`skip ${slugDir}: manifest.json parse error: ${err.message}`);
110
+ continue;
111
+ }
112
+ if (!manifest || typeof manifest !== 'object' || !manifest.slug || !manifest.kind || !manifest.target_path) {
113
+ warn(`skip ${slugDir}: manifest missing required fields (slug/kind/target_path)`);
114
+ continue;
115
+ }
116
+
117
+ drafts.push({
118
+ slug: manifest.slug,
119
+ kind: manifest.kind,
120
+ target_path: manifest.target_path,
121
+ draft_path: draftPath,
122
+ origin_path: fs.existsSync(originPath) ? originPath : null,
123
+ manifest,
124
+ });
125
+ }
126
+
127
+ // Deterministic ordering by slug ascending — matches incubator-author.cjs style.
128
+ drafts.sort((a, b) => (a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0));
129
+ return drafts;
130
+ }
131
+
132
+ // --- renderProposal ---
133
+
134
+ /**
135
+ * Render a draft as markdown: header (slug + kind), diff vs nearest existing
136
+ * artifact (or "net-new"), Origin section, full draft body.
137
+ */
138
+ function renderProposal(draft, options) {
139
+ const o = options || {};
140
+ const resolver = typeof o.existingArtifactResolver === 'function' ? o.existingArtifactResolver : () => null;
141
+
142
+ const body = safeReadFileSync(draft.draft_path) || '';
143
+ const origin = draft.origin_path ? safeReadFileSync(draft.origin_path) : null;
144
+
145
+ const existing = resolver(draft.target_path);
146
+ let diffSection;
147
+ if (existing == null) {
148
+ diffSection = `### Diff vs existing\n\nNo existing artifact — net-new proposal.\n`;
149
+ } else {
150
+ diffSection = `### Diff vs existing\n\n\`\`\`diff\n--- ${draft.target_path} (existing)\n+++ ${draft.target_path} (proposed)\n${renderUnifiedDiff(existing, body)}\n\`\`\`\n`;
151
+ }
152
+
153
+ const originSection = origin
154
+ ? `## Origin\n\n${origin.trim()}\n`
155
+ : `## Origin\n\n(no ORIGIN.md found in incubator subdir)\n`;
156
+
157
+ return [
158
+ `## Proposal — ${draft.slug} (${draft.kind})`,
159
+ `Target: \`${draft.target_path}\``,
160
+ '',
161
+ diffSection,
162
+ originSection,
163
+ '### Draft body',
164
+ '',
165
+ body.trim(),
166
+ '',
167
+ ].join('\n');
168
+ }
169
+
170
+ /**
171
+ * Minimal unified-diff renderer (line-level, no LCS) for human review only.
172
+ * Not for round-trip patch application — that would need an actual diff
173
+ * library, which we deliberately avoid to keep deps at zero.
174
+ */
175
+ function renderUnifiedDiff(oldText, newText) {
176
+ const oldLines = (oldText || '').split('\n');
177
+ const newLines = (newText || '').split('\n');
178
+ // Cheap diff: emit `-` for old lines absent in new, `+` for new lines
179
+ // absent in old, ` ` for shared lines. Order: all `-`, then all `+`.
180
+ const oldSet = new Set(oldLines);
181
+ const newSet = new Set(newLines);
182
+ const out = [];
183
+ for (const ln of oldLines) {
184
+ if (!newSet.has(ln)) out.push(`-${ln}`);
185
+ }
186
+ for (const ln of newLines) {
187
+ if (!oldSet.has(ln)) out.push(`+${ln}`);
188
+ }
189
+ if (out.length === 0) {
190
+ return '(no line-level differences)';
191
+ }
192
+ return out.join('\n');
193
+ }
194
+
195
+ // --- applyAccept (D-04 + D-05) ---
196
+
197
+ /**
198
+ * Promote draft → final artifact + registry entry in one call (D-04).
199
+ *
200
+ * Order: validateScope (D-05; throws → no writes) → read DRAFT.md →
201
+ * [dryRun: return intent] → mkdirp parent → atomic-write target →
202
+ * append-and-atomic-write registry → fs.rm incubator subdir last
203
+ * (so partial failure leaves draft retryable — T-29.05-04).
204
+ */
205
+ function applyAccept(draft, options) {
206
+ const o = options || {};
207
+ const repoRoot = o.repoRoot || process.cwd();
208
+ const registryPath = path.isAbsolute(o.registryPath || '')
209
+ ? o.registryPath
210
+ : path.join(repoRoot, o.registryPath || DEFAULT_REGISTRY_PATH);
211
+ const dryRun = !!o.dryRun;
212
+
213
+ // Step 1 — D-05 scope guard. THROWS on failure; registry untouched.
214
+ validateScope(draft.target_path, { repoRoot });
215
+
216
+ const draftBody = fs.readFileSync(draft.draft_path, 'utf8');
217
+ const targetAbs = path.resolve(repoRoot, draft.target_path);
218
+
219
+ const registryEntry = {
220
+ slug: draft.slug,
221
+ path: draft.target_path.replace(/\\/g, '/'),
222
+ added: new Date().toISOString(),
223
+ origin: 'incubator',
224
+ };
225
+
226
+ if (dryRun) {
227
+ return {
228
+ wouldWrite: draft.target_path.replace(/\\/g, '/'),
229
+ wouldRegister: registryEntry,
230
+ kind: draft.kind,
231
+ };
232
+ }
233
+
234
+ // Step 4 — mkdirp parent
235
+ fs.mkdirSync(path.dirname(targetAbs), { recursive: true });
236
+
237
+ // Step 5 — atomic write of target file
238
+ atomicWriteFileSync(targetAbs, draftBody);
239
+
240
+ // Step 6 — append registry entry
241
+ appendRegistryEntry(registryPath, draft.kind, registryEntry);
242
+
243
+ // Step 7 — remove incubator subdir last (partial-failure rollback safety)
244
+ const slugDir = path.dirname(path.resolve(draft.draft_path));
245
+ fs.rmSync(slugDir, { recursive: true, force: true });
246
+
247
+ return { accepted: true, path: draft.target_path.replace(/\\/g, '/') };
248
+ }
249
+
250
+ function atomicWriteFileSync(targetAbs, body) {
251
+ const tmp = `${targetAbs}.tmp-${process.pid}-${Date.now()}`;
252
+ fs.writeFileSync(tmp, body);
253
+ fs.renameSync(tmp, targetAbs);
254
+ }
255
+
256
+ function appendRegistryEntry(registryPath, kind, entry) {
257
+ let registry;
258
+ if (fs.existsSync(registryPath)) {
259
+ try {
260
+ registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
261
+ } catch (err) {
262
+ throw new Error(`[incubator-proposals] registry parse error at ${registryPath}: ${err.message}`);
263
+ }
264
+ } else {
265
+ registry = { agents: [], skills: [] };
266
+ }
267
+ if (!registry || typeof registry !== 'object') {
268
+ throw new Error(`[incubator-proposals] registry root must be an object: ${registryPath}`);
269
+ }
270
+
271
+ // Phase 14.5 self-authoring shape: { agents: [...], skills: [...] }.
272
+ // Initialize missing arrays additively so we never clobber another schema's data.
273
+ if (kind === 'agent') {
274
+ if (!Array.isArray(registry.agents)) registry.agents = [];
275
+ registry.agents.push(entry);
276
+ } else if (kind === 'skill') {
277
+ if (!Array.isArray(registry.skills)) registry.skills = [];
278
+ registry.skills.push(entry);
279
+ } else {
280
+ throw new Error(`[incubator-proposals] unknown kind: ${kind} (expected 'agent' or 'skill')`);
281
+ }
282
+
283
+ atomicWriteFileSync(registryPath, JSON.stringify(registry, null, 2) + '\n');
284
+ }
285
+
286
+ // --- applyReject ---
287
+
288
+ /**
289
+ * Remove the incubator subdir for this draft. Registry untouched.
290
+ *
291
+ * @param {object} draft
292
+ * @returns {{rejected:true, slug:string}}
293
+ */
294
+ function applyReject(draft) {
295
+ const slugDir = path.dirname(path.resolve(draft.draft_path));
296
+ fs.rmSync(slugDir, { recursive: true, force: true });
297
+ return { rejected: true, slug: draft.slug };
298
+ }
299
+
300
+ // --- applyEdit ---
301
+
302
+ /**
303
+ * Open the user's editor ($EDITOR / editorEnv / 'vi' fallback) on a temp copy
304
+ * of DRAFT.md. On exit-0, copy edits back and return the reloaded draft. On
305
+ * non-zero exit, return {edited:false, reason}. editorEnv may include args
306
+ * (split on whitespace, e.g. "node /path/to/mock-editor.cjs").
307
+ */
308
+ function applyEdit(draft, options) {
309
+ const o = options || {};
310
+ const editorEnv = o.editorEnv || process.env.EDITOR || 'vi';
311
+
312
+ // Write a temp copy
313
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'incu-edit-'));
314
+ const tmpFile = path.join(tmpDir, path.basename(draft.draft_path));
315
+ fs.copyFileSync(draft.draft_path, tmpFile);
316
+
317
+ try {
318
+ // Two invocation modes:
319
+ // options.editorCmd: [exec, ...args] -- no shell, fully tokenized
320
+ // options.editorEnv: shell command line (default: $EDITOR or 'vi')
321
+ // The array form avoids shell quoting headaches for Windows editor paths
322
+ // that contain spaces (e.g. node.exe installed under a Program-Files
323
+ // location) in tests.
324
+ let r;
325
+ if (Array.isArray(o.editorCmd) && o.editorCmd.length) {
326
+ const [cmd, ...args] = o.editorCmd;
327
+ r = child_process.spawnSync(cmd, args.concat([tmpFile]), { stdio: 'inherit' });
328
+ } else {
329
+ const cmdline = `${editorEnv} ${quoteArg(tmpFile)}`;
330
+ r = child_process.spawnSync(cmdline, { stdio: 'inherit', shell: true });
331
+ }
332
+
333
+ if (r.status !== 0) {
334
+ return { edited: false, reason: 'editor_aborted', exit_code: r.status };
335
+ }
336
+
337
+ // Copy edited tmp back over the draft
338
+ fs.copyFileSync(tmpFile, draft.draft_path);
339
+
340
+ // Reload the draft so target_path / manifest re-sync if anything in
341
+ // the editor changed (manifest itself is not edited here, but the body
342
+ // may have changed). discoverIncubatorDrafts re-reads manifest.json.
343
+ const incubatorDir = path.dirname(path.dirname(path.resolve(draft.draft_path)));
344
+ const all = discoverIncubatorDrafts({ incubatorDir });
345
+ const reloaded = all.find((d) => d.slug === draft.slug);
346
+ return reloaded || { edited: false, reason: 'draft_vanished_post_edit' };
347
+ } finally {
348
+ fs.rmSync(tmpDir, { recursive: true, force: true });
349
+ }
350
+ }
351
+
352
+ // --- checkStage1Gate (D-01: read-only) ---
353
+
354
+ /**
355
+ * Read-only Stage-1 gate inspection (D-01).
356
+ * thresholdMet = count(registry entries with origin === 'incubator') ≥ K
357
+ * optInRecorded = state file contains an opt-in token
358
+ * summary = human-readable one-liner
359
+ * Reads only — never writes. Surfacing the threshold is a prompt, not a flip.
360
+ */
361
+ function checkStage1Gate(options) {
362
+ const o = options || {};
363
+ const gateSpecPath = o.gateSpecPath || DEFAULT_GATE_SPEC_PATH;
364
+ const statePath = o.statePath || DEFAULT_STATE_PATH;
365
+ const registryPath = o.registryPath || DEFAULT_REGISTRY_PATH;
366
+
367
+ const K = readK(gateSpecPath);
368
+
369
+ let acceptedCount = 0;
370
+ const regSrc = safeReadFileSync(registryPath);
371
+ if (regSrc) {
372
+ try {
373
+ const reg = JSON.parse(regSrc);
374
+ const skills = Array.isArray(reg.skills) ? reg.skills : [];
375
+ const agents = Array.isArray(reg.agents) ? reg.agents : [];
376
+ for (const e of skills.concat(agents)) {
377
+ if (e && e.origin === 'incubator') acceptedCount += 1;
378
+ }
379
+ } catch (_) {
380
+ // Malformed registry — treat as zero accepted; do not throw.
381
+ }
382
+ }
383
+
384
+ const thresholdMet = acceptedCount >= K;
385
+
386
+ const stateSrc = safeReadFileSync(statePath) || '';
387
+ const optInRecorded = OPT_IN_TOKEN_RE.test(stateSrc);
388
+
389
+ return {
390
+ thresholdMet,
391
+ summary: `${acceptedCount} of ${K} incubator-origin entries accepted` +
392
+ (thresholdMet ? ' (Stage-1 gate met)' : ' (Stage-1 gate not yet met)'),
393
+ optInRecorded,
394
+ };
395
+ }
396
+
397
+ /**
398
+ * Pull `K` out of capability-gap-stage-gate.md. The doc encodes K as a row
399
+ * in a markdown table: `| K | 3 | Minimum number of stable clusters... |`.
400
+ * If absent or unparseable, fall back to 3 (Phase 29 D-03 default).
401
+ */
402
+ function readK(gateSpecPath) {
403
+ const src = safeReadFileSync(gateSpecPath);
404
+ if (!src) return 3;
405
+ const m = src.match(/\|\s*`?K`?\s*\|\s*`?(\d+)`?\s*\|/);
406
+ if (!m) return 3;
407
+ const v = parseInt(m[1], 10);
408
+ return Number.isFinite(v) && v > 0 ? v : 3;
409
+ }
410
+
411
+ // --- recordOptIn (D-01: explicit-only) ---
412
+
413
+ /**
414
+ * Persist the user's explicit Stage-1 opt-in to STATE.md. Idempotent.
415
+ * IMPORTANT: this is the SOLE state writer in this module. Only invoke after
416
+ * explicit user confirmation in the apply-reflections UX (D-01).
417
+ */
418
+ function recordOptIn(options) {
419
+ const o = options || {};
420
+ const statePath = o.statePath || DEFAULT_STATE_PATH;
421
+ const confirmedBy = o.confirmedBy || 'user';
422
+
423
+ const existing = safeReadFileSync(statePath) || '';
424
+ if (OPT_IN_TOKEN_RE.test(existing)) {
425
+ return { alreadyRecorded: true };
426
+ }
427
+
428
+ const at = new Date().toISOString();
429
+ const block =
430
+ `\n${OPT_IN_HEADING}\n\n` +
431
+ `- recorded_at: ${at}\n` +
432
+ `- confirmed_by: ${confirmedBy}\n`;
433
+ const next = existing + (existing.endsWith('\n') ? '' : '\n') + block;
434
+ atomicWriteFileSync(statePath, next);
435
+ return { optInRecorded: true, at, confirmedBy };
436
+ }
437
+
438
+ // --- Exports ---
439
+
440
+ module.exports = {
441
+ discoverIncubatorDrafts,
442
+ renderProposal,
443
+ applyAccept,
444
+ applyReject,
445
+ applyEdit,
446
+ checkStage1Gate,
447
+ recordOptIn,
448
+ };
@@ -26,6 +26,18 @@
26
26
  * neutral — the same TIER_PRIOR shape, on the assumption that we
27
27
  * have no prior to favour any delegate over local; data drives.
28
28
  *
29
+ * Bootstrap discipline (Phase 29 Plan 06 / CONTEXT D-04):
30
+ * - Default `prior_class` (omitted or 'default'): existing informed
31
+ * TIER_PRIOR bootstrap (Phase 23.5) — byte-for-byte unchanged.
32
+ * - `prior_class: 'promoted_incubator'`: Beta(2, 8) bootstrap for
33
+ * arms registered when `/gdd:apply-reflections accept` promotes
34
+ * an incubator draft. The conservative prior (posterior mean 0.2)
35
+ * suppresses preferential selection until ~8-10 successful pulls
36
+ * accumulate. The bandit-fairness gate IS the promotion staging
37
+ * mechanism (D-04: no two-step staging/ratify split).
38
+ * - The `prior_class` value is persisted on the arm so subsequent
39
+ * reads + decay calculations preserve it (forward-compat).
40
+ *
29
41
  * Atomic .tmp + rename. Discounted Thompson via per-arm time-decay
30
42
  * factor `rho^days_since_last_use` applied at sample time, not stored.
31
43
  *
@@ -59,6 +71,14 @@ const TIER_PRIOR = Object.freeze({
59
71
  const PRIOR_STRENGTH = 10;
60
72
  const DEFAULT_TIERS = Object.freeze(['haiku', 'sonnet', 'opus']);
61
73
 
74
+ // Phase 29 Plan 06 / CONTEXT D-04. Conservative prior for arms
75
+ // bootstrapped via `/gdd:apply-reflections accept` (incubator → live
76
+ // agent/skill). Beta(2, 8) — posterior mean 0.2 — suppresses
77
+ // preferential selection until ~8-10 successful pulls accumulate.
78
+ // The bandit-fairness gate IS the staging mechanism (D-04: no
79
+ // two-step staging/ratify split).
80
+ const PROMOTED_INCUBATOR_PRIOR = Object.freeze({ alpha: 2, beta: 8 });
81
+
62
82
  // Plan 27-07 / D-08. Delegate context dimension. 'none' = local Anthropic
63
83
  // call; the other 5 are peer-CLI delegations via ACP/ASP. Adding this as
64
84
  // a third context dimension expands the arm space 6× (78 → ~468 contexts).
@@ -158,7 +178,26 @@ function reset(opts = {}) {
158
178
  return { deleted: existed, path: p, reason: opts.reason };
159
179
  }
160
180
 
161
- function priorFor(tier, strength) {
181
+ /**
182
+ * Compute the bootstrap prior for a freshly-created arm.
183
+ *
184
+ * @param {string} tier
185
+ * @param {number} strength
186
+ * @param {string} [prior_class] — 'default' (existing behaviour, omittable)
187
+ * or 'promoted_incubator' (Beta(2,8) bootstrap per Phase 29 Plan 06 /
188
+ * CONTEXT D-04). The promoted-incubator class is tier-independent —
189
+ * the conservative suppression applies uniformly across haiku/sonnet/
190
+ * opus until evidence accumulates.
191
+ * @returns {{alpha: number, beta: number}}
192
+ */
193
+ function priorFor(tier, strength, prior_class) {
194
+ if (prior_class === 'promoted_incubator') {
195
+ return {
196
+ alpha: PROMOTED_INCUBATOR_PRIOR.alpha,
197
+ beta: PROMOTED_INCUBATOR_PRIOR.beta,
198
+ };
199
+ }
200
+ // Default-path (Phase 23.5) — byte-for-byte unchanged.
162
201
  const prior = TIER_PRIOR[tier];
163
202
  if (prior === undefined) {
164
203
  return { alpha: strength / 2, beta: strength / 2 };
@@ -207,11 +246,17 @@ function findArm(arms, agent, bin, tier, delegate) {
207
246
  * 23.5 prior — no migration needed because the legacy slice and the
208
247
  * 'none' slice are independent contexts) and for the 5 peer delegates
209
248
  * (each starts neutral with the same TIER_PRIOR shape; data drives).
249
+ *
250
+ * For Phase 29 Plan 06: when `prior_class === 'promoted_incubator'`, the
251
+ * bootstrap prior is Beta(2, 8) regardless of tier/delegate (CONTEXT D-04).
252
+ * The `prior_class` is persisted on the arm so re-reads + decay preserve it.
253
+ * If omitted or 'default', no `prior_class` field is added (clean
254
+ * round-trip with existing posterior files — non-breaking change).
210
255
  */
211
- function ensureArm(posterior, agent, bin, tier, strength, delegate) {
256
+ function ensureArm(posterior, agent, bin, tier, strength, delegate, prior_class) {
212
257
  let arm = findArm(posterior.arms, agent, bin, tier, delegate);
213
258
  if (arm) return arm;
214
- const { alpha, beta } = priorFor(tier, strength);
259
+ const { alpha, beta } = priorFor(tier, strength, prior_class);
215
260
  arm = {
216
261
  agent,
217
262
  bin,
@@ -224,6 +269,9 @@ function ensureArm(posterior, agent, bin, tier, strength, delegate) {
224
269
  if (delegate !== undefined) {
225
270
  arm.delegate = delegate;
226
271
  }
272
+ if (prior_class !== undefined && prior_class !== 'default') {
273
+ arm.prior_class = prior_class;
274
+ }
227
275
  posterior.arms.push(arm);
228
276
  return arm;
229
277
  }
@@ -312,7 +360,10 @@ function decayArm(arm, opts = {}) {
312
360
  * counters. Bandit pull does NOT update the success/fail counters —
313
361
  * that happens in `update()` once the outcome is known.
314
362
  *
315
- * @param {{agent: string, bin: string, tiers?: string[], baseDir?: string, posteriorPath?: string, decay?: number, strength?: number, now?: Date}} input
363
+ * @param {{agent: string, bin: string, tiers?: string[], baseDir?: string, posteriorPath?: string, decay?: number, strength?: number, now?: Date, prior_class?: string}} input
364
+ * `prior_class` (optional, Phase 29 Plan 06 / D-04): 'promoted_incubator'
365
+ * bootstraps fresh arms with Beta(2,8). Omitting it preserves Phase 23.5
366
+ * informed-prior behaviour (non-breaking).
316
367
  * @returns {{tier: string, samples: Record<string, number>, posteriorPath: string}}
317
368
  */
318
369
  function pull(input) {
@@ -332,7 +383,7 @@ function pull(input) {
332
383
  let bestTier = tiers[0];
333
384
  let bestSample = -1;
334
385
  for (const tier of tiers) {
335
- const arm = ensureArm(posterior, input.agent, input.bin, tier, strength);
386
+ const arm = ensureArm(posterior, input.agent, input.bin, tier, strength, undefined, input.prior_class);
336
387
  const decayed = decayArm(arm, { decay: input.decay, now, strength });
337
388
  const s = sampleBeta(decayed.alpha, decayed.beta);
338
389
  samples[tier] = s;
@@ -342,7 +393,7 @@ function pull(input) {
342
393
  }
343
394
  }
344
395
  // Bump counters on the chosen arm.
345
- const chosen = ensureArm(posterior, input.agent, input.bin, bestTier, strength);
396
+ const chosen = ensureArm(posterior, input.agent, input.bin, bestTier, strength, undefined, input.prior_class);
346
397
  chosen.last_used = now.toISOString();
347
398
  chosen.count += 1;
348
399
  const written = savePosterior(posterior, input);
@@ -353,7 +404,11 @@ function pull(input) {
353
404
  * Update the posterior with a reward signal. Reward is applied as a
354
405
  * Bernoulli observation: success → α += reward, β += (1 - reward).
355
406
  *
356
- * @param {{agent: string, bin: string, tier: string, reward: number, baseDir?: string, posteriorPath?: string, strength?: number}} input
407
+ * @param {{agent: string, bin: string, tier: string, reward: number, baseDir?: string, posteriorPath?: string, strength?: number, prior_class?: string}} input
408
+ * `prior_class` (optional, Phase 29 Plan 06 / D-04): 'promoted_incubator'
409
+ * bootstraps fresh arms with Beta(2,8). Omitting preserves Phase 23.5
410
+ * informed-prior behaviour (non-breaking). The reward math is unchanged
411
+ * — `prior_class` only affects bootstrap, not the Bernoulli update.
357
412
  * @returns {{alpha: number, beta: number, posteriorPath: string}}
358
413
  */
359
414
  function update(input) {
@@ -369,7 +424,15 @@ function update(input) {
369
424
  // Reward must be in [0, 1].
370
425
  const r = Math.min(1, Math.max(0, input.reward));
371
426
  const posterior = loadPosterior(input);
372
- const arm = ensureArm(posterior, input.agent, input.bin, input.tier, input.strength ?? PRIOR_STRENGTH);
427
+ const arm = ensureArm(
428
+ posterior,
429
+ input.agent,
430
+ input.bin,
431
+ input.tier,
432
+ input.strength ?? PRIOR_STRENGTH,
433
+ undefined,
434
+ input.prior_class,
435
+ );
373
436
  arm.alpha += r;
374
437
  arm.beta += 1 - r;
375
438
  const p = savePosterior(posterior, input);
@@ -401,7 +464,11 @@ function update(input) {
401
464
  * decay?: number,
402
465
  * strength?: number,
403
466
  * now?: Date,
467
+ * prior_class?: string,
404
468
  * }} input
469
+ * `prior_class` (optional, Phase 29 Plan 06 / D-04): 'promoted_incubator'
470
+ * bootstraps fresh arms with Beta(2,8). Omitting preserves Phase 23.5 +
471
+ * Plan 27-07 behaviour (non-breaking).
405
472
  * @returns {{
406
473
  * tier: string,
407
474
  * delegate: string,
@@ -435,7 +502,15 @@ function pullWithDelegate(input) {
435
502
  for (const delegate of delegates) {
436
503
  samples[delegate] = {};
437
504
  for (const tier of tiers) {
438
- const arm = ensureArm(posterior, input.agent, input.bin, tier, strength, delegate);
505
+ const arm = ensureArm(
506
+ posterior,
507
+ input.agent,
508
+ input.bin,
509
+ tier,
510
+ strength,
511
+ delegate,
512
+ input.prior_class,
513
+ );
439
514
  const decayed = decayArm(arm, { decay: input.decay, now, strength });
440
515
  const s = sampleBeta(decayed.alpha, decayed.beta);
441
516
  samples[delegate][tier] = s;
@@ -453,6 +528,7 @@ function pullWithDelegate(input) {
453
528
  bestTier,
454
529
  strength,
455
530
  bestDelegate,
531
+ input.prior_class,
456
532
  );
457
533
  chosen.last_used = now.toISOString();
458
534
  chosen.count += 1;
@@ -482,7 +558,12 @@ function pullWithDelegate(input) {
482
558
  * baseDir?: string,
483
559
  * posteriorPath?: string,
484
560
  * strength?: number,
561
+ * prior_class?: string,
485
562
  * }} input
563
+ * `prior_class` (optional, Phase 29 Plan 06 / D-04): 'promoted_incubator'
564
+ * bootstraps fresh arms with Beta(2,8). Omitting preserves Plan 27-07
565
+ * behaviour (non-breaking). The reward math is unchanged — `prior_class`
566
+ * only affects bootstrap, not the Bernoulli update.
486
567
  * @returns {{alpha: number, beta: number, posteriorPath: string}}
487
568
  */
488
569
  function updateWithDelegate(input) {
@@ -506,6 +587,7 @@ function updateWithDelegate(input) {
506
587
  input.tier,
507
588
  input.strength ?? PRIOR_STRENGTH,
508
589
  input.delegate,
590
+ input.prior_class,
509
591
  );
510
592
  arm.alpha += r;
511
593
  arm.beta += 1 - r;
@@ -569,6 +651,7 @@ module.exports = {
569
651
  DELEGATE_NONE,
570
652
  TIER_PRIOR,
571
653
  PRIOR_STRENGTH,
654
+ PROMOTED_INCUBATOR_PRIOR,
572
655
  TOUCHES_BINS,
573
656
  DEFAULT_POSTERIOR_PATH,
574
657
  SCHEMA_VERSION,