@bastani/atomic 0.9.0-alpha.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +7 -0
  3. package/dist/builtin/cursor/package.json +2 -2
  4. package/dist/builtin/intercom/CHANGELOG.md +8 -0
  5. package/dist/builtin/intercom/package.json +1 -1
  6. package/dist/builtin/mcp/CHANGELOG.md +12 -0
  7. package/dist/builtin/mcp/package.json +1 -1
  8. package/dist/builtin/subagents/CHANGELOG.md +17 -0
  9. package/dist/builtin/subagents/package.json +1 -1
  10. package/dist/builtin/web-access/CHANGELOG.md +8 -0
  11. package/dist/builtin/web-access/package.json +1 -1
  12. package/dist/builtin/workflows/CHANGELOG.md +50 -0
  13. package/dist/builtin/workflows/README.md +12 -12
  14. package/dist/builtin/workflows/builtin/goal-prompts.ts +8 -0
  15. package/dist/builtin/workflows/builtin/goal-runner.ts +96 -1
  16. package/dist/builtin/workflows/builtin/goal-types.ts +2 -0
  17. package/dist/builtin/workflows/builtin/goal.d.ts +3 -0
  18. package/dist/builtin/workflows/builtin/goal.ts +12 -1
  19. package/dist/builtin/workflows/builtin/index.d.ts +8 -8
  20. package/dist/builtin/workflows/builtin/open-claude-design-feedback.ts +359 -0
  21. package/dist/builtin/workflows/builtin/open-claude-design-phases.ts +254 -352
  22. package/dist/builtin/workflows/builtin/open-claude-design-runner.ts +256 -414
  23. package/dist/builtin/workflows/builtin/open-claude-design-setup.ts +272 -0
  24. package/dist/builtin/workflows/builtin/open-claude-design-utils.ts +58 -68
  25. package/dist/builtin/workflows/builtin/open-claude-design.d.ts +5 -9
  26. package/dist/builtin/workflows/builtin/open-claude-design.ts +14 -26
  27. package/dist/builtin/workflows/package.json +1 -1
  28. package/dist/builtin/workflows/skills/impeccable/SKILL.md +14 -23
  29. package/dist/builtin/workflows/skills/impeccable/reference/brand.md +2 -2
  30. package/dist/builtin/workflows/skills/impeccable/reference/live.md +25 -4
  31. package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +1 -1
  32. package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +724 -29
  33. package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +1 -1
  34. package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +219 -7
  35. package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +57 -11
  36. package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +750 -0
  37. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +648 -53
  38. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns.mjs +7 -0
  39. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +29 -4
  40. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +44 -11
  41. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +29 -0
  42. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +27 -1
  43. package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +1 -1
  44. package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +29 -0
  45. package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +401 -46
  46. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/inline-ignores.mjs +148 -0
  47. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/page.mjs +6 -6
  48. package/dist/builtin/workflows/skills/impeccable/scripts/{design-parser.mjs → lib/design-parser.mjs} +8 -1
  49. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +638 -0
  50. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-paths.mjs +128 -0
  51. package/dist/builtin/workflows/skills/impeccable/scripts/{is-generated.mjs → lib/is-generated.mjs} +2 -2
  52. package/dist/builtin/workflows/skills/impeccable/scripts/lib/target-args.mjs +42 -0
  53. package/dist/builtin/workflows/skills/impeccable/scripts/live/browser-script-parts.mjs +49 -0
  54. package/dist/builtin/workflows/skills/impeccable/scripts/{live-completion.mjs → live/completion.mjs} +1 -0
  55. package/dist/builtin/workflows/skills/impeccable/scripts/{live-event-validation.mjs → live/event-validation.mjs} +6 -5
  56. package/dist/builtin/workflows/skills/impeccable/scripts/live/manual-apply.mjs +939 -0
  57. package/dist/builtin/workflows/skills/impeccable/scripts/live/manual-edit-routes.mjs +357 -0
  58. package/dist/builtin/workflows/skills/impeccable/scripts/{live-manual-edits-buffer.mjs → live/manual-edits-buffer.mjs} +1 -1
  59. package/dist/builtin/workflows/skills/impeccable/scripts/{live-session-store.mjs → live/session-store.mjs} +21 -3
  60. package/dist/builtin/workflows/skills/impeccable/scripts/live/svelte-component.mjs +835 -0
  61. package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +274 -0
  62. package/dist/builtin/workflows/skills/impeccable/scripts/live/ui-core.mjs +180 -0
  63. package/dist/builtin/workflows/skills/impeccable/scripts/live/vocabulary.mjs +36 -0
  64. package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +185 -60
  65. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser-dom.js +146 -0
  66. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +3369 -1026
  67. package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +2 -2
  68. package/dist/builtin/workflows/skills/impeccable/scripts/live-complete.mjs +2 -2
  69. package/dist/builtin/workflows/skills/impeccable/scripts/live-discard-manual-edits.mjs +1 -1
  70. package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +133 -9
  71. package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +42 -2
  72. package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +4 -4
  73. package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +21 -15
  74. package/dist/builtin/workflows/skills/impeccable/scripts/live-resume.mjs +1 -1
  75. package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +205 -1269
  76. package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +2 -2
  77. package/dist/builtin/workflows/skills/impeccable/scripts/live-target.mjs +30 -0
  78. package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +69 -26
  79. package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +73 -22
  80. package/dist/core/atomic-guide-command.d.ts.map +1 -1
  81. package/dist/core/atomic-guide-command.js +5 -5
  82. package/dist/core/atomic-guide-command.js.map +1 -1
  83. package/docs/index.md +2 -2
  84. package/docs/quickstart.md +9 -9
  85. package/docs/workflows.md +42 -23
  86. package/package.json +2 -2
  87. package/dist/builtin/workflows/skills/impeccable/scripts/cleanup-deprecated.mjs +0 -284
  88. package/dist/builtin/workflows/skills/impeccable/scripts/impeccable-paths.mjs +0 -126
  89. /package/dist/builtin/workflows/skills/impeccable/scripts/{live-insert-ui.mjs → live/insert-ui.mjs} +0 -0
