Complete reference for Kror's two tools: code_writer and xcode_builder. Covers every action, its inputs, outputs, side effects, and error behaviour.
Tools are TypeScript classes in src/tools/. Each implements a run(task, projectDir) method that returns a ToolResult. Tools are the only things that touch the filesystem, spawn subprocesses, or call the LLM for code generation. The Executor calls them; tools do not call each other.
interface ToolResult {
success: boolean;
output: string; // one-line human summary, printed to terminal
filesWritten?: string[]; // relative paths, shown in completion summary
error?: string; // present when success is false
}Both tools read project_state.json at the start of every action and write it back before returning. This is the contract:
interface ProjectState {
// Identity
app_name: string; // e.g. "HabitForge"
bundle_id: string; // e.g. "com.yourco.habitforge"
ios_deployment_target: string; // "17.0"
swift_version: string; // "5.10"
team_id: string; // Apple Developer Team ID
// Project
xcode_project_path: string; // set by create_project, read by build_and_verify
features_requested: string[]; // e.g. ["onboarding", "habits_list", "paywall"]
features_implemented: string[]; // updated after each write_feature_views call
// Credentials (seeded from ~/.kror/config.json)
supabase: {
url: string;
anon_key: string;
};
revenuecat: {
api_key: string;
entitlements: string[]; // default: ["pro"]
};
// Build
last_build_status: "success" | "failure" | null;
// Meta
schema_version: number; // current: 1
}File: src/tools/codeWriter.ts
Makes an LLM call with a task-specific prompt, receives a JSON map of { filepath → content }, and writes every file in the map to disk. Creates intermediate directories as needed.
Writes the SwiftUI app entry point.
Files written:
<app_name>/<app_name>App.swift
What it generates:
@mainstruct conforming toAppWindowGroupcontaining the root viewModelContainersetup for SwiftData, configured with all@ModeltypesRevenueCatSDK configured with key fromConfig.revenueCatKey
State read: app_name, features_requested
State written: appends "app_entry" to features_implemented
Writes the static config file with credentials from project_state.json.
Files written:
<app_name>/Configuration/Config.swift
What it generates:
enum Configwithstatic letconstantssupabaseURL,supabaseAnonKey,revenueCatKey- No dynamic reading from
BundleorInfo.plist— values are hardcoded strings generated at build time
State read: supabase.url, supabase.anon_key, revenuecat.api_key
State written: none
Writes SwiftData @Model classes inferred from the app description and requested features.
Files written:
<app_name>/Models/<ModelName>.swift (one file per model)
What it generates:
@Model final classfor each core entity- All fields typed appropriately (no
Any, no untyped JSON blobs) remoteId: String?,syncedAt: Date?,isDirty: Boolon every modelinit()that setsisDirty = trueon creation
State read: app_name, features_requested
State written: none (models are infrastructure, not features)
Writes the API client and auth manager.
Files written:
<app_name>/Core/Network/APIClient.swift
<app_name>/Core/Auth/AuthManager.swift
What it generates:
APIClient.swift:
- Singleton wrapping
SupabaseClient - Initialised from
Config.supabaseURL+Config.supabaseAnonKey func from(_ table: String) -> PostgrestQueryBuilderconvenience passthrough
AuthManager.swift:
@ObservablesingletoncurrentUser: User?computed fromsupabase.auth.currentUseruserId: UUID?convenience accessorsignInWithApple()stub (wired to Supabase Apple OAuth flow)signOut()async
State read: app_name
State written: none
Writes a View + ViewModel pair for one feature. Called once per feature in features_requested.
Files written:
<app_name>/Features/<FeatureName>/<FeatureName>View.swift
<app_name>/Features/<FeatureName>/<FeatureName>ViewModel.swift
What it generates (by feature):
| Feature | View contents | ViewModel contents |
|---|---|---|
onboarding |
Multi-step form with TabView, answer collection, completion call |
completeOnboarding() — saves to Supabase + seeds habits |
habits_list |
List of habit rows, swipe actions, add button |
loadHabits(), createHabit(), archiveHabit() |
paywall |
Offering cards from RevenueCat, purchase button, restore link | loadOfferings(), purchase(), restorePurchases(), isPro |
State read: app_name, features_requested
State written: appends feature name to features_implemented after successful write
Rewrites a single existing file with targeted fixes. Used by the auto-fix loop after a build failure.
Input params (via task.params):
{
file: string; // relative path to the file to patch
errors: string[]; // build error lines from xcodebuild output
}Files written: the single file at task.params.file (overwrites in place)
What it generates: the LLM receives the current file content + the error messages and returns a corrected version of the complete file. It does not return a diff — it returns the full file, which is written atomically.
State read: app_name
State written: none (build status is updated by build_and_verify)
| Failure mode | Behaviour |
|---|---|
| LLM returns non-JSON | Retry once with correction message; if second attempt fails, exit(1) with raw LLM output for debugging |
| JSON parses but has no file entries | Exit(1) — treated as a generation failure |
| File write fails (permissions, disk full) | Propagate OS error, exit(1) — not retried |
File: src/tools/xcodeBuilder.ts
Calls Ruby scripts via child_process.execSync or child_process.spawn to create and manipulate Xcode projects, then runs xcodebuild to verify the result compiles.
Prerequisite check: On first call per run, xcode_builder verifies:
xcode-select -preturns a path (Xcode command line tools installed)ruby --versionsucceedsgem list xcodeprojconfirms the gem is installed
If any check fails, the tool prints a specific install instruction and exits(1) before attempting any work.
Creates a new .xcodeproj using the xcodeproj Ruby gem.
Subprocess: ruby src/scripts/create_project.rb
Script behaviour:
require 'xcodeproj'
project = Xcodeproj::Project.new("#{app_name}.xcodeproj")
target = project.new_target(:application, app_name, :ios, '17.0')
target.build_configurations.each do |config|
config.build_settings['PRODUCT_BUNDLE_IDENTIFIER'] = bundle_id
config.build_settings['DEVELOPMENT_TEAM'] = team_id
config.build_settings['SWIFT_VERSION'] = '5.10'
config.build_settings['MARKETING_VERSION'] = '1.0.0'
config.build_settings['CURRENT_PROJECT_VERSION'] = '1'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '17.0'
end
project.saveFiles created: <app_name>.xcodeproj/project.pbxproj (and supporting structure)
State read: app_name, bundle_id, team_id
State written: xcode_project_path set to the absolute path of the new .xcodeproj
Validation: After the script runs, xcode_builder calls xcodebuild -list -project <path> and checks exit code 0 before returning success. A project that doesn't pass -list is already broken before any source files are added.
Adds Swift Package Manager dependencies to the .xcodeproj.
Subprocess: ruby src/scripts/add_spm_packages.rb
Packages added (MVP):
| Package | URL | Version rule |
|---|---|---|
supabase-swift |
https://github.com/supabase/supabase-swift |
upToNextMajorVersion: 2.0.0 |
purchases-ios |
https://github.com/RevenueCat/purchases-ios |
upToNextMajorVersion: 4.0.0 |
Script behaviour (per package):
pkg = project.new(Xcodeproj::Project::Object::XCRemoteSwiftPackageReference)
pkg.repositoryURL = package_url
pkg.requirement = { kind: 'upToNextMajorVersion', minimumVersion: version }
project.root_object.package_references << pkg
dep = project.new(Xcodeproj::Project::Object::XCSwiftPackageProductDependency)
dep.package = pkg
dep.product_name = product_name
target.package_product_dependencies << dep
target.frameworks_build_phase.add_file_reference(dep)
project.saveState read: xcode_project_path, app_name
State written: none (packages are not tracked in state — they are visible in project.pbxproj)
Important: Package.resolved is written by Xcode on first open, not by this script. The project will show "resolving packages" on first open in Xcode — this is normal.
Updates build settings in an existing .xcodeproj. Used when project_state.json values change after project creation (e.g. the user updates their bundle ID or team ID).
Subprocess: ruby src/scripts/set_build_settings.rb
State read: xcode_project_path, bundle_id, team_id
State written: none
Not called automatically during kror generate — create_project sets build settings at creation time. Only called if explicitly included in a task graph (e.g. after kror add detects a changed credential).
Runs xcodebuild build for the iOS simulator and streams output to stdout.
Subprocess: xcodebuild build -scheme <app_name> -destination 'platform=iOS Simulator,name=iPhone 16 Pro' -quiet
Output handling:
- Lines containing
error:→ printed in red viachalk.red, collected intoerrors[] - Lines containing
warning:→ printed in yellow viachalk.yellow - All other lines → printed in dim grey via
chalk.dim
On exit code 0: last_build_status set to "success", returns { success: true }
On exit code non-0: last_build_status set to "failure", returns { success: false, error: errors.join("\n") }. The Executor's auto-fix loop then decides whether to attempt a patch.
State read: xcode_project_path, app_name
State written: last_build_status
Note on -quiet: The -quiet flag suppresses the voluminous per-file compile lines from xcodebuild, showing only errors, warnings, and the final build/link step. This keeps terminal output readable. Remove -quiet for debugging if needed.
| Failure mode | Behaviour |
|---|---|
| Ruby script exits non-zero | Print stderr, exit(1) — Ruby errors are not retried |
xcodeproj gem missing |
Print gem install xcodeproj instruction, exit(1) |
xcodebuild not found |
Print xcode-select --install instruction, exit(1) |
| Build fails (exit non-0) | Return success: false with errors — Executor handles retry logic |
| Build fails on retry | Print full error list, exit(1) |
- Create
src/tools/<toolName>.tsimplementingrun(task: Task, projectDir: string): Promise<ToolResult> - Add the tool name to the
toolunion insrc/types.ts - Register the tool in
src/agent/executor.ts'stoolsmap - Add the tool's actions to the Planner's system prompt in
src/agent/planner.ts - Add a section to this file documenting every action
- Add the action handler to the tool's
switchstatement - Add the action to the Planner's available actions list
- Document it in this file with: files written, state read, state written, error behaviour