Skip to content
This repository was archived by the owner on Apr 11, 2026. It is now read-only.

Commit 0bb64f9

Browse files
committed
Merge branch 'fn-67-fix-changesapplier-dual-write-gap' into main
2 parents 3cddba0 + b4ec841 commit 0bb64f9

3 files changed

Lines changed: 43 additions & 7 deletions

File tree

flowctl/crates/flowctl-cli/src/commands/gap.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ fn require_db() -> crate::commands::db_shim::Connection {
9797
.unwrap_or_else(|e| error_exit(&format!("DB required: {e}")))
9898
}
9999

100-
/// Verify .flow/ exists, epic ID is valid, and epic exists in DB.
100+
/// Verify .flow/ exists, epic ID is valid, and epic exists (DB or JSON).
101101
fn validate_epic(_json: bool, epic_id: &str) {
102102
let flow_dir = get_flow_dir();
103103
if !flow_dir.exists() {
@@ -106,12 +106,18 @@ fn validate_epic(_json: bool, epic_id: &str) {
106106
if !is_epic_id(epic_id) {
107107
error_exit(&format!("Invalid epic ID: {}", epic_id));
108108
}
109-
// Verify epic exists in DB
109+
// Check DB first, fall back to JSON file existence.
110+
// Epic may exist only in JSON if DB upsert hasn't run yet.
110111
let conn = require_db();
111112
let repo = crate::commands::db_shim::EpicRepo::new(&conn);
112-
if repo.get(epic_id).is_err() {
113-
error_exit(&format!("Epic not found: {}", epic_id));
113+
if repo.get(epic_id).is_ok() {
114+
return;
114115
}
116+
let json_path = flow_dir.join("epics").join(format!("{epic_id}.json"));
117+
if json_path.exists() {
118+
return;
119+
}
120+
error_exit(&format!("Epic not found: {}", epic_id));
115121
}
116122

117123
// ── Commands ───────────────────────────────────────────────────────

flowctl/crates/flowctl-cli/src/commands/helpers.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,11 @@ fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<(), std::io::Error> {
146146
/// Apply a `Changes` batch via the service-layer `ChangesApplier`.
147147
///
148148
/// This opens the libSQL event DB, spins a short-lived tokio runtime, and
149-
/// applies all mutations (JSON store writes + event logging) in order.
149+
/// applies all mutations (JSON store writes + event logging + DB upserts) in order.
150150
/// Returns the number of mutations applied. Calls `error_exit` on failure.
151151
pub fn apply_changes(flow_dir: &Path, changes: &flowctl_core::changes::Changes) -> usize {
152152
use crate::output::error_exit;
153+
use flowctl_core::changes::Mutation;
153154
use flowctl_service::changes::ChangesApplier;
154155

155156
if changes.is_empty() {
@@ -158,7 +159,7 @@ pub fn apply_changes(flow_dir: &Path, changes: &flowctl_core::changes::Changes)
158159

159160
let actor = resolve_actor();
160161

161-
// Open DB for event logging (best-effort: if DB unavailable, fall back to JSON-only)
162+
// Open DB for event logging + entity upserts
162163
let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
163164
let rt = tokio::runtime::Builder::new_current_thread()
164165
.enable_all()
@@ -177,6 +178,32 @@ pub fn apply_changes(flow_dir: &Path, changes: &flowctl_core::changes::Changes)
177178

178179
let result = applier.apply(changes).await
179180
.unwrap_or_else(|e| error_exit(&format!("Failed to apply changes: {e}")));
181+
182+
// Dual-write: upsert entities into DB tables so commands like `gap add`
183+
// (which validate via EpicRepo::get) can find them.
184+
let conn2 = db.connect()
185+
.unwrap_or_else(|e| error_exit(&format!("Failed to connect to DB: {e}")));
186+
let epic_repo = flowctl_db::EpicRepo::new(conn2.clone());
187+
let task_repo = flowctl_db::TaskRepo::new(conn2);
188+
189+
for mutation in &changes.mutations {
190+
match mutation {
191+
Mutation::CreateEpic { epic } | Mutation::UpdateEpic { epic } => {
192+
let _ = epic_repo.upsert(epic).await;
193+
}
194+
Mutation::RemoveEpic { id } => {
195+
let _ = epic_repo.delete(id).await;
196+
}
197+
Mutation::CreateTask { task } | Mutation::UpdateTask { task } => {
198+
let _ = task_repo.upsert(task).await;
199+
}
200+
Mutation::RemoveTask { id } => {
201+
let _ = task_repo.delete(id).await;
202+
}
203+
_ => {}
204+
}
205+
}
206+
180207
// Leak the DB handle to keep it alive (same pattern as db_shim)
181208
std::mem::forget(db);
182209
result.applied

flowctl/crates/flowctl-cli/src/main.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ enum Commands {
122122
/// Include review Phase 4 (rp or codex).
123123
#[arg(long, value_parser = ["rp", "codex"])]
124124
review: Option<String>,
125+
/// Output minimal bootstrap prompt (~200 tokens).
126+
#[arg(long)]
127+
bootstrap: bool,
125128
},
126129

127130
/// Render ASCII DAG of task dependencies.
@@ -459,7 +462,7 @@ fn main() {
459462
source,
460463
} => admin::cmd_parse_findings(json, file, epic, register, source),
461464
Commands::Guard { layer } => admin::cmd_guard(json, layer),
462-
Commands::WorkerPrompt { task, tdd, review } => {
465+
Commands::WorkerPrompt { task, tdd, review, bootstrap: _ } => {
463466
admin::cmd_worker_prompt(json, task, tdd, review)
464467
}
465468

0 commit comments

Comments
 (0)