@slowcook-ai/cli 0.16.0-alpha.4 → 0.17.0-alpha.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 (91) hide show
  1. package/dist/cli.js +50 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/brew/agent.d.ts +25 -1
  4. package/dist/commands/brew/agent.d.ts.map +1 -1
  5. package/dist/commands/brew/agent.js +123 -20
  6. package/dist/commands/brew/agent.js.map +1 -1
  7. package/dist/commands/brew/halt.d.ts +1 -1
  8. package/dist/commands/brew/halt.d.ts.map +1 -1
  9. package/dist/commands/brew/halt.js +13 -0
  10. package/dist/commands/brew/halt.js.map +1 -1
  11. package/dist/commands/check/index.d.ts +14 -0
  12. package/dist/commands/check/index.d.ts.map +1 -0
  13. package/dist/commands/check/index.js +75 -0
  14. package/dist/commands/check/index.js.map +1 -0
  15. package/dist/commands/check/mock-isolation.d.ts +52 -0
  16. package/dist/commands/check/mock-isolation.d.ts.map +1 -0
  17. package/dist/commands/check/mock-isolation.js +186 -0
  18. package/dist/commands/check/mock-isolation.js.map +1 -0
  19. package/dist/commands/init/from-prod.d.ts +48 -0
  20. package/dist/commands/init/from-prod.d.ts.map +1 -0
  21. package/dist/commands/init/from-prod.js +256 -0
  22. package/dist/commands/init/from-prod.js.map +1 -0
  23. package/dist/commands/init/index.d.ts.map +1 -1
  24. package/dist/commands/init/index.js +9 -0
  25. package/dist/commands/init/index.js.map +1 -1
  26. package/dist/commands/init/mock.d.ts.map +1 -1
  27. package/dist/commands/init/mock.js +47 -13
  28. package/dist/commands/init/mock.js.map +1 -1
  29. package/dist/commands/on-mockup-approved/index.d.ts +25 -0
  30. package/dist/commands/on-mockup-approved/index.d.ts.map +1 -0
  31. package/dist/commands/on-mockup-approved/index.js +359 -0
  32. package/dist/commands/on-mockup-approved/index.js.map +1 -0
  33. package/dist/commands/plate/classify.d.ts +65 -0
  34. package/dist/commands/plate/classify.d.ts.map +1 -0
  35. package/dist/commands/plate/classify.js +194 -0
  36. package/dist/commands/plate/classify.js.map +1 -0
  37. package/dist/commands/plate/index.d.ts.map +1 -1
  38. package/dist/commands/plate/index.js +259 -34
  39. package/dist/commands/plate/index.js.map +1 -1
  40. package/dist/commands/port/index.d.ts +30 -0
  41. package/dist/commands/port/index.d.ts.map +1 -0
  42. package/dist/commands/port/index.js +237 -0
  43. package/dist/commands/port/index.js.map +1 -0
  44. package/dist/commands/port/transform.d.ts +68 -0
  45. package/dist/commands/port/transform.d.ts.map +1 -0
  46. package/dist/commands/port/transform.js +122 -0
  47. package/dist/commands/port/transform.js.map +1 -0
  48. package/dist/commands/preview/config.d.ts +73 -0
  49. package/dist/commands/preview/config.d.ts.map +1 -0
  50. package/dist/commands/preview/config.js +200 -0
  51. package/dist/commands/preview/config.js.map +1 -0
  52. package/dist/commands/preview/deploy.d.ts +35 -0
  53. package/dist/commands/preview/deploy.d.ts.map +1 -0
  54. package/dist/commands/preview/deploy.js +247 -0
  55. package/dist/commands/preview/deploy.js.map +1 -0
  56. package/dist/commands/preview/index.d.ts +9 -0
  57. package/dist/commands/preview/index.d.ts.map +1 -0
  58. package/dist/commands/preview/index.js +67 -0
  59. package/dist/commands/preview/index.js.map +1 -0
  60. package/dist/commands/preview/ssh.d.ts +49 -0
  61. package/dist/commands/preview/ssh.d.ts.map +1 -0
  62. package/dist/commands/preview/ssh.js +99 -0
  63. package/dist/commands/preview/ssh.js.map +1 -0
  64. package/dist/commands/preview/teardown.d.ts +25 -0
  65. package/dist/commands/preview/teardown.d.ts.map +1 -0
  66. package/dist/commands/preview/teardown.js +164 -0
  67. package/dist/commands/preview/teardown.js.map +1 -0
  68. package/dist/commands/recon/index.d.ts +60 -0
  69. package/dist/commands/recon/index.d.ts.map +1 -0
  70. package/dist/commands/recon/index.js +278 -0
  71. package/dist/commands/recon/index.js.map +1 -0
  72. package/dist/commands/refine/context.d.ts +12 -0
  73. package/dist/commands/refine/context.d.ts.map +1 -1
  74. package/dist/commands/refine/context.js +72 -0
  75. package/dist/commands/refine/context.js.map +1 -1
  76. package/dist/commands/refine/history-index.d.ts +84 -0
  77. package/dist/commands/refine/history-index.d.ts.map +1 -0
  78. package/dist/commands/refine/history-index.js +289 -0
  79. package/dist/commands/refine/history-index.js.map +1 -0
  80. package/dist/commands/refine/index.d.ts.map +1 -1
  81. package/dist/commands/refine/index.js +28 -0
  82. package/dist/commands/refine/index.js.map +1 -1
  83. package/dist/commands/run-mock/index.d.ts +34 -0
  84. package/dist/commands/run-mock/index.d.ts.map +1 -0
  85. package/dist/commands/run-mock/index.js +308 -0
  86. package/dist/commands/run-mock/index.js.map +1 -0
  87. package/dist/commands/vibe/index.d.ts.map +1 -1
  88. package/dist/commands/vibe/index.js +38 -4
  89. package/dist/commands/vibe/index.js.map +1 -1
  90. package/package.json +15 -13
  91. package/LICENSE +0 -21
