Skip to content
Closed
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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ members = [
"engine/crates/fx-improve",
"engine/crates/fx-transactions",
"engine/crates/fx-scratchpad",

"engine/crates/fx-preprocess",
"engine/crates/fawx-ripcord",
"engine/crates/fx-ripcord",
Expand Down
35 changes: 33 additions & 2 deletions ENGINEERING.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,42 @@ feature/* → dev → staging → main

- **feature branches**: cut from `dev`, PRs target `dev`
- **dev**: integration branch — merge freely after CI + TUI smoke test pass. Multiple features tested together here.
- **staging**: release candidate — maintainer promotes `dev → staging` after integration testing passes
- **staging**: release candidate — Joe manually promotes `dev → staging` after integration testing passes
- **main**: production releases only — `staging → main` for releases

All three long-lived branches are protected: no force push, no deletion.

---

*This file defines the engineering standards for the Fawx codebase. All contributions are held to these rules. For style preferences, see `TASTE.md`.*
## 7. Agent Execution Model

### Roles
- **Clawdio main session** is the lead. Orchestrates, designs, reviews results, makes architectural calls. Does NOT write code, regardless of size. All code is delegated to subagents.
- **Subagents** do all implementation, review, and fix work.

### Model policy
- **Implementers + Fixers** (code generation): `model: "openai-codex/gpt-5.4"`, `thinking: "xhigh"`.
- **Reviewers** (code analysis): `model: "anthropic/claude-opus-4-6"`, `thinking: "adaptive"`.
- GPT-5.4 xhigh for writing code, Opus adaptive for judging code. No Sonnet unless Joe explicitly requests it.
- Always use full model paths — never aliases (can silently fall back to wrong provider).

### Orchestration model
- **Main session owns the state machine.** Clawdio directly manages implement → review → fix → re-review loops. Do not delegate lifecycle management to N+1 orchestrator subagents.
- **Subagents get single-responsibility prompts.** One job each: "implement this spec," "review this diff," "fix these findings."
- **Spec-driven implementation.** Implementers receive a written spec file, not prose descriptions.

### Concurrency
- **Simple** (< 50 lines): Direct Codex worker + Opus review. Parallel OK.
- **Standard** (single-PR features): Main session spawns workers directly. Parallel PRs OK (max 2-3) if no file overlap.
- **Complex** (multi-crate, architectural): **Sequential only — one PR at a time.** Main session manages full context.

### Rules
1. Main session NEVER writes code. All code work delegated to subagents, no exceptions.
2. Implementers + Fixers use `openai-codex/gpt-5.4` with `thinking: "xhigh"`. Reviewers use `anthropic/claude-opus-4-6` with `thinking: "adaptive"`.
3. Main session chains stages (implement → review → fix → re-review) directly — no N+1 orchestrator layer.
4. All review findings (blocking, non-blocking, nice-to-have) must be fixed. Fresh reviewer for R2.
5. Every subagent prompt includes ENGINEERING.md rules and the spec file path.

---

*This file is immutable doctrine. Cite it in PR reviews. Changes require explicit user approval. For evolving preferences and style, see `TASTE.md`.*
4 changes: 2 additions & 2 deletions engine/crates/fx-api/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ mod tests {

#[test]
fn find_bundle_root_finds_nested_app() {
let path = Path::new("/Applications/Fawx.app/Contents/MacOS/fawx-server");
let path = Path::new("/Users/joe/Desktop/Fawx.app/Contents/MacOS/fawx-server");
let root = find_bundle_root(path);
assert_eq!(root, Some(PathBuf::from("/Applications/Fawx.app")));
assert_eq!(root, Some(PathBuf::from("/Users/joe/Desktop/Fawx.app")));
}

#[test]
Expand Down
16 changes: 8 additions & 8 deletions engine/crates/fx-api/src/devices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ mod tests {
#[test]
fn create_device_returns_hashed_token() {
let mut store = DeviceStore::new();
let (raw_token, device) = store.create_device("Example MacBook");
let (raw_token, device) = store.create_device("Joe's MacBook");

assert!(raw_token.starts_with(DEVICE_TOKEN_PREFIX));
assert_eq!(
Expand All @@ -235,18 +235,18 @@ mod tests {
#[test]
fn list_device_info_excludes_token_hash() {
let mut store = DeviceStore::new();
let _ = store.create_device("Example MacBook");
let _ = store.create_device("Joe's MacBook");

let json = serde_json::to_value(store.list_device_info()).expect("serialize device info");

assert!(json[0].get("token_hash").is_none());
assert_eq!(json[0]["device_name"], "Example MacBook");
assert_eq!(json[0]["device_name"], "Joe's MacBook");
}

#[test]
fn authenticate_works() {
let mut store = DeviceStore::new();
let (raw_token, device) = store.create_device("Example MacBook");
let (raw_token, device) = store.create_device("Joe's MacBook");
store.list_devices_mut()[0].last_used_at = 0;

assert_eq!(store.authenticate(&raw_token), Some(device.id));
Expand All @@ -257,7 +257,7 @@ mod tests {
#[test]
fn revoke_invalidates_device() {
let mut store = DeviceStore::new();
let (raw_token, device) = store.create_device("Example MacBook");
let (raw_token, device) = store.create_device("Joe's MacBook");

assert_eq!(store.revoke(&device.id), Some(device.clone()));
assert!(store.revoke(&device.id).is_none());
Expand All @@ -269,7 +269,7 @@ mod tests {
let temp = tempdir().expect("tempdir");
let path = temp.path().join("devices.json");
let mut store = DeviceStore::new();
let (raw_token, _) = store.create_device("Example MacBook");
let (raw_token, _) = store.create_device("Joe's MacBook");

store.save(&path).expect("save device store");
let mut loaded = DeviceStore::load(&path);
Expand All @@ -286,7 +286,7 @@ mod tests {
let temp = tempdir().expect("tempdir");
let path = temp.path().join("devices.json");
let mut store = DeviceStore::new();
let _ = store.create_device("Example MacBook");
let _ = store.create_device("Joe's MacBook");

store.save(&path).expect("save device store");
let mode = fs::metadata(&path).expect("metadata").permissions().mode() & 0o777;
Expand All @@ -302,7 +302,7 @@ mod tests {
devices: vec![DeviceToken {
id: "dev-123".to_string(),
token_hash: "hash".to_string(),
device_name: "Example MacBook".to_string(),
device_name: "Joe's MacBook".to_string(),
created_at: 1_700_000_000_000,
last_used_at: 1_700_000_005_000,
}],
Expand Down
4 changes: 2 additions & 2 deletions engine/crates/fx-api/src/handlers/fleet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ mod tests {
let temp_dir = tempfile::TempDir::new().expect("tempdir should create");
let mut manager = FleetManager::init(temp_dir.path()).expect("fleet should initialize");
let token = manager
.add_node("node-a", "203.0.113.10", 8400)
.add_node("macmini", "198.51.100.19", 8400)
.expect("node should add");
TestFleet {
_temp_dir: temp_dir,
Expand All @@ -168,7 +168,7 @@ mod tests {

fn registration_request(token: &str) -> FleetRegistrationRequest {
FleetRegistrationRequest {
node_name: "node-a".to_string(),
node_name: "macmini".to_string(),
bearer_token: token.to_string(),
capabilities: vec!["agentic_loop".to_string(), "macos-aarch64".to_string()],
rust_version: Some("1.85.0".to_string()),
Expand Down
6 changes: 3 additions & 3 deletions engine/crates/fx-api/src/handlers/fleet_dashboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ mod tests {
fn node_dto_serializes() {
let response = FleetNodeDto {
id: "node-1".to_string(),
name: "Worker Node A".to_string(),
name: "Mac Mini".to_string(),
status: "healthy".to_string(),
last_seen_at: 1_742_000_100,
active_tasks: 0,
Expand All @@ -373,7 +373,7 @@ mod tests {
json,
json!({
"id": "node-1",
"name": "Worker Node A",
"name": "Mac Mini",
"status": "healthy",
"last_seen_at": 1_742_000_100,
"active_tasks": 0,
Expand Down Expand Up @@ -433,7 +433,7 @@ mod tests {
fn effective_status_marks_old_busy_nodes_degraded() {
let node = NodeInfo {
node_id: "node-1".to_string(),
name: "Worker Node A".to_string(),
name: "Mac Mini".to_string(),
endpoint: "https://127.0.0.1:8400".to_string(),
auth_token: None,
capabilities: vec![NodeCapability::AgenticLoop],
Expand Down
8 changes: 4 additions & 4 deletions engine/crates/fx-api/src/handlers/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -725,15 +725,15 @@ mod tests {
hash: "abcdef123456".to_string(),
short_hash: "abcdef1".to_string(),
message: "feat: add git api".to_string(),
author: "Example Author".to_string(),
author: "Joe".to_string(),
timestamp: "2026-03-15T20:00:00Z".to_string(),
}],
};

let json = serde_json::to_value(response).unwrap();

assert_eq!(json["commits"][0]["hash"], "abcdef123456");
assert_eq!(json["commits"][0]["author"], "Example Author");
assert_eq!(json["commits"][0]["author"], "Joe");
}

#[test]
Expand Down Expand Up @@ -779,14 +779,14 @@ mod tests {
#[test]
fn parse_log_line() {
let commit = super::parse_log_line(
"abcdef123456|abcdef1|feat: support pipes | in messages|Example Author|2026-03-15T20:00:00Z",
"abcdef123456|abcdef1|feat: support pipes | in messages|Joe|2026-03-15T20:00:00Z",
)
.unwrap();

assert_eq!(commit.hash, "abcdef123456");
assert_eq!(commit.short_hash, "abcdef1");
assert_eq!(commit.message, "feat: support pipes | in messages");
assert_eq!(commit.author, "Example Author");
assert_eq!(commit.author, "Joe");
}

#[test]
Expand Down
6 changes: 3 additions & 3 deletions engine/crates/fx-api/src/handlers/pairing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,14 +396,14 @@ mod phase4_tests {
let response = qr_pairing_response(
&test_runtime(false),
&QrTailscaleStatus {
hostname: Some("node.example.ts.net".to_string()),
hostname: Some("myhost.example.com".to_string()),
cert_ready: true,
},
);
assert_eq!(response.display_host, "node.example.ts.net");
assert_eq!(response.display_host, "myhost.example.com");
assert_eq!(response.transport, "tailscale_https");
assert!(!response.same_network_only);
assert!(response.scheme_url.contains("host=node.example.ts.net"));
assert!(response.scheme_url.contains("host=myhost.example.com"));
}

#[test]
Expand Down
4 changes: 2 additions & 2 deletions engine/crates/fx-api/src/handlers/phase4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ mod tests {
installed: true,
running: true,
logged_in: true,
hostname: Some("node.example.ts.net".to_string()),
hostname: Some("myhost.example.com".to_string()),
cert_ready: true,
},
};
Expand All @@ -268,7 +268,7 @@ mod tests {
assert_eq!(json["launchagent"]["loaded"], true);
assert_eq!(json["local_server"]["port"], 8400);
assert_eq!(json["auth"]["providers_configured"][0], "anthropic");
assert_eq!(json["tailscale"]["hostname"], "node.example.ts.net");
assert_eq!(json["tailscale"]["hostname"], "myhost.example.com");
}

#[test]
Expand Down
15 changes: 8 additions & 7 deletions engine/crates/fx-api/src/tailscale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,11 @@ mod tests {

#[test]
fn parse_tailscale_cli_output_returns_cgnat_ip() {
let stdout = b"100.64.0.42\n";
let stdout = b"198.51.100.1\n";

assert_eq!(
parse_tailscale_cli_output(stdout),
Some(IpAddr::V4(Ipv4Addr::new(100, 64, 0, 42)))
Some(IpAddr::V4(Ipv4Addr::new(100, 100, 100, 1)))
);
}

Expand All @@ -132,17 +132,17 @@ mod tests {

#[test]
fn parse_macos_ifconfig_line_extracts_cgnat_ip() {
let line = "inet 100.64.0.43 --> 100.64.0.43 netmask 0xffffffff";
let line = "inet 198.51.100.63 --> 198.51.100.63 netmask 0xffffffff";

assert_eq!(
extract_ip_from_line(line),
Some(IpAddr::V4(Ipv4Addr::new(100, 64, 0, 43)))
Some(IpAddr::V4(Ipv4Addr::new(100, 101, 20, 63)))
);
}

#[test]
fn parse_macos_ifconfig_line_without_inet_prefix_returns_none() {
let line = "100.64.0.43 --> 100.64.0.43 netmask 0xffffffff";
let line = "198.51.100.63 --> 198.51.100.63 netmask 0xffffffff";

assert_eq!(extract_ip_from_line(line), None);
}
Expand All @@ -157,11 +157,12 @@ mod tests {

#[test]
fn linux_ip_output_still_parsed_correctly() {
let text = "7: tailscale0 inet 100.64.0.42/32 brd 100.64.0.42 scope global tailscale0";
let text =
"7: tailscale0 inet 198.51.100.1/32 brd 198.51.100.1 scope global tailscale0";

assert_eq!(
find_cgnat_ip(text),
Some(IpAddr::V4(Ipv4Addr::new(100, 64, 0, 42)))
Some(IpAddr::V4(Ipv4Addr::new(100, 100, 100, 1)))
);
}
}
Loading
Loading