Skip to content

Commit 4c14460

Browse files
masonwyatt23claude
andcommitted
feat: add AI completion workflow for generated projects
Generate .claude/commands/ (complete-screen, verify, next, complete-all), enrich TODO comments with MORPHKIT-TODO format including screen/entity/ endpoint context, add completion manifest and Swift conventions to CLAUDE.md, create morphkit verify CLI command, add screen_context/verify/ next_task MCP tools, and auto-register MCP server in generated projects. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8280e25 commit 4c14460

5 files changed

Lines changed: 1327 additions & 25 deletions

File tree

src/generator/project-generator.ts

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,77 @@ function generateClaudeMd(model: SemanticAppModel, stats: GeneratedProject['stat
11421142
lines.push('| `// TODO:` in loadData() | Morphkit detected a type mismatch and emitted a safe placeholder | Read the TODO comment for the specific mismatch and fix the types |');
11431143
lines.push('');
11441144

1145+
// ── Swift Conventions (Step 7) ──
1146+
lines.push('## Swift Conventions (Must Follow)');
1147+
lines.push('');
1148+
lines.push('These rules prevent common AI coding mistakes in this project:');
1149+
lines.push('');
1150+
lines.push('| Rule | Wrong | Right |');
1151+
lines.push('|------|-------|-------|');
1152+
lines.push('| Observable pattern | `class Store: ObservableObject` | `@Observable class Store` |');
1153+
lines.push('| Task lifecycle | `.onAppear { Task { await load() } }` | `.task { await load() }` |');
1154+
lines.push('| Token storage | `UserDefaults.standard.set(token, ...)` | `KeychainHelper.save(key: "authToken", value: token)` |');
1155+
lines.push('| ForEach identity | `ForEach(items) { item in` | `ForEach(items, id: \\.id) { item in` or conform to `Identifiable` |');
1156+
lines.push('| State access | `@State var name = ""` | `@State private var name = ""` |');
1157+
lines.push('| Async button | `Button("Save") { await save() }` | `Button("Save") { Task { await save() } }` |');
1158+
lines.push('| Navigation | `NavigationView { }` | `NavigationStack { }` |');
1159+
lines.push('| Preview macro | `struct X_Previews: PreviewProvider` | `#Preview { }` |');
1160+
lines.push('| Error handling | `try! await ...` | `do { try await ... } catch { errorMessage = error.localizedDescription }` |');
1161+
lines.push('| List deletion | `.onDelete` with non-binding array | `.swipeActions { Button(role: .destructive) { } }` |');
1162+
lines.push('');
1163+
1164+
// ── Completion Manifest (Step 4) ──
1165+
lines.push('## Completion Manifest');
1166+
lines.push('');
1167+
lines.push('Machine-readable project state for AI tooling. Do not edit manually.');
1168+
lines.push('');
1169+
lines.push('```json');
1170+
1171+
const manifestScreens = screens.map(s => {
1172+
const isRef = referenceScreenNames.has(s.name);
1173+
const apiReqs = (s.dataRequirements ?? []).filter(r => r.fetchStrategy === 'api');
1174+
const apiMethods = apiReqs.map(r => {
1175+
const source = r.source ?? '';
1176+
return `fetch${pascalCase(source.endsWith('s') ? source : pluralize(source))}`;
1177+
});
1178+
// Determine navigation targets from actions
1179+
const navTargets = (s.actions ?? [])
1180+
.filter(a => a.effect?.type === 'navigate')
1181+
.map(a => pascalCase(a.effect?.target ?? ''))
1182+
.filter(Boolean);
1183+
1184+
const entry: Record<string, unknown> = {
1185+
name: s.name,
1186+
file: `Views/${pascalCase(s.name)}View.swift`,
1187+
status: isRef ? 'reference' : 'scaffold',
1188+
layout: s.layout ?? 'custom',
1189+
};
1190+
if (!isRef) {
1191+
entry.todoCount = '?'; // Will be filled by verify
1192+
}
1193+
if (apiMethods.length > 0) entry.apiMethods = apiMethods;
1194+
if (navTargets.length > 0) entry.navigatesTo = navTargets;
1195+
return entry;
1196+
});
1197+
1198+
const manifest = {
1199+
screens: manifestScreens,
1200+
incompleteModels: incompleteEntities.map(e => pascalCase(e.name)),
1201+
unwiredEndpoints: endpoints
1202+
.filter(ep => {
1203+
const url = ep.url.toLowerCase();
1204+
return !url.includes('auth') && !url.includes('login') && !url.includes('register');
1205+
})
1206+
.map(ep => `${ep.method ?? 'GET'} ${ep.url.replace(/`/g, '').replace(/\$\{[^}]+\}/g, ':param')}`),
1207+
completionOrder: screens
1208+
.filter(s => !referenceScreenNames.has(s.name))
1209+
.map(s => s.name),
1210+
};
1211+
1212+
lines.push(JSON.stringify(manifest, null, 2));
1213+
lines.push('```');
1214+
lines.push('');
1215+
11451216
// ── Warnings ──
11461217
if (stats.warnings.length > 0) {
11471218
lines.push('## Build Warnings');
@@ -1348,6 +1419,203 @@ async function validateSwiftSyntax(appDir: string, files: GeneratedFile[]): Prom
13481419
return warnings;
13491420
}
13501421

1422+
// ---------------------------------------------------------------------------
1423+
// .claude/commands/ generation (Step 1)
1424+
// ---------------------------------------------------------------------------
1425+
1426+
function generateClaudeCommands(model: SemanticAppModel): GeneratedFile[] {
1427+
const appName = pascalCase(model.appName ?? 'MyApp');
1428+
const screens = (model.screens ?? []).filter(s => !isMarketingScreen(s));
1429+
const referenceScreenNames = getReferenceScreenNames(model);
1430+
const files: GeneratedFile[] = [];
1431+
1432+
// /complete-screen command
1433+
files.push({
1434+
path: '../.claude/commands/complete-screen.md',
1435+
content: `# Complete Screen: $ARGUMENTS
1436+
1437+
Gather all context needed to complete the screen named \`$ARGUMENTS\`, then implement all TODOs.
1438+
1439+
## Steps
1440+
1441+
1. Read \`${appName}/Views/\${ARGUMENTS}View.swift\` — find all \`MORPHKIT-TODO:\` blocks
1442+
2. Read the reference implementation: ${[...referenceScreenNames].slice(0, 2).map(n => `\`${appName}/Views/${pascalCase(n)}View.swift\``).join(' or ')}
1443+
3. Read relevant model files from \`${appName}/Models/\` — check @State declarations for type names
1444+
4. Read \`${appName}/Networking/APIClient.swift\` — find the methods referenced in the TODOs
1445+
5. For each MORPHKIT-TODO block:
1446+
- Follow the pattern shown in the Reference line
1447+
- Use the APIClient method specified
1448+
- Match the endpoint and request/response types
1449+
6. Run \`swift build\` to verify the changes compile
1450+
7. Report completion status
1451+
1452+
## Rules
1453+
- Use \`@Observable\` not \`ObservableObject\`
1454+
- Use \`.task { }\` not \`.onAppear { Task { } }\`
1455+
- Use \`KeychainHelper\` not \`UserDefaults\` for tokens
1456+
- \`ForEach\` requires \`id:\` parameter or \`Identifiable\` conformance
1457+
- \`@State\` must be \`private\`
1458+
- Button async actions need \`Task { }\` wrapper
1459+
`,
1460+
sourceMapping: 'morphkit:claude-commands',
1461+
confidence: 'high',
1462+
warnings: [],
1463+
});
1464+
1465+
// /verify command
1466+
files.push({
1467+
path: '../.claude/commands/verify.md',
1468+
content: `# Verify Project Completion
1469+
1470+
Count remaining TODOs, check build status, and report completion percentage.
1471+
1472+
## Steps
1473+
1474+
1. Search all \`.swift\` files in \`${appName}/\` for \`MORPHKIT-TODO:\` comments
1475+
2. Group TODOs by category (wire-api-fetch, wire-api-action, wire-navigation, wire-mutation)
1476+
3. Count TODOs per file from \`MORPHKIT-TODO-COUNT:\` headers
1477+
4. Run \`swift build\` and report errors
1478+
5. Check if API base URL is still placeholder in \`${appName}/Networking/APIConfiguration.swift\`
1479+
6. Check if auth is still stubbed in \`${appName}/State/AuthManager.swift\`
1480+
1481+
## Output Format
1482+
1483+
\`\`\`
1484+
Build Status: ✅ PASS | ❌ FAIL (N errors)
1485+
Screen Completion: N/M (XX%)
1486+
API Wiring: N/M (XX%)
1487+
Overall: XX% complete
1488+
1489+
TODO Breakdown:
1490+
wire-api-fetch: N
1491+
wire-api-action: N
1492+
wire-navigation: N
1493+
1494+
Files with TODOs:
1495+
Views/CartView.swift: 3 TODOs
1496+
Views/CheckoutView.swift: 2 TODOs
1497+
1498+
Next step: Complete <ScreenName> (N TODOs)
1499+
\`\`\`
1500+
`,
1501+
sourceMapping: 'morphkit:claude-commands',
1502+
confidence: 'high',
1503+
warnings: [],
1504+
});
1505+
1506+
// /next command
1507+
const screenList = screens
1508+
.filter(s => !referenceScreenNames.has(s.name))
1509+
.map(s => {
1510+
const apiReqs = (s.dataRequirements ?? []).filter(r => r.fetchStrategy === 'api');
1511+
return `- ${s.name} (${s.layout}, ${apiReqs.length} API deps, ${(s.actions ?? []).length} actions)`;
1512+
}).join('\n');
1513+
1514+
files.push({
1515+
path: '../.claude/commands/next.md',
1516+
content: `# What to Complete Next
1517+
1518+
Analyze which screen to complete next based on dependency order and TODO count.
1519+
1520+
## Decision Criteria (in priority order)
1521+
1522+
1. **Leaf screens first** — screens that don't navigate to other incomplete screens
1523+
2. **Fewer TODOs** — easier wins build momentum
1524+
3. **API method availability** — prefer screens whose APIClient methods are already implemented
1525+
1526+
## Screens to Consider
1527+
1528+
${screenList}
1529+
1530+
## Steps
1531+
1532+
1. Read each View file's \`MORPHKIT-TODO-COUNT:\` header to get TODO counts
1533+
2. Check the completion manifest in \`CLAUDE.md\` for dependency info (\`navigatesTo\` field)
1534+
3. Identify leaf screens (screens whose \`navigatesTo\` targets are all complete or reference impls)
1535+
4. Among leaves, pick the one with fewest TODOs
1536+
5. Output: screen name, file path, TODO count, and what TODOs need to be resolved
1537+
1538+
## Output Format
1539+
1540+
\`\`\`
1541+
Next: Complete <ScreenName>View (<N> TODOs)
1542+
File: ${appName}/Views/<ScreenName>View.swift
1543+
Reason: Leaf screen with fewest TODOs
1544+
1545+
TODOs:
1546+
- wire-api-fetch: fetchItems() -> [Item] (GET /api/items)
1547+
- wire-api-action: addToCart(...) (POST /api/cart)
1548+
1549+
Run: /complete-screen <ScreenName>
1550+
\`\`\`
1551+
`,
1552+
sourceMapping: 'morphkit:claude-commands',
1553+
confidence: 'high',
1554+
warnings: [],
1555+
});
1556+
1557+
// /complete-all command
1558+
files.push({
1559+
path: '../.claude/commands/complete-all.md',
1560+
content: `# Complete All Screens
1561+
1562+
Systematically complete all remaining screens in dependency order.
1563+
1564+
## Strategy
1565+
1566+
1. Run \`/verify\` to get current completion status
1567+
2. Read the reference implementation(s): ${[...referenceScreenNames].slice(0, 2).map(n => `\`${appName}/Views/${pascalCase(n)}View.swift\``).join(', ')}
1568+
3. Build a dependency graph from the completion manifest in \`CLAUDE.md\`
1569+
4. Complete screens bottom-up (leaf screens first):
1570+
a. For each screen, run \`/complete-screen <name>\`
1571+
b. After each screen, run \`swift build\` to verify
1572+
c. If build fails, fix errors before moving to next screen
1573+
5. After all screens: run \`/verify\` to confirm 100% completion
1574+
1575+
## Completion Order Rules
1576+
1577+
- Reference implementation screens are already done — skip them
1578+
- Auth screens should be completed early (other screens may depend on auth state)
1579+
- Detail screens before their parent list screens (list needs NavigationLink targets)
1580+
- Settings/profile screens last (least critical path)
1581+
1582+
## Rules
1583+
- Use \`@Observable\` not \`ObservableObject\`
1584+
- Use \`.task { }\` not \`.onAppear { Task { } }\`
1585+
- Use \`KeychainHelper\` not \`UserDefaults\` for tokens
1586+
- \`ForEach\` requires \`id:\` parameter or \`Identifiable\` conformance
1587+
- \`@State\` must be \`private\`
1588+
- Button async actions need \`Task { }\` wrapper
1589+
`,
1590+
sourceMapping: 'morphkit:claude-commands',
1591+
confidence: 'high',
1592+
warnings: [],
1593+
});
1594+
1595+
return files;
1596+
}
1597+
1598+
// ---------------------------------------------------------------------------
1599+
// .claude/settings.json generation (Step 6)
1600+
// ---------------------------------------------------------------------------
1601+
1602+
function generateClaudeSettings(): GeneratedFile {
1603+
return {
1604+
path: '../.claude/settings.json',
1605+
content: JSON.stringify({
1606+
mcpServers: {
1607+
morphkit: {
1608+
command: 'npx',
1609+
args: ['-y', 'morphkit-cli@latest', 'mcp'],
1610+
},
1611+
},
1612+
}, null, 2) + '\n',
1613+
sourceMapping: 'morphkit:claude-settings',
1614+
confidence: 'high',
1615+
warnings: [],
1616+
};
1617+
}
1618+
13511619
// ---------------------------------------------------------------------------
13521620
// Orchestrator
13531621
// ---------------------------------------------------------------------------
@@ -1415,6 +1683,13 @@ export async function generateXcodeProject(
14151683
// 11. Xcode workspace settings (opens cleanly in Xcode)
14161684
allFiles.push(generateWorkspaceSettings());
14171685

1686+
// 12b. .claude/commands/ (slash commands for AI assistant workflow)
1687+
const commandFiles = generateClaudeCommands(model);
1688+
allFiles.push(...commandFiles);
1689+
1690+
// 12c. .claude/settings.json (MCP server auto-registration)
1691+
allFiles.push(generateClaudeSettings());
1692+
14181693
// --- Calculate stats ---
14191694

14201695
const stats: GeneratedProject['stats'] = {

0 commit comments

Comments
 (0)