@hegemonart/get-design-done 1.24.2 → 1.26.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 (60) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +87 -0
  4. package/README.de.md +679 -0
  5. package/README.fr.md +679 -0
  6. package/README.it.md +679 -0
  7. package/README.ja.md +679 -0
  8. package/README.ko.md +679 -0
  9. package/README.md +399 -728
  10. package/README.zh-CN.md +480 -133
  11. package/SKILL.md +2 -0
  12. package/agents/README.md +60 -0
  13. package/agents/design-reflector.md +43 -0
  14. package/agents/gdd-intel-updater.md +34 -1
  15. package/agents/prototype-gate.md +122 -0
  16. package/agents/quality-gate-runner.md +125 -0
  17. package/hooks/budget-enforcer.ts +275 -11
  18. package/hooks/gdd-decision-injector.js +183 -3
  19. package/hooks/gdd-turn-closeout.js +238 -0
  20. package/hooks/hooks.json +10 -0
  21. package/package.json +5 -5
  22. package/reference/STATE-TEMPLATE.md +41 -0
  23. package/reference/config-schema.md +30 -0
  24. package/reference/model-prices.md +40 -19
  25. package/reference/prices/antigravity.md +21 -0
  26. package/reference/prices/augment.md +21 -0
  27. package/reference/prices/claude.md +42 -0
  28. package/reference/prices/cline.md +23 -0
  29. package/reference/prices/codebuddy.md +21 -0
  30. package/reference/prices/codex.md +25 -0
  31. package/reference/prices/copilot.md +21 -0
  32. package/reference/prices/cursor.md +21 -0
  33. package/reference/prices/gemini.md +25 -0
  34. package/reference/prices/kilo.md +21 -0
  35. package/reference/prices/opencode.md +23 -0
  36. package/reference/prices/qwen.md +25 -0
  37. package/reference/prices/trae.md +23 -0
  38. package/reference/prices/windsurf.md +21 -0
  39. package/reference/registry.json +107 -1
  40. package/reference/runtime-models.md +446 -0
  41. package/reference/schemas/runtime-models.schema.json +123 -0
  42. package/scripts/install.cjs +8 -0
  43. package/scripts/lib/budget-enforcer.cjs +446 -0
  44. package/scripts/lib/cost-arbitrage.cjs +294 -0
  45. package/scripts/lib/gdd-state/mutator.ts +454 -0
  46. package/scripts/lib/gdd-state/parser.ts +351 -1
  47. package/scripts/lib/gdd-state/types.ts +193 -0
  48. package/scripts/lib/install/installer.cjs +188 -11
  49. package/scripts/lib/install/parse-runtime-models.cjs +267 -0
  50. package/scripts/lib/install/runtimes.cjs +43 -0
  51. package/scripts/lib/quality-gate-detect.cjs +126 -0
  52. package/scripts/lib/runtime-detect.cjs +96 -0
  53. package/scripts/lib/tier-resolver.cjs +311 -0
  54. package/scripts/validate-frontmatter.ts +138 -1
  55. package/skills/quality-gate/SKILL.md +222 -0
  56. package/skills/router/SKILL.md +79 -10
  57. package/skills/sketch-wrap-up/SKILL.md +47 -2
  58. package/skills/spike-wrap-up/SKILL.md +41 -2
  59. package/skills/turn-closeout/SKILL.md +115 -0
  60. package/skills/verify/SKILL.md +22 -0
@@ -29,6 +29,13 @@ import {
29
29
  type MustHave,
30
30
  type ParsedState,
31
31
  type Position,
32
+ type PrototypingBlock,
33
+ type QualityGateBlock,
34
+ type QualityGateRun,
35
+ type QualityGateStatus,
36
+ type SketchEntry,
37
+ type SkippedEntry,
38
+ type SpikeEntry,
32
39
  } from './types.ts';
33
40
 
