Add /graph G6: plan-boundary topology optimizer

A restricted optimizer pass now reviews the graph at plan boundaries —
right after initial planning and piggybacked on each replan version
boundary, never mid-execution. An optimizer subagent may emit four ops
over Waiting/Ready nodes only: remove_dep (delete a false dependency,
restoring parallelism — the highest-value edit), reorder (pending
priority for the serial scheduler), merge (fold two tiny nodes; specs
concatenate, deps union, dependents re-point, absorbed self-deps drop),
and split (2-3 focused replacements inheriting the original's deps and
dependents). The optimizer changes graph DATA only; the executor stays
pure deterministic Rust. KIGI_GRAPH_OPTIMIZER=0 disables it entirely.

apply_optimization enforces the contract twice: per-op checks
(pending-only targets, known ids, terminal node untouchable, dead-node
deps rejected as DeadDep, merge/split targets with non-pending
dependents rejected with the true reason instead of tripping the
immutable invariant later), then FINAL invariants — every non-pending
node byte-identical in the result, the gn-final gate rebuilt over all
survivors, node cap, whole-graph acyclicity, and a BIDIRECTIONAL status
re-derivation for pending nodes (adversarial review caught the critical
hole: a merge grafting unsatisfied deps onto a Ready node would
otherwise dispatch it ahead of its new prerequisites, since
recompute_ready is promote-only). Applied passes bump plan_version,
freeze an immutable baseline, and consume a slot of the SHARED replan
cap; an explicit {"ops": []} is a respected free no-op; any failure
degrades to keeping the current plan. Plumbing reuses a new shared
artifact-pass runner (stale-artifact delete, size cap, missing-file
fail-closed) extracted from the replanner.

Tests: remove_dep parallelism restore + loud no-such-dep, immutable and
terminal-node rejections across all four ops, merge/split dependent
rewiring incl. final-gate rebuild and intra-split dep resolution,
result-cycle rejection, dead-dep splits, Ready-demote-on-merge, and
three e2e flows — false-dep removal proven ACTUALLY parallel by the
held-reply fan-out gate, OPTIMIZER=0 spawning zero passes, and the
shared-cap guard. kigi-shell 4959 lib tests green; clippy clean.
This commit is contained in:
2026-07-20 20:21:49 -04:00
parent db05ed0751
commit 02cf5deebd
19 changed files with 1123 additions and 0 deletions
@@ -269,6 +269,71 @@ pub(crate) async fn run_graph_replanner(
}
}
/// Generic single-shot artifact pass: render `template` + `sections`,
/// spawn the role child, enforce the stale-artifact/size/missing-file
/// discipline, and return the artifact's raw JSON for the caller to
/// validate. Shared by the optimizer (and any future boundary pass).
pub(crate) struct ArtifactPassSpec<'a> {
pub template: &'a str,
pub sections: &'a str,
pub graph_file: &'a Path,
pub tool_names: &'a RoleToolNames,
pub role: &'a str,
}
pub(crate) async fn run_graph_artifact_pass(
spawner: Arc<dyn GoalPlannerSpawner>,
spec: ArtifactPassSpec<'_>,
) -> Result<String, String> {
if let Some(parent) = spec.graph_file.parent()
&& let Err(err) = tokio::fs::create_dir_all(parent).await
{
return Err(format!("failed to create graph dir: {err}"));
}
if let Err(err) = tokio::fs::remove_file(spec.graph_file).await
&& err.kind() != std::io::ErrorKind::NotFound
{
return Err(format!("failed to clear stale artifact: {err}"));
}
let graph_file_str = spec.graph_file.to_string_lossy();
let rendered = spec
.tool_names
.apply(&spec.template.replace("{GRAPH_FILE}", &graph_file_str));
let prompt_text = format!("{rendered}\n\n{}", spec.sections);
let prompt = RoleRenderedPrompt {
primary: prompt_text.clone(),
fallback: prompt_text,
};
let spawn_id = uuid::Uuid::now_v7().to_string();
match spawner.spawn_planner(&spawn_id, prompt).await {
Ok(_) => {}
Err(SpawnError::Transport(detail)) => {
return Err(format!("{} transport error: {detail}", spec.role));
}
Err(SpawnError::Runtime { message, cancelled }) => {
return Err(if cancelled {
format!("{} aborted: {message}", spec.role)
} else {
format!("{} runtime error: {message}", spec.role)
});
}
}
match tokio::fs::metadata(spec.graph_file).await {
Ok(meta) if meta.is_file() && meta.len() > 0 && meta.len() <= MAX_GRAPH_JSON_BYTES => {}
Ok(meta) if meta.len() > MAX_GRAPH_JSON_BYTES => {
return Err(format!(
"{} artifact is {} bytes; the cap is {MAX_GRAPH_JSON_BYTES}",
spec.role,
meta.len()
));
}
_ => return Err(format!("{} produced no artifact", spec.role)),
}
tokio::fs::read_to_string(spec.graph_file)
.await
.map_err(|err| format!("failed to read {} artifact: {err}", spec.role))
}
#[cfg(test)]
mod tests {
use super::*;