@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.
- package/dist/cli.js +50 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/brew/agent.d.ts +25 -1
- package/dist/commands/brew/agent.d.ts.map +1 -1
- package/dist/commands/brew/agent.js +123 -20
- package/dist/commands/brew/agent.js.map +1 -1
- package/dist/commands/brew/halt.d.ts +1 -1
- package/dist/commands/brew/halt.d.ts.map +1 -1
- package/dist/commands/brew/halt.js +13 -0
- package/dist/commands/brew/halt.js.map +1 -1
- package/dist/commands/check/index.d.ts +14 -0
- package/dist/commands/check/index.d.ts.map +1 -0
- package/dist/commands/check/index.js +75 -0
- package/dist/commands/check/index.js.map +1 -0
- package/dist/commands/check/mock-isolation.d.ts +52 -0
- package/dist/commands/check/mock-isolation.d.ts.map +1 -0
- package/dist/commands/check/mock-isolation.js +186 -0
- package/dist/commands/check/mock-isolation.js.map +1 -0
- package/dist/commands/init/from-prod.d.ts +48 -0
- package/dist/commands/init/from-prod.d.ts.map +1 -0
- package/dist/commands/init/from-prod.js +256 -0
- package/dist/commands/init/from-prod.js.map +1 -0
- package/dist/commands/init/index.d.ts.map +1 -1
- package/dist/commands/init/index.js +9 -0
- package/dist/commands/init/index.js.map +1 -1
- package/dist/commands/init/mock.d.ts.map +1 -1
- package/dist/commands/init/mock.js +47 -13
- package/dist/commands/init/mock.js.map +1 -1
- package/dist/commands/on-mockup-approved/index.d.ts +25 -0
- package/dist/commands/on-mockup-approved/index.d.ts.map +1 -0
- package/dist/commands/on-mockup-approved/index.js +359 -0
- package/dist/commands/on-mockup-approved/index.js.map +1 -0
- package/dist/commands/plate/classify.d.ts +65 -0
- package/dist/commands/plate/classify.d.ts.map +1 -0
- package/dist/commands/plate/classify.js +194 -0
- package/dist/commands/plate/classify.js.map +1 -0
- package/dist/commands/plate/index.d.ts.map +1 -1
- package/dist/commands/plate/index.js +259 -34
- package/dist/commands/plate/index.js.map +1 -1
- package/dist/commands/port/index.d.ts +30 -0
- package/dist/commands/port/index.d.ts.map +1 -0
- package/dist/commands/port/index.js +237 -0
- package/dist/commands/port/index.js.map +1 -0
- package/dist/commands/port/transform.d.ts +68 -0
- package/dist/commands/port/transform.d.ts.map +1 -0
- package/dist/commands/port/transform.js +122 -0
- package/dist/commands/port/transform.js.map +1 -0
- package/dist/commands/preview/config.d.ts +73 -0
- package/dist/commands/preview/config.d.ts.map +1 -0
- package/dist/commands/preview/config.js +200 -0
- package/dist/commands/preview/config.js.map +1 -0
- package/dist/commands/preview/deploy.d.ts +35 -0
- package/dist/commands/preview/deploy.d.ts.map +1 -0
- package/dist/commands/preview/deploy.js +247 -0
- package/dist/commands/preview/deploy.js.map +1 -0
- package/dist/commands/preview/index.d.ts +9 -0
- package/dist/commands/preview/index.d.ts.map +1 -0
- package/dist/commands/preview/index.js +67 -0
- package/dist/commands/preview/index.js.map +1 -0
- package/dist/commands/preview/ssh.d.ts +49 -0
- package/dist/commands/preview/ssh.d.ts.map +1 -0
- package/dist/commands/preview/ssh.js +99 -0
- package/dist/commands/preview/ssh.js.map +1 -0
- package/dist/commands/preview/teardown.d.ts +25 -0
- package/dist/commands/preview/teardown.d.ts.map +1 -0
- package/dist/commands/preview/teardown.js +164 -0
- package/dist/commands/preview/teardown.js.map +1 -0
- package/dist/commands/recon/index.d.ts +60 -0
- package/dist/commands/recon/index.d.ts.map +1 -0
- package/dist/commands/recon/index.js +278 -0
- package/dist/commands/recon/index.js.map +1 -0
- package/dist/commands/refine/context.d.ts +12 -0
- package/dist/commands/refine/context.d.ts.map +1 -1
- package/dist/commands/refine/context.js +72 -0
- package/dist/commands/refine/context.js.map +1 -1
- package/dist/commands/refine/history-index.d.ts +84 -0
- package/dist/commands/refine/history-index.d.ts.map +1 -0
- package/dist/commands/refine/history-index.js +289 -0
- package/dist/commands/refine/history-index.js.map +1 -0
- package/dist/commands/refine/index.d.ts.map +1 -1
- package/dist/commands/refine/index.js +28 -0
- package/dist/commands/refine/index.js.map +1 -1
- package/dist/commands/run-mock/index.d.ts +34 -0
- package/dist/commands/run-mock/index.d.ts.map +1 -0
- package/dist/commands/run-mock/index.js +308 -0
- package/dist/commands/run-mock/index.js.map +1 -0
- package/dist/commands/vibe/index.d.ts.map +1 -1
- package/dist/commands/vibe/index.js +38 -4
- package/dist/commands/vibe/index.js.map +1 -1
- package/package.json +15 -13
- 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":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/plate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;
|
|
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
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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:
|
|
279
|
-
inlineComments
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|