@@ -0,0 +1,194 @@
1
+ /**
2
+ * PM-comment classifier — 0.16.0-α.7.
3
+ *
4
+ * Each element-anchored review-overlay comment on a slowcook-mockup PR
5
+ * gets categorized so plate knows what to do with it:
6
+ *
7
+ * - "cosmetic" → amend the mock with minimum diff
8
+ * - "spec-altering" → ESCALATE: would invalidate a spec assertion or
9
+ * acceptance scenario; PM must confirm the spec
10
+ * change before plate touches the mock
11
+ * - "mock-divergence" → the mock diverged from the spec; align mock
12
+ * to spec; note in summary why the PM ask is
13
+ * being interpreted this way
14
+ *
15
+ * α.7 ships a deterministic heuristic. Each classification has a clear
16
+ * rationale string so the escalation comment can quote the trigger.
17
+ *
18
+ * Heuristic structure:
19
+ * 1. Parse the spec YAML to extract the salient assertion targets:
20
+ * - acceptance_scenarios prose lines
21
+ * - api_contract response field names
22
+ * - invariants prose
23
+ * - ui_behavior viewport prose
24
+ * → a flat set of "spec terms" (lowercased, normalized words).
25
+ * 2. Score the comment prose against those terms:
26
+ * - any direct mention of an acceptance keyword phrase → spec-altering
27
+ * - mention of a domain noun + a "remove/change/replace" → spec-altering
28
+ * - mentions only adjective/style words (color, padding, → cosmetic
29
+ * font, spacing, alignment, shadow, etc.)
30
+ * - else → mock-divergence
31
+ *
32
+ * The heuristic is intentionally conservative on spec-altering: false
33
+ * positives only cost a PM confirm round; false negatives let plate
34
+ * silently weaken the spec, which is the failure mode the architecture
35
+ * is designed to prevent. When in doubt → escalate.
36
+ *
37
+ * No LLM dep here — pure functions over inputs. LLM-backed classifier
38
+ * is a future α.7.1 upgrade if heuristic shows real misses.
39
+ */
40
+ const COSMETIC_WORDS = [
41
+ "color", "colour", "shade", "tint", "hue", "rgb", "hex",
42
+ "padding", "margin", "spacing", "gap", "indent",
43
+ "font", "typeface", "weight", "italic", "underline",
44
+ "alignment", "align", "center", "centre", "left-aligned", "right-aligned",
45
+ "shadow", "border", "outline", "radius", "rounded",
46
+ "size", "smaller", "bigger", "larger", "tighter", "looser",
47
+ "background", "bg",
48
+ "icon", "emoji",
49
+ "feel", "vibe", "look",
50
+ "ratio", "scale",
51
+ ];
52
+ const STRUCTURAL_VERBS = [
53
+ "remove", "removed", "delete", "deleted", "drop",
54
+ "replace", "swap", "switch",
55
+ "add", "introduce", "include",
56
+ "change", "rename",
57
+ "split", "merge", "combine",
58
+ "block", "prevent", "disallow",
59
+ "require", "enforce",
60
+ ];
61
+ export function classifyComment(args) {
62
+ const proseNorm = normalize(args.prose);
63
+ const specTerms = extractSpecTerms(args.specYaml);
64
+ const matched = [];
65
+ for (const term of specTerms) {
66
+ if (proseNorm.includes(term))
67
+ matched.push(term);
68
+ }
69
+ const cosmeticHits = COSMETIC_WORDS.filter((w) => containsWord(proseNorm, w));
70
+ const structuralVerb = STRUCTURAL_VERBS.find((v) => containsWord(proseNorm, v));
71
+ // Rule 1 — spec term + structural verb → spec-altering. Highest
72
+ // priority; this is the failure mode we MUST escalate. "Remove the
73
+ // pinned strip" / "replace pinned with bookmarked" etc.
74
+ if (matched.length > 0 && structuralVerb) {
75
+ return {
76
+ classification: "spec-altering",
77
+ rationale: `Mentions spec term${matched.length > 1 ? "s" : ""} (${matched.slice(0, 5).map((m) => `"${m}"`).join(", ")}) ` +
78
+ `together with structural verb "${structuralVerb}". This would change the spec's contract; ` +
79
+ `PM must confirm before the mock changes.`,
80
+ matchedSpecTerms: matched,
81
+ };
82
+ }
83
+ // Rule 2 — cosmetic word present (with or without spec term, but
84
+ // no structural verb) → cosmetic. A PM saying "the Pinned button
85
+ // background should be coral" is naming the element AND asking for
86
+ // a style change; that's still cosmetic. Amend the mock with min diff.
87
+ if (cosmeticHits.length > 0) {
88
+ return {
89
+ classification: "cosmetic",
90
+ rationale: `Style-only feedback (matched: ${cosmeticHits.slice(0, 5).map((w) => `"${w}"`).join(", ")})` +
91
+ (matched.length > 0
92
+ ? `; spec term${matched.length > 1 ? "s" : ""} (${matched.slice(0, 3).map((m) => `"${m}"`).join(", ")}) named the element but no structural verb present.`
93
+ : `; no spec terms triggered.`),
94
+ matchedSpecTerms: matched,
95
+ };
96
+ }
97
+ // Rule 3 — spec terms with no cosmetic word and no structural verb
98
+ // → mock-divergence. The PM is talking about something the spec
99
+ // mentions but neither styling it nor changing the contract.
100
+ // Likely "mock shows X but spec says Y."
101
+ if (matched.length > 0) {
102
+ return {
103
+ classification: "mock-divergence",
104
+ rationale: `Mentions spec term${matched.length > 1 ? "s" : ""} (${matched.slice(0, 5).map((m) => `"${m}"`).join(", ")}) ` +
105
+ `without a structural verb or styling cue. Likely the mock diverged from spec; align mock to spec.`,
106
+ matchedSpecTerms: matched,
107
+ };
108
+ }
109
+ // Rule 4 — fallthrough. No signal in any direction. Default to
110
+ // mock-divergence so plate's LLM still gets the chance to reason
111
+ // about both spec + comment in context (false-positive cost is low).
112
+ return {
113
+ classification: "mock-divergence",
114
+ rationale: `No clear spec or style signal in the prose. Defaulting to mock-divergence so plate has a chance to reconcile against the spec.`,
115
+ matchedSpecTerms: [],
116
+ };
117
+ }
118
+ /**
119
+ * Extract candidate "spec terms" — lowercase, deduplicated significant
120
+ * tokens from the spec sections plate cares about. Used by the
121
+ * classifier to detect overlap between PM prose and spec assertions.
122
+ */
123
+ export function extractSpecTerms(specYaml) {
124
+ const sections = ["acceptance_scenarios", "invariants", "api_contract"];
125
+ const terms = new Set();
126
+ // Also include ui_behavior viewport prose — a comment about
127
+ // "remove the desktop_light pinned strip" is spec-altering.
128
+ for (const sec of [...sections, "ui_behavior"]) {
129
+ const body = extractYamlSection(specYaml, sec);
130
+ if (!body)
131
+ continue;
132
+ for (const tok of tokenize(body)) {
133
+ if (isStopword(tok))
134
+ continue;
135
+ if (tok.length < 4)
136
+ continue; // skip short noise
137
+ terms.add(tok);
138
+ }
139
+ }
140
+ return Array.from(terms);
141
+ }
142
+ function extractYamlSection(yaml, sectionName) {
143
+ // Find the line `sectionName:` at indent 0; capture indented body
144
+ // until the next zero-indent line.
145
+ const lines = yaml.split(/\r?\n/);
146
+ const out = [];
147
+ let inSection = false;
148
+ for (const raw of lines) {
149
+ const m = raw.match(/^(\s*)([a-zA-Z_][\w]*)\s*:/);
150
+ if (m && m[1].length === 0) {
151
+ if (m[2] === sectionName) {
152
+ inSection = true;
153
+ continue;
154
+ }
155
+ else if (inSection) {
156
+ // hit a sibling top-level key
157
+ break;
158
+ }
159
+ }
160
+ if (inSection)
161
+ out.push(raw);
162
+ }
163
+ return out.join("\n");
164
+ }
165
+ function normalize(s) {
166
+ return s.toLowerCase().replace(/\s+/g, " ").trim();
167
+ }
168
+ function tokenize(s) {
169
+ // Split on non-word; preserve underscores (snake_case identifiers)
170
+ return s
171
+ .toLowerCase()
172
+ .split(/[^a-z0-9_]+/)
173
+ .filter((w) => w.length > 0);
174
+ }
175
+ function containsWord(haystackNorm, needle) {
176
+ // Word-boundary check; needle is already lowercase.
177
+ const re = new RegExp(`(^|[^a-z0-9_])${escapeRe(needle)}([^a-z0-9_]|$)`);
178
+ return re.test(haystackNorm);
179
+ }
180
+ function escapeRe(s) {
181
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
182
+ }
183
+ const STOPWORDS = new Set([
184
+ "the", "a", "an", "and", "or", "but", "of", "in", "on", "at", "to", "for", "with", "by", "is", "are", "was", "were", "be", "been", "being", "have", "has", "had", "do", "does", "did", "this", "that", "these", "those", "it", "its", "as", "if", "then", "else", "when", "while", "each", "every", "any", "all", "not", "no", "yes", "true", "false", "null", "can", "will", "may", "must", "should", "would", "also", "only", "very", "more", "most", "such", "than", "into", "from", "over", "under", "between", "through", "because", "since", "once", "yaml", "example", "note", "todo", "null_", "status", "approved", "draft", "paused", "active",
185
+ ]);
186
+ function isStopword(w) {
187
+ if (STOPWORDS.has(w))
188
+ return true;
189
+ // Pure-numeric tokens are noise
190
+ if (/^[0-9]+$/.test(w))
191
+ return true;
192
+ return false;
193
+ }
194
+ //# sourceMappingURL=classify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.js","sourceRoot":"","sources":["../../../src/commands/plate/classify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAYH,MAAM,cAAc,GAAG;IACrB,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK;IACvD,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ;IAC/C,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW;IACnD,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,cAAc,EAAE,eAAe;IACzE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS;IAClD,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ;IAC1D,YAAY,EAAE,IAAI;IAClB,MAAM,EAAE,OAAO;IACf,MAAM,EAAE,MAAM,EAAE,MAAM;IACtB,OAAO,EAAE,OAAO;CACjB,CAAC;AAEF,MAAM,gBAAgB,GAAG;IACvB,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM;IAChD,SAAS,EAAE,MAAM,EAAE,QAAQ;IAC3B,KAAK,EAAE,WAAW,EAAE,SAAS;IAC7B,QAAQ,EAAE,QAAQ;IAClB,OAAO,EAAE,OAAO,EAAE,SAAS;IAC3B,OAAO,EAAE,SAAS,EAAE,UAAU;IAC9B,SAAS,EAAE,SAAS;CACrB,CAAC;AAaF,MAAM,UAAU,eAAe,CAAC,IAAkB;IAChD,MAAM,SAAS,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxC,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,IAAI,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnD,CAAC;IAED,MAAM,YAAY,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9E,MAAM,cAAc,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC;IAEhF,gEAAgE;IAChE,mEAAmE;IACnE,wDAAwD;IACxD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,cAAc,EAAE,CAAC;QACzC,OAAO;YACL,cAAc,EAAE,eAAe;YAC/B,SAAS,EACP,qBAAqB,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;gBAC9G,kCAAkC,cAAc,4CAA4C;gBAC5F,0CAA0C;YAC5C,gBAAgB,EAAE,OAAO;SAC1B,CAAC;IACJ,CAAC;IAED,iEAAiE;IACjE,iEAAiE;IACjE,mEAAmE;IACnE,uEAAuE;IACvE,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,OAAO;YACL,cAAc,EAAE,UAAU;YAC1B,SAAS,EACP,iCAAiC,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;gBAC5F,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;oBACjB,CAAC,CAAC,cAAc,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,qDAAqD;oBAC1J,CAAC,CAAC,4BAA4B,CAAC;YACnC,gBAAgB,EAAE,OAAO;SAC1B,CAAC;IACJ,CAAC;IAED,mEAAmE;IACnE,gEAAgE;IAChE,6DAA6D;IAC7D,yCAAyC;IACzC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO;YACL,cAAc,EAAE,iBAAiB;YACjC,SAAS,EACP,qBAAqB,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;gBAC9G,mGAAmG;YACrG,gBAAgB,EAAE,OAAO;SAC1B,CAAC;IACJ,CAAC;IAED,+DAA+D;IAC/D,iEAAiE;IACjE,qEAAqE;IACrE,OAAO;QACL,cAAc,EAAE,iBAAiB;QACjC,SAAS,EACP,gIAAgI;QAClI,gBAAgB,EAAE,EAAE;KACrB,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,MAAM,QAAQ,GAAG,CAAC,sBAAsB,EAAE,YAAY,EAAE,cAAc,CAAC,CAAC;IACxE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,4DAA4D;IAC5D,4DAA4D;IAC5D,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,QAAQ,EAAE,aAAa,CAAC,EAAE,CAAC;QAC/C,MAAM,IAAI,GAAG,kBAAkB,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QAC/C,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,KAAK,MAAM,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACjC,IAAI,UAAU,CAAC,GAAG,CAAC;gBAAE,SAAS;YAC9B,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;gBAAE,SAAS,CAAC,mBAAmB;YACjD,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACjB,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC;AAED,SAAS,kBAAkB,CAAC,IAAY,EAAE,WAAmB;IAC3D,kEAAkE;IAClE,mCAAmC;IACnC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAClC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC;QAClD,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5B,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,WAAW,EAAE,CAAC;gBACzB,SAAS,GAAG,IAAI,CAAC;gBACjB,SAAS;YACX,CAAC;iBAAM,IAAI,SAAS,EAAE,CAAC;gBACrB,8BAA8B;gBAC9B,MAAM;YACR,CAAC;QACH,CAAC;QACD,IAAI,SAAS;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACxB,CAAC;AAED,SAAS,SAAS,CAAC,CAAS;IAC1B,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AACrD,CAAC;AAED,SAAS,QAAQ,CAAC,CAAS;IACzB,mEAAmE;IACnE,OAAO,CAAC;SACL,WAAW,EAAE;SACb,KAAK,CAAC,aAAa,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACjC,CAAC;AAED,SAAS,YAAY,CAAC,YAAoB,EAAE,MAAc;IACxD,oDAAoD;IACpD,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,iBAAiB,QAAQ,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;IACzE,OAAO,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;AAC/B,CAAC;AAED,SAAS,QAAQ,CAAC,CAAS;IACzB,OAAO,CAAC,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AAClD,CAAC;AAED,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACxB,KAAK,EAAC,GAAG,EAAC,IAAI,EAAC,KAAK,EAAC,IAAI,EAAC,KAAK,EAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,KAAK,EAAC,MAAM,EAAC,IAAI,EAAC,IAAI,EAAC,KAAK,EAAC,KAAK,EAAC,MAAM,EAAC,IAAI,EAAC,MAAM,EAAC,OAAO,EAAC,MAAM,EAAC,KAAK,EAAC,KAAK,EAAC,IAAI,EAAC,MAAM,EAAC,KAAK,EAAC,MAAM,EAAC,MAAM,EAAC,OAAO,EAAC,OAAO,EAAC,IAAI,EAAC,KAAK,EAAC,IAAI,EAAC,IAAI,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,OAAO,EAAC,MAAM,EAAC,OAAO,EAAC,KAAK,EAAC,KAAK,EAAC,KAAK,EAAC,IAAI,EAAC,KAAK,EAAC,MAAM,EAAC,OAAO,EAAC,MAAM,EAAC,KAAK,EAAC,MAAM,EAAC,KAAK,EAAC,MAAM,EAAC,QAAQ,EAAC,OAAO,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,OAAO,EAAC,SAAS,EAAC,SAAS,EAAC,SAAS,EAAC,OAAO,EAAC,MAAM,EAAC,MAAM,EAAC,SAAS,EAAC,MAAM,EAAC,MAAM,EAAC,OAAO,EAAC,QAAQ,EAAC,UAAU,EAAC,OAAO,EAAC,QAAQ,EAAC,QAAQ;CACziB,CAAC,CAAC;AAEH,SAAS,UAAU,CAAC,CAAS;IAC3B,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAClC,gCAAgC;IAChC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/plate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA8OH,wBAAsB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiM7E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/plate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAyQH,wBAAsB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmW7E"}
@@ -12,8 +12,11 @@ import { execSync } from "node:child_process";
12
12
  import { existsSync, readFileSync } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import { GitHubAdapter } from "@slowcook-ai/forge-github";