@@ -0,0 +1,359 @@
1
+ /**
2
+ * open-claude-design feedback threading.
3
+ *
4
+ * The `user-feedback-*` stages capture Playwright annotation feedback (user
5
+ * notes + annotated snapshot) from the user. This module is the durable carrier
6
+ * for that feedback: it parses the feedback-stage output, persists it as a
7
+ * workflow artifact, and renders the user annotations that the next `generate-*`
8
+ * stage must honor. cross-ref: issue #1464.
9
+ */
10
+
11
+ import { copyFileSync, existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
12
+ import { isAbsolute, dirname, join, resolve, sep } from "node:path";
13
+
14
+ /** A single captured user-feedback round. */
15
+ export type PreviewFeedback = {
16
+ /** 1..N for generate/user-feedback loop iterations. */
17
+ readonly iteration: number;
18
+ /** Originating stage name, e.g. `user-feedback-1`. */
19
+ readonly stageName: string;
20
+ /** Full markdown result text emitted by the user-feedback stage. */
21
+ readonly text: string;
22
+ /** Extracted user annotation notes when the user actually annotated. */
23
+ readonly userNotes?: string;
24
+ /** Extracted annotated-snapshot artifact path when one was captured. */
25
+ readonly annotatedSnapshot?: string;
26
+ /** Extracted summary of the variants/edits the user accepted in the live QA session. */
27
+ readonly liveChanges?: string;
28
+ /** ISO timestamp when the feedback was captured. */
29
+ readonly capturedAt: string;
30
+ };
31
+
32
+ type PreviewResultLike = { readonly text?: string };
33
+
34
+ /**
35
+ * Field labels the user-feedback stages are instructed to emit, stored in
36
+ * canonical (alphanumeric-only, lowercase) form. Used to bound multi-line value
37
+ * extraction (a value ends when the next known field starts).
38
+ */
39
+ const FIELD_LABELS = new Set<string>([
40
+ "displaymethod",
41
+ "previewpath",
42
+ "previewfileurl",
43
+ "annotatedsnapshot",
44
+ "usernotes",
45
+ "livechanges",
46
+ "nextactionhint",
47
+ "manualopeninstructions",
48
+ "specpath",
49
+ ]);
50
+
51
+ const PLACEHOLDER_TOKENS = new Set<string>([
52
+ "none",
53
+ "na",
54
+ "null",
55
+ "undefined",
56
+ "notavailable",
57
+ "unavailable",
58
+ "notcaptured",
59
+ "nonotes",
60
+ "nousernotes",
61
+ "nofeedback",
62
+ "noannotations",
63
+ "nonecaptured",
64
+ "tbd",
65
+ "pending",
66
+ ]);
67
+
68
+ function isPlaceholderValue(value: string): boolean {
69
+ const compact = value
70
+ .replace(/\//g, "")
71
+ .replace(/[\s().,*_`~–—\-:]/g, "")
72
+ .toLowerCase();
73
+ if (compact.length === 0) return true;
74
+ return PLACEHOLDER_TOKENS.has(compact);
75
+ }
76
+
77
+ /** Canonicalize a label to lowercase alphanumerics so `user_notes`, `User Notes`,
78
+ * and `**user_notes**` all compare equal. */
79
+ function canonicalLabel(value: string): string {
80
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
81
+ }
82
+
83
+ /** Normalize a candidate label line into a canonical key (or undefined). */
84
+ function labelOf(line: string): string | undefined {
85
+ const stripped = line
86
+ .replace(/^\s*#{1,6}\s+/, "")
87
+ .replace(/^\s*[-*+]\s+/, "")
88
+ .replace(/^\s*\d+\.\s+/, "");
89
+ const colonIdx = stripped.indexOf(":");
90
+ const candidate = colonIdx >= 0 ? stripped.slice(0, colonIdx) : stripped;
91
+ const key = canonicalLabel(candidate);
92
+ return key.length > 0 ? key : undefined;
93
+ }
94
+
95
+ /** Inline value following a `label:` on the same line. */
96
+ function inlineValueOf(line: string): string {
97
+ const stripped = line
98
+ .replace(/^\s*#{1,6}\s+/, "")
99
+ .replace(/^\s*[-*+]\s+/, "")
100
+ .replace(/^\s*\d+\.\s+/, "");
101
+ const colonIdx = stripped.indexOf(":");
102
+ if (colonIdx < 0) return "";
103
+ return stripped.slice(colonIdx + 1).replace(/[`*]/g, "").trim();
104
+ }
105
+
106
+ function isHorizontalRule(line: string): boolean {
107
+ return /^\s*([-*_])(\s*\1){2,}\s*$/.test(line);
108
+ }
109
+
110
+ /**
111
+ * Extract the value of a labeled field (e.g. `user_notes`) from a user-feedback
112
+ * markdown blob, tolerating heading / bullet / bold / backtick label styles and
113
+ * multi-line values that run until the next known field label or a rule.
114
+ */
115
+ export function extractField(text: string, field: string): string | undefined {
116
+ if (text.trim().length === 0) return undefined;
117
+ const target = canonicalLabel(field);
118
+ const lines = text.split(/\r?\n/);
119
+ let collecting = false;
120
+ const collected: string[] = [];
121
+ for (const line of lines) {
122
+ if (collecting) {
123
+ const label = labelOf(line);
124
+ if (label !== undefined && label !== target && FIELD_LABELS.has(label)) break;
125
+ if (isHorizontalRule(line)) break;
126
+ collected.push(line);
127
+ continue;
128
+ }
129
+ if (labelOf(line) === target) {
130
+ const inline = inlineValueOf(line);
131
+ if (inline.length > 0) collected.push(inline);
132
+ collecting = true;
133
+ }
134
+ }
135
+ const value = collected.join("\n").trim();
136
+ if (value.length === 0 || isPlaceholderValue(value)) return undefined;
137
+ return value;
138
+ }
139
+
140
+ export function extractUserNotes(text: string): string | undefined {
141
+ return extractField(text, "user_notes");
142
+ }
143
+
144
+ export function extractAnnotatedSnapshot(text: string): string | undefined {
145
+ return extractField(text, "annotated_snapshot");
146
+ }
147
+
148
+ export function extractLiveChanges(text: string): string | undefined {
149
+ return extractField(text, "live_changes");
150
+ }
151
+
152
+ /** Build a PreviewFeedback record from a (possibly missing) stage result. */
153
+ export function toPreviewFeedback(input: {
154
+ readonly iteration: number;
155
+ readonly stageName: string;
156
+ readonly result: PreviewResultLike | undefined;
157
+ }): PreviewFeedback {
158
+ const text = (input.result?.text ?? "").trim();
159
+ const userNotes = extractUserNotes(text);
160
+ const annotatedSnapshot = extractAnnotatedSnapshot(text);
161
+ const liveChanges = extractLiveChanges(text);
162
+ return {
163
+ iteration: input.iteration,
164
+ stageName: input.stageName,
165
+ text,
166
+ capturedAt: new Date().toISOString(),
167
+ ...(userNotes !== undefined ? { userNotes } : {}),
168
+ ...(annotatedSnapshot !== undefined ? { annotatedSnapshot } : {}),
169
+ ...(liveChanges !== undefined ? { liveChanges } : {}),
170
+ };
171
+ }
172
+
173
+ export function hasMeaningfulUserNotes(feedback: PreviewFeedback): boolean {
174
+ return typeof feedback.userNotes === "string" && feedback.userNotes.length > 0;
175
+ }
176
+
177
+ export function hasMeaningfulLiveChanges(feedback: PreviewFeedback): boolean {
178
+ return typeof feedback.liveChanges === "string" && feedback.liveChanges.length > 0;
179
+ }
180
+
181
+ /** Whether a feedback round carries any meaningful user signal: typed notes or accepted live variants. */
182
+ export function hasMeaningfulFeedback(feedback: PreviewFeedback): boolean {
183
+ return hasMeaningfulUserNotes(feedback) || hasMeaningfulLiveChanges(feedback);
184
+ }
185
+
186
+ function feedbackLabel(feedback: PreviewFeedback): string {
187
+ return feedback.iteration === 0
188
+ ? "the initial preview"
189
+ : `refinement iteration ${feedback.iteration}`;
190
+ }
191
+
192
+ /**
193
+ * Render the captured user annotations (latest first) as a markdown section.
194
+ * Returns "" when no iteration captured meaningful user notes.
195
+ */
196
+ export function buildUserAnnotationsSection(history: readonly PreviewFeedback[]): string {
197
+ const withFeedback = history.filter(
198
+ (feedback) => hasMeaningfulUserNotes(feedback) || hasMeaningfulLiveChanges(feedback),
199
+ );
200
+ if (withFeedback.length === 0) return "";
201
+ return [...withFeedback]
202
+ .reverse()
203
+ .map((feedback) => {
204
+ const lines = [
205
+ `### User annotations from ${feedbackLabel(feedback)} (${feedback.stageName})`,
206
+ "",
207
+ ];
208
+ if (hasMeaningfulUserNotes(feedback)) {
209
+ lines.push(feedback.userNotes ?? "");
210
+ }
211
+ if (hasMeaningfulLiveChanges(feedback)) {
212
+ lines.push("", "Accepted live variants/edits:", feedback.liveChanges ?? "");
213
+ }
214
+ if (feedback.annotatedSnapshot !== undefined) {
215
+ lines.push("", `Annotated snapshot: ${feedback.annotatedSnapshot}`);
216
+ }
217
+ return lines.join("\n");
218
+ })
219
+ .join("\n\n");
220
+ }
221
+
222
+ /**
223
+ * The user-annotations block injected into refinement prompts, plus whether any
224
+ * real annotations exist. When none exist, downstream stages are told to fall
225
+ * back to an impeccable critique rather than fabricating user feedback.
226
+ */
227
+ export function userAnnotationsBlock(history: readonly PreviewFeedback[]): {
228
+ readonly hasNotes: boolean;
229
+ readonly text: string;
230
+ } {
231
+ const section = buildUserAnnotationsSection(history);
232
+ if (section.length === 0) {
233
+ return {
234
+ hasNotes: false,
235
+ text: "No interactive user annotations were captured in the user-feedback stage. There is no user feedback to honor for this iteration.",
236
+ };
237
+ }
238
+ return { hasNotes: true, text: section };
239
+ }
240
+
241
+ /**
242
+ * Guardrail: every captured user annotation must be present verbatim in the
243
+ * next generate prompt. If a `user-feedback-*` stage captured `user_notes` but
244
+ * they did not thread through, fail loudly instead of silently generating
245
+ * without user feedback. cross-ref: issue #1464 fix (6).
246
+ */
247
+ export function assertUserAnnotationsThreaded(
248
+ prompt: string,
249
+ history: readonly PreviewFeedback[],
250
+ stageName: string,
251
+ ): void {
252
+ for (const feedback of history) {
253
+ if (hasMeaningfulUserNotes(feedback)) {
254
+ const notes = (feedback.userNotes ?? "").trim();
255
+ if (notes.length > 0 && !prompt.includes(notes)) {
256
+ throw new Error(
257
+ `open-claude-design ${stageName}: user annotations captured in ${feedback.stageName} were not threaded into the refinement context. Refusing to refine without user feedback (see issue #1464).`,
258
+ );
259
+ }
260
+ }
261
+ if (hasMeaningfulLiveChanges(feedback)) {
262
+ const changes = (feedback.liveChanges ?? "").trim();
263
+ if (changes.length > 0 && !prompt.includes(changes)) {
264
+ throw new Error(
265
+ `open-claude-design ${stageName}: accepted live variants captured in ${feedback.stageName} were not threaded into the refinement context. Refusing to refine without user feedback.`,
266
+ );
267
+ }
268
+ }
269
+ }
270
+ }
271
+
272
+ /** Whether `childPath` resolves to `parentDir` itself or somewhere beneath it. */
273
+ function isWithin(childPath: string, parentDir: string): boolean {
274
+ const child = resolve(childPath);
275
+ const parent = resolve(parentDir);
276
+ return child === parent || child.startsWith(parent + sep);
277
+ }
278
+
279
+ function copyAnnotationArtifacts(
280
+ feedbackDir: string,
281
+ slug: string,
282
+ feedback: PreviewFeedback,
283
+ workflowCwd: string,
284
+ ): void {
285
+ if (feedback.annotatedSnapshot === undefined) return;
286
+ const raw = feedback.annotatedSnapshot.trim();
287
+ if (raw.length === 0) return;
288
+ const source = isAbsolute(raw) ? raw : resolve(workflowCwd, raw);
289
+ // Constrain the model-supplied path to within the project or the run's
290
+ // artifact dir before copying, so an absolute path outside the project (e.g.
291
+ // an arbitrary file the model emitted) is never copied in.
292
+ const artifactDir = dirname(feedbackDir);
293
+ if (!isWithin(source, workflowCwd) && !isWithin(source, artifactDir)) return;
294
+ try {
295
+ if (!existsSync(source) || !statSync(source).isFile()) return;
296
+ } catch {
297
+ return;
298
+ }
299
+ const extMatch = source.match(/\.[A-Za-z0-9]+$/);
300
+ const ext = extMatch ? extMatch[0] : ".png";
301
+ try {
302
+ copyFileSync(source, join(feedbackDir, `${slug}-annotations${ext}`));
303
+ } catch {
304
+ /* best-effort */
305
+ }
306
+ for (const yamlExt of [".yaml", ".yml"]) {
307
+ const sibling = source.replace(/\.[A-Za-z0-9]+$/, yamlExt);
308
+ try {
309
+ if (existsSync(sibling) && statSync(sibling).isFile()) {
310
+ copyFileSync(sibling, join(feedbackDir, `${slug}-annotations${yamlExt}`));
311
+ break;
312
+ }
313
+ } catch {
314
+ /* best-effort */
315
+ }
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Persist captured annotations as durable workflow artifacts under
321
+ * `<artifactDir>/feedback/`. Only writes when the user actually provided
322
+ * annotations (notes or an annotated snapshot). Best-effort: never throws.
323
+ * cross-ref: issue #1464 fix (5).
324
+ */
325
+ export function persistPreviewFeedback(input: {
326
+ readonly artifactDir: string;
327
+ readonly workflowCwd: string;
328
+ readonly feedback: PreviewFeedback;
329
+ }): void {
330
+ const { feedback } = input;
331
+ if (
332
+ !hasMeaningfulUserNotes(feedback) &&
333
+ !hasMeaningfulLiveChanges(feedback) &&
334
+ feedback.annotatedSnapshot === undefined
335
+ ) {
336
+ return;
337
+ }
338
+ try {
339
+ const feedbackDir = join(input.artifactDir, "feedback");
340
+ mkdirSync(feedbackDir, { recursive: true });
341
+ const slug = `iteration-${feedback.iteration}`;
342
+ writeFileSync(join(feedbackDir, `${slug}.md`), `${feedback.text}\n`);
343
+ writeFileSync(
344
+ join(feedbackDir, `${slug}.json`),
345
+ `${JSON.stringify(
346
+ {
347
+ ...feedback,
348
+ hasUserNotes: hasMeaningfulUserNotes(feedback),
349
+ hasLiveChanges: hasMeaningfulLiveChanges(feedback),
350
+ },
351
+ null,
352
+ 2,
353
+ )}\n`,
354
+ );
355
+ copyAnnotationArtifacts(feedbackDir, slug, feedback, input.workflowCwd);
356
+ } catch {
357
+ /* best-effort durability; never block the workflow */
358
+ }
359
+ }