@bastani/atomic 0.5.25 → 0.5.26-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/ado-commit/SKILL.md +92 -0
- package/.agents/skills/ado-create-pr/SKILL.md +209 -0
- package/.claude/settings.json +1 -0
- package/.mcp.json +5 -0
- package/.opencode/opencode.json +7 -1
- package/README.md +150 -116
- package/assets/settings.schema.json +2 -2
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/open-claude-design/claude/index.d.ts +46 -0
- package/dist/sdk/workflows/builtin/open-claude-design/claude/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/open-claude-design/copilot/index.d.ts +34 -0
- package/dist/sdk/workflows/builtin/open-claude-design/copilot/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/open-claude-design/helpers/constants.d.ts +72 -0
- package/dist/sdk/workflows/builtin/open-claude-design/helpers/constants.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/open-claude-design/helpers/design-system.d.ts +46 -0
- package/dist/sdk/workflows/builtin/open-claude-design/helpers/design-system.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/open-claude-design/helpers/export.d.ts +32 -0
- package/dist/sdk/workflows/builtin/open-claude-design/helpers/export.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/open-claude-design/helpers/import.d.ts +33 -0
- package/dist/sdk/workflows/builtin/open-claude-design/helpers/import.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/open-claude-design/helpers/prompts.d.ts +106 -0
- package/dist/sdk/workflows/builtin/open-claude-design/helpers/prompts.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/open-claude-design/helpers/scan.d.ts +50 -0
- package/dist/sdk/workflows/builtin/open-claude-design/helpers/scan.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/open-claude-design/helpers/validation.d.ts +12 -0
- package/dist/sdk/workflows/builtin/open-claude-design/helpers/validation.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts +36 -0
- package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts.map +1 -0
- package/dist/services/config/atomic-config.d.ts +6 -0
- package/dist/services/config/atomic-config.d.ts.map +1 -1
- package/dist/services/config/scm-sync.d.ts +37 -0
- package/dist/services/config/scm-sync.d.ts.map +1 -0
- package/package.json +7 -7
- package/src/cli.ts +2 -2
- package/src/commands/cli/chat/index.ts +8 -1
- package/src/commands/cli/config.ts +34 -10
- package/src/commands/cli/init/index.ts +9 -0
- package/src/sdk/runtime/executor.ts +13 -0
- package/src/sdk/workflows/builtin/open-claude-design/claude/index.ts +499 -0
- package/src/sdk/workflows/builtin/open-claude-design/copilot/index.ts +507 -0
- package/src/sdk/workflows/builtin/open-claude-design/helpers/constants.ts +159 -0
- package/src/sdk/workflows/builtin/open-claude-design/helpers/design-system.ts +88 -0
- package/src/sdk/workflows/builtin/open-claude-design/helpers/export.ts +193 -0
- package/src/sdk/workflows/builtin/open-claude-design/helpers/import.ts +52 -0
- package/src/sdk/workflows/builtin/open-claude-design/helpers/prompts.ts +1110 -0
- package/src/sdk/workflows/builtin/open-claude-design/helpers/scan.ts +117 -0
- package/src/sdk/workflows/builtin/open-claude-design/helpers/validation.ts +38 -0
- package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +570 -0
- package/src/services/config/atomic-config.ts +12 -0
- package/src/services/config/scm-sync.ts +174 -0
- package/src/services/config/settings.ts +19 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* open-claude-design / opencode
|
|
3
|
+
*
|
|
4
|
+
* OpenCode replica of the Claude open-claude-design workflow. Orchestrates
|
|
5
|
+
* the design skill ecosystem (impeccable, critique, shape, polish, audit,
|
|
6
|
+
* etc.) into a deterministic 5-phase pipeline:
|
|
7
|
+
*
|
|
8
|
+
* Phase 1: Design System Onboarding — extract tokens, build Design.md (HIL)
|
|
9
|
+
* Phase 2: Import — aggregate text/URL/file references
|
|
10
|
+
* Phase 3: Generation — produce first design version
|
|
11
|
+
* Phase 4: Refinement Loop — bounded iterate with critique + user feedback
|
|
12
|
+
* Phase 5: Export/Handoff — HTML export + Claude Code handoff bundle
|
|
13
|
+
*
|
|
14
|
+
* OpenCode-specific concerns (see references/failure-modes.md):
|
|
15
|
+
*
|
|
16
|
+
* • F5 — every `ctx.stage()` is a FRESH session. Every specialist receives
|
|
17
|
+
* its required context verbatim in its first prompt.
|
|
18
|
+
* • F3 — `result.data!.parts` is a heterogenous array (text/tool/reasoning/
|
|
19
|
+
* file parts). Use `extractResponseText()` to filter to text parts only;
|
|
20
|
+
* concatenating raw `parts` produces `[object Object]` strings.
|
|
21
|
+
* • F6 — every prompt explicitly requires trailing prose so transcripts and
|
|
22
|
+
* `extractResponseText()` reads are never empty.
|
|
23
|
+
* • F9 — `s.save()` receives the unwrapped `{ info, parts }` payload from
|
|
24
|
+
* `result.data!`; passing the full `result` breaks downstream reads.
|
|
25
|
+
*
|
|
26
|
+
* Sub-agents are dispatched via the `agent` parameter on
|
|
27
|
+
* `s.client.session.prompt()` (OpenCode's SDK-native way to route a turn
|
|
28
|
+
* to a named sub-agent).
|
|
29
|
+
*
|
|
30
|
+
* See claude/index.ts for the full topology diagram and design rationale.
|
|
31
|
+
*
|
|
32
|
+
* Run: atomic workflow -n open-claude-design -a opencode "design a dashboard"
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { defineWorkflow } from "../../../index.ts";
|
|
36
|
+
import { mkdir } from "node:fs/promises";
|
|
37
|
+
import path from "node:path";
|
|
38
|
+
|
|
39
|
+
import { MAX_REFINEMENTS, DESIGNS_DIR } from "../helpers/constants.ts";
|
|
40
|
+
import {
|
|
41
|
+
loadDesignSystem,
|
|
42
|
+
persistDesignSystem,
|
|
43
|
+
readImpeccableMd,
|
|
44
|
+
slugifyPrompt,
|
|
45
|
+
ensureScratchDir,
|
|
46
|
+
} from "../helpers/design-system.ts";
|
|
47
|
+
import {
|
|
48
|
+
isUrl,
|
|
49
|
+
isFilePath,
|
|
50
|
+
aggregateImportResults,
|
|
51
|
+
} from "../helpers/import.ts";
|
|
52
|
+
import { isRefinementComplete } from "../helpers/validation.ts";
|
|
53
|
+
import { writeHandoffBundle } from "../helpers/export.ts";
|
|
54
|
+
import {
|
|
55
|
+
hasBlockingFindings,
|
|
56
|
+
renderScanFindings,
|
|
57
|
+
runImpeccableScan,
|
|
58
|
+
} from "../helpers/scan.ts";
|
|
59
|
+
import {
|
|
60
|
+
buildDesignLocatorPrompt,
|
|
61
|
+
buildDesignAnalyzerPrompt,
|
|
62
|
+
buildDesignPatternPrompt,
|
|
63
|
+
buildDesignSystemBuilderPrompt,
|
|
64
|
+
buildWebCapturePrompt,
|
|
65
|
+
buildFileParserPrompt,
|
|
66
|
+
buildGeneratorPrompt,
|
|
67
|
+
buildRefineFeedbackPrompt,
|
|
68
|
+
buildCritiquePrompt,
|
|
69
|
+
buildScreenshotValidationPrompt,
|
|
70
|
+
buildApplyChangesPrompt,
|
|
71
|
+
buildForcedFixPrompt,
|
|
72
|
+
buildExportPrompt,
|
|
73
|
+
} from "../helpers/prompts.ts";
|
|
74
|
+
|
|
75
|
+
/** Filter for text parts only — non-text parts produce [object Object]. */
|
|
76
|
+
function extractResponseText(
|
|
77
|
+
parts: Array<{ type: string; [key: string]: unknown }>,
|
|
78
|
+
): string {
|
|
79
|
+
return parts
|
|
80
|
+
.filter((p) => p.type === "text")
|
|
81
|
+
.map((p) => (p as { type: string; text: string }).text)
|
|
82
|
+
.join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export default defineWorkflow({
|
|
86
|
+
name: "open-claude-design",
|
|
87
|
+
description:
|
|
88
|
+
"AI-powered design workflow: design system onboarding → import → generate → refine → export/handoff",
|
|
89
|
+
inputs: [
|
|
90
|
+
{
|
|
91
|
+
name: "prompt",
|
|
92
|
+
type: "text",
|
|
93
|
+
required: true,
|
|
94
|
+
description:
|
|
95
|
+
"What to design (e.g., 'a dashboard for monitoring API latency')",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "reference",
|
|
99
|
+
type: "text",
|
|
100
|
+
required: false,
|
|
101
|
+
description:
|
|
102
|
+
"URL, file path, or codebase path to import as design reference",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "output-type",
|
|
106
|
+
type: "enum",
|
|
107
|
+
required: false,
|
|
108
|
+
values: ["prototype", "wireframe", "page", "component"],
|
|
109
|
+
default: "prototype",
|
|
110
|
+
description: "Type of design output to generate",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "design-system",
|
|
114
|
+
type: "text",
|
|
115
|
+
required: false,
|
|
116
|
+
description: "Path to existing Design.md (skips onboarding if provided)",
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
})
|
|
120
|
+
.for<"opencode">()
|
|
121
|
+
.run(async (ctx) => {
|
|
122
|
+
const prompt = ctx.inputs.prompt ?? "";
|
|
123
|
+
const reference = ctx.inputs.reference ?? "";
|
|
124
|
+
const outputType = ctx.inputs["output-type"] ?? "prototype";
|
|
125
|
+
const designSystemPath = ctx.inputs["design-system"] ?? "";
|
|
126
|
+
|
|
127
|
+
const root = process.cwd();
|
|
128
|
+
const slug = slugifyPrompt(prompt);
|
|
129
|
+
const isoDate = new Date().toISOString().slice(0, 10);
|
|
130
|
+
const scratchDir = await ensureScratchDir(root);
|
|
131
|
+
const designDir = path.join(scratchDir, slug);
|
|
132
|
+
await mkdir(designDir, { recursive: true });
|
|
133
|
+
|
|
134
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
135
|
+
// PHASE 1: Design System Onboarding
|
|
136
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
137
|
+
|
|
138
|
+
let designSystem;
|
|
139
|
+
|
|
140
|
+
if (designSystemPath.trim()) {
|
|
141
|
+
// Skip onboarding — user provided an existing Design.md
|
|
142
|
+
designSystem = await loadDesignSystem(designSystemPath);
|
|
143
|
+
} else {
|
|
144
|
+
// Layer 1: Parallel headless codebase analysis
|
|
145
|
+
const [locator, analyzer, patterns] = await Promise.all([
|
|
146
|
+
ctx.stage(
|
|
147
|
+
{
|
|
148
|
+
name: "ds-locator",
|
|
149
|
+
headless: true,
|
|
150
|
+
description: "Locate design files and tokens",
|
|
151
|
+
},
|
|
152
|
+
{},
|
|
153
|
+
{ title: "ds-locator" },
|
|
154
|
+
async (s) => {
|
|
155
|
+
const result = await s.client.session.prompt({
|
|
156
|
+
sessionID: s.session.id,
|
|
157
|
+
parts: [
|
|
158
|
+
{ type: "text", text: buildDesignLocatorPrompt({ root }) },
|
|
159
|
+
],
|
|
160
|
+
agent: "codebase-locator",
|
|
161
|
+
});
|
|
162
|
+
s.save(result.data!);
|
|
163
|
+
return extractResponseText(result.data!.parts);
|
|
164
|
+
},
|
|
165
|
+
),
|
|
166
|
+
ctx.stage(
|
|
167
|
+
{
|
|
168
|
+
name: "ds-analyzer",
|
|
169
|
+
headless: true,
|
|
170
|
+
description: "Analyze design tokens and patterns",
|
|
171
|
+
},
|
|
172
|
+
{},
|
|
173
|
+
{ title: "ds-analyzer" },
|
|
174
|
+
async (s) => {
|
|
175
|
+
const result = await s.client.session.prompt({
|
|
176
|
+
sessionID: s.session.id,
|
|
177
|
+
parts: [
|
|
178
|
+
{ type: "text", text: buildDesignAnalyzerPrompt({ root }) },
|
|
179
|
+
],
|
|
180
|
+
agent: "codebase-analyzer",
|
|
181
|
+
});
|
|
182
|
+
s.save(result.data!);
|
|
183
|
+
return extractResponseText(result.data!.parts);
|
|
184
|
+
},
|
|
185
|
+
),
|
|
186
|
+
ctx.stage(
|
|
187
|
+
{
|
|
188
|
+
name: "ds-patterns",
|
|
189
|
+
headless: true,
|
|
190
|
+
description: "Find existing design patterns",
|
|
191
|
+
},
|
|
192
|
+
{},
|
|
193
|
+
{ title: "ds-patterns" },
|
|
194
|
+
async (s) => {
|
|
195
|
+
const result = await s.client.session.prompt({
|
|
196
|
+
sessionID: s.session.id,
|
|
197
|
+
parts: [
|
|
198
|
+
{ type: "text", text: buildDesignPatternPrompt({ root }) },
|
|
199
|
+
],
|
|
200
|
+
agent: "codebase-pattern-finder",
|
|
201
|
+
});
|
|
202
|
+
s.save(result.data!);
|
|
203
|
+
return extractResponseText(result.data!.parts);
|
|
204
|
+
},
|
|
205
|
+
),
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
// Layer 2: Visible stage with HIL — presents findings, asks user to
|
|
209
|
+
// approve/modify each design element category
|
|
210
|
+
await ctx.stage(
|
|
211
|
+
{
|
|
212
|
+
name: "design-system-builder",
|
|
213
|
+
description: "Build design system with user approval (HIL)",
|
|
214
|
+
},
|
|
215
|
+
{},
|
|
216
|
+
{ title: "design-system-builder" },
|
|
217
|
+
async (s) => {
|
|
218
|
+
const result = await s.client.session.prompt({
|
|
219
|
+
sessionID: s.session.id,
|
|
220
|
+
parts: [
|
|
221
|
+
{
|
|
222
|
+
type: "text",
|
|
223
|
+
text: buildDesignSystemBuilderPrompt({
|
|
224
|
+
root,
|
|
225
|
+
locatorOutput: locator.result,
|
|
226
|
+
analyzerOutput: analyzer.result,
|
|
227
|
+
patternsOutput: patterns.result,
|
|
228
|
+
existingImpeccable: await readImpeccableMd(root),
|
|
229
|
+
}),
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
});
|
|
233
|
+
s.save(result.data!);
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Deterministic: read back the Design.md the agent wrote
|
|
238
|
+
designSystem = await persistDesignSystem(root);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
242
|
+
// PHASE 2: Import
|
|
243
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
244
|
+
|
|
245
|
+
const importResults = await Promise.all([
|
|
246
|
+
// Web capture (only if reference is a URL)
|
|
247
|
+
isUrl(reference)
|
|
248
|
+
? ctx.stage(
|
|
249
|
+
{
|
|
250
|
+
name: "web-capture",
|
|
251
|
+
headless: true,
|
|
252
|
+
description: "Capture web reference via playwright",
|
|
253
|
+
},
|
|
254
|
+
{},
|
|
255
|
+
{ title: "web-capture" },
|
|
256
|
+
async (s) => {
|
|
257
|
+
const result = await s.client.session.prompt({
|
|
258
|
+
sessionID: s.session.id,
|
|
259
|
+
parts: [
|
|
260
|
+
{
|
|
261
|
+
type: "text",
|
|
262
|
+
text: buildWebCapturePrompt({
|
|
263
|
+
url: reference,
|
|
264
|
+
screenshotDir: scratchDir,
|
|
265
|
+
}),
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
agent: "codebase-online-researcher",
|
|
269
|
+
});
|
|
270
|
+
s.save(result.data!);
|
|
271
|
+
return extractResponseText(result.data!.parts);
|
|
272
|
+
},
|
|
273
|
+
)
|
|
274
|
+
: null,
|
|
275
|
+
|
|
276
|
+
// File parser (only if reference is a file path)
|
|
277
|
+
isFilePath(reference)
|
|
278
|
+
? ctx.stage(
|
|
279
|
+
{
|
|
280
|
+
name: "file-parser",
|
|
281
|
+
headless: true,
|
|
282
|
+
description: "Parse reference document",
|
|
283
|
+
},
|
|
284
|
+
{},
|
|
285
|
+
{ title: "file-parser" },
|
|
286
|
+
async (s) => {
|
|
287
|
+
const result = await s.client.session.prompt({
|
|
288
|
+
sessionID: s.session.id,
|
|
289
|
+
parts: [
|
|
290
|
+
{
|
|
291
|
+
type: "text",
|
|
292
|
+
text: buildFileParserPrompt({ filePath: reference }),
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
agent: "codebase-analyzer",
|
|
296
|
+
});
|
|
297
|
+
s.save(result.data!);
|
|
298
|
+
return extractResponseText(result.data!.parts);
|
|
299
|
+
},
|
|
300
|
+
)
|
|
301
|
+
: null,
|
|
302
|
+
]);
|
|
303
|
+
|
|
304
|
+
// Deterministic aggregation
|
|
305
|
+
const importContext = aggregateImportResults({
|
|
306
|
+
prompt,
|
|
307
|
+
reference,
|
|
308
|
+
webCapture: importResults[0]?.result ?? null,
|
|
309
|
+
fileParse: importResults[1]?.result ?? null,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
313
|
+
// PHASE 3: Generation
|
|
314
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
315
|
+
|
|
316
|
+
await ctx.stage(
|
|
317
|
+
{ name: "generator", description: "Generate first design version" },
|
|
318
|
+
{},
|
|
319
|
+
{ title: "generator" },
|
|
320
|
+
async (s) => {
|
|
321
|
+
const result = await s.client.session.prompt({
|
|
322
|
+
sessionID: s.session.id,
|
|
323
|
+
parts: [
|
|
324
|
+
{
|
|
325
|
+
type: "text",
|
|
326
|
+
text: buildGeneratorPrompt({
|
|
327
|
+
prompt,
|
|
328
|
+
outputType,
|
|
329
|
+
designSystem,
|
|
330
|
+
importContext,
|
|
331
|
+
root,
|
|
332
|
+
outputDir: designDir,
|
|
333
|
+
}),
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
});
|
|
337
|
+
s.save(result.data!);
|
|
338
|
+
},
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
342
|
+
// PHASE 4: Refinement Loop
|
|
343
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
344
|
+
|
|
345
|
+
for (let iteration = 1; iteration <= MAX_REFINEMENTS; iteration++) {
|
|
346
|
+
// Step 1: Collect user feedback via HIL
|
|
347
|
+
const feedback = await ctx.stage(
|
|
348
|
+
{
|
|
349
|
+
name: `user-feedback-${iteration}`,
|
|
350
|
+
description: `Collect refinement feedback (iteration ${iteration})`,
|
|
351
|
+
},
|
|
352
|
+
{},
|
|
353
|
+
{ title: `user-feedback-${iteration}` },
|
|
354
|
+
async (s) => {
|
|
355
|
+
const result = await s.client.session.prompt({
|
|
356
|
+
sessionID: s.session.id,
|
|
357
|
+
parts: [
|
|
358
|
+
{
|
|
359
|
+
type: "text",
|
|
360
|
+
text: buildRefineFeedbackPrompt({
|
|
361
|
+
prompt,
|
|
362
|
+
designDir,
|
|
363
|
+
iteration,
|
|
364
|
+
maxIterations: MAX_REFINEMENTS,
|
|
365
|
+
}),
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
});
|
|
369
|
+
s.save(result.data!);
|
|
370
|
+
return extractResponseText(result.data!.parts);
|
|
371
|
+
},
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
// Check if user signaled "done" via HIL response
|
|
375
|
+
if (isRefinementComplete(feedback.result)) break;
|
|
376
|
+
|
|
377
|
+
// Step 2: Parallel validation — critique + screenshot
|
|
378
|
+
const [critiqueResult, screenshotResult] = await Promise.all([
|
|
379
|
+
ctx.stage(
|
|
380
|
+
{
|
|
381
|
+
name: `critique-${iteration}`,
|
|
382
|
+
headless: true,
|
|
383
|
+
description: `Design critique (iteration ${iteration})`,
|
|
384
|
+
},
|
|
385
|
+
{},
|
|
386
|
+
{ title: `critique-${iteration}` },
|
|
387
|
+
async (s) => {
|
|
388
|
+
const result = await s.client.session.prompt({
|
|
389
|
+
sessionID: s.session.id,
|
|
390
|
+
parts: [
|
|
391
|
+
{
|
|
392
|
+
type: "text",
|
|
393
|
+
text: buildCritiquePrompt({
|
|
394
|
+
designDir,
|
|
395
|
+
designSystem,
|
|
396
|
+
userFeedback: feedback.result,
|
|
397
|
+
}),
|
|
398
|
+
},
|
|
399
|
+
],
|
|
400
|
+
agent: "reviewer",
|
|
401
|
+
});
|
|
402
|
+
s.save(result.data!);
|
|
403
|
+
return extractResponseText(result.data!.parts);
|
|
404
|
+
},
|
|
405
|
+
),
|
|
406
|
+
ctx.stage(
|
|
407
|
+
{
|
|
408
|
+
name: `screenshot-${iteration}`,
|
|
409
|
+
headless: true,
|
|
410
|
+
description: `Visual validation (iteration ${iteration})`,
|
|
411
|
+
},
|
|
412
|
+
{},
|
|
413
|
+
{ title: `screenshot-${iteration}` },
|
|
414
|
+
async (s) => {
|
|
415
|
+
const result = await s.client.session.prompt({
|
|
416
|
+
sessionID: s.session.id,
|
|
417
|
+
parts: [
|
|
418
|
+
{
|
|
419
|
+
type: "text",
|
|
420
|
+
text: buildScreenshotValidationPrompt({
|
|
421
|
+
designDir,
|
|
422
|
+
scratchDir,
|
|
423
|
+
}),
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
agent: "codebase-analyzer",
|
|
427
|
+
});
|
|
428
|
+
s.save(result.data!);
|
|
429
|
+
return extractResponseText(result.data!.parts);
|
|
430
|
+
},
|
|
431
|
+
),
|
|
432
|
+
]);
|
|
433
|
+
|
|
434
|
+
// Step 3: Deterministic scan — surface banned anti-patterns so the
|
|
435
|
+
// apply-changes stage can fix them alongside user feedback. No LLM
|
|
436
|
+
// call; runs the `impeccable detect` CLI directly.
|
|
437
|
+
const scan = await runImpeccableScan(designDir);
|
|
438
|
+
const scanFindings =
|
|
439
|
+
scan.available && scan.findings.length > 0
|
|
440
|
+
? renderScanFindings(scan.findings)
|
|
441
|
+
: scan.available
|
|
442
|
+
? ""
|
|
443
|
+
: `(scanner unavailable: ${scan.reason} — proceed without scan input)`;
|
|
444
|
+
|
|
445
|
+
// Step 4: Apply changes based on feedback + critique + scanner findings
|
|
446
|
+
await ctx.stage(
|
|
447
|
+
{
|
|
448
|
+
name: `apply-changes-${iteration}`,
|
|
449
|
+
description: `Apply refinements (iteration ${iteration})`,
|
|
450
|
+
},
|
|
451
|
+
{},
|
|
452
|
+
{ title: `apply-changes-${iteration}` },
|
|
453
|
+
async (s) => {
|
|
454
|
+
const result = await s.client.session.prompt({
|
|
455
|
+
sessionID: s.session.id,
|
|
456
|
+
parts: [
|
|
457
|
+
{
|
|
458
|
+
type: "text",
|
|
459
|
+
text: buildApplyChangesPrompt({
|
|
460
|
+
prompt,
|
|
461
|
+
designDir,
|
|
462
|
+
designSystem,
|
|
463
|
+
userFeedback: feedback.result,
|
|
464
|
+
critiqueOutput: critiqueResult.result,
|
|
465
|
+
screenshotOutput: screenshotResult.result,
|
|
466
|
+
scanFindings,
|
|
467
|
+
iteration,
|
|
468
|
+
}),
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
});
|
|
472
|
+
s.save(result.data!);
|
|
473
|
+
},
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
478
|
+
// Hard enforcement gate — runs before export, independent of the
|
|
479
|
+
// refinement loop's exit condition. Guarantees no design ships with
|
|
480
|
+
// scanner findings even if the user approved early or MAX_REFINEMENTS
|
|
481
|
+
// was reached with the agent still introducing banned patterns.
|
|
482
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
483
|
+
|
|
484
|
+
const preExportScan = await runImpeccableScan(designDir);
|
|
485
|
+
if (hasBlockingFindings(preExportScan)) {
|
|
486
|
+
const findings = (
|
|
487
|
+
preExportScan as Extract<typeof preExportScan, { available: true }>
|
|
488
|
+
).findings;
|
|
489
|
+
const findingsText = renderScanFindings(findings);
|
|
490
|
+
|
|
491
|
+
await ctx.stage(
|
|
492
|
+
{
|
|
493
|
+
name: "forced-fix",
|
|
494
|
+
description: "Remove banned anti-patterns before export",
|
|
495
|
+
},
|
|
496
|
+
{},
|
|
497
|
+
{ title: "forced-fix" },
|
|
498
|
+
async (s) => {
|
|
499
|
+
const result = await s.client.session.prompt({
|
|
500
|
+
sessionID: s.session.id,
|
|
501
|
+
parts: [
|
|
502
|
+
{
|
|
503
|
+
type: "text",
|
|
504
|
+
text: buildForcedFixPrompt({
|
|
505
|
+
designDir,
|
|
506
|
+
designSystem,
|
|
507
|
+
scanFindings: findingsText,
|
|
508
|
+
}),
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
});
|
|
512
|
+
s.save(result.data!);
|
|
513
|
+
},
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
const rescan = await runImpeccableScan(designDir);
|
|
517
|
+
if (hasBlockingFindings(rescan)) {
|
|
518
|
+
const remaining = (
|
|
519
|
+
rescan as Extract<typeof rescan, { available: true }>
|
|
520
|
+
).findings;
|
|
521
|
+
throw new Error(
|
|
522
|
+
`open-claude-design: export blocked — ${remaining.length} ` +
|
|
523
|
+
`banned anti-pattern(s) remain after forced fix:\n` +
|
|
524
|
+
renderScanFindings(remaining),
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
530
|
+
// PHASE 5: Export / Handoff
|
|
531
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
532
|
+
|
|
533
|
+
const finalDesignDir = path.join(root, DESIGNS_DIR, `${isoDate}-${slug}`);
|
|
534
|
+
|
|
535
|
+
await ctx.stage(
|
|
536
|
+
{
|
|
537
|
+
name: "exporter",
|
|
538
|
+
description: "Export design and create handoff bundle",
|
|
539
|
+
},
|
|
540
|
+
{},
|
|
541
|
+
{ title: "exporter" },
|
|
542
|
+
async (s) => {
|
|
543
|
+
const result = await s.client.session.prompt({
|
|
544
|
+
sessionID: s.session.id,
|
|
545
|
+
parts: [
|
|
546
|
+
{
|
|
547
|
+
type: "text",
|
|
548
|
+
text: buildExportPrompt({
|
|
549
|
+
prompt,
|
|
550
|
+
designDir,
|
|
551
|
+
finalDesignDir,
|
|
552
|
+
designSystem,
|
|
553
|
+
outputType,
|
|
554
|
+
}),
|
|
555
|
+
},
|
|
556
|
+
],
|
|
557
|
+
});
|
|
558
|
+
s.save(result.data!);
|
|
559
|
+
},
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
// Deterministic: package handoff bundle (copies Design.md, writes
|
|
563
|
+
// handoff-prompt.md and README.md — no LLM call)
|
|
564
|
+
await writeHandoffBundle(finalDesignDir, {
|
|
565
|
+
designSystem,
|
|
566
|
+
prompt,
|
|
567
|
+
outputType,
|
|
568
|
+
});
|
|
569
|
+
})
|
|
570
|
+
.compile();
|
|
@@ -16,6 +16,14 @@ import { ensureDir } from "../system/copy.ts";
|
|
|
16
16
|
const SETTINGS_DIR = ".atomic";
|
|
17
17
|
const SETTINGS_FILENAME = "settings.json";
|
|
18
18
|
|
|
19
|
+
/** Source control providers Atomic can auto-configure MCP servers for. */
|
|
20
|
+
export const SCM_PROVIDERS = ["github", "azure-devops", "sapling"] as const;
|
|
21
|
+
export type ScmProvider = (typeof SCM_PROVIDERS)[number];
|
|
22
|
+
|
|
23
|
+
export function isScmProvider(value: unknown): value is ScmProvider {
|
|
24
|
+
return typeof value === "string" && (SCM_PROVIDERS as readonly string[]).includes(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
/**
|
|
20
28
|
* Atomic project configuration schema.
|
|
21
29
|
*/
|
|
@@ -24,6 +32,8 @@ export interface AtomicConfig {
|
|
|
24
32
|
version?: number;
|
|
25
33
|
/** Timestamp of last init */
|
|
26
34
|
lastUpdated?: string;
|
|
35
|
+
/** Selected source control provider (drives MCP server enable/disable sync). */
|
|
36
|
+
scm?: ScmProvider;
|
|
27
37
|
/** Per-provider overrides for chatFlags and envVars */
|
|
28
38
|
providers?: Partial<Record<AgentKey, ProviderOverrides>>;
|
|
29
39
|
}
|
|
@@ -91,6 +101,7 @@ function pickAtomicConfig(record: JsonRecord | null): AtomicConfig | null {
|
|
|
91
101
|
|
|
92
102
|
if (typeof version === "number") config.version = version;
|
|
93
103
|
if (typeof lastUpdated === "string") config.lastUpdated = lastUpdated;
|
|
104
|
+
if (isScmProvider(record.scm)) config.scm = record.scm;
|
|
94
105
|
|
|
95
106
|
const providers = pickProviders(record.providers);
|
|
96
107
|
if (providers) config.providers = providers;
|
|
@@ -134,6 +145,7 @@ function mergeConfigs(...configs: Array<AtomicConfig | null>): AtomicConfig | nu
|
|
|
134
145
|
if (!config) continue;
|
|
135
146
|
if (config.version !== undefined) merged.version = config.version;
|
|
136
147
|
if (config.lastUpdated !== undefined) merged.lastUpdated = config.lastUpdated;
|
|
148
|
+
if (config.scm !== undefined) merged.scm = config.scm;
|
|
137
149
|
|
|
138
150
|
if (config.providers) {
|
|
139
151
|
if (!merged.providers) merged.providers = {};
|