Single-commit clean baseline after security scrub of niche-tells, project codenames, internal jargon, and contributor-email leaks. Contents: - 100 Rust crates (_primitives/_rust/) - 37 agent manifests (_manifests/) + generated specs (_generated/) - 67 user-invocable skills (skills/) - 33 hooks (hooks/) - Composition blocks (_blocks/) - Documentation (docs/, README.md) - TS adapter packages (_ts_packages/) - Assembler (_assembler/) - Roles (_roles/) - Templates (_templates/) - Forgejo CI (.forgejo/) Author: Denis Parfionovich <info@greendragon.info> License: see LICENSE.
143 lines
5.5 KiB
Rust
143 lines
5.5 KiB
Rust
//! `RankedAction → task.toml` emitter (kei-spawn-compatible).
|
|
//!
|
|
//! Each emitted file is a minimal kei-spawn task with three sections:
|
|
//!
|
|
//! [task] role + description + body-from-master-line
|
|
//! [scope] files-whitelist guessed from kind
|
|
//! [body] long-form text (the action title + source-line ref)
|
|
//!
|
|
//! The orchestrator can edit the file before passing to `kei-spawn spawn`.
|
|
|
|
use anyhow::{Context, Result};
|
|
use serde::Serialize;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use crate::classifier::ActionKind;
|
|
use crate::ranker::RankedAction;
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct EmitOutput {
|
|
pub action_id: String,
|
|
pub path: PathBuf,
|
|
pub bytes: usize,
|
|
}
|
|
|
|
/// Emit one task.toml under `out_dir`. Returns its path + size.
|
|
pub fn emit_task_toml(action: &RankedAction, out_dir: &Path, master: &Path) -> Result<EmitOutput> {
|
|
std::fs::create_dir_all(out_dir)
|
|
.with_context(|| format!("create dir {}", out_dir.display()))?;
|
|
let slug = make_slug(&action.raw.title);
|
|
let file_name = format!("action-{}-{}.toml", action.raw.id, slug);
|
|
let path = out_dir.join(&file_name);
|
|
let body = build_body(action, master);
|
|
std::fs::write(&path, &body).with_context(|| format!("write {}", path.display()))?;
|
|
Ok(EmitOutput {
|
|
action_id: action.raw.id.clone(),
|
|
path,
|
|
bytes: body.len(),
|
|
})
|
|
}
|
|
|
|
fn build_body(action: &RankedAction, master: &Path) -> String {
|
|
let role = role_for_kind(action.kind);
|
|
let whitelist = whitelist_for_kind(action.kind, &action.raw.title);
|
|
let mut s = String::new();
|
|
s.push_str(&format!("# Auto-emitted by kei-decision from {}\n", master.display()));
|
|
s.push_str(&format!("# Source line: {}\n", action.raw.source_line));
|
|
s.push_str(&format!("# Score: {:.3} Rank: {}\n\n", action.score, action.rank));
|
|
s.push_str("[task]\n");
|
|
s.push_str(&format!("role = \"{}\"\n", role));
|
|
s.push_str(&format!("description = {}\n", toml_escape(&action.raw.title)));
|
|
s.push_str("\n[scope]\n");
|
|
s.push_str("files-whitelist = [\n");
|
|
for w in &whitelist {
|
|
s.push_str(&format!(" {},\n", toml_escape(w)));
|
|
}
|
|
s.push_str("]\n\n");
|
|
s.push_str("[body]\n");
|
|
s.push_str(&format!("text = {}\n", toml_escape(&render_body_text(action))));
|
|
s
|
|
}
|
|
|
|
fn render_body_text(action: &RankedAction) -> String {
|
|
let mut t = String::new();
|
|
t.push_str(&format!("Action #{} (kind: {:?})\n\n", action.raw.id, action.kind));
|
|
t.push_str(&format!("Title: {}\n", action.raw.title));
|
|
t.push_str(&format!("Severity: {}\n", action.raw.severity));
|
|
t.push_str(&format!("Effort: {}\n", action.raw.effort));
|
|
if !action.raw.deps.is_empty() {
|
|
t.push_str(&format!("Dependencies: {}\n", action.raw.deps.join(", ")));
|
|
}
|
|
t.push_str("\nThis task was auto-generated from a /research MASTER-REPORT.md actionable-plan row. ");
|
|
t.push_str("The orchestrator should review the [scope] whitelist before spawning.");
|
|
t
|
|
}
|
|
|
|
fn role_for_kind(kind: ActionKind) -> &'static str {
|
|
match kind {
|
|
ActionKind::Refactor => "code-implementer",
|
|
ActionKind::Migrate => "code-implementer",
|
|
ActionKind::NewPrimitive => "code-implementer",
|
|
ActionKind::Decompose => "code-implementer",
|
|
ActionKind::Doc => "doc-writer",
|
|
ActionKind::Unknown => "code-implementer",
|
|
}
|
|
}
|
|
|
|
/// Best-effort whitelist guess. Conservative — orchestrator should verify.
|
|
fn whitelist_for_kind(kind: ActionKind, title: &str) -> Vec<String> {
|
|
match kind {
|
|
ActionKind::NewPrimitive => {
|
|
let slug = primitive_slug_from_title(title);
|
|
vec![format!("_primitives/_rust/kei-{}/**", slug)]
|
|
}
|
|
ActionKind::Decompose => {
|
|
// Decompose targets a specific monolith — leave placeholder.
|
|
vec!["<TODO-orchestrator-fill: file or dir to decompose>".to_string()]
|
|
}
|
|
ActionKind::Refactor | ActionKind::Migrate => {
|
|
vec!["<TODO-orchestrator-fill: scope of refactor/migrate>".to_string()]
|
|
}
|
|
ActionKind::Doc => vec!["docs/**".to_string(), "README.md".to_string()],
|
|
ActionKind::Unknown => vec!["<TODO-orchestrator-fill>".to_string()],
|
|
}
|
|
}
|
|
|
|
/// Heuristic: pick the longest run of letters after "kei-" or after "primitive ".
|
|
fn primitive_slug_from_title(title: &str) -> String {
|
|
let lower = title.to_lowercase();
|
|
if let Some(after) = lower.split("kei-").nth(1) {
|
|
let s: String = after.chars().take_while(|c| c.is_alphanumeric() || *c == '-').collect();
|
|
if !s.is_empty() {
|
|
return s.trim_end_matches('-').to_string();
|
|
}
|
|
}
|
|
if let Some(after) = lower.split("primitive ").nth(1) {
|
|
let s: String = after.chars().take_while(|c| c.is_alphanumeric() || *c == '-').collect();
|
|
if !s.is_empty() {
|
|
return s.trim_end_matches('-').to_string();
|
|
}
|
|
}
|
|
"TODO-name".to_string()
|
|
}
|
|
|
|
/// `make_slug("Refactor 4 hooks to call kei-leak-matrix") → "refactor-4-hooks-to-call-kei-leak"`.
|
|
fn make_slug(title: &str) -> String {
|
|
let lower = title.to_lowercase();
|
|
let cleaned: String = lower
|
|
.chars()
|
|
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
|
|
.collect();
|
|
let collapsed: String = cleaned
|
|
.split('-')
|
|
.filter(|p| !p.is_empty())
|
|
.take(8)
|
|
.collect::<Vec<&str>>()
|
|
.join("-");
|
|
if collapsed.is_empty() { "action".to_string() } else { collapsed }
|
|
}
|
|
|
|
/// Use serde_json string escape for safety, then wrap in TOML basic-string quotes.
|
|
fn toml_escape(s: &str) -> String {
|
|
serde_json::to_string(s).unwrap_or_else(|_| format!("\"{}\"", s.replace('"', "\\\"")))
|
|
}
|