34
41
  /**
@@ -250,6 +257,10 @@ function emitBlock(
250
257
  return emitDecisions(state.decisions, rawBody);
251
258
  case 'must_haves':
252
259
  return emitMustHaves(state.must_haves, rawBody);
260
+ case 'prototyping':
261
+ return emitPrototyping(state.prototyping, rawBody);
262
+ case 'quality_gate':
263
+ return emitQualityGate(state.quality_gate, rawBody);
253
264
  case 'connections':
254
265
  return emitConnections(state.connections, rawBody);
255
266
  case 'blockers':
@@ -340,6 +351,151 @@ function emitBlockers(blockers: Blocker[], rawBody: string | null): string {
340
351
  return blockers.map((b) => `[${b.stage}] [${b.date}]: ${b.text}`).join('\n');
341
352
  }
342
353
 
354
+ /**
355
+ * Emit the body of a `<prototyping>` block (Phase 25 Plan 25-01).
356
+ *
357
+ * Returns `null` when the block should be omitted entirely — i.e. the
358
+ * parsed state has `prototyping === null` AND no raw body is on file.
359
+ * That signal short-circuits `emitBlock` so we don't litter the output
360
+ * with empty `<prototyping></prototyping>` pairs on fresh files.
361
+ *
362
+ * Fidelity rule (matches the other blocks): when `rawBody` round-trips
363
+ * through `tryReparsePrototyping` and matches the current value
364
+ * structurally, emit the raw body verbatim. Otherwise canonicalize.
365
+ */
366
+ function emitPrototyping(
367
+ block: PrototypingBlock | null,
368
+ rawBody: string | null,
369
+ ): string | null {
370
+ if (block === null && rawBody === null) return null;
371
+ if (block === null) {
372
+ // Block was present in source but state set it to null — caller wants
373
+ // to drop the block. Still return null so emitBlock skips emission.
374
+ return null;
375
+ }
376
+ if (rawBody !== null) {
377
+ const reparsed = tryReparsePrototyping(rawBody);
378
+ if (reparsed !== null && prototypingEqual(reparsed, block)) {
379
+ return rawBody;
380
+ }
381
+ }
382
+ return canonicalPrototyping(block);
383
+ }
384
+
385
+ function canonicalPrototyping(block: PrototypingBlock): string {
386
+ const lines: string[] = [];
387
+ for (const s of block.sketches) {
388
+ lines.push(canonicalSketch(s));
389
+ }
390
+ for (const sp of block.spikes) {
391
+ lines.push(canonicalSpike(sp));
392
+ }
393
+ for (const sk of block.skipped) {
394
+ lines.push(canonicalSkipped(sk));
395
+ }
396
+ return lines.join('\n');
397
+ }
398
+
399
+ function canonicalSketch(s: SketchEntry): string {
400
+ const parts: string[] = [
401
+ `slug=${formatPrototypingAttr(s.slug)}`,
402
+ `cycle=${formatPrototypingAttr(s.cycle)}`,
403
+ `decision=${formatPrototypingAttr(s.decision)}`,
404
+ `status=${formatPrototypingAttr(s.status)}`,
405
+ ];
406
+ for (const [k, v] of Object.entries(s.extra_attrs)) {
407
+ parts.push(`${k}=${formatPrototypingAttr(v)}`);
408
+ }
409
+ return `<sketch ${parts.join(' ')}/>`;
410
+ }
411
+
412
+ function canonicalSpike(s: SpikeEntry): string {
413
+ const parts: string[] = [
414
+ `slug=${formatPrototypingAttr(s.slug)}`,
415
+ `cycle=${formatPrototypingAttr(s.cycle)}`,
416
+ `decision=${formatPrototypingAttr(s.decision)}`,
417
+ `verdict=${formatPrototypingAttr(s.verdict)}`,
418
+ `status=${formatPrototypingAttr(s.status)}`,
419
+ ];
420
+ for (const [k, v] of Object.entries(s.extra_attrs)) {
421
+ parts.push(`${k}=${formatPrototypingAttr(v)}`);
422
+ }
423
+ return `<spike ${parts.join(' ')}/>`;
424
+ }
425
+
426
+ function canonicalSkipped(s: SkippedEntry): string {
427
+ const parts: string[] = [
428
+ `at=${formatPrototypingAttr(s.at)}`,
429
+ `cycle=${formatPrototypingAttr(s.cycle)}`,
430
+ `reason=${formatPrototypingAttr(s.reason)}`,
431
+ ];
432
+ for (const [k, v] of Object.entries(s.extra_attrs)) {
433
+ parts.push(`${k}=${formatPrototypingAttr(v)}`);
434
+ }
435
+ return `<skipped ${parts.join(' ')}/>`;
436
+ }
437
+
438
+ /**
439
+ * Format an attribute value for emission. We always quote with double
440
+ * quotes for canonical output: this avoids ambiguity on values containing
441
+ * whitespace, equal signs, or solidus. Embedded `"` are escaped to `&quot;`
442
+ * which is what the parser already strips when re-reading. Empty strings
443
+ * are emitted as `""` (the parser tolerates them).
444
+ */
445
+ function formatPrototypingAttr(v: string): string {
446
+ return `"${v.replace(/"/g, '&quot;')}"`;
447
+ }
448
+
449
+ /**
450
+ * Emit the body of a `<quality_gate>` block (Phase 25 Plan 25-03).
451
+ *
452
+ * Returns `null` when the block should be omitted entirely — i.e. the
453
+ * parsed state has `quality_gate === null` AND no raw body is on file.
454
+ * Mirror of `emitPrototyping`'s short-circuit behavior so fresh STATE.md
455
+ * files don't carry an empty `<quality_gate></quality_gate>` pair.
456
+ *
457
+ * Fidelity rule (matches the other blocks): when `rawBody` round-trips
458
+ * through `tryReparseQualityGate` and matches the current value
459
+ * structurally, emit the raw body verbatim. Otherwise canonicalize.
460
+ */
461
+ function emitQualityGate(
462
+ block: QualityGateBlock | null,
463
+ rawBody: string | null,
464
+ ): string | null {
465
+ if (block === null && rawBody === null) return null;
466
+ if (block === null) {
467
+ // Block was present in source but state set it to null — caller wants
468
+ // to drop the block. Still return null so emitBlock skips emission.
469
+ return null;
470
+ }
471
+ if (rawBody !== null) {
472
+ const reparsed = tryReparseQualityGate(rawBody);
473
+ if (reparsed !== null && qualityGateEqual(reparsed, block)) {
474
+ return rawBody;
475
+ }
476
+ }
477
+ return canonicalQualityGate(block);
478
+ }
479
+
480
+ function canonicalQualityGate(block: QualityGateBlock): string {
481
+ if (block.run === null) return '';
482
+ return canonicalQualityGateRun(block.run);
483
+ }
484
+
485
+ function canonicalQualityGateRun(run: QualityGateRun): string {
486
+ const parts: string[] = [
487
+ `started_at=${formatPrototypingAttr(run.started_at)}`,
488
+ `completed_at=${formatPrototypingAttr(run.completed_at)}`,
489
+ `status=${formatPrototypingAttr(run.status)}`,
490
+ `iteration=${formatPrototypingAttr(String(run.iteration))}`,
491
+ `commands_run=${formatPrototypingAttr(run.commands_run)}`,
492
+ ];
493
+ for (const [k, v] of Object.entries(run.extra_attrs)) {
494
+ parts.push(`${k}=${formatPrototypingAttr(v)}`);
495
+ }
496
+ return `<run ${parts.join(' ')}/>`;
497
+ }
498
+
343
499
  function emitTimestamps(
344
500
  ts: Record<string, string>,
345
501
  rawBody: string | null,
@@ -415,6 +571,90 @@ function blockersEqual(a: Blocker[], b: Blocker[]): boolean {
415
571
  return true;
416
572
  }
417
573
 
574
+ function prototypingEqual(
575
+ a: PrototypingBlock,
576
+ b: PrototypingBlock,
577
+ ): boolean {
578
+ if (a.sketches.length !== b.sketches.length) return false;
579
+ if (a.spikes.length !== b.spikes.length) return false;
580
+ if (a.skipped.length !== b.skipped.length) return false;
581
+ for (let i = 0; i < a.sketches.length; i++) {
582
+ if (!sketchEqual(a.sketches[i]!, b.sketches[i]!)) return false;
583
+ }
584
+ for (let i = 0; i < a.spikes.length; i++) {
585
+ if (!spikeEqual(a.spikes[i]!, b.spikes[i]!)) return false;
586
+ }
587
+ for (let i = 0; i < a.skipped.length; i++) {
588
+ if (!skippedEqual(a.skipped[i]!, b.skipped[i]!)) return false;
589
+ }
590
+ return true;
591
+ }
592
+
593
+ function sketchEqual(a: SketchEntry, b: SketchEntry): boolean {
594
+ return (
595
+ a.slug === b.slug &&
596
+ a.cycle === b.cycle &&
597
+ a.decision === b.decision &&
598
+ a.status === b.status &&
599
+ extraAttrsEqual(a.extra_attrs, b.extra_attrs)
600
+ );
601
+ }
602
+
603
+ function spikeEqual(a: SpikeEntry, b: SpikeEntry): boolean {
604
+ return (
605
+ a.slug === b.slug &&
606
+ a.cycle === b.cycle &&
607
+ a.decision === b.decision &&
608
+ a.verdict === b.verdict &&
609
+ a.status === b.status &&
610
+ extraAttrsEqual(a.extra_attrs, b.extra_attrs)
611
+ );
612
+ }
613
+
614
+ function skippedEqual(a: SkippedEntry, b: SkippedEntry): boolean {
615
+ return (
616
+ a.at === b.at &&
617
+ a.cycle === b.cycle &&
618
+ a.reason === b.reason &&
619
+ extraAttrsEqual(a.extra_attrs, b.extra_attrs)
620
+ );
621
+ }
622
+
623
+ function extraAttrsEqual(
624
+ a: Record<string, string>,
625
+ b: Record<string, string>,
626
+ ): boolean {
627
+ const ak = Object.keys(a);
628
+ const bk = Object.keys(b);
629
+ if (ak.length !== bk.length) return false;
630
+ for (let i = 0; i < ak.length; i++) {
631
+ if (ak[i] !== bk[i]) return false;
632
+ const key = ak[i]!;
633
+ if (a[key] !== b[key]) return false;
634
+ }
635
+ return true;
636
+ }
637
+
638
+ function qualityGateEqual(
639
+ a: QualityGateBlock,
640
+ b: QualityGateBlock,
641
+ ): boolean {
642
+ if (a.run === null && b.run === null) return true;
643
+ if (a.run === null || b.run === null) return false;
644
+ return qualityGateRunEqual(a.run, b.run);
645
+ }
646
+
647
+ function qualityGateRunEqual(a: QualityGateRun, b: QualityGateRun): boolean {
648
+ return (
649
+ a.started_at === b.started_at &&
650
+ a.completed_at === b.completed_at &&
651
+ a.status === b.status &&
652
+ a.iteration === b.iteration &&
653
+ a.commands_run === b.commands_run &&
654
+ extraAttrsEqual(a.extra_attrs, b.extra_attrs)
655
+ );
656
+ }
657
+
418
658
  function recordsEqual(
419
659
  a: Record<string, string>,
420
660
  b: Record<string, string>,
@@ -553,6 +793,220 @@ function tryReparseBlockers(raw: string): Blocker[] | null {
553
793
  }
554
794
  }
555
795
 
796
+ /**
797
+ * Reparse a `<prototyping>` body for fidelity comparison. Mirrors the
798
+ * shape of `parsePrototypingBody` in parser.ts but is intentionally
799
+ * separate (and tolerant) — returns `null` on any structural surprise so
800
+ * the caller falls back to canonical emission rather than throwing.
801
+ *
802
+ * Unlike the parser, this helper does NOT throw on missing required
803
+ * attributes. If the source body has been hand-edited into something the
804
+ * parser would reject, we treat it as "definitely changed" and return
805
+ * `null` so the canonical writer takes over.
806
+ */
807
+ function tryReparsePrototyping(raw: string): PrototypingBlock | null {
808
+ try {
809
+ const sketches: SketchEntry[] = [];
810
+ const spikes: SpikeEntry[] = [];
811
+ const skipped: SkippedEntry[] = [];
812
+ const selfClose = /^<([a-z_]+)(\s+[^>]*?)?\s*\/>\s*$/;
813
+ for (const line of raw.split('\n')) {
814
+ const t = line.trim();
815
+ if (t === '' || t.startsWith('<!--')) continue;
816
+ const m = t.match(selfClose);
817
+ if (!m) {
818
+ // Anything non-comment that isn't a self-closing tag means the
819
+ // raw body is no longer a clean match for the parsed value.
820
+ return null;
821
+ }
822
+ const tag = m[1] ?? '';
823
+ const attrs = parseAttrInline(m[2] ?? '');
824
+ if (tag === 'sketch') {
825
+ const slug = attrs['slug'];
826
+ const cycle = attrs['cycle'];
827
+ const decision = attrs['decision'];
828
+ const status = attrs['status'] ?? 'resolved';
829
+ if (slug === undefined || cycle === undefined || decision === undefined) {
830
+ return null;
831
+ }
832
+ if (status !== 'resolved') return null;
833
+ sketches.push({
834
+ slug,
835
+ cycle,
836
+ decision,
837
+ status: 'resolved',
838
+ extra_attrs: extractExtras(attrs, [
839
+ 'slug',
840
+ 'cycle',
841
+ 'decision',
842
+ 'status',
843
+ ]),
844
+ });
845
+ } else if (tag === 'spike') {
846
+ const slug = attrs['slug'];
847
+ const cycle = attrs['cycle'];
848
+ const decision = attrs['decision'];
849
+ const verdict = attrs['verdict'];
850
+ const status = attrs['status'] ?? 'resolved';
851
+ if (
852
+ slug === undefined ||
853
+ cycle === undefined ||
854
+ decision === undefined ||
855
+ verdict === undefined
856
+ ) {
857
+ return null;
858
+ }
859
+ if (verdict !== 'yes' && verdict !== 'no' && verdict !== 'partial') {
860
+ return null;
861
+ }
862
+ if (status !== 'resolved') return null;
863
+ spikes.push({
864
+ slug,
865
+ cycle,
866
+ decision,
867
+ verdict,
868
+ status: 'resolved',
869
+ extra_attrs: extractExtras(attrs, [
870
+ 'slug',
871
+ 'cycle',
872
+ 'decision',
873
+ 'verdict',
874
+ 'status',
875
+ ]),
876
+ });
877
+ } else if (tag === 'skipped') {
878
+ const at = attrs['at'];
879
+ const cycle = attrs['cycle'];
880
+ const reason = attrs['reason'];
881
+ if (at === undefined || cycle === undefined || reason === undefined) {
882
+ return null;
883
+ }
884
+ skipped.push({
885
+ at,
886
+ cycle,
887
+ reason,
888
+ extra_attrs: extractExtras(attrs, ['at', 'cycle', 'reason']),
889
+ });
890
+ } else {
891
+ // Unknown self-closing tag — return null to force canonical path.
892
+ return null;
893
+ }
894
+ }
895
+ return { sketches, spikes, skipped };
896
+ } catch {
897
+ return null;
898
+ }
899
+ }
900
+
901
+ /** Mirror of parser's `parsePrototypingAttrs` — kept local to avoid
902
+ * cross-file circular reach (mutator must not import parser internals). */
903
+ function parseAttrInline(span: string): Record<string, string> {
904
+ const out: Record<string, string> = {};
905
+ const re = /([a-zA-Z_][\w-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s/>]+))/g;
906
+ let m: RegExpExecArray | null;
907
+ while ((m = re.exec(span)) !== null) {
908
+ const key = m[1] ?? '';
909
+ const value: string =
910
+ (m[2] !== undefined ? m[2] : undefined) ??
911
+ (m[3] !== undefined ? m[3] : undefined) ??
912
+ m[4] ??
913
+ '';
914
+ if (key !== '') out[key] = value;
915
+ }
916
+ return out;
917
+ }
918
+
919
+ function extractExtras(
920
+ all: Record<string, string>,
921
+ known: readonly string[],
922
+ ): Record<string, string> {
923
+ const out: Record<string, string> = {};
924
+ for (const [k, v] of Object.entries(all)) {
925
+ if (!known.includes(k)) out[k] = v;
926
+ }
927
+ return out;
928
+ }
929
+
930
+ /**
931
+ * Reparse a `<quality_gate>` body for fidelity comparison. Mirror of
932
+ * `tryReparsePrototyping` — returns `null` on any structural surprise so
933
+ * the caller falls back to canonical emission rather than throwing.
934
+ *
935
+ * Tolerant of multiple `<run/>` lines (last-wins, matching the parser),
936
+ * blank lines, and comments. Strict on attribute presence and enum
937
+ * validity — a hand-edited body that drops `commands_run` will fail to
938
+ * round-trip and fall through to canonical form, which is correct.
939
+ */
940
+ function tryReparseQualityGate(raw: string): QualityGateBlock | null {
941
+ try {
942
+ let run: QualityGateRun | null = null;
943
+ const selfClose = /^<([a-z_]+)(\s+[^>]*?)?\s*\/>\s*$/;
944
+ for (const line of raw.split('\n')) {
945
+ const t = line.trim();
946
+ if (t === '' || t.startsWith('<!--')) continue;
947
+ const m = t.match(selfClose);
948
+ if (!m) {
949
+ // Anything non-comment that isn't a self-closing tag means the
950
+ // raw body is no longer a clean match for the parsed value.
951
+ return null;
952
+ }
953
+ const tag = m[1] ?? '';
954
+ if (tag !== 'run') {
955
+ // Unknown self-closing tag inside <quality_gate> — force canonical.
956
+ return null;
957
+ }
958
+ const attrs = parseAttrInline(m[2] ?? '');
959
+ const started_at = attrs['started_at'];
960
+ const completed_at = attrs['completed_at'];
961
+ const status = attrs['status'];
962
+ const iterationRaw = attrs['iteration'];
963
+ const commands_run = attrs['commands_run'];
964
+ if (
965
+ started_at === undefined ||
966
+ completed_at === undefined ||
967
+ status === undefined ||
968
+ iterationRaw === undefined ||
969
+ commands_run === undefined
970
+ ) {
971
+ return null;
972
+ }
973
+ if (
974
+ status !== 'pass' &&
975
+ status !== 'fail' &&
976
+ status !== 'timeout' &&
977
+ status !== 'skipped'
978
+ ) {
979
+ return null;
980
+ }
981
+ const iteration = Number(iterationRaw);
982
+ if (
983
+ !Number.isFinite(iteration) ||
984
+ !Number.isInteger(iteration) ||
985
+ iteration < 0
986
+ ) {
987
+ return null;
988
+ }
989
+ run = {
990
+ started_at,
991
+ completed_at,
992
+ status: status as QualityGateStatus,
993
+ iteration,
994
+ commands_run,
995
+ extra_attrs: extractExtras(attrs, [
996
+ 'started_at',
997
+ 'completed_at',
998
+ 'status',
999
+ 'iteration',
1000
+ 'commands_run',
1001
+ ]),
1002
+ };
1003
+ }
1004
+ return { run };
1005
+ } catch {
1006
+ return null;
1007
+ }
1008
+ }
1009
+
556
1010
  function tryReparseTimestamps(
557
1011
  raw: string,
558
1012
  ): Record<string, string> | null {