@hegemonart/get-design-done 1.24.2 → 1.25.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +41 -0
- package/README.de.md +679 -0
- package/README.fr.md +679 -0
- package/README.it.md +679 -0
- package/README.ja.md +679 -0
- package/README.ko.md +679 -0
- package/README.md +396 -729
- package/README.zh-CN.md +480 -133
- package/SKILL.md +2 -0
- package/agents/prototype-gate.md +122 -0
- package/agents/quality-gate-runner.md +125 -0
- package/hooks/budget-enforcer.ts +132 -7
- package/hooks/gdd-decision-injector.js +183 -3
- package/hooks/gdd-turn-closeout.js +238 -0
- package/hooks/hooks.json +10 -0
- package/package.json +5 -5
- package/reference/STATE-TEMPLATE.md +41 -0
- package/reference/config-schema.md +30 -0
- package/scripts/lib/gdd-state/mutator.ts +454 -0
- package/scripts/lib/gdd-state/parser.ts +351 -1
- package/scripts/lib/gdd-state/types.ts +193 -0
- package/scripts/lib/quality-gate-detect.cjs +126 -0
- package/skills/quality-gate/SKILL.md +222 -0
- package/skills/router/SKILL.md +29 -9
- package/skills/sketch-wrap-up/SKILL.md +47 -2
- package/skills/spike-wrap-up/SKILL.md +41 -2
- package/skills/turn-closeout/SKILL.md +115 -0
- package/skills/verify/SKILL.md +22 -0
|
@@ -31,6 +31,9 @@ import {
|
|
|
31
31
|
isConnectionStatus,
|
|
32
32
|
isDecisionStatus,
|
|
33
33
|
isMustHaveStatus,
|
|
34
|
+
isPrototypingEntryStatus,
|
|
35
|
+
isQualityGateStatus,
|
|
36
|
+
isSpikeVerdict,
|
|
34
37
|
type Blocker,
|
|
35
38
|
type ConnectionStatus,
|
|
36
39
|
type Decision,
|
|
@@ -40,13 +43,34 @@ import {
|
|
|
40
43
|
type MustHaveStatus,
|
|
41
44
|
type ParsedState,
|
|
42
45
|
type Position,
|
|
46
|
+
type PrototypingBlock,
|
|
47
|
+
type PrototypingEntryStatus,
|
|
48
|
+
type QualityGateBlock,
|
|
49
|
+
type QualityGateRun,
|
|
50
|
+
type QualityGateStatus,
|
|
51
|
+
type SketchEntry,
|
|
52
|
+
type SkippedEntry,
|
|
53
|
+
type SpikeEntry,
|
|
54
|
+
type SpikeVerdict,
|
|
43
55
|
} from './types.ts';
|
|
44
56
|
|
|
45
|
-
/** Block names recognized by the parser in canonical order.
|
|
57
|
+
/** Block names recognized by the parser in canonical order.
|
|
58
|
+
*
|
|
59
|
+
* Phase 25 Plan 25-01 inserted `prototyping` between `must_haves` and
|
|
60
|
+
* `connections` (matches the position chosen for STATE-TEMPLATE.md). The
|
|
61
|
+
* block is OPTIONAL — most fresh files don't carry it, so the serializer
|
|
62
|
+
* omits the block entirely when `state.prototyping === null`.
|
|
63
|
+
*
|
|
64
|
+
* Phase 25 Plan 25-03 inserted `quality_gate` immediately after
|
|
65
|
+
* `prototyping` (the two are conceptually related — both are
|
|
66
|
+
* checkpoint-style blocks that surface mid-pipeline gate outcomes). Same
|
|
67
|
+
* optionality rules as `prototyping`. */
|
|
46
68
|
export const BLOCK_ORDER = [
|
|
47
69
|
'position',
|
|
48
70
|
'decisions',
|
|
49
71
|
'must_haves',
|
|
72
|
+
'prototyping',
|
|
73
|
+
'quality_gate',
|
|
50
74
|
'connections',
|
|
51
75
|
'blockers',
|
|
52
76
|
'parallelism_decision',
|
|
@@ -62,6 +86,8 @@ export interface RawBlockBodies {
|
|
|
62
86
|
position: string | null;
|
|
63
87
|
decisions: string | null;
|
|
64
88
|
must_haves: string | null;
|
|
89
|
+
prototyping: string | null;
|
|
90
|
+
quality_gate: string | null;
|
|
65
91
|
connections: string | null;
|
|
66
92
|
blockers: string | null;
|
|
67
93
|
parallelism_decision: string | null;
|
|
@@ -77,6 +103,8 @@ export interface BlockGaps {
|
|
|
77
103
|
position: string;
|
|
78
104
|
decisions: string;
|
|
79
105
|
must_haves: string;
|
|
106
|
+
prototyping: string;
|
|
107
|
+
quality_gate: string;
|
|
80
108
|
connections: string;
|
|
81
109
|
blockers: string;
|
|
82
110
|
parallelism_decision: string;
|
|
@@ -106,6 +134,8 @@ const EMPTY_RAW_BODIES: RawBlockBodies = {
|
|
|
106
134
|
position: null,
|
|
107
135
|
decisions: null,
|
|
108
136
|
must_haves: null,
|
|
137
|
+
prototyping: null,
|
|
138
|
+
quality_gate: null,
|
|
109
139
|
connections: null,
|
|
110
140
|
blockers: null,
|
|
111
141
|
parallelism_decision: null,
|
|
@@ -193,6 +223,8 @@ export function parse(raw: string): ParseResult {
|
|
|
193
223
|
position: '',
|
|
194
224
|
decisions: '',
|
|
195
225
|
must_haves: '',
|
|
226
|
+
prototyping: '',
|
|
227
|
+
quality_gate: '',
|
|
196
228
|
connections: '',
|
|
197
229
|
blockers: '',
|
|
198
230
|
parallelism_decision: '',
|
|
@@ -244,6 +276,8 @@ export function parse(raw: string): ParseResult {
|
|
|
244
276
|
let blockers: Blocker[] = [];
|
|
245
277
|
let parallelism_decision: string | null = null;
|
|
246
278
|
let todos: string | null = null;
|
|
279
|
+
let prototyping: PrototypingBlock | null = null;
|
|
280
|
+
let quality_gate: QualityGateBlock | null = null;
|
|
247
281
|
let timestamps: Record<string, string> = {};
|
|
248
282
|
|
|
249
283
|
for (const blk of blocks) {
|
|
@@ -262,6 +296,12 @@ export function parse(raw: string): ParseResult {
|
|
|
262
296
|
case 'must_haves':
|
|
263
297
|
must_haves = parseMustHavesBody(rawBody, fileLineOfBody);
|
|
264
298
|
break;
|
|
299
|
+
case 'prototyping':
|
|
300
|
+
prototyping = parsePrototypingBody(rawBody, fileLineOfBody);
|
|
301
|
+
break;
|
|
302
|
+
case 'quality_gate':
|
|
303
|
+
quality_gate = parseQualityGateBody(rawBody, fileLineOfBody);
|
|
304
|
+
break;
|
|
265
305
|
case 'connections':
|
|
266
306
|
connections = parseConnectionsBody(rawBody, fileLineOfBody);
|
|
267
307
|
break;
|
|
@@ -305,6 +345,8 @@ export function parse(raw: string): ParseResult {
|
|
|
305
345
|
blockers,
|
|
306
346
|
parallelism_decision,
|
|
307
347
|
todos,
|
|
348
|
+
prototyping,
|
|
349
|
+
quality_gate,
|
|
308
350
|
timestamps,
|
|
309
351
|
body_preamble,
|
|
310
352
|
body_trailer,
|
|
@@ -504,6 +546,314 @@ function parseBlockersBody(body: string, startLine: number): Blocker[] {
|
|
|
504
546
|
return out;
|
|
505
547
|
}
|
|
506
548
|
|
|
549
|
+
/**
|
|
550
|
+
* Parse the body of a `<prototyping>` block (Phase 25 Plan 25-01 / D-01).
|
|
551
|
+
*
|
|
552
|
+
* Recognized self-closing children (each on its own line, attribute order
|
|
553
|
+
* tolerant, unknown attributes preserved into `extra_attrs` for forward
|
|
554
|
+
* compat):
|
|
555
|
+
* <sketch slug=… cycle=… decision=D-XX status=resolved/>
|
|
556
|
+
* <spike slug=… cycle=… decision=D-XX verdict=yes|no|partial status=resolved/>
|
|
557
|
+
* <skipped at=… cycle=… reason=…/>
|
|
558
|
+
*
|
|
559
|
+
* Lines that are blank, HTML comments, or unrecognized self-closing tags
|
|
560
|
+
* are tolerated (forward-compat). Required attributes that are missing or
|
|
561
|
+
* carry an invalid enum value throw `ParseError` so operators see a real
|
|
562
|
+
* problem instead of a silent drop.
|
|
563
|
+
*/
|
|
564
|
+
function parsePrototypingBody(body: string, startLine: number): PrototypingBlock {
|
|
565
|
+
const sketches: SketchEntry[] = [];
|
|
566
|
+
const spikes: SpikeEntry[] = [];
|
|
567
|
+
const skipped: SkippedEntry[] = [];
|
|
568
|
+
const lines = body.split('\n');
|
|
569
|
+
// Match an XML-ish self-closing tag: `<name attr=value attr="quoted"/>`.
|
|
570
|
+
// We capture the tag name and the attribute span; attributes are split
|
|
571
|
+
// by `parsePrototypingAttrs` (handles both quoted and bare values).
|
|
572
|
+
const selfClose = /^<([a-z_]+)(\s+[^>]*?)?\s*\/>\s*$/;
|
|
573
|
+
for (let i = 0; i < lines.length; i++) {
|
|
574
|
+
const line = (lines[i] ?? '').trim();
|
|
575
|
+
if (line === '' || line.startsWith('<!--')) continue;
|
|
576
|
+
const m = line.match(selfClose);
|
|
577
|
+
if (!m) {
|
|
578
|
+
// Unknown shape inside the block — tolerate (forward-compat). Don't
|
|
579
|
+
// throw; downstream sketch/spike-wrap-up flows may add new child
|
|
580
|
+
// tags before the parser learns about them.
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const tag = m[1] ?? '';
|
|
584
|
+
const attrs = parsePrototypingAttrs(m[2] ?? '');
|
|
585
|
+
const fileLine = startLine + i;
|
|
586
|
+
if (tag === 'sketch') {
|
|
587
|
+
sketches.push(buildSketchEntry(attrs, fileLine));
|
|
588
|
+
} else if (tag === 'spike') {
|
|
589
|
+
spikes.push(buildSpikeEntry(attrs, fileLine));
|
|
590
|
+
} else if (tag === 'skipped') {
|
|
591
|
+
skipped.push(buildSkippedEntry(attrs, fileLine));
|
|
592
|
+
}
|
|
593
|
+
// else: unknown self-closing tag inside <prototyping> — ignore.
|
|
594
|
+
}
|
|
595
|
+
return { sketches, spikes, skipped };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Split a self-closing-tag attribute span into a `{name → value}` map.
|
|
600
|
+
* Handles double-quoted, single-quoted, and bare (no-whitespace) values.
|
|
601
|
+
* Unknown to whitespace tokenization is fine here because attribute names
|
|
602
|
+
* in our schema are simple identifiers.
|
|
603
|
+
*/
|
|
604
|
+
function parsePrototypingAttrs(span: string): Record<string, string> {
|
|
605
|
+
const out: Record<string, string> = {};
|
|
606
|
+
// Token shape: `name="dq"`, `name='sq'`, or `name=bare`. Bare values run
|
|
607
|
+
// until the next whitespace or EOS. Greedy regex with three alternatives
|
|
608
|
+
// walks the span position-by-position.
|
|
609
|
+
const re = /([a-zA-Z_][\w-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s/>]+))/g;
|
|
610
|
+
let m: RegExpExecArray | null;
|
|
611
|
+
while ((m = re.exec(span)) !== null) {
|
|
612
|
+
const key = m[1] ?? '';
|
|
613
|
+
const value: string =
|
|
614
|
+
(m[2] !== undefined ? m[2] : undefined) ??
|
|
615
|
+
(m[3] !== undefined ? m[3] : undefined) ??
|
|
616
|
+
m[4] ??
|
|
617
|
+
'';
|
|
618
|
+
if (key !== '') out[key] = value;
|
|
619
|
+
}
|
|
620
|
+
return out;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function buildSketchEntry(
|
|
624
|
+
attrs: Record<string, string>,
|
|
625
|
+
fileLine: number,
|
|
626
|
+
): SketchEntry {
|
|
627
|
+
const slug = attrs['slug'];
|
|
628
|
+
const cycle = attrs['cycle'];
|
|
629
|
+
const decision = attrs['decision'];
|
|
630
|
+
const status = attrs['status'] ?? 'resolved';
|
|
631
|
+
if (slug === undefined) {
|
|
632
|
+
throw new ParseError('<sketch/> missing required attribute slug', fileLine);
|
|
633
|
+
}
|
|
634
|
+
if (cycle === undefined) {
|
|
635
|
+
throw new ParseError('<sketch/> missing required attribute cycle', fileLine);
|
|
636
|
+
}
|
|
637
|
+
if (decision === undefined) {
|
|
638
|
+
throw new ParseError(
|
|
639
|
+
'<sketch/> missing required attribute decision',
|
|
640
|
+
fileLine,
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
if (!isPrototypingEntryStatus(status)) {
|
|
644
|
+
throw new ParseError(
|
|
645
|
+
`<sketch/> invalid status: ${status}`,
|
|
646
|
+
fileLine,
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
slug,
|
|
651
|
+
cycle,
|
|
652
|
+
decision,
|
|
653
|
+
status: status as PrototypingEntryStatus,
|
|
654
|
+
extra_attrs: extractExtraAttrs(attrs, ['slug', 'cycle', 'decision', 'status']),
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function buildSpikeEntry(
|
|
659
|
+
attrs: Record<string, string>,
|
|
660
|
+
fileLine: number,
|
|
661
|
+
): SpikeEntry {
|
|
662
|
+
const slug = attrs['slug'];
|
|
663
|
+
const cycle = attrs['cycle'];
|
|
664
|
+
const decision = attrs['decision'];
|
|
665
|
+
const verdict = attrs['verdict'];
|
|
666
|
+
const status = attrs['status'] ?? 'resolved';
|
|
667
|
+
if (slug === undefined) {
|
|
668
|
+
throw new ParseError('<spike/> missing required attribute slug', fileLine);
|
|
669
|
+
}
|
|
670
|
+
if (cycle === undefined) {
|
|
671
|
+
throw new ParseError('<spike/> missing required attribute cycle', fileLine);
|
|
672
|
+
}
|
|
673
|
+
if (decision === undefined) {
|
|
674
|
+
throw new ParseError(
|
|
675
|
+
'<spike/> missing required attribute decision',
|
|
676
|
+
fileLine,
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
if (verdict === undefined) {
|
|
680
|
+
throw new ParseError(
|
|
681
|
+
'<spike/> missing required attribute verdict',
|
|
682
|
+
fileLine,
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
if (!isSpikeVerdict(verdict)) {
|
|
686
|
+
throw new ParseError(
|
|
687
|
+
`<spike/> invalid verdict: ${verdict}`,
|
|
688
|
+
fileLine,
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
if (!isPrototypingEntryStatus(status)) {
|
|
692
|
+
throw new ParseError(
|
|
693
|
+
`<spike/> invalid status: ${status}`,
|
|
694
|
+
fileLine,
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
slug,
|
|
699
|
+
cycle,
|
|
700
|
+
decision,
|
|
701
|
+
verdict: verdict as SpikeVerdict,
|
|
702
|
+
status: status as PrototypingEntryStatus,
|
|
703
|
+
extra_attrs: extractExtraAttrs(attrs, [
|
|
704
|
+
'slug',
|
|
705
|
+
'cycle',
|
|
706
|
+
'decision',
|
|
707
|
+
'verdict',
|
|
708
|
+
'status',
|
|
709
|
+
]),
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function buildSkippedEntry(
|
|
714
|
+
attrs: Record<string, string>,
|
|
715
|
+
fileLine: number,
|
|
716
|
+
): SkippedEntry {
|
|
717
|
+
const at = attrs['at'];
|
|
718
|
+
const cycle = attrs['cycle'];
|
|
719
|
+
const reason = attrs['reason'];
|
|
720
|
+
if (at === undefined) {
|
|
721
|
+
throw new ParseError('<skipped/> missing required attribute at', fileLine);
|
|
722
|
+
}
|
|
723
|
+
if (cycle === undefined) {
|
|
724
|
+
throw new ParseError(
|
|
725
|
+
'<skipped/> missing required attribute cycle',
|
|
726
|
+
fileLine,
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
if (reason === undefined) {
|
|
730
|
+
throw new ParseError(
|
|
731
|
+
'<skipped/> missing required attribute reason',
|
|
732
|
+
fileLine,
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
at,
|
|
737
|
+
cycle,
|
|
738
|
+
reason,
|
|
739
|
+
extra_attrs: extractExtraAttrs(attrs, ['at', 'cycle', 'reason']),
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function extractExtraAttrs(
|
|
744
|
+
all: Record<string, string>,
|
|
745
|
+
known: readonly string[],
|
|
746
|
+
): Record<string, string> {
|
|
747
|
+
const out: Record<string, string> = {};
|
|
748
|
+
for (const [k, v] of Object.entries(all)) {
|
|
749
|
+
if (!known.includes(k)) out[k] = v;
|
|
750
|
+
}
|
|
751
|
+
return out;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Parse the body of a `<quality_gate>` block (Phase 25 Plan 25-03).
|
|
756
|
+
*
|
|
757
|
+
* The block houses at most one self-closing `<run/>` element. Multiple
|
|
758
|
+
* `<run/>` lines are not part of the v1.25 schema — append-mode would
|
|
759
|
+
* be overkill (the SKILL overwrites on every gate completion). If the
|
|
760
|
+
* source carries multiple, we accept the LAST one (most recent wins) so
|
|
761
|
+
* a hand-edit that adds a newer entry above an older one still parses
|
|
762
|
+
* sensibly. Lines that are blank or HTML comments are tolerated.
|
|
763
|
+
*
|
|
764
|
+
* Required attributes: `started_at`, `completed_at`, `status`,
|
|
765
|
+
* `iteration`, `commands_run`. Missing attributes throw `ParseError` so
|
|
766
|
+
* operators see the problem (mirrors prototyping behavior). `iteration`
|
|
767
|
+
* must parse as a finite non-negative integer.
|
|
768
|
+
*/
|
|
769
|
+
function parseQualityGateBody(body: string, startLine: number): QualityGateBlock {
|
|
770
|
+
let run: QualityGateRun | null = null;
|
|
771
|
+
const lines = body.split('\n');
|
|
772
|
+
const selfClose = /^<([a-z_]+)(\s+[^>]*?)?\s*\/>\s*$/;
|
|
773
|
+
for (let i = 0; i < lines.length; i++) {
|
|
774
|
+
const line = (lines[i] ?? '').trim();
|
|
775
|
+
if (line === '' || line.startsWith('<!--')) continue;
|
|
776
|
+
const m = line.match(selfClose);
|
|
777
|
+
if (!m) {
|
|
778
|
+
// Forward-compat: tolerate unknown shapes inside the block.
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
const tag = m[1] ?? '';
|
|
782
|
+
if (tag !== 'run') continue; // forward-compat for unknown child tags
|
|
783
|
+
const attrs = parsePrototypingAttrs(m[2] ?? '');
|
|
784
|
+
run = buildQualityGateRun(attrs, startLine + i);
|
|
785
|
+
}
|
|
786
|
+
return { run };
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function buildQualityGateRun(
|
|
790
|
+
attrs: Record<string, string>,
|
|
791
|
+
fileLine: number,
|
|
792
|
+
): QualityGateRun {
|
|
793
|
+
const started_at = attrs['started_at'];
|
|
794
|
+
const completed_at = attrs['completed_at'];
|
|
795
|
+
const status = attrs['status'];
|
|
796
|
+
const iterationRaw = attrs['iteration'];
|
|
797
|
+
const commands_run = attrs['commands_run'];
|
|
798
|
+
if (started_at === undefined) {
|
|
799
|
+
throw new ParseError(
|
|
800
|
+
'<quality_gate> <run/> missing required attribute started_at',
|
|
801
|
+
fileLine,
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
if (completed_at === undefined) {
|
|
805
|
+
throw new ParseError(
|
|
806
|
+
'<quality_gate> <run/> missing required attribute completed_at',
|
|
807
|
+
fileLine,
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
if (status === undefined) {
|
|
811
|
+
throw new ParseError(
|
|
812
|
+
'<quality_gate> <run/> missing required attribute status',
|
|
813
|
+
fileLine,
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
if (!isQualityGateStatus(status)) {
|
|
817
|
+
throw new ParseError(
|
|
818
|
+
`<quality_gate> <run/> invalid status: ${status}`,
|
|
819
|
+
fileLine,
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
if (iterationRaw === undefined) {
|
|
823
|
+
throw new ParseError(
|
|
824
|
+
'<quality_gate> <run/> missing required attribute iteration',
|
|
825
|
+
fileLine,
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
const iteration = Number(iterationRaw);
|
|
829
|
+
if (!Number.isFinite(iteration) || !Number.isInteger(iteration) || iteration < 0) {
|
|
830
|
+
throw new ParseError(
|
|
831
|
+
`<quality_gate> <run/> iteration not a non-negative integer: ${iterationRaw}`,
|
|
832
|
+
fileLine,
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
if (commands_run === undefined) {
|
|
836
|
+
throw new ParseError(
|
|
837
|
+
'<quality_gate> <run/> missing required attribute commands_run',
|
|
838
|
+
fileLine,
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
return {
|
|
842
|
+
started_at,
|
|
843
|
+
completed_at,
|
|
844
|
+
status: status as QualityGateStatus,
|
|
845
|
+
iteration,
|
|
846
|
+
commands_run,
|
|
847
|
+
extra_attrs: extractExtraAttrs(attrs, [
|
|
848
|
+
'started_at',
|
|
849
|
+
'completed_at',
|
|
850
|
+
'status',
|
|
851
|
+
'iteration',
|
|
852
|
+
'commands_run',
|
|
853
|
+
]),
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
507
857
|
function parseTimestampsBody(
|
|
508
858
|
body: string,
|
|
509
859
|
_startLine: number,
|
|
@@ -36,6 +36,23 @@ export type DecisionStatus = 'locked' | 'tentative';
|
|
|
36
36
|
/** Verification state for a `<must_haves>` entry. */
|
|
37
37
|
export type MustHaveStatus = 'pending' | 'pass' | 'fail';
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Verdict for a `<spike>` entry — the answer the spike produced.
|
|
41
|
+
* Phase 25 Plan 25-01: spikes resolve a "can this work?" question with one
|
|
42
|
+
* of three outcomes. `partial` means the spike answered for some cases but
|
|
43
|
+
* not all (e.g., works on one platform, not another).
|
|
44
|
+
*/
|
|
45
|
+
export type SpikeVerdict = 'yes' | 'no' | 'partial';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolution status for a sketch or spike entry. Phase 25 keeps the surface
|
|
49
|
+
* minimal — `resolved` is the only value v1.25 writes (sketches and spikes
|
|
50
|
+
* either complete and produce a D-XX, or they get a `<skipped/>` entry).
|
|
51
|
+
* Kept as a string union to leave room for `pending`/`abandoned` later
|
|
52
|
+
* without a parser change.
|
|
53
|
+
*/
|
|
54
|
+
export type PrototypingEntryStatus = 'resolved';
|
|
55
|
+
|
|
39
56
|
/**
|
|
40
57
|
* Frontmatter block (between leading `---` fences). STATE.md frontmatter is
|
|
41
58
|
* flat `key: value` — we parse it with a tiny hand-rolled reader (no YAML
|
|
@@ -90,6 +107,77 @@ export interface Blocker {
|
|
|
90
107
|
text: string;
|
|
91
108
|
}
|
|
92
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Single `<sketch/>` child entry inside the `<prototyping>` block.
|
|
112
|
+
*
|
|
113
|
+
* Phase 25 Plan 25-01 (D-01): a sketch records a resolved exploration of a
|
|
114
|
+
* visual / direction question. The wrap-up flow (Plan 25-05) writes the
|
|
115
|
+
* resolution as a D-XX decision AND appends a `<sketch slug=… cycle=…
|
|
116
|
+
* decision=D-XX status=resolved/>` entry here so the decision-injector
|
|
117
|
+
* (Plan 25-06) can surface prior sketch outcomes to downstream agents.
|
|
118
|
+
*
|
|
119
|
+
* Unknown attributes seen by the parser are preserved in `extra_attrs` so
|
|
120
|
+
* forward-compat additions (e.g., a future `confidence=` attribute) round-
|
|
121
|
+
* trip through parse → serialize without loss.
|
|
122
|
+
*/
|
|
123
|
+
export interface SketchEntry {
|
|
124
|
+
slug: string;
|
|
125
|
+
cycle: string;
|
|
126
|
+
decision: string;
|
|
127
|
+
status: PrototypingEntryStatus;
|
|
128
|
+
/** Verbatim copy of any attributes the parser did not recognize. Keys
|
|
129
|
+
* are attribute names; values are the unquoted attribute strings. */
|
|
130
|
+
extra_attrs: Record<string, string>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Single `<spike/>` child entry inside the `<prototyping>` block.
|
|
135
|
+
*
|
|
136
|
+
* Phase 25 Plan 25-01 (D-01): a spike records a resolved feasibility
|
|
137
|
+
* probe. The `verdict` field captures the answer (`yes` / `no` /
|
|
138
|
+
* `partial`); the `decision` field links to the D-XX written by
|
|
139
|
+
* `spike-wrap-up` (Plan 25-05).
|
|
140
|
+
*/
|
|
141
|
+
export interface SpikeEntry {
|
|
142
|
+
slug: string;
|
|
143
|
+
cycle: string;
|
|
144
|
+
decision: string;
|
|
145
|
+
verdict: SpikeVerdict;
|
|
146
|
+
status: PrototypingEntryStatus;
|
|
147
|
+
/** Forward-compat passthrough — same semantics as on `SketchEntry`. */
|
|
148
|
+
extra_attrs: Record<string, string>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Single `<skipped/>` child entry inside the `<prototyping>` block.
|
|
153
|
+
*
|
|
154
|
+
* Phase 25 Plan 25-01 (D-02): cycle-scoped suppression of further prototype
|
|
155
|
+
* gate prompts. `at` is the firing point that was skipped (typically
|
|
156
|
+
* `explore` or `plan`); `cycle` mirrors the active cycle id; `reason` is a
|
|
157
|
+
* short free-form string captured at skip time.
|
|
158
|
+
*/
|
|
159
|
+
export interface SkippedEntry {
|
|
160
|
+
at: string;
|
|
161
|
+
cycle: string;
|
|
162
|
+
reason: string;
|
|
163
|
+
/** Forward-compat passthrough — same semantics as on `SketchEntry`. */
|
|
164
|
+
extra_attrs: Record<string, string>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Parsed `<prototyping>` block. `null` on `ParsedState` when the block is
|
|
169
|
+
* absent; an instance with all three arrays empty represents a present-but-
|
|
170
|
+
* empty block (rare — wrap-up flows always append something).
|
|
171
|
+
*
|
|
172
|
+
* The three arrays preserve insertion order — round-trip serialization
|
|
173
|
+
* emits children in the same order they appeared in the source file.
|
|
174
|
+
*/
|
|
175
|
+
export interface PrototypingBlock {
|
|
176
|
+
sketches: SketchEntry[];
|
|
177
|
+
spikes: SpikeEntry[];
|
|
178
|
+
skipped: SkippedEntry[];
|
|
179
|
+
}
|
|
180
|
+
|
|
93
181
|
/**
|
|
94
182
|
* Canonical parsed shape of a STATE.md file. Consumers mutate this in-place
|
|
95
183
|
* inside `mutate(path, fn)`, then the serializer projects it back to
|
|
@@ -115,6 +203,24 @@ export interface ParsedState {
|
|
|
115
203
|
* with illustrative comments, so most fresh files carry a non-null body.
|
|
116
204
|
*/
|
|
117
205
|
todos: string | null;
|
|
206
|
+
/**
|
|
207
|
+
* Parsed `<prototyping>` block (Phase 25 Plan 25-01 / D-01). `null` when
|
|
208
|
+
* the block is absent in the source — the serializer omits the block
|
|
209
|
+
* entirely in that case rather than emitting an empty `<prototyping>`
|
|
210
|
+
* pair. A non-null instance with all three arrays empty is permitted
|
|
211
|
+
* but only emitted when the source already had a present-but-empty
|
|
212
|
+
* block (preserves byte-identical round-trip).
|
|
213
|
+
*/
|
|
214
|
+
prototyping: PrototypingBlock | null;
|
|
215
|
+
/**
|
|
216
|
+
* Parsed `<quality_gate>` block (Phase 25 Plan 25-03 / D-06..D-09).
|
|
217
|
+
* `null` when the block is absent in the source — the serializer omits
|
|
218
|
+
* the block entirely in that case rather than emitting an empty
|
|
219
|
+
* `<quality_gate>` pair. A non-null instance with `run === null` is
|
|
220
|
+
* permitted but only emitted when the source already had a present-but-
|
|
221
|
+
* empty block (preserves byte-identical round-trip).
|
|
222
|
+
*/
|
|
223
|
+
quality_gate: QualityGateBlock | null;
|
|
118
224
|
timestamps: Record<string, string>;
|
|
119
225
|
/** Verbatim span between frontmatter end and the first recognized block. */
|
|
120
226
|
body_preamble: string;
|
|
@@ -177,3 +283,90 @@ export function isDecisionStatus(value: unknown): value is DecisionStatus {
|
|
|
177
283
|
export function isMustHaveStatus(value: unknown): value is MustHaveStatus {
|
|
178
284
|
return value === 'pending' || value === 'pass' || value === 'fail';
|
|
179
285
|
}
|
|
286
|
+
|
|
287
|
+
/** Type-guard for `SpikeVerdict`. */
|
|
288
|
+
export function isSpikeVerdict(value: unknown): value is SpikeVerdict {
|
|
289
|
+
return value === 'yes' || value === 'no' || value === 'partial';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Type-guard for `PrototypingEntryStatus`. */
|
|
293
|
+
export function isPrototypingEntryStatus(
|
|
294
|
+
value: unknown,
|
|
295
|
+
): value is PrototypingEntryStatus {
|
|
296
|
+
return value === 'resolved';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Status of a `<quality_gate>` run (Phase 25 Plan 25-03 / D-06..D-09).
|
|
301
|
+
*
|
|
302
|
+
* - `pass` — every detected command exited 0 within the timeout budget.
|
|
303
|
+
* - `fail` — at least one command failed AND the fix loop reached
|
|
304
|
+
* `max_iters` without producing a clean run. Verify entry
|
|
305
|
+
* refuses on this status (Plan 25-07 territory).
|
|
306
|
+
* - `timeout` — the parallel command run exceeded
|
|
307
|
+
* `quality_gate.timeout_seconds`. Treated as a non-blocking
|
|
308
|
+
* warning per D-07 — verify entry warns, does not refuse.
|
|
309
|
+
* - `skipped` — the detection chain (D-06) resolved zero commands. The
|
|
310
|
+
* gate emits a notice and continues; verify entry does not
|
|
311
|
+
* block on `skipped`.
|
|
312
|
+
*/
|
|
313
|
+
export type QualityGateStatus = 'pass' | 'fail' | 'timeout' | 'skipped';
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Single resolved run captured in the `<quality_gate>` block (Phase 25
|
|
317
|
+
* Plan 25-03 / D-06..D-09). Append-mode would be overkill — only the most
|
|
318
|
+
* recent run is retained. The wrap-up flow (the SKILL's Step 5) overwrites
|
|
319
|
+
* this entry on every gate completion.
|
|
320
|
+
*
|
|
321
|
+
* Shape mirrors the corresponding `<run …/>` self-closing tag attribute set:
|
|
322
|
+
* `<run started_at=… completed_at=… status=… iteration=N commands_run="lint,typecheck,test"/>`
|
|
323
|
+
*
|
|
324
|
+
* `commands_run` is a comma-separated list rather than a `string[]` so the
|
|
325
|
+
* STATE block stays a single self-closing tag — no nested children, no
|
|
326
|
+
* order ambiguity in serialization.
|
|
327
|
+
*
|
|
328
|
+
* Unknown attributes seen on the `<run/>` tag are preserved in `extra_attrs`
|
|
329
|
+
* for forwards-compat (mirrors the prototyping pattern).
|
|
330
|
+
*/
|
|
331
|
+
export interface QualityGateRun {
|
|
332
|
+
/** ISO 8601 timestamp at which Step 2 (parallel run) entered. */
|
|
333
|
+
started_at: string;
|
|
334
|
+
/** ISO 8601 timestamp at which the gate produced its terminal status. */
|
|
335
|
+
completed_at: string;
|
|
336
|
+
/** Terminal status emitted by Step 6 (event emission). */
|
|
337
|
+
status: QualityGateStatus;
|
|
338
|
+
/**
|
|
339
|
+
* Loop count from Step 4 (fix loop). `1` = single clean pass; `2..N` =
|
|
340
|
+
* required at least one fixer iteration; `N === max_iters` with
|
|
341
|
+
* `status === 'fail'` = bounded exhaustion.
|
|
342
|
+
*/
|
|
343
|
+
iteration: number;
|
|
344
|
+
/**
|
|
345
|
+
* Comma-separated list of command names actually executed in Step 2 —
|
|
346
|
+
* e.g. `"lint,typecheck,test"`. Empty string when `status === 'skipped'`.
|
|
347
|
+
*/
|
|
348
|
+
commands_run: string;
|
|
349
|
+
/** Forward-compat passthrough — same semantics as on `SketchEntry`. */
|
|
350
|
+
extra_attrs: Record<string, string>;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Parsed `<quality_gate>` block. The block houses a single most-recent
|
|
355
|
+
* `<run/>` entry; `null` on `ParsedState` means the block is absent in the
|
|
356
|
+
* source (no gate has run yet on this STATE.md). `run === null` inside a
|
|
357
|
+
* non-null block represents a present-but-empty block (rare — the SKILL
|
|
358
|
+
* always writes a `<run/>` before closing).
|
|
359
|
+
*/
|
|
360
|
+
export interface QualityGateBlock {
|
|
361
|
+
run: QualityGateRun | null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Type-guard for `QualityGateStatus`. */
|
|
365
|
+
export function isQualityGateStatus(value: unknown): value is QualityGateStatus {
|
|
366
|
+
return (
|
|
367
|
+
value === 'pass' ||
|
|
368
|
+
value === 'fail' ||
|
|
369
|
+
value === 'timeout' ||
|
|
370
|
+
value === 'skipped'
|
|
371
|
+
);
|
|
372
|
+
}
|