@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.
Files changed (51) hide show
  1. package/.agents/skills/ado-commit/SKILL.md +92 -0
  2. package/.agents/skills/ado-create-pr/SKILL.md +209 -0
  3. package/.claude/settings.json +1 -0
  4. package/.mcp.json +5 -0
  5. package/.opencode/opencode.json +7 -1
  6. package/README.md +150 -116
  7. package/assets/settings.schema.json +2 -2
  8. package/dist/sdk/runtime/executor.d.ts.map +1 -1
  9. package/dist/sdk/workflows/builtin/open-claude-design/claude/index.d.ts +46 -0
  10. package/dist/sdk/workflows/builtin/open-claude-design/claude/index.d.ts.map +1 -0
  11. package/dist/sdk/workflows/builtin/open-claude-design/copilot/index.d.ts +34 -0
  12. package/dist/sdk/workflows/builtin/open-claude-design/copilot/index.d.ts.map +1 -0
  13. package/dist/sdk/workflows/builtin/open-claude-design/helpers/constants.d.ts +72 -0
  14. package/dist/sdk/workflows/builtin/open-claude-design/helpers/constants.d.ts.map +1 -0
  15. package/dist/sdk/workflows/builtin/open-claude-design/helpers/design-system.d.ts +46 -0
  16. package/dist/sdk/workflows/builtin/open-claude-design/helpers/design-system.d.ts.map +1 -0
  17. package/dist/sdk/workflows/builtin/open-claude-design/helpers/export.d.ts +32 -0
  18. package/dist/sdk/workflows/builtin/open-claude-design/helpers/export.d.ts.map +1 -0
  19. package/dist/sdk/workflows/builtin/open-claude-design/helpers/import.d.ts +33 -0
  20. package/dist/sdk/workflows/builtin/open-claude-design/helpers/import.d.ts.map +1 -0
  21. package/dist/sdk/workflows/builtin/open-claude-design/helpers/prompts.d.ts +106 -0
  22. package/dist/sdk/workflows/builtin/open-claude-design/helpers/prompts.d.ts.map +1 -0
  23. package/dist/sdk/workflows/builtin/open-claude-design/helpers/scan.d.ts +50 -0
  24. package/dist/sdk/workflows/builtin/open-claude-design/helpers/scan.d.ts.map +1 -0
  25. package/dist/sdk/workflows/builtin/open-claude-design/helpers/validation.d.ts +12 -0
  26. package/dist/sdk/workflows/builtin/open-claude-design/helpers/validation.d.ts.map +1 -0
  27. package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts +36 -0
  28. package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts.map +1 -0
  29. package/dist/services/config/atomic-config.d.ts +6 -0
  30. package/dist/services/config/atomic-config.d.ts.map +1 -1
  31. package/dist/services/config/scm-sync.d.ts +37 -0
  32. package/dist/services/config/scm-sync.d.ts.map +1 -0
  33. package/package.json +7 -7
  34. package/src/cli.ts +2 -2
  35. package/src/commands/cli/chat/index.ts +8 -1
  36. package/src/commands/cli/config.ts +34 -10
  37. package/src/commands/cli/init/index.ts +9 -0
  38. package/src/sdk/runtime/executor.ts +13 -0
  39. package/src/sdk/workflows/builtin/open-claude-design/claude/index.ts +499 -0
  40. package/src/sdk/workflows/builtin/open-claude-design/copilot/index.ts +507 -0
  41. package/src/sdk/workflows/builtin/open-claude-design/helpers/constants.ts +159 -0
  42. package/src/sdk/workflows/builtin/open-claude-design/helpers/design-system.ts +88 -0
  43. package/src/sdk/workflows/builtin/open-claude-design/helpers/export.ts +193 -0
  44. package/src/sdk/workflows/builtin/open-claude-design/helpers/import.ts +52 -0
  45. package/src/sdk/workflows/builtin/open-claude-design/helpers/prompts.ts +1110 -0
  46. package/src/sdk/workflows/builtin/open-claude-design/helpers/scan.ts +117 -0
  47. package/src/sdk/workflows/builtin/open-claude-design/helpers/validation.ts +38 -0
  48. package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +570 -0
  49. package/src/services/config/atomic-config.ts +12 -0
  50. package/src/services/config/scm-sync.ts +174 -0
  51. 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 = {};