Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
.git
.github
.github/*
!.github/
!.github/pull_request_template.md
**/target
elixir/_build
elixir/deps
Expand Down
27 changes: 22 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
.PHONY: docker-bootstrap-local docker-bootstrap-local-logs
.PHONY: install fmt fmt-check test clippy ci audit docker-build

docker-bootstrap-local:
$(MAKE) -C rust docker-bootstrap-local
install:
./install.sh

docker-bootstrap-local-logs:
$(MAKE) -C rust docker-bootstrap-local-logs
fmt:
cd rust && cargo fmt

fmt-check:
cd rust && cargo fmt --check

test:
cd rust && cargo test

clippy:
cd rust && cargo clippy --all-targets --all-features -- -D warnings

ci: fmt-check test clippy

audit:
bash scripts/public-readiness-audit.sh

docker-build:
docker build -f rust/Dockerfile -t kairastra:local .
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ You are set up correctly when all of these are true:
| --- | --- |
| `krstr run` | Start the orchestrator loop using repo-root `WORKFLOW.md` by default. |
| `krstr run --once` | Run one dispatch pass and wait for started workers before exit. |
| `krstr setup` | Generate repo-root `WORKFLOW.md`, `.kairastra/kairastra.env`, add `.kairastra/` to `.gitignore`, sync required `.agents/skills/` workflow skills into the repo with confirmation, scaffold repo support dirs, and optionally bootstrap GitHub metadata. |
| `krstr setup` | Generate repo-root `WORKFLOW.md`, `.kairastra/provider-settings.json`, `.kairastra/kairastra.env`, add `.kairastra/` to `.gitignore`, sync required `.agents/skills/` workflow skills into the repo with confirmation, scaffold repo support dirs, and optionally bootstrap GitHub metadata. |
| `krstr setup --reconfigure` | Re-run setup and overwrite the generated local Kairastra files for the current repo. |
| `krstr doctor` | Validate workflow, GitHub connectivity, local commands, auth state, and GitHub metadata readiness. |
| `krstr auth status` | Show provider auth status. |
Expand All @@ -120,6 +120,8 @@ You are set up correctly when all of these are true:
- `krstr setup` writes operator files under `.kairastra/` by default.
- The generated repo-root `WORKFLOW.md` keeps setup-specific front matter and uses the canonical prompt body from the repo root template.
- The runtime reads `WORKFLOW.md` directly from the host filesystem.
- `.kairastra/provider-settings.json` stores compact provider defaults such as model, reasoning,
and autonomy settings for `codex`, `claude`, and `gemini`.
- The generated env file keeps machine-specific settings and secrets outside the workflow file.

Use [docs/workflow-reference.md](docs/workflow-reference.md) for the schema and operational rules.
Expand Down
5 changes: 0 additions & 5 deletions WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,13 +407,8 @@ providers:
networkAccess: true
claude:
command: claude
model: $KAIRASTRA_CLAUDE_MODEL
reasoning_effort: $KAIRASTRA_CLAUDE_REASONING_EFFORT
approval_policy: never
gemini:
command: gemini
model: $KAIRASTRA_GEMINI_MODEL
approval_mode: $KAIRASTRA_GEMINI_APPROVAL_MODE
---