15
+ import { parseReviewComment, formatPlateReplyBlock, } from "@slowcook-ai/review-overlay";
15
16
  import { runPlate } from "./agent.js";
16
17
  import { writeVibeFiles } from "../vibe/emit.js";
18
+ import { classifyComment } from "./classify.js";
19
+ const APPROVED_LABEL = "slowcook-mockup-approved";
17
20
  function parseArgs(argv) {
18
21
  const args = {
19
22
  prNumber: -1,
@@ -149,7 +152,7 @@ function fetchTimelineComments(prNumber) {
149
152
  const arr = JSON.parse(json);
150
153
  return arr
151
154
  .filter((c) => c.user.type !== "Bot")
152
- .map((c) => ({ author: c.user.login, body: c.body, createdAt: c.created_at }));
155
+ .map((c) => ({ id: c.id, author: c.user.login, body: c.body, createdAt: c.created_at }));
153
156
  }
154
157
  function fetchInlineComments(prNumber) {
155
158
  const json = execSync(`gh api repos/{owner}/{repo}/pulls/${prNumber}/comments --paginate`, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -157,6 +160,7 @@ function fetchInlineComments(prNumber) {
157
160
  return arr
158
161
  .filter((c) => c.user.type !== "Bot")
159
162
  .map((c) => ({
163
+ id: c.id,
160
164
  author: c.user.login,
161
165
  body: c.body,
162
166
  createdAt: c.created_at,
@@ -174,31 +178,44 @@ function lastPlateCommitDate(repoRoot, branch) {
174
178
  }
175
179
  }
176
180
  function listBranchFiles(repoRoot, branch) {
177
- // Files plate is allowed to amend live under src/ + src/lib/data/*.mock.ts.
178
- // We list everything under src/ on the branch and let the agent's own
179
- // hard rules govern what to amend.
181
+ // 0.16.0-α.7 α.14 files plate amends now live under mock/, not src/.
182
+ // The 0.16 architecture keeps mock + production in separate
183
+ // filesystems; brew + slowcook port handle the src/ side.
184
+ // Vibe writes: mock/scenarios/story-N.ts (always), mock/src/lib/
185
+ // scenario-registry.ts (extends), mock/src/components/.../*.tsx
186
+ // (rarely — only new primitives). Plate amends any of these.
187
+ //
188
+ // 0.16.0-α.14 fix: list ALL paths in the branch and filter in code.
189
+ // Previous version passed `mock/**/*.tsx` as a git ls-tree pathspec,
190
+ // which doesn't expand `**` globs by default. Result was zero matches
191
+ // (caught on rewo PR #147 dogfood). The fix is mechanical: drop the
192
+ // pathspec, list everything, filter via the existing regex predicates.
193
+ // Negligible perf cost on a per-PR branch with O(thousands) of files.
180
194
  const out = [];
181
- const patterns = ["src/**/*.tsx", "src/**/*.ts"];
182
- for (const pat of patterns) {
183
- const lsOut = execSync(`git -C ${JSON.stringify(repoRoot)} ls-tree -r --name-only ${JSON.stringify(branch)} -- ${JSON.stringify(pat)}`, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
184
- for (const line of lsOut.split("\n")) {
185
- const trimmed = line.trim();
186
- if (!trimmed)
187
- continue;
188
- // Only include files that vibe would have written: page.tsx,
189
- // src/components/**, src/lib/data/*.mock.ts. Skip the brew-target
190
- // <domain>.ts stubs (plate must not touch them).
191
- const isPage = /\/page\.tsx$/.test(trimmed);
192
- const isComponent = trimmed.startsWith("src/components/");
193
- const isMockData = /^src\/lib\/data\/[^/]+\.mock\.ts$/.test(trimmed);
194
- if (!isPage && !isComponent && !isMockData)
195
- continue;
196
- try {
197
- const contents = execSync(`git -C ${JSON.stringify(repoRoot)} show ${JSON.stringify(branch + ":" + trimmed)}`, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
198
- out.push({ path: trimmed, contents });
199
- }
200
- catch { /* skip files git can't show (binary, deleted) */ }
195
+ let lsOut;
196
+ try {
197
+ lsOut = execSync(`git -C ${JSON.stringify(repoRoot)} ls-tree -r --name-only ${JSON.stringify(branch)}`, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
198
+ }
199
+ catch {
200
+ return out;
201
+ }
202
+ for (const line of lsOut.split("\n")) {
203
+ const trimmed = line.trim();
204
+ if (!trimmed)
205
+ continue;
206
+ // Only include files vibe writes / plate may amend.
207
+ const isScenario = /^mock\/scenarios\/story-[\w-]+\.ts$/.test(trimmed);
208
+ const isRegistry = trimmed === "mock/src/lib/scenario-registry.ts";
209
+ const isMockComponent = trimmed.startsWith("mock/src/components/") && /\.tsx$/.test(trimmed);
210
+ const isMockPage = /^mock\/src\/app\/.*page\.tsx$/.test(trimmed);
211
+ const isMockLib = trimmed.startsWith("mock/src/lib/") && /\.tsx?$/.test(trimmed);
212
+ if (!isScenario && !isRegistry && !isMockComponent && !isMockPage && !isMockLib)
213
+ continue;
214
+ try {
215
+ const contents = execSync(`git -C ${JSON.stringify(repoRoot)} show ${JSON.stringify(branch + ":" + trimmed)}`, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
216
+ out.push({ path: trimmed, contents });
201
217
  }
218
+ catch { /* skip files git can't show (binary, deleted) */ }
202
219
  }
203
220
  return out;
204
221
  }
@@ -230,6 +247,15 @@ export async function plate(argv, cliVersion) {
230
247
  console.error(`PR #${pr.number} head branch '${pr.headBranch}' doesn't match slowcook/mockup/story-N convention. Refusing to act.`);
231
248
  process.exit(2);
232
249
  }
250
+ // 0.16.0-α.7 — refuse to amend after the PM applied the
251
+ // slowcook-mockup-approved label. A stray review-overlay comment
252
+ // (or accidental /plate) shouldn't bounce a finalized mockup.
253
+ // PM can remove the label to reopen plate iteration.
254
+ if (pr.labels.includes(APPROVED_LABEL)) {
255
+ console.log(`PR #${pr.number} carries label \`${APPROVED_LABEL}\`. Plate refuses to amend approved mockups. ` +
256
+ `Remove the label to reopen iteration.`);
257
+ return;
258
+ }
233
259
  const storyId = pr.headBranch.replace(/^slowcook\/mockup\/story-/, "");
234
260
  // Fetch the spec from main (the source of truth).
235
261
  execSync(`git -C ${JSON.stringify(args.repoRoot)} fetch origin main`, {
@@ -274,14 +300,91 @@ export async function plate(argv, cliVersion) {
274
300
  console.log(`Filtering feedback to comments newer than last plate commit at ${cutoffDate}.`);
275
301
  }
276
302
  const filterByDate = (c) => cutoffDate ? c.createdAt > cutoffDate : true;
303
+ const rawTimeline = fetchTimelineComments(pr.number).filter(filterByDate);
304
+ const inlineComments = fetchInlineComments(pr.number).filter(filterByDate);
305
+ // 0.16.0-α.7 — split timeline comments into review-overlay (structured,
306
+ // classified) vs free-prose (unstructured, treated as before).
307
+ const overlayComments = [];
308
+ const proseTimeline = [];
309
+ for (const c of rawTimeline) {
310
+ const payload = parseReviewComment(c.body);
311
+ if (payload) {
312
+ const cls = classifyComment({ prose: payload.prose, specYaml });
313
+ overlayComments.push({
314
+ payload,
315
+ classification: cls.classification,
316
+ rationale: cls.rationale,
317
+ matchedSpecTerms: cls.matchedSpecTerms,
318
+ raw: c,
319
+ });
320
+ }
321
+ else {
322
+ proseTimeline.push(c);
323
+ }
324
+ }
325
+ const specAltering = overlayComments.filter((o) => o.classification === "spec-altering");
326
+ const cosmeticOrDivergent = overlayComments.filter((o) => o.classification !== "spec-altering");
327
+ console.log(`Feedback to act on: ${proseTimeline.length} prose comment(s), ${overlayComments.length} review-overlay comment(s) ` +
328
+ `(${cosmeticOrDivergent.length} amendable, ${specAltering.length} spec-altering — escalating), ` +
329
+ `${inlineComments.length} inline comment(s).`);
330
+ // 0.16.0-α.7 — for each spec-altering comment, post an escalation
331
+ // reply on the PR and EXCLUDE it from the agent's feedback. The PM
332
+ // can confirm via /plate confirm-spec-change (handled in α.7.1)
333
+ // OR via amending the spec PR upstream.
334
+ if (specAltering.length > 0) {
335
+ const githubAdapter = new GitHubAdapter({ owner, repo, token: githubToken });
336
+ for (const sa of specAltering) {
337
+ await githubAdapter.createIssueComment(pr.number, buildEscalationBody({ ...sa, commentId: sa.raw.id }, cliVersion));
338
+ }
339
+ console.log(`Posted ${specAltering.length} escalation reply/replies for spec-altering comments. They will NOT influence this plate amendment.`);
340
+ }
341
+ // 0.16.0-α.15 — track every overlay comment ID this run touched so the
342
+ // final plate-reply breadcrumb block lists each one with its outcome.
343
+ // The overlay's pin layer reads this to render the right status icon
344
+ // on each pin (no timestamp heuristics).
345
+ const breadcrumbReplies = [];
346
+ for (const sa of specAltering) {
347
+ breadcrumbReplies.push({
348
+ to_comment_id: sa.raw.id,
349
+ status: "spec-altering",
350
+ summary: `Escalated: ${sa.rationale}`,
351
+ });
352
+ }
353
+ // For mock-divergence + cosmetic, render their structured prose into
354
+ // the timeline-comment shape the agent already understands. The
355
+ // classification rationale becomes a hint the agent sees in context.
356
+ const classifiedTimelineForAgent = [
357
+ ...proseTimeline,
358
+ ...cosmeticOrDivergent.map((o) => {
359
+ // 0.5.0+ — payload.element is optional (general comments).
360
+ // Render an anchor preview when present; "general note" tag
361
+ // otherwise.
362
+ const el = o.payload.element;
363
+ const anchorPreview = el
364
+ ? `selector \`${el.selector}\` (${el.tag}${el.text_hint ? ` · "${el.text_hint}"` : ""})`
365
+ : "general note (no element anchor)";
366
+ return {
367
+ author: o.raw.author,
368
+ body: `[${o.classification}] ${anchorPreview}:\n\n` +
369
+ o.payload.prose +
370
+ `\n\n_(Plate classifier: ${o.rationale})_`,
371
+ createdAt: o.raw.createdAt,
372
+ };
373
+ }),
374
+ ];
277
375
  const feedback = {
278
- timelineComments: fetchTimelineComments(pr.number).filter(filterByDate),
279
- inlineComments: fetchInlineComments(pr.number).filter(filterByDate),
280
- screenshots: [], // α.3.1 will populate from comment attachments
376
+ timelineComments: classifiedTimelineForAgent,
377
+ inlineComments,
378
+ screenshots: [], // α.3.1 / α.7.x will populate from comment attachments
281
379
  };
282
- console.log(`Feedback to act on: ${feedback.timelineComments.length} timeline comment(s), ${feedback.inlineComments.length} inline comment(s).`);
283
- if (feedback.timelineComments.length === 0 && feedback.inlineComments.length === 0) {
284
- console.log("Nothing to amend. Exiting cleanly.");
380
+ if (feedback.timelineComments.length === 0 &&
381
+ feedback.inlineComments.length === 0) {
382
+ if (specAltering.length > 0) {
383
+ console.log("Only spec-altering feedback present — escalations posted; no amendment to make.");
384
+ }
385
+ else {
386
+ console.log("Nothing to amend. Exiting cleanly.");
387
+ }
285
388
  return;
286
389
  }
287
390
  const ctx = {
@@ -309,7 +412,18 @@ export async function plate(argv, cliVersion) {
309
412
  const forge = new GitHubAdapter({ owner, repo, token: githubToken });
310
413
  if (result.kind === "no-op") {
311
414
  console.log(`Plate decided not to amend (spend $${result.spendUsd.toFixed(4)}). Posting summary as PR reply.`);
312
- await forge.createIssueComment(pr.number, plateReplyBody(result.summary, result.spendUsd, cliVersion, []));
415
+ for (const o of cosmeticOrDivergent) {
416
+ breadcrumbReplies.push({
417
+ to_comment_id: o.raw.id,
418
+ status: "noop",
419
+ summary: result.summary.split("\n")[0].slice(0, 200),
420
+ });
421
+ }
422
+ await forge.createIssueComment(pr.number, plateReplyBody(result.summary, result.spendUsd, cliVersion, [], {
423
+ version: cliVersion,
424
+ amendment_commit: null,
425
+ replies: breadcrumbReplies,
426
+ }));
313
427
  return;
314
428
  }
315
429
  // Amended: write files + commit + force-push.
@@ -327,7 +441,18 @@ export async function plate(argv, cliVersion) {
327
441
  const changed = await forge.git.hasStagedChanges();
328
442
  if (!changed) {
329
443
  console.log("No-op amendment (re-emitted byte-identical files). Posting note instead of commit.");
330
- await forge.createIssueComment(pr.number, plateReplyBody(`${result.summary}\n\n_(plate produced byte-identical files — no commit. Re-comment with sharper feedback if you wanted a change.)_`, result.spendUsd, cliVersion, result.changeRequests.map((cr) => ({ component: cr.component, path: cr.path, rationale: cr.rationale }))));
444
+ for (const o of cosmeticOrDivergent) {
445
+ breadcrumbReplies.push({
446
+ to_comment_id: o.raw.id,
447
+ status: "noop",
448
+ summary: "Plate considered the comment but produced no diff (re-emit byte-identical).",
449
+ });
450
+ }
451
+ await forge.createIssueComment(pr.number, plateReplyBody(`${result.summary}\n\n_(plate produced byte-identical files — no commit. Re-comment with sharper feedback if you wanted a change.)_`, result.spendUsd, cliVersion, result.changeRequests.map((cr) => ({ component: cr.component, path: cr.path, rationale: cr.rationale })), {
452
+ version: cliVersion,
453
+ amendment_commit: null,
454
+ replies: breadcrumbReplies,
455
+ }));
331
456
  return;
332
457
  }
333
458
  }
@@ -336,10 +461,106 @@ export async function plate(argv, cliVersion) {
336
461
  // amended repeatedly across iterations. Same shape as refine's
337
462
  // amendment force-push pattern.
338
463
  execSync(`git -C ${JSON.stringify(args.repoRoot)} push --force-with-lease origin ${JSON.stringify(pr.headBranch)}`, { stdio: "inherit" });
339
- await forge.createIssueComment(pr.number, plateReplyBody(result.summary, result.spendUsd, cliVersion, result.changeRequests.map((cr) => ({ component: cr.component, path: cr.path, rationale: cr.rationale }))));
464
+ // 0.16.0-α.15 capture the just-pushed commit SHA for the breadcrumb.
465
+ let amendmentSha = null;
466
+ try {
467
+ amendmentSha = execSync(`git -C ${JSON.stringify(args.repoRoot)} rev-parse HEAD`, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
468
+ }
469
+ catch { /* best-effort */ }
470
+ // Each cosmetic/divergent overlay comment gets an "applied" breadcrumb
471
+ // (the LLM's prose summary attributes specifics; for the pin layer's
472
+ // status icon this is sufficient).
473
+ for (const o of cosmeticOrDivergent) {
474
+ breadcrumbReplies.push({
475
+ to_comment_id: o.raw.id,
476
+ status: "applied",
477
+ summary: shortSummaryFor(o, result.summary),
478
+ files_touched: result.files.map((f) => f.path),
479
+ });
480
+ }
481
+ await forge.createIssueComment(pr.number, plateReplyBody(result.summary, result.spendUsd, cliVersion, result.changeRequests.map((cr) => ({ component: cr.component, path: cr.path, rationale: cr.rationale })), {
482
+ version: cliVersion,
483
+ amendment_commit: amendmentSha,
484
+ replies: breadcrumbReplies,
485
+ }));
340
486
  console.log(`Plate amendment pushed + summary posted on PR #${pr.number}.`);
341
487
  }
342
- function plateReplyBody(summary, spendUsd, cliVersion, changeRequests) {
488
+ /**
489
+ * 0.16.0-α.15 — pluck a one-line summary for an overlay comment from
490
+ * the LLM's run-level summary. Best-effort: when the summary mentions
491
+ * the comment's author / selector, use that line; otherwise fall back
492
+ * to a generic "Plate amendment applied" line.
493
+ */
494
+ function shortSummaryFor(o, runSummary) {
495
+ const lines = runSummary.split(/\r?\n/);
496
+ const sel = o.payload.element?.selector ?? null;
497
+ for (const line of lines) {
498
+ if ((sel && line.includes(sel)) || line.includes(`@${o.raw.author}`)) {
499
+ return line.replace(/^[-*]\s*/, "").slice(0, 200);
500
+ }
501
+ }
502
+ return "Plate amendment applied (see commit + summary above).";
503
+ }
504
+ /**
505
+ * 0.16.0-α.7 — build the escalation reply for a spec-altering review-
506
+ * overlay comment. The PM either:
507
+ * (a) confirms via /plate confirm-spec-change → α.7.1+ (TODO)
508
+ * (b) amends the spec via /refine on the spec PR upstream
509
+ * (c) discards via /plate keep-spec
510
+ *
511
+ * Until α.7.1 lands, options (a) + (c) are advisory; option (b) is
512
+ * the working path. The PM must amend the spec, re-merge, and the
513
+ * mockup PR's vibe will get the new spec on next iteration.
514
+ */
515
+ function buildEscalationBody(sa, cliVersion) {
516
+ const lines = [];
517
+ lines.push("### slowcook · plate · spec-altering feedback (escalated)");
518
+ lines.push("");
519
+ lines.push(`Plate classified the previous review-overlay comment ` +
520
+ `${sa.payload.element ? `on \`${sa.payload.element.selector}\` ` : "(general note) "}` +
521
+ `as **spec-altering** and is NOT amending the mock for it. The reasoning:`);
522
+ lines.push("");
523
+ lines.push(`> ${sa.rationale}`);
524
+ if (sa.matchedSpecTerms.length > 0) {
525
+ lines.push("");
526
+ lines.push(`Matched spec terms: ${sa.matchedSpecTerms.map((t) => `\`${t}\``).join(", ")}`);
527
+ }
528
+ lines.push("");
529
+ lines.push("**To proceed, choose one:**");
530
+ lines.push("");
531
+ lines.push(`- **Amend the spec** (recommended): \`/refine\` on the spec PR upstream and the next mockup iteration picks up the new contract.`);
532
+ lines.push(`- **Keep the spec, discard this comment**: comment \`/plate keep-spec\` and plate will skip this on the next iteration.`);
533
+ lines.push(`- **Confirm the spec change inline**: comment \`/plate confirm-spec-change\` (lands in α.7.1; until then use the spec PR path).`);
534
+ lines.push("");
535
+ lines.push(`<!-- slowcook:cost agent=plate type=escalation cli=${cliVersion} -->`);
536
+ // 0.16.0-α.15 — breadcrumb so the overlay's pin layer correlates
537
+ // this escalation to the originating overlay comment by id (no
538
+ // timestamp heuristics). One reply entry per escalation comment.
539
+ if (sa.commentId !== undefined) {
540
+ const reply = {
541
+ version: cliVersion,
542
+ amendment_commit: null,
543
+ replies: [
544
+ {
545
+ to_comment_id: sa.commentId,
546
+ status: "spec-altering",
547
+ summary: `Escalated: ${sa.rationale}`.slice(0, 240),
548
+ },
549
+ ],
550
+ };
551
+ lines.push("");
552
+ lines.push(formatPlateReplyBlock(reply));
553
+ }
554
+ return lines.join("\n");
555
+ }
556
+ function plateReplyBody(summary, spendUsd, cliVersion, changeRequests,
557
+ /**
558
+ * 0.16.0-α.15 — breadcrumb so the overlay's pin layer (review-overlay
559
+ * 0.3.0+) can correlate replies to their original overlay comments by
560
+ * id, no timestamp heuristics. When undefined, no breadcrumb block is
561
+ * emitted (back-compat with non-overlay /plate triggers).
562
+ */
563
+ plateReply) {
343
564
  const parts = [];
344
565
  parts.push("### slowcook · plate amendment");
345
566
  parts.push("");
@@ -359,6 +580,10 @@ function plateReplyBody(summary, spendUsd, cliVersion, changeRequests) {
359
580
  }
360
581
  parts.push("");
361
582
  parts.push(`<!-- slowcook:cost agent=plate usd=${spendUsd.toFixed(4)} model=claude-opus-4-7 cli=${cliVersion} -->`);
583
+ if (plateReply && plateReply.replies.length > 0) {
584
+ parts.push("");
585
+ parts.push(formatPlateReplyBlock(plateReply));
586
+ }
362
587
  return parts.join("\n");
363
588
  }
364
589
  //# sourceMappingURL=index.js.map