quiver-cli 1.2.0 → 1.3.1

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 (121) hide show
  1. package/dist/cli.js +237 -10
  2. package/package.json +1 -1
  3. package/template/.agents/skills/agent-browser/SKILL.md +1 -0
  4. package/template/.agents/skills/apps/skybridge/SKILL.md +2 -0
  5. package/template/.agents/skills/data/prisma-cli/SKILL.md +25 -7
  6. package/template/.agents/skills/data/prisma-cli/references/agent-safety.md +27 -0
  7. package/template/.agents/skills/data/prisma-cli/references/complete.md +22 -0
  8. package/template/.agents/skills/data/prisma-cli/references/db-push.md +2 -0
  9. package/template/.agents/skills/data/prisma-cli/references/init.md +3 -0
  10. package/template/.agents/skills/data/prisma-cli/references/mcp.md +2 -1
  11. package/template/.agents/skills/data/prisma-cli/references/migrate-dev.md +1 -1
  12. package/template/.agents/skills/data/prisma-cli/references/migrate-reset.md +2 -0
  13. package/template/.agents/skills/data/prisma-client-api/SKILL.md +1 -1
  14. package/template/.agents/skills/data/prisma-client-api/references/constructor.md +13 -0
  15. package/template/.agents/skills/data/prisma-client-api/references/raw-queries.md +4 -0
  16. package/template/.agents/skills/design/impeccable/SKILL.md +5 -5
  17. package/template/.agents/skills/design/impeccable/reference/android.md +6 -0
  18. package/template/.agents/skills/design/impeccable/reference/animate.md +3 -0
  19. package/template/.agents/skills/design/impeccable/reference/bolder.md +3 -1
  20. package/template/.agents/skills/design/impeccable/reference/craft-floor.md +2 -0
  21. package/template/.agents/skills/design/impeccable/reference/critique.md +23 -5
  22. package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +15 -68
  23. package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +13 -12
  24. package/template/.agents/skills/design/impeccable/reference/distill.md +1 -1
  25. package/template/.agents/skills/design/impeccable/reference/doctor.md +1 -0
  26. package/template/.agents/skills/design/impeccable/reference/document.md +1 -1
  27. package/template/.agents/skills/design/impeccable/reference/extract.md +1 -1
  28. package/template/.agents/skills/design/impeccable/reference/hooks.md +17 -11
  29. package/template/.agents/skills/design/impeccable/reference/init.md +9 -3
  30. package/template/.agents/skills/design/impeccable/reference/ios.md +6 -0
  31. package/template/.agents/skills/design/impeccable/reference/new-work.md +69 -29
  32. package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -1
  33. package/template/.agents/skills/design/impeccable/reference/polish.md +13 -5
  34. package/template/.agents/skills/design/impeccable/reference/quieter.md +1 -1
  35. package/template/.agents/skills/design/impeccable/reference/routing.md +1 -1
  36. package/template/.agents/skills/design/impeccable/reference/visualize.md +21 -22
  37. package/template/.agents/skills/design/impeccable/scripts/build-phase.mjs +1022 -0
  38. package/template/.agents/skills/design/impeccable/scripts/comp-diff.mjs +391 -0
  39. package/template/.agents/skills/design/impeccable/scripts/comp-spec.mjs +513 -0
  40. package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +297 -41
  41. package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +10 -19
  42. package/template/.agents/skills/design/impeccable/scripts/context.mjs +124 -9
  43. package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +279 -19
  44. package/template/.agents/skills/design/impeccable/scripts/data/font-index-failures.json +121 -0
  45. package/template/.agents/skills/design/impeccable/scripts/data/font-index.json +1 -0
  46. package/template/.agents/skills/design/impeccable/scripts/detect.mjs +9 -0
  47. package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +192 -11
  48. package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +10 -16
  49. package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +339 -11
  50. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +1482 -722
  51. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns.mjs +1 -0
  52. package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +64 -2
  53. package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +580 -29
  54. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +62 -7
  55. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +31 -7
  56. package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +23 -22
  57. package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +18 -0
  58. package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +471 -370
  59. package/template/.agents/skills/design/impeccable/scripts/detector/shared/color.mjs +474 -2
  60. package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +17 -2
  61. package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +14 -21
  62. package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +81 -48
  63. package/template/.agents/skills/design/impeccable/scripts/font-match.mjs +457 -0
  64. package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +219 -12
  65. package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +93 -15
  66. package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +41 -19
  67. package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +507 -117
  68. package/template/.agents/skills/design/impeccable/scripts/hook.mjs +10 -9
  69. package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +40 -1
  70. package/template/.agents/skills/design/impeccable/scripts/lib/design-parser.mjs +120 -82
  71. package/template/.agents/skills/design/impeccable/scripts/lib/font-fingerprint.mjs +564 -0
  72. package/template/.agents/skills/design/impeccable/scripts/lib/font-index.mjs +130 -0
  73. package/template/.agents/skills/design/impeccable/scripts/lib/hero-checks.mjs +246 -0
  74. package/template/.agents/skills/design/impeccable/scripts/lib/image-metrics.mjs +306 -0
  75. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +41 -59
  76. package/template/.agents/skills/design/impeccable/scripts/lib/is-generated.mjs +5 -2
  77. package/template/.agents/skills/design/impeccable/scripts/lib/live-path-globs.mjs +37 -0
  78. package/template/.agents/skills/design/impeccable/scripts/lib/open-system-browser.mjs +26 -0
  79. package/template/.agents/skills/design/impeccable/scripts/lib/png.mjs +281 -0
  80. package/template/.agents/skills/design/impeccable/scripts/lib/raster.mjs +194 -0
  81. package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +26 -19
  82. package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +31 -3
  83. package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +93 -17
  84. package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +9 -11
  85. package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +31 -2
  86. package/template/.agents/skills/design/impeccable/scripts/live/project-ignores.mjs +139 -0
  87. package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +10 -2
  88. package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +26 -2
  89. package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +15 -27
  90. package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +4 -25
  91. package/template/.agents/skills/design/impeccable/scripts/live/ui-surfaces.mjs +75 -0
  92. package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +21 -37
  93. package/template/.agents/skills/design/impeccable/scripts/live-browser-ignores.js +242 -0
  94. package/template/.agents/skills/design/impeccable/scripts/live-browser.js +62 -23
  95. package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +68 -112
  96. package/template/.agents/skills/design/impeccable/scripts/live-copy-edit-agent.mjs +132 -15
  97. package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +2 -42
  98. package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +5 -4
  99. package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +56 -19
  100. package/template/.agents/skills/design/impeccable/scripts/live.mjs +12 -37
  101. package/template/.agents/skills/design/impeccable/scripts/pin.mjs +8 -5
  102. package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +1006 -155
  103. package/template/.agents/skills/design/shadcn/rules/chat.md +26 -0
  104. package/template/.agents/skills/hono/SKILL.md +17 -3
  105. package/template/.agents/skills/integrations/langfuse/SKILL.md +8 -4
  106. package/template/.agents/skills/integrations/langfuse/references/cli.md +1 -1
  107. package/template/.agents/skills/integrations/langfuse/references/create-dataset.md +35 -0
  108. package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +9 -10
  109. package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +8 -6
  110. package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +34 -0
  111. package/template/.agents/skills/integrations/langfuse/references/setting-up-evals.md +65 -0
  112. package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +20 -40
  113. package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +4 -30
  114. package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +3 -1
  115. package/template/.agents/skills/supabase/CHANGELOG.md +7 -0
  116. package/template/.agents/skills/supabase/SKILL.md +5 -1
  117. package/template/.agents/skills/writing/humanizer/LICENSE +21 -0
  118. package/template/.agents/skills/writing/humanizer/README.md +209 -0
  119. package/template/.agents/skills/writing/humanizer/SKILL.md +173 -338
  120. package/template/.agents/upstreams.json +21 -22
  121. package/template/.agents/skills/design/impeccable/scripts/live/ui-core.mjs +0 -180