You are working on GitHub issue `{{ issue.identifier }}`.
Expand Down
11 changes: 11 additions & 0 deletions docs/workflow-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ The repo-root `WORKFLOW.md` is the canonical template shipped with Kairastra. `k
generates repo-root `WORKFLOW.md` by combining setup-specific front matter with the canonical
prompt body from that root template.

Provider runtime defaults are loaded from `.kairastra/provider-settings.json` when that file is
present. `WORKFLOW.md` remains the orchestration source of truth, and any explicit provider values
in workflow front matter override the local JSON defaults.

The generated provider settings file is intentionally compact. It carries the operator-facing
defaults that materially affect day-to-day runs:

- `codex`: `model`, `reasoning_effort`, `collaboration_mode`, `fast`
- `claude`: `model`, `reasoning_effort`, `permission_mode`
- `gemini`: `model`, `approval_mode`, `sandbox`

## Structure

`WORKFLOW.md` is split into:
Expand Down
7 changes: 7 additions & 0 deletions rust/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ COPY rust/Cargo.toml ./Cargo.toml
COPY rust/Cargo.lock ./Cargo.lock
COPY rust/src ./src
COPY rust/README.md ./README.md
COPY WORKFLOW.md ../WORKFLOW.md
COPY .github/pull_request_template.md ../.github/pull_request_template.md
COPY .agents/skills ../.agents/skills

RUN cargo build --locked --release

Expand Down Expand Up @@ -44,6 +47,10 @@ RUN apt-get update \
&& rm -f /tmp/claude \
&& rm -rf /var/lib/apt/lists/*

RUN mkdir -p /home/kairastra/.gemini \
&& printf '{"projects":{}}\n' > /home/kairastra/.gemini/projects.json \
&& chown -R kairastra:kairastra /home/kairastra/.gemini

COPY --from=builder /src/rust/target/release/kairastra /usr/local/bin/kairastra

USER kairastra
Expand Down
1 change: 1 addition & 0 deletions rust/src/agent/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub enum AgentEventKind {
ToolCallCompleted,
ToolCallFailed,
UnsupportedToolCall,
UserInputRequested,
ToolInputAutoAnswered,
Malformed,
OtherMessage,
Expand Down
184 changes: 180 additions & 4 deletions rust/src/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ use crate::github_bootstrap::{
};
use crate::providers;
use crate::shared_skills;
use crate::workflow::{default_env_file_path, default_workflow_path, load_definition};
use crate::workflow::{
default_env_file_path, default_workflow_path, load_definition,
provider_settings_path_for_workflow,
};

#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum DoctorFormat {
Expand Down Expand Up @@ -102,6 +105,11 @@ pub async fn run(options: DoctorOptions) -> Result<DoctorReport> {
status: DoctorStatus::Warn,
detail: "workflow not loaded".to_string(),
};
let mut provider_settings_check = DoctorCheck {
name: "provider_settings_defaults",
status: DoctorStatus::Warn,
detail: "workflow not loaded".to_string(),
};
let mut provider_auth_check = DoctorCheck {
name: "agent_provider_auth",
status: DoctorStatus::Warn,
Expand Down Expand Up @@ -143,6 +151,7 @@ pub async fn run(options: DoctorOptions) -> Result<DoctorReport> {
status: DoctorStatus::Pass,
detail: format!("loaded {}", path.display()),
});
provider_settings_check = check_provider_settings_defaults(path);
provider_command_check = check_named_command(
"agent_provider_command",
provider_command_name(&settings)?,
Expand Down Expand Up @@ -181,6 +190,7 @@ pub async fn run(options: DoctorOptions) -> Result<DoctorReport> {
});
}

checks.push(provider_settings_check);
checks.push(provider_command_check);
checks.push(provider_auth_check);
checks.push(provider_runtime_check);
Expand Down Expand Up @@ -256,6 +266,105 @@ fn check_named_command(check_name: &'static str, command_name: &'static str) ->
}
}

fn check_provider_settings_defaults(workflow_path: &Path) -> DoctorCheck {
let path = provider_settings_path_for_workflow(workflow_path);
if !path.is_file() {
return DoctorCheck {
name: "provider_settings_defaults",
status: DoctorStatus::Pass,
detail: format!("provider settings file not present at {}", path.display()),
};
}

let raw = match std::fs::read_to_string(&path) {
Ok(raw) => raw,
Err(error) => {
return DoctorCheck {
name: "provider_settings_defaults",
status: DoctorStatus::Warn,
detail: format!("could not read {}: {}", path.display(), error),
};
}
};
let value: serde_json::Value = match serde_json::from_str(&raw) {
Ok(value) => value,
Err(error) => {
return DoctorCheck {
name: "provider_settings_defaults",
status: DoctorStatus::Warn,
detail: format!(
"could not parse provider settings file {}: {}",
path.display(),
error
),
};
}
};
let Some(providers) = value.as_object() else {
return DoctorCheck {
name: "provider_settings_defaults",
status: DoctorStatus::Warn,
detail: format!(
"provider settings file {} must contain a top-level JSON object",
path.display()
),
};
};

let expected = [
(
"codex",
["model", "reasoning_effort", "collaboration_mode", "fast"].as_slice(),
),
(
"claude",
["model", "reasoning_effort", "permission_mode"].as_slice(),
),
("gemini", ["model", "approval_mode", "sandbox"].as_slice()),
];
let mut missing = Vec::new();

for (provider, keys) in expected {
match providers
.get(provider)
.and_then(serde_json::Value::as_object)
{
Some(object) => {
let missing_keys = keys
.iter()
.copied()
.filter(|key| !object.contains_key(*key))
.collect::<Vec<_>>();
if !missing_keys.is_empty() {
missing.push(format!("{provider}: {}", missing_keys.join(", ")));
}
}
None => missing.push(format!("{provider}: missing provider object")),
}
}

if missing.is_empty() {
DoctorCheck {
name: "provider_settings_defaults",
status: DoctorStatus::Pass,
detail: format!(
"recommended compact provider defaults present in {}",
path.display()
),
}
} else {
DoctorCheck {
name: "provider_settings_defaults",
status: DoctorStatus::Warn,
detail: format!(
"{} is missing recommended compact defaults for {}; run `krstr setup --reconfigure` to refresh it",
path.display(),
missing.join("; ")
),
}
}
}

fn provider_command_name(settings: &Settings) -> Result<&'static str> {
providers::command_name(settings.agent.provider.as_str())
}
Expand Down Expand Up @@ -1008,12 +1117,13 @@ fn print_text_report(report: &DoctorReport) {
#[cfg(test)]
mod tests {
use super::{
check_auth_status, check_seed_repo_git, check_seed_repo_skills,
check_seed_repo_support_dirs, check_workspace_root, infer_mode, DoctorStatus,
check_auth_status, check_provider_settings_defaults, check_seed_repo_git,
check_seed_repo_skills, check_seed_repo_support_dirs, check_workspace_root, infer_mode,
DoctorStatus,
};
use crate::config::Settings;
use crate::deploy::DeployMode;
use crate::workflow::load_definition;
use crate::workflow::{load_definition, provider_settings_path_for_workflow};
use std::ffi::OsString;
use std::fs;
use std::path::Path;
Expand Down Expand Up @@ -1206,6 +1316,12 @@ providers:
workflow_path
}

fn write_provider_settings(workflow_path: &Path, body: &str) {
let settings_path = provider_settings_path_for_workflow(workflow_path);
fs::create_dir_all(settings_path.parent().unwrap()).unwrap();
fs::write(settings_path, body).unwrap();
}

#[test]
fn infer_mode_defaults_to_native() {
std::env::set_var("KAIRASTRA_DEPLOY_MODE", "native");
Expand Down Expand Up @@ -1369,4 +1485,64 @@ providers:
assert!(check.detail.contains("auth_problem=expired_oauth_token"));
assert!(check.detail.contains("credentials_usable=false"));
}

#[test]
fn provider_settings_defaults_warn_when_compact_keys_are_missing() {
let dir = tempdir().unwrap();
let workflow_path = write_minimal_workflow(dir.path());
write_provider_settings(
&workflow_path,
r#"{
"codex": {
"model": "gpt-5.4"
},
"claude": {
"model": "sonnet"
}
}"#,
);

let check = check_provider_settings_defaults(&workflow_path);
assert_eq!(check.status, DoctorStatus::Warn);
assert!(check
.detail
.contains("codex: reasoning_effort, collaboration_mode, fast"));
assert!(check
.detail
.contains("claude: reasoning_effort, permission_mode"));
assert!(check.detail.contains("gemini: missing provider object"));
}

#[test]
fn provider_settings_defaults_pass_when_compact_defaults_are_present() {
let dir = tempdir().unwrap();
let workflow_path = write_minimal_workflow(dir.path());
write_provider_settings(
&workflow_path,
r#"{
"codex": {
"model": "gpt-5.4",
"reasoning_effort": "medium",
"collaboration_mode": "default",
"fast": true
},
"claude": {
"model": "sonnet",
"reasoning_effort": "medium",
"permission_mode": "bypassPermissions"
},
"gemini": {
"model": "gemini-2.5-pro",
"approval_mode": "yolo",
"sandbox": true
}
}"#,
);

let check = check_provider_settings_defaults(&workflow_path);
assert_eq!(check.status, DoctorStatus::Pass);
assert!(check
.detail
.contains("recommended compact provider defaults present"));
}
}
Loading
Loading