46 crates, 744 tests green (up from 726 at v0.31.0). ## kei-dna-index (new) — read-only adjacency analysis over kei-ledger Answers "who else touched same files / solved same task / ran nearby in time". Does NOT mutate ledger — parses DNA strings in memory. Respects SSoT (DNA string is the single source; columns NOT duplicated). Public API: - adjacent(target_dna, kind) — 5 kinds: Scope / Body / Role / Temporal / All - cluster_by(scope|body|role) — group DNAs, ≥2 members per cluster - precedent(body_sha, status_filter) — find past successful runs of same task - stats — totals, unique scopes/bodies, avg cluster size CLI: - kei-dna-index adjacent --dna D [--by kind] [--limit N] [--db PATH] - kei-dna-index cluster --by scope|body|role - kei-dna-index precedent --body HEX [--status merged|failed|all] - kei-dna-index stats 18 tests pass (13 integration + 5 parsed unit). Zero sibling deps (no kei-ledger, no kei-agent-runtime path imports — standalone tool). Separation of concerns: kei-ledger stays PURE provenance primitive. Analytical layer lives in kei-dna-index. Can swap implementations (naive scan → cached → embeddings) without touching ledger schema. ## kei-fork v0.31.2 — Option D path convention Moved fork worktree root from `.claude/forks/<id>/` to `_forks/<id>/`. Reasons: - `.claude/` is Anthropic-reserved; kit artefacts shouldn't pollute it - Claude Code sandbox denies Write in `.claude/forks/` for agents - `_forks/` matches existing kit convention (_primitives/, _roles/, _archive/, _blocks/, _capabilities/, _agents/) - Independent namespace — no coupling to Claude Code internals 13 existing kei-fork tests still pass (they use tempfile kit_roots so path convention is transparent). ## Usage enabled by these two - kei-prune can now query "all DNAs in same scope-cluster" → retire dupes - kei-brain-view can cluster-render instead of tree-render - Three-role pipeline (writer/auditor/merger) can use precedent() to find successful past patterns for same body-hash - Agents with worktree isolation can write to _forks/ without sandbox permission issues Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
118 lines
3.8 KiB
Rust
118 lines
3.8 KiB
Rust
//! `collect(agent_id, commit_msg, kit_root)` — merge the fork back.
|
|
//!
|
|
//! Contract:
|
|
//! 1. `.DONE` must exist inside the worktree, else `Error::NotDone`
|
|
//! 2. `git add -A && git commit` inside the worktree
|
|
//! 3. Capture commit SHA, then `git merge --no-ff fork/<id>` in kit_root
|
|
//! 4. Move worktree to `_archive/forks/YYYY-MM-DD/<id>/` (preserving
|
|
//! the agent's artefacts for post-hoc review / rescue)
|
|
//! 5. `git worktree prune && git branch -D fork/<id>` to clean up refs
|
|
//! 6. `kei-ledger done <id>` unless `KEI_FORK_SKIP_LEDGER=1`
|
|
//!
|
|
//! On SUCCESS: `_forks/<id>/` is gone, archive exists, merge
|
|
//! commit is on HEAD of kit_root. Return value carries the SHA and
|
|
//! count of files added by the agent.
|
|
|
|
use crate::error::Error;
|
|
use crate::git;
|
|
use chrono::Utc;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CollectReport {
|
|
pub files_added: usize,
|
|
pub commit_sha: String,
|
|
pub archive_path: PathBuf,
|
|
}
|
|
|
|
pub fn collect(agent_id: &str, commit_msg: &str, kit_root: &Path) -> Result<CollectReport, Error> {
|
|
let worktree_abs = kit_root.join("_forks").join(agent_id);
|
|
if !worktree_abs.join(".DONE").exists() {
|
|
return Err(Error::NotDone(agent_id.to_string()));
|
|
}
|
|
let files_added = count_tracked_files(&worktree_abs);
|
|
|
|
let branch = format!("fork/{agent_id}");
|
|
git::add_all(&worktree_abs)?;
|
|
git::commit(&worktree_abs, commit_msg)?;
|
|
let commit_sha = git::rev_parse_head(&worktree_abs)?;
|
|
|
|
let merge_msg = format!("Merge {branch}");
|
|
git::merge_no_ff(kit_root, &branch, &merge_msg)?;
|
|
|
|
let archive_path = archive_worktree(kit_root, agent_id, &worktree_abs)?;
|
|
|
|
// worktree_remove is unnecessary after fs::rename — prune cleans the
|
|
// stale worktree metadata and branch -D removes the ref.
|
|
let _ = git::worktree_prune(kit_root);
|
|
let _ = git::branch_delete(kit_root, &branch);
|
|
|
|
ledger_done(agent_id)?;
|
|
|
|
Ok(CollectReport {
|
|
files_added,
|
|
commit_sha,
|
|
archive_path,
|
|
})
|
|
}
|
|
|
|
fn count_tracked_files(worktree_abs: &Path) -> usize {
|
|
// Cheap approximation — walk the worktree, skip `.git*` and the
|
|
// KEI_FORK meta file. Used for the report only, not for decisions.
|
|
fn walk(dir: &Path) -> usize {
|
|
let mut n = 0;
|
|
let Ok(rd) = fs::read_dir(dir) else { return 0 };
|
|
for e in rd.flatten() {
|
|
let p = e.path();
|
|
let name = e.file_name();
|
|
let s = name.to_string_lossy();
|
|
if s.starts_with(".git") {
|
|
continue;
|
|
}
|
|
if p.is_dir() {
|
|
n += walk(&p);
|
|
} else {
|
|
n += 1;
|
|
}
|
|
}
|
|
n
|
|
}
|
|
walk(worktree_abs)
|
|
}
|
|
|
|
fn archive_worktree(
|
|
kit_root: &Path,
|
|
agent_id: &str,
|
|
worktree_abs: &Path,
|
|
) -> Result<PathBuf, Error> {
|
|
let date = Utc::now().format("%Y-%m-%d").to_string();
|
|
let archive_dir = kit_root.join("_archive/forks").join(&date);
|
|
fs::create_dir_all(&archive_dir)?;
|
|
let target = archive_dir.join(agent_id);
|
|
if target.exists() {
|
|
fs::remove_dir_all(&target)?;
|
|
}
|
|
fs::rename(worktree_abs, &target)?;
|
|
Ok(target)
|
|
}
|
|
|
|
fn ledger_skipped() -> bool {
|
|
std::env::var("KEI_FORK_SKIP_LEDGER").ok().as_deref() == Some("1")
|
|
}
|
|
|
|
fn ledger_done(agent_id: &str) -> Result<(), Error> {
|
|
if ledger_skipped() {
|
|
return Ok(());
|
|
}
|
|
let status = Command::new("kei-ledger")
|
|
.args(["done", agent_id, "--summary", "fork collected"])
|
|
.status();
|
|
match status {
|
|
Ok(s) if s.success() => Ok(()),
|
|
Ok(s) => Err(Error::Ledger(format!("kei-ledger done exit {s}"))),
|
|
Err(e) => Err(Error::Ledger(format!("kei-ledger not runnable: {e}"))),
|
|
}
|
|
}
|