Skip to content

Latest commit

 

History

History
355 lines (245 loc) · 12 KB

File metadata and controls

355 lines (245 loc) · 12 KB

TOOLS.md

Complete reference for Kror's two tools: code_writer and xcode_builder. Covers every action, its inputs, outputs, side effects, and error behaviour.


Overview

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
}

ProjectState Schema

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
}

Tool: code_writer

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.

Actions


write_app_entry

Writes the SwiftUI app entry point.

Files written:

<app_name>/<app_name>App.swift

What it generates:

  • @main struct conforming to App
  • WindowGroup containing the root view
  • ModelContainer setup for SwiftData, configured with all @Model types
  • RevenueCat SDK configured with key from Config.revenueCatKey

State read: app_name, features_requested State written: appends "app_entry" to features_implemented


write_config

Writes the static config file with credentials from project_state.json.

Files written:

<app_name>/Configuration/Config.swift

What it generates:

  • enum Config with static let constants
  • supabaseURL, supabaseAnonKey, revenueCatKey
  • No dynamic reading from Bundle or Info.plist — values are hardcoded strings generated at build time

State read: supabase.url, supabase.anon_key, revenuecat.api_key State written: none


write_models

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 class for each core entity
  • All fields typed appropriately (no Any, no untyped JSON blobs)
  • remoteId: String?, syncedAt: Date?, isDirty: Bool on every model
  • init() that sets isDirty = true on creation

State read: app_name, features_requested State written: none (models are infrastructure, not features)


write_network_layer

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) -> PostgrestQueryBuilder convenience passthrough

AuthManager.swift:

  • @Observable singleton
  • currentUser: User? computed from supabase.auth.currentUser
  • userId: UUID? convenience accessor
  • signInWithApple() stub (wired to Supabase Apple OAuth flow)
  • signOut() async

State read: app_name State written: none


write_feature_views

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


patch_file

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)


code_writer error behaviour

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

Tool: xcode_builder

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 -p returns a path (Xcode command line tools installed)
  • ruby --version succeeds
  • gem list xcodeproj confirms the gem is installed

If any check fails, the tool prints a specific install instruction and exits(1) before attempting any work.

Actions


create_project

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.save

Files 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.


add_spm_packages

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.save

State 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.


set_build_settings

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 generatecreate_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).


build_and_verify

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 via chalk.red, collected into errors[]
  • Lines containing warning: → printed in yellow via chalk.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.


xcode_builder error behaviour

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)

Adding a New Tool

  1. Create src/tools/<toolName>.ts implementing run(task: Task, projectDir: string): Promise<ToolResult>
  2. Add the tool name to the tool union in src/types.ts
  3. Register the tool in src/agent/executor.ts's tools map
  4. Add the tool's actions to the Planner's system prompt in src/agent/planner.ts
  5. Add a section to this file documenting every action

Adding a New Action to an Existing Tool

  1. Add the action handler to the tool's switch statement
  2. Add the action to the Planner's available actions list
  3. Document it in this file with: files written, state read, state written, error behaviour