@@ -31,6 +31,16 @@
31
31
  * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a
32
32
  * fresh assigned index, challengers, and compositions. One base key therefore
33
33
  * reproduces the entire chain of rounds.
34
+ * - REGISTER (--register safer|bolder): the user's steering on the
35
+ * familiar-to-bold axis, applied to a re-roll round. A register changes
36
+ * only what this round instructs, never what it dealt: the same key and
37
+ * reroll count reproduce the same deal whatever the register, so the
38
+ * exclusion chain never forks. bolder presents the dealt foreign forms
39
+ * as the whole hand (first-dealt leads, dice-assigned by deal order);
40
+ * safer spends the dealt hand unseen and presents the familiar register,
41
+ * the model's conventional grounded candidates plus the canon against
42
+ * named competitors, the one sanctioned lineup of the model's own list.
43
+ * Registers are user-requested, never pre-selected by the model.
34
44
  * - RATINGS: the reviewer's approval ratings weight the challenger draw
35
45
  * (3-star doubles the odds, 1-star sits out); the approved pool itself
36
46
  * is unchanged.
@@ -41,7 +51,9 @@
41
51
  * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow
42
52
  * node scripts/concept-seed.mjs --scope direction --candidate-count 6
43
53
  * node scripts/concept-seed.mjs --scope direction --mode persuade --from <key> --reroll 1
44
- * node scripts/concept-seed.mjs --chosen <challenger-id> --from <key> --scope direction
54
+ * node scripts/concept-seed.mjs --scope direction --mode persuade --from <key> --reroll 1 --register bolder
55
+ * node scripts/concept-seed.mjs --chosen <challenger-id> --kind challenger --from <key> --scope direction
56
+ * node scripts/concept-seed.mjs --kind assigned --from <key> --scope direction
45
57
  *
46
58
  * --grain names how much of the product is in play: product, flow, view, or
47
59
  * region. A docs site, an onboarding flow, a landing page and a data table are
@@ -62,8 +74,13 @@
62
74
  * Challenger data resolves in order: a local catalog directory (the private
63
75
  * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll
64
76
  * API at impeccable.style, then a degraded assignment-only seed when both are
65
- * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls;
66
- * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it.
77
+ * unavailable. The anonymous choice ping fires once per resolved attended
78
+ * round on API-dealt rolls: --kind names which card class won (assigned,
79
+ * pick, challenger, canon) so share metrics have a denominator, --chosen
80
+ * carries the catalog id when a dealt challenger won, and --register rides
81
+ * along when the round came from a steered hand. Grounded candidates' names
82
+ * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables
83
+ * the ping entirely.
67
84
  *
68
85
  * Env vars:
69
86
  * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs.
@@ -73,7 +90,8 @@
73
90
  */
74
91
 
75
92
  import crypto from 'node:crypto';
76
- import { dirname, join, resolve } from 'node:path';
93
+ import { dirname, join, relative, resolve } from 'node:path';
94
+ import { readFileSync, realpathSync } from 'node:fs';
77
95
  import { fileURLToPath } from 'node:url';
78
96
  import {
79
97
  approvedPoolRevision,
@@ -172,17 +190,35 @@ function telemetryDisabled() {
172
190
  return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK);
173
191
  }
174
192
 
175
- // Anonymous choice ping: records only that a dealt world was selected.
193
+ // Anonymous choice ping: one per resolved attended direction round. kind
194
+ // says which card class won (assigned / pick / challenger / canon), so
195
+ // pick-share and canon-share have a denominator; chosenId rides along only
196
+ // when a dealt catalog world won, and register only when the round came from
197
+ // a steered hand. Grounded candidates' names never leave the machine: they
198
+ // are derived from the user's project, so the ping carries the kind alone.
176
199
  // Fire-and-forget; never fails the caller.
177
- export async function pingChosen({ chosenId, key, scope, mode }) {
178
- if (telemetryDisabled() || !chosenId) return false;
200
+ const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']);
201
+ export async function pingChosen({ chosenId, key, scope, mode, kind, register }) {
202
+ if (telemetryDisabled()) return false;
203
+ if (kind && !PING_KINDS.has(kind)) return false;
204
+ if (register && register !== 'safer' && register !== 'bolder') return false;
205
+ // Legacy shape: a bare challenger id with no kind stays a valid ping.
206
+ if (!chosenId && !kind) return false;
207
+ if ((kind === 'challenger' || !kind) && !chosenId) return false;
179
208
  const controller = new AbortController();
180
209
  const timer = setTimeout(() => controller.abort(), apiBudgetMs());
181
210
  try {
182
211
  await fetch(`${API_BASE}/chosen`, {
183
212
  method: 'POST',
184
213
  headers: { 'Content-Type': 'application/json' },
185
- body: JSON.stringify({ chosenId, key, scope, mode }),
214
+ body: JSON.stringify({
215
+ ...(chosenId ? { chosenId } : {}),
216
+ key,
217
+ scope,
218
+ mode,
219
+ ...(kind ? { kind } : {}),
220
+ ...(register ? { register } : {}),
221
+ }),
186
222
  signal: controller.signal,
187
223
  });
188
224
  return true;
@@ -260,6 +296,7 @@ export function renderConceptSeed({
260
296
  scope = 'surface',
261
297
  key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'),
262
298
  reroll = 0,
299
+ register = null,
263
300
  mode = null,
264
301
  grain = null,
265
302
  platform = null,
@@ -273,6 +310,15 @@ export function renderConceptSeed({
273
310
  if (!Number.isInteger(reroll) || reroll < 0) {
274
311
  throw new Error('concept-seed: --reroll must be a non-negative integer');
275
312
  }
313
+ if (register !== null && register !== 'safer' && register !== 'bolder') {
314
+ throw new Error('concept-seed: --register must be safer or bolder');
315
+ }
316
+ if (register !== null && reroll < 1) {
317
+ throw new Error('concept-seed: --register steers a re-roll round; pass --reroll <n> with it');
318
+ }
319
+ if (register !== null && scope !== 'direction') {
320
+ throw new Error('concept-seed: --register applies to direction rounds only');
321
+ }
276
322
  if (mode !== null && !SEED_MODES.has(mode)) {
277
323
  throw new Error('concept-seed: --mode must be persuade, operate, read, or experience');
278
324
  }
@@ -293,6 +339,20 @@ export function renderConceptSeed({
293
339
  };
294
340
  const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`;
295
341
  const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount
342
+ // Surface scope deals a hand of three grounded structures: one card is not
343
+ // a choice, and the full ranked list would hand selection back to the
344
+ // model's taste. The dice pick all three; the primary index leads. The
345
+ // no-lineup rule stays direction-only, where it was written for worlds.
346
+ const dealtIndices = [buildIndex];
347
+ for (let draw = 0; scope === 'surface' && dealtIndices.length < Math.min(3, candidateCount); draw += 1) {
348
+ const idx = 1 + Math.floor(unit(`${indexSalt}:deal-${draw}`) * candidateCount);
349
+ if (!dealtIndices.includes(idx)) dealtIndices.push(idx);
350
+ if (draw > 64) { // hash repeats cannot stall the deal
351
+ for (let fill = 1; dealtIndices.length < Math.min(3, candidateCount); fill += 1) {
352
+ if (!dealtIndices.includes(fill)) dealtIndices.push(fill);
353
+ }
354
+ }
355
+ }
296
356
 
297
357
  // Local catalog first (private repo, evals, tests), then the roll API,
298
358
  // then a degraded assignment-only seed. The assigned index is pure local
@@ -326,6 +386,7 @@ export function renderConceptSeed({
326
386
  scope,
327
387
  key,
328
388
  reroll,
389
+ register,
329
390
  mode,
330
391
  grain,
331
392
  platform,
@@ -357,16 +418,32 @@ export function renderConceptSeed({
357
418
  survive the current task plus navigation, quiet and dense content,
358
419
  interaction and state, and a substantially different future surface. In an
359
420
  attended run, present the assigned direction fully committed and offer
360
- re-roll; never present a ranked lineup to choose from. Re-roll yourself only
421
+ re-roll. You may add ONE card for your top-ranked grounded candidate when
422
+ it is not the assigned direction, kicker IMPECCABLE’S PICK, with an honest risk line
423
+ naming its familiarity; one pick card, never a ranked lineup, and the pick
424
+ never takes the lead position. When the assignment IS your top candidate,
425
+ there is no pick card. Re-roll yourself only
361
426
  on named factual grounds, when the assignment cannot carry the product's
362
427
  truth or task; taste is never grounds.`
363
428
  : `After ordering the task's grounded structural candidates by resonance,
364
- build candidate ${buildIndex} of your own grounded list; the assignment never
365
- points at a challenger. The assignment is the roll, not a suggestion.
366
- In an attended run, present the assigned structure and offer re-roll; never
367
- present a ranked lineup to choose from. Re-roll yourself only when the
368
- assignment fails audience identification or product clarity on named
369
- factual grounds.`;
429
+ deal candidates ${dealtIndices.join(', ')} of your own grounded list to the
430
+ table; index ${buildIndex} leads, and the deal never points at a challenger.
431
+ The deal is the roll, not a suggestion: the dice decide which structures
432
+ reach the user, so the ranking rut stays broken while the user still gets a
433
+ real choice, and the full ranked list stays yours. In an attended run,
434
+ present the three dealt structures as full cards of equal salience, the
435
+ lead carrying kicker THE ROLL, with steer and re-roll, and let the user
436
+ lock one in; the world is already settled, so this choice is composition.
437
+ Visualize every dealt card: with image generation available and a
438
+ comp-led default (.impeccable/config.json buildPath; the page toggle
439
+ handles the exception), declare a comp per card and generate after
440
+ serving, lead first; otherwise author each card's wireframe field (see
441
+ serve-question --schema) and the page draws the schematic. Carry the
442
+ recorded default in the payload as buildPath with toggle: true. Locking a card
443
+ approves its comp: a surface round that put three visualized structures on
444
+ the table replaces the three-option comp round in visualize.md. Re-roll
445
+ yourself only when every dealt structure fails audience identification or
446
+ product clarity on named factual grounds.`;
370
447
 
371
448
  const challengerInstruction = scope === 'direction'
372
449
  ? `Fuse each challenger before judging it: the challenger supplies the form
@@ -374,7 +451,16 @@ export function renderConceptSeed({
374
451
  conflicts. Weigh the fused result against the assigned direction on exactly
375
452
  two axes, audience identification and product clarity. Losing to strong
376
453
  grounded material is a valid outcome; beating a thin or tool-monoculture
377
- list is the point. A fused challenger that wins both axes becomes the build.`
454
+ list is the point. A fused challenger that wins both axes becomes the build.
455
+ Close the weighing with a verdict per challenger, decided before any
456
+ borrowing is considered: wins (beats the assigned direction on both axes),
457
+ competitive (holds one axis), or declined (loses both). A declined
458
+ challenger is not spent: name the one discipline of its system the assigned
459
+ direction lacks, and raise the assigned direction to match before
460
+ presenting it. A donation transfers ambition and system discipline, never
461
+ the challenger's clothes; one world owns the page. Write each raise as its
462
+ own named line on the presented direction, and carry every verdict, kept
463
+ line, and raise into the decision page payload.`
378
464
  : `A challenger wins only when its fused result beats the grounded list on
379
465
  audience identification and product clarity. It may change task topology or
380
466
  interaction, but never the committed visual identity.`;
@@ -399,8 +485,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens
399
485
  the product without weakening semantics, performance, or fallback behavior.`;
400
486
 
401
487
  if (!data) {
402
- return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount})
403
- ASSIGNED INDEX: ${buildIndex}
488
+ // A degraded roll can still serve the safer register, which needs no
489
+ // catalog at all: the assignment machinery is suppressed entirely, the
490
+ // same as the non-degraded safer round, because emitting both "the user
491
+ // picks" and a mandatory numbered build order hands the model two
492
+ // contradicting instructions and the mandatory one tends to win. The
493
+ // bolder register is exactly the thing degradation took away, so it
494
+ // falls back to a plain grounded round, disclosed.
495
+ const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`;
496
+ if (register === 'safer') {
497
+ return `${degradedHeader}
498
+ SAFER REGISTER (user-requested): the assigned index is suspended this
499
+ round; the user picks, and no candidate is mandated. Present the familiar
500
+ register: your remaining grounded candidates from the conventional end, at
501
+ most three, as full cards with an honest risk line each, plus the canon
502
+ executed against two or three named competitors. This is the one sanctioned
503
+ lineup of your own ranked candidates; it exists only by this explicit
504
+ request. When the user voices a standing preference for it, record a brand
505
+ commitment in PRODUCT.md.
506
+ ${authorityInstruction}
507
+ A user- or brief-pinned decision beats the roll, always.
508
+ REGISTER (restated for truncated readers): safer, user-requested; the
509
+ assigned index is suspended this round and the user picks; seed key ${key}.
510
+ `;
511
+ }
512
+ const degradedRegister = register === 'bolder'
513
+ ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran
514
+ degraded with no catalog and no roll service, so there is nothing bold to
515
+ deal. Tell the user, then run this round as a plain grounded re-roll; the
516
+ assignment below applies.
517
+ `
518
+ : '';
519
+ return `${degradedHeader}
520
+ ${degradedRegister}${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`}
404
521
  ${promotedInstruction}
405
522
  The assignment exists to refuse the model's ranking rut, never to outrank
406
523
  the user or the brief. Never expose assignment metadata in user-facing labels.
@@ -424,8 +541,11 @@ channel: when a browser can open, present the direction on the decision page
424
541
  the no-browser fallback.
425
542
  ${authorityInstruction}
426
543
  A user- or brief-pinned decision beats the roll, always.
427
- ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate
428
- ${buildIndex} of your own grounded list; seed key ${key}.
544
+ ${scope === 'direction'
545
+ ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate
546
+ ${buildIndex} of your own grounded list; seed key ${key}.`
547
+ : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index
548
+ ${buildIndex} leads. Present all three dealt structures; seed key ${key}.`}
429
549
  `;
430
550
  }
431
551
 
@@ -471,58 +591,186 @@ structure only, never a palette, typeface, or material. Treat them as serious
471
591
  rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n`
472
592
  : '';
473
593
  const rerollBlock = reroll > 0
474
- ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded
475
- and challenger alike, is eliminated and may not return reworded. Derive
594
+ ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded
595
+ and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive
476
596
  genuinely new grounded candidates from unexplored angles before judging
477
- these fresh challengers.\n`
597
+ these fresh challengers.`}\n`
478
598
  : '';
479
- const telemetryBlock = data.source === 'api'
480
- ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun
481
- this script once with --chosen <challenger-id> --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}
482
- after resolution. The ping is anonymous (chosen id only) and is skipped
483
- automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n`
484
- : '';
485
- return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision)
486
- ${rerollBlock}ASSIGNED INDEX: ${buildIndex}
599
+ // A register swaps the round's presentation, never its deal: the assigned
600
+ // index and challenger fetch stay identical so the chain reproduces, and
601
+ // only the instructions change.
602
+ const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this
603
+ round's dealt hand is spent unseen, stays excluded from future rounds, and
604
+ is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded
605
+ candidates from the conventional end, at most three, as full cards with an
606
+ honest risk line each, plus the canon executed against two or three named
607
+ competitors. This is the one sanctioned lineup of your own ranked
608
+ candidates; it exists only by this explicit request. When the user voices a
609
+ standing preference for it, record a brand commitment in PRODUCT.md.`;
610
+ const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no
611
+ grounded direction is presented this round and the assigned index is
612
+ suspended. The hand is every dealt challenger below, each fused with the
613
+ product and presented as a full card; the FIRST dealt challenger leads, an
614
+ assignment by deal order, so the dice still choose. Verdicts and donations
615
+ apply between the challengers, weighed against the leader. The pick card
616
+ sits out; the canon stays, as always.`;
617
+ // The one command that follows a resolved choice. It records the choice
618
+ // (anonymous telemetry on API-dealt rolls; skipped under DO_NOT_TRACK /
619
+ // IMPECCABLE_NO_TELEMETRY) and opens the build's phase machine, whose
620
+ // first gate is the comp round on a comp-led build. Every run that skipped
621
+ // the comp round did so by treating a separate "telemetry ping" as
622
+ // bookkeeping: suppressed with >/dev/null, run after the page was written,
623
+ // or never run. So there is no separate ping; the start command is the
624
+ // ping, and it is not optional.
625
+ const nextCommand = scope === 'direction'
626
+ ? `AFTER THE CHOICE, run exactly one command and follow what it prints (do not suppress its output; do not write page code before it):
627
+ node ${relative(process.cwd(), here) || '.'}/build-phase.mjs start --direction ${key} --kind <assigned|pick|challenger|canon>${data.source === 'api' ? ' [--chosen <challenger-id>]' : ''}${register ? ` --register ${register}` : ''}
628
+ It records the choice${data.source === 'api' ? ' (anonymous: card kind plus catalog id; skipped under DO_NOT_TRACK / IMPECCABLE_NO_TELEMETRY)' : ''} and opens the build phases: on a comp-led build the comp round is the first gate (three comps, one approved) and no page code is written before it closes; on a code-led build it prints the contract step. A build without this state file is a build the finish reviewer treats as having skipped the round.\n`
629
+ : (data.source === 'api'
630
+ ? `AFTER THE CHOICE, run once: node ${relative(process.cwd(), here) || '.'}/concept-seed.mjs --kind <assigned|pick|challenger|canon> --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} (records the choice; the locked card's comp is the approved comp, so then: node ${relative(process.cwd(), here) || '.'}/build-phase.mjs start --comp <that comp>).\n`
631
+ : `AFTER THE CHOICE: the locked card's comp is the approved comp; run node ${relative(process.cwd(), here) || '.'}/build-phase.mjs start --comp <that comp> and follow what it prints.\n`);
632
+ const telemetryBlock = nextCommand;
633
+ const assignedBlock = register === null
634
+ ? `${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`}
487
635
  ${promotedInstruction}
488
636
  The assignment exists to refuse the model's ranking rut, never to outrank
489
- the user or the brief. Never expose assignment metadata in user-facing labels.
490
- CHALLENGERS:
637
+ the user or the brief. Never expose assignment metadata in user-facing labels.`
638
+ : register === 'safer' ? saferBlock : bolderBlock;
639
+ // A bolder round has no assigned grounded direction, so the generic
640
+ // weighing instruction (which measures against the assignment) would
641
+ // contradict the register; the bolder variant weighs against the leader.
642
+ const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form
643
+ and its system grammar, the product supplies every fact, and clarity wins
644
+ conflicts. Weigh every fused challenger against the fused LEADER, the first
645
+ dealt, on exactly two axes, audience identification and product clarity;
646
+ verdicts and donations apply between the challengers, and one that beats
647
+ the leader on both axes presents as the hand's strongest alternate.`;
648
+ const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction;
649
+ const challengerSection = register === 'safer'
650
+ ? ''
651
+ : `CHALLENGERS:
491
652
  ${data.challengers.map(renderChallenger).join('\n')}
492
- ${compositionBlock}${challengerInstruction}
653
+ ${compositionBlock}${roundChallengerInstruction}
493
654
  When you can view images, open the QUALITY BAR board and hero for any
494
655
  challenger you weigh seriously and for the world you build. They exist as a
495
656
  craft bar, the finish level and commitment the build is expected to reach,
496
657
  never as a mockup to copy; your surface serves this product, not that render.
497
- ${authorityInstruction}
658
+ `;
659
+ const restated = register === null
660
+ ? (scope === 'direction'
661
+ ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate
662
+ ${buildIndex} of your own grounded list; seed key ${key}.`
663
+ : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index
664
+ ${buildIndex} leads. Present all three dealt structures; seed key ${key}.`)
665
+ : `REGISTER (restated for truncated readers): ${register}, user-requested; the
666
+ assigned index is suspended this round; seed key ${key}.`;
667
+ return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision)
668
+ ${rerollBlock}${assignedBlock}
669
+ ${challengerSection}${authorityInstruction}
498
670
  ${richnessInstruction}
499
671
  ${telemetryBlock}A user- or brief-pinned decision beats the roll, always.
500
- ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate
501
- ${buildIndex} of your own grounded list; seed key ${key}.
672
+ ${restated}
502
673
  `;
503
674
  }
504
675
 
505
- if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
676
+ /**
677
+ * What the model must do next, once a direction (or surface structure) is
678
+ * chosen. Read from the same config the boot directive reads:
679
+ * `.impeccable/config.local.json` over `.impeccable/config.json`,
680
+ * `buildPath` comp|code; with neither, comp-led whenever image generation
681
+ * exists (an OpenAI key here; a harness-native image tool is invisible to
682
+ * this script, so the text names it too), code-led otherwise.
683
+ */
684
+ export function nextStepAfterChoice({ key, scope, cwd = process.cwd(), env = process.env } = {}) {
685
+ let buildPath = null;
686
+ for (const name of ['config.json', 'config.local.json']) {
687
+ try {
688
+ const raw = JSON.parse(readFileSync(resolve(cwd, '.impeccable', name), 'utf8'));
689
+ if (raw?.buildPath === 'comp' || raw?.buildPath === 'code') buildPath = raw.buildPath;
690
+ } catch { /* absent */ }
691
+ }
692
+ const scriptsDir = dirname(fileURLToPath(import.meta.url));
693
+ const scripts = relative(cwd, scriptsDir) || '.';
694
+ const imageGen = !!env.OPENAI_API_KEY;
695
+ const seed = key ? ` --direction ${key}` : '';
696
+ if (buildPath === 'code') {
697
+ return `NEXT (code-led, from .impeccable config): write the direction contract, then build; no comp round. Load reference/new-work.md section 5 and 6.\n`;
698
+ }
699
+ const why = buildPath === 'comp' ? 'from .impeccable config' : imageGen ? 'default: image generation is available' : 'default: comp-led unless no image tool exists; if your harness truly has none and there is no OpenAI key, this is code-led and you say so in one line';
700
+ if (scope === 'surface') {
701
+ return `NEXT (comp-led, ${why}): the locked card's comp is the approved comp. Run: node ${scripts}/build-phase.mjs start --comp <that comp> and follow its NEXT lines. Do not write page code before build-phase.mjs advance has closed the spec, plates, and hero gates.\n`;
702
+ }
703
+ return `NEXT (comp-led, ${why}): the world is chosen; the composition is not. Run: node ${scripts}/build-phase.mjs start${seed} and follow its NEXT lines: it opens the comps phase (three comps under .impeccable/mocks/, one approved by the user through the decision page or structured question, sidecar "approved": true), then spec, plates, hero, sections, motion, responsive, review. Do not write page code before those gates close. Reference: reference/visualize.md for the comp round.\n`;
704
+ }
705
+
706
+ export function sameMainModulePath(left, right, platform = process.platform) {
707
+ if (platform !== 'win32') return left === right;
708
+ const normalizeDriveLetter = (value) => value.replace(/^([a-z]):/i, (_, drive) => `${drive.toUpperCase()}:`);
709
+ return normalizeDriveLetter(left) === normalizeDriveLetter(right);
710
+ }
711
+
712
+ function isMainModule() {
713
+ if (!process.argv[1]) return false;
714
+ try {
715
+ // Node resolves import.meta.url through symlinks but leaves argv[1] as the
716
+ // invoked path. Compare real paths so a linked skill still runs its CLI,
717
+ // normalizing the drive-letter casing that Windows junctions can change.
718
+ return sameMainModulePath(
719
+ realpathSync(process.argv[1]),
720
+ realpathSync(fileURLToPath(import.meta.url))
721
+ );
722
+ } catch {
723
+ return false;
724
+ }
725
+ }
726
+
727
+ if (isMainModule()) {
506
728
  const args = process.argv.slice(2);
507
729
  const fromIdx = args.indexOf('--from');
508
730
  const scopeIdx = args.indexOf('--scope');
509
731
  const rerollIdx = args.indexOf('--reroll');
732
+ const registerIdx = args.indexOf('--register');
510
733
  const modeIdx = args.indexOf('--mode');
511
734
  const grainIdx = args.indexOf('--grain');
512
735
  const platformIdx = args.indexOf('--platform');
513
736
  const candidateCountIdx = args.indexOf('--candidate-count');
514
737
  const chosenIdx = args.indexOf('--chosen');
738
+ const kindIdx = args.indexOf('--kind');
515
739
  try {
516
- if (chosenIdx !== -1) {
740
+ if (chosenIdx !== -1 || kindIdx !== -1) {
517
741
  // Choice ping: always exits 0, telemetry must never fail a design flow.
742
+ // --kind alone pings a non-challenger outcome (assigned/pick/canon);
743
+ // --chosen alone stays the legacy challenger-win ping.
518
744
  const sent = await pingChosen({
519
- chosenId: args[chosenIdx + 1],
745
+ chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined,
520
746
  key: fromIdx !== -1 ? args[fromIdx + 1] : undefined,
521
747
  scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined,
522
748
  mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined,
749
+ kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined,
750
+ register: registerIdx !== -1 ? args[registerIdx + 1] : undefined,
523
751
  });
524
752
  process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n');
753
+ // The choice is resolved; this is the last script output the model
754
+ // reads before it decides what to do next, and every run that skipped
755
+ // the comp round did so right here: prose 20 KB into new-work.md lost
756
+ // to "direction locked, building now". So the ping prints the next
757
+ // mandatory step from the recorded build path, and the phase machine
758
+ // takes it from there.
759
+ process.stdout.write(nextStepAfterChoice({
760
+ key: fromIdx !== -1 ? args[fromIdx + 1] : undefined,
761
+ scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined,
762
+ }));
525
763
  } else {
764
+ // A dealt roll leaves a marker the build phase clears: context.mjs and
765
+ // detect.mjs read it and refuse to treat page work as done while a
766
+ // direction is chosen but the build never started (COMP_ROUND_OPEN).
767
+ try {
768
+ const { mkdirSync, writeFileSync: wf } = await import('node:fs');
769
+ if (scopeIdx !== -1 && args[scopeIdx + 1] === 'direction') {
770
+ mkdirSync(resolve(process.cwd(), '.impeccable', 'build'), { recursive: true });
771
+ wf(resolve(process.cwd(), '.impeccable', 'build', 'pending.json'), JSON.stringify({ scope: 'direction', at: new Date().toISOString() }, null, 2));
772
+ }
773
+ } catch { /* marker is best-effort */ }
526
774
  // Mechanical init gate: prose alone does not keep a model from dealing
527
775
  // before init, and fresh repos produced exactly that skip (the model
528
776
  // rolled directions with no PRODUCT.md, so nothing grounded the fusion).
@@ -542,6 +790,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
542
790
  ? args[fromIdx + 1]
543
791
  : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')),
544
792
  reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0,
793
+ register: registerIdx !== -1 ? args[registerIdx + 1] : null,
545
794
  mode: modeIdx !== -1 ? args[modeIdx + 1] : null,
546
795
  grain: grainIdx !== -1 ? args[grainIdx + 1] : null,
547
796
  platform: platformIdx !== -1 ? args[platformIdx + 1] : null,
@@ -553,6 +802,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
553
802
  process.exitCode = 1;
554
803
  }
555
804
  // A raced-out fetch may still hold a socket; exit explicitly so the CLI
556
- // never lingers on a dead network path after output is written.
805
+ // never lingers on a dead network path after output is written. Destroy
806
+ // fetch's global undici dispatcher first: process.exit() with a live
807
+ // keep-alive socket trips a libuv assertion on Windows and aborts the
808
+ // process after a successful roll (nodejs/node#56645).
809
+ const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')];
810
+ if (dispatcher && typeof dispatcher.destroy === 'function') {
811
+ try { await dispatcher.destroy(); } catch { /* exit regardless */ }
812
+ }
557
813
  process.exit(process.exitCode ?? 0);
558
814
  }
@@ -22,7 +22,7 @@ import path from 'node:path';
22
22
  import { fileURLToPath } from 'node:url';
23
23
  import { execFileSync } from 'node:child_process';
24
24
  import { loadContext, extractPlatform } from './context.mjs';
25
- import { getCritiqueDir } from './lib/impeccable-paths.mjs';
25
+ import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs';
26
26
 
27
27
  /** Is there code here at all, or just context files / an empty repo? */
28
28
  function hasCode(cwd) {
@@ -34,34 +34,25 @@ function hasCode(cwd) {
34
34
  }
35
35
 
36
36
  /**
37
- * The most recent critique snapshot across all targets. Filenames are
38
- * timestamp-prefixed (`<iso>__<slug>.md`), so a lexical sort is chronological.
39
- * Parses the small frontmatter for score + P0/P1 counts.
37
+ * Summarize the most recent critique snapshot across all targets.
40
38
  */
41
39
  function latestCritique(cwd) {
42
40
  try {
43
- const dir = getCritiqueDir(cwd);
44
- if (!fs.existsSync(dir)) return null;
45
- const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort();
46
- if (!files.length) return null;
47
- const newest = files[files.length - 1];
48
- const text = fs.readFileSync(path.join(dir, newest), 'utf-8');
49
- const front = text.split('---')[1] || '';
50
- const get = (k) => {
51
- const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm'));
52
- return m ? m[1].trim() : null;
53
- };
41
+ const latest = readLatestSnapshotAcrossTargets({ cwd });
42
+ if (!latest) return null;
43
+ const get = (key) => latest.meta[key] ?? null;
54
44
  const num = (v) => {
45
+ if (v == null || (typeof v === 'string' && v.trim() === '')) return null;
55
46
  const n = Number(v);
56
47
  return Number.isFinite(n) ? n : null;
57
48
  };
58
49
  return {
59
50
  slug: get('slug'),
60
- score: num(get('score')),
61
- p0: num(get('p0')),
62
- p1: num(get('p1')),
51
+ score: num(get('total_score') ?? get('score')),
52
+ p0: num(get('p0_count') ?? get('p0')),
53
+ p1: num(get('p1_count') ?? get('p1')),
63
54
  timestamp: get('timestamp'),
64
- file: path.relative(cwd, path.join(dir, newest)),
55
+ file: path.relative(cwd, latest.path),
65
56
  };
66
57
  } catch {
67
58
  return null;