Skip to content

Commit c97f10a

Browse files
committed
Knowledge: add durable handoff report
1 parent ff9e4cf commit c97f10a

3 files changed

Lines changed: 287 additions & 0 deletions

File tree

docs/book/src/reference/cli.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ Use `--json` for machine-readable automation output, and add `--compact` when yo
285285

286286
```bash
287287
sonechka knowledge status [--json] [--compact]
288+
sonechka knowledge handoff [--json] [--compact] [--output <path>] [--export-latest]
288289
```
289290

290291
Use this when you want one CLI report for how durable project knowledge is currently moving between sessions, topic memories, `MEMORY.md`, and reusable skills.
@@ -303,6 +304,29 @@ The report includes:
303304

304305
Use `--json` for machine-readable output, add `--compact` for single-line JSON, and pass `--output <path>` when you want the report written directly to disk for cron or dashboard ingestion.
305306

307+
## `sonechka knowledge handoff`
308+
309+
```bash
310+
sonechka knowledge handoff [--json] [--compact] [--output <path>] [--export-latest]
311+
```
312+
313+
Use this when you want one durable handoff snapshot that combines:
314+
315+
- the current `knowledge status` view
316+
- the latest session continuity snapshot
317+
- the current `health continuity` trust posture
318+
319+
The report includes:
320+
321+
- `schema_version` and `generated_at_unix_ms`
322+
- `status` and `handoff_ready`
323+
- `blocking_surfaces`
324+
- `recommended_command`
325+
- `continuity_handoff` with trust level, source, and summary
326+
- embedded `knowledge_flow`, `session_continuity`, and `health_continuity` payloads
327+
328+
Use `--export-latest` when you want the handoff snapshot written to `~/.sonechka/exports/knowledge-handoff/latest.json`.
329+
306330
## `sonechka knowledge refresh`
307331

308332
```bash

src/main.rs

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,21 @@ enum KnowledgeCommands {
463463
#[arg(long)]
464464
output: Option<std::path::PathBuf>,
465465
},
466+
/// Refresh a durable knowledge handoff snapshot across knowledge, session, and continuity surfaces
467+
Handoff {
468+
/// Emit machine-readable JSON
469+
#[arg(long, default_value_t = false)]
470+
json: bool,
471+
/// Emit compact JSON instead of pretty-printed JSON
472+
#[arg(long, default_value_t = false)]
473+
compact: bool,
474+
/// Write the handoff report to a file path instead of only printing it
475+
#[arg(long)]
476+
output: Option<std::path::PathBuf>,
477+
/// Write the handoff report to ~/.sonechka/exports/knowledge-handoff/latest.json
478+
#[arg(long, default_value_t = false)]
479+
export_latest: bool,
480+
},
466481
/// Refresh the latest machine-readable knowledge-flow snapshot only
467482
Refresh {
468483
/// Emit compact JSON instead of pretty-printed JSON for the refreshed snapshot
@@ -2025,6 +2040,27 @@ async fn main() -> anyhow::Result<()> {
20252040
tool::render_knowledge_flow_refresh(output.as_deref(), compact)?
20262041
);
20272042
}
2043+
KnowledgeCommands::Handoff {
2044+
json,
2045+
compact,
2046+
output,
2047+
export_latest,
2048+
} => {
2049+
let report = build_knowledge_handoff_report()?;
2050+
if let Some(path) =
2051+
resolve_knowledge_handoff_output_path(output.as_deref(), export_latest)
2052+
{
2053+
println!("{}", write_json_to_path(&path, &report, compact)?);
2054+
if json {
2055+
return Ok(());
2056+
}
2057+
}
2058+
if json {
2059+
print_json(&report, compact)?;
2060+
} else {
2061+
println!("{}", render_knowledge_handoff_report_text(&report));
2062+
}
2063+
}
20282064
},
20292065
Some(Commands::Health { action }) => match action {
20302066
HealthCommands::Status {
@@ -3701,6 +3737,27 @@ struct OperatorReleaseReport {
37013737
operator_refresh: OperatorRefreshReport,
37023738
}
37033739

3740+
#[derive(Debug, Serialize)]
3741+
struct KnowledgeHandoffContinuityReport {
3742+
trust_level: Option<String>,
3743+
source: Option<String>,
3744+
summary: Option<String>,
3745+
}
3746+
3747+
#[derive(Debug, Serialize)]
3748+
struct KnowledgeHandoffReport {
3749+
schema_version: String,
3750+
generated_at_unix_ms: u128,
3751+
status: String,
3752+
handoff_ready: bool,
3753+
blocking_surfaces: Vec<String>,
3754+
recommended_command: String,
3755+
knowledge_flow: serde_json::Value,
3756+
session_continuity: serde_json::Value,
3757+
health_continuity: serde_json::Value,
3758+
continuity_handoff: KnowledgeHandoffContinuityReport,
3759+
}
3760+
37043761
fn operator_artifact_refs() -> Vec<OperatorArtifactRef> {
37053762
[
37063763
(
@@ -3869,6 +3926,71 @@ fn build_operator_release_report(
38693926
})
38703927
}
38713928

3929+
fn build_knowledge_handoff_report() -> anyhow::Result<KnowledgeHandoffReport> {
3930+
let knowledge_flow: serde_json::Value =
3931+
serde_json::from_str(&tool::render_knowledge_flow_status_json(true)?)?;
3932+
let session_continuity: serde_json::Value =
3933+
serde_json::from_str(&tool::render_session_status_json(None, true)?)?;
3934+
let health_continuity: serde_json::Value =
3935+
serde_json::from_str(&tool::render_health_continuity_json(true)?)?;
3936+
3937+
let trust_level = health_continuity
3938+
.get("handoff")
3939+
.and_then(|value| value.get("continuity"))
3940+
.and_then(|value| value.get("trust_level"))
3941+
.and_then(|value| value.as_str())
3942+
.map(ToOwned::to_owned);
3943+
let source = health_continuity
3944+
.get("handoff")
3945+
.and_then(|value| value.get("continuity"))
3946+
.and_then(|value| value.get("source"))
3947+
.and_then(|value| value.as_str())
3948+
.map(ToOwned::to_owned);
3949+
let summary = health_continuity
3950+
.get("handoff")
3951+
.and_then(|value| value.get("continuity"))
3952+
.and_then(|value| value.get("summary"))
3953+
.and_then(|value| value.as_str())
3954+
.map(ToOwned::to_owned);
3955+
3956+
let mut blocking_surfaces = Vec::new();
3957+
let trusted = trust_level.as_deref() == Some("trusted");
3958+
if !trusted {
3959+
blocking_surfaces.push("continuity".to_string());
3960+
}
3961+
let handoff_ready = blocking_surfaces.is_empty();
3962+
let recommended_command = if trusted {
3963+
knowledge_flow
3964+
.get("recommended_command")
3965+
.and_then(|value| value.as_str())
3966+
.unwrap_or("sonechka knowledge status")
3967+
.to_string()
3968+
} else {
3969+
"sonechka health continuity --json --compact".to_string()
3970+
};
3971+
3972+
Ok(KnowledgeHandoffReport {
3973+
schema_version: "knowledge-handoff-report.v1".to_string(),
3974+
generated_at_unix_ms: report_generated_at_unix_ms(),
3975+
status: if handoff_ready {
3976+
"ready".to_string()
3977+
} else {
3978+
"action-needed".to_string()
3979+
},
3980+
handoff_ready,
3981+
blocking_surfaces,
3982+
recommended_command,
3983+
continuity_handoff: KnowledgeHandoffContinuityReport {
3984+
trust_level,
3985+
source,
3986+
summary,
3987+
},
3988+
knowledge_flow,
3989+
session_continuity,
3990+
health_continuity,
3991+
})
3992+
}
3993+
38723994
fn render_operator_status_report_text(report: &OperatorStatusReport) -> String {
38733995
let advisor_profile = report
38743996
.provider_routing
@@ -4027,6 +4149,68 @@ fn render_operator_release_report_text(report: &OperatorReleaseReport) -> String
40274149
)
40284150
}
40294151

4152+
fn render_knowledge_handoff_report_text(report: &KnowledgeHandoffReport) -> String {
4153+
let trust_level = report
4154+
.continuity_handoff
4155+
.trust_level
4156+
.as_deref()
4157+
.unwrap_or("unknown");
4158+
let source = report
4159+
.continuity_handoff
4160+
.source
4161+
.as_deref()
4162+
.unwrap_or("unknown");
4163+
let summary = report
4164+
.continuity_handoff
4165+
.summary
4166+
.as_deref()
4167+
.unwrap_or("No continuity summary available.");
4168+
let mut lines = vec![
4169+
"Knowledge handoff".to_string(),
4170+
format!("- status: {}", report.status),
4171+
format!(
4172+
"- handoff ready: {}",
4173+
if report.handoff_ready { "yes" } else { "no" }
4174+
),
4175+
format!(
4176+
"- blocking surfaces: {}",
4177+
if report.blocking_surfaces.is_empty() {
4178+
"none".to_string()
4179+
} else {
4180+
report.blocking_surfaces.join(", ")
4181+
}
4182+
),
4183+
format!("- continuity trust: {}", trust_level),
4184+
format!("- continuity source: {}", source),
4185+
format!("- recommended command: {}", report.recommended_command),
4186+
String::new(),
4187+
"Continuity handoff".to_string(),
4188+
format!("- {}", summary),
4189+
];
4190+
if let Some(session_id) = report
4191+
.session_continuity
4192+
.get("session_id")
4193+
.and_then(|value| value.as_str())
4194+
{
4195+
lines.push(String::new());
4196+
lines.push("Latest session".to_string());
4197+
lines.push(format!("- session id: {}", session_id));
4198+
}
4199+
if let Some(command) = report
4200+
.knowledge_flow
4201+
.get("recommended_command")
4202+
.and_then(|value| value.as_str())
4203+
{
4204+
lines.push(String::new());
4205+
lines.push("Knowledge flow".to_string());
4206+
lines.push(format!("- recommended command: {}", command));
4207+
}
4208+
lines.join(
4209+
"
4210+
",
4211+
)
4212+
}
4213+
40304214
fn report_generated_at_unix_ms() -> u128 {
40314215
std::time::SystemTime::now()
40324216
.duration_since(std::time::UNIX_EPOCH)
@@ -4346,6 +4530,13 @@ fn operator_report_latest_path(report_family: &str) -> std::path::PathBuf {
43464530
.join("latest.json")
43474531
}
43484532

4533+
fn knowledge_handoff_latest_path() -> std::path::PathBuf {
4534+
config::config_dir()
4535+
.join("exports")
4536+
.join("knowledge-handoff")
4537+
.join("latest.json")
4538+
}
4539+
43494540
fn resolve_operator_report_output_path(
43504541
output: Option<&std::path::Path>,
43514542
export_latest: bool,
@@ -4356,6 +4547,15 @@ fn resolve_operator_report_output_path(
43564547
.or_else(|| export_latest.then(|| operator_report_latest_path(report_family)))
43574548
}
43584549

4550+
fn resolve_knowledge_handoff_output_path(
4551+
output: Option<&std::path::Path>,
4552+
export_latest: bool,
4553+
) -> Option<std::path::PathBuf> {
4554+
output
4555+
.map(std::path::Path::to_path_buf)
4556+
.or_else(|| export_latest.then(knowledge_handoff_latest_path))
4557+
}
4558+
43594559
fn print_json<T: Serialize>(value: &T, compact: bool) -> anyhow::Result<()> {
43604560
let body = if compact {
43614561
serde_json::to_string(value)?

tests/plugin_cli.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7357,6 +7357,69 @@ fn knowledge_refresh_honors_output_path_and_compact_json() {
73577357
assert_eq!(body["topic_memory"]["topic_files"], 1);
73587358
}
73597359

7360+
#[test]
7361+
fn knowledge_handoff_json_embeds_continuity_and_exports_latest_snapshot() {
7362+
let home = tempdir().unwrap();
7363+
let config_dir = home.path().join(".sonechka");
7364+
seed_background_continuity_fixture(&config_dir);
7365+
7366+
let output = Command::cargo_bin("sonechka")
7367+
.unwrap()
7368+
.env("HOME", home.path())
7369+
.arg("knowledge")
7370+
.arg("handoff")
7371+
.arg("--json")
7372+
.arg("--compact")
7373+
.arg("--export-latest")
7374+
.output()
7375+
.unwrap();
7376+
7377+
assert!(output.status.success());
7378+
assert!(String::from_utf8_lossy(&output.stdout).contains("Wrote report to"));
7379+
7380+
let latest_path = config_dir
7381+
.join("exports")
7382+
.join("knowledge-handoff")
7383+
.join("latest.json");
7384+
assert!(latest_path.exists());
7385+
let body: Value =
7386+
serde_json::from_str(&std::fs::read_to_string(&latest_path).unwrap()).unwrap();
7387+
assert_eq!(body["schema_version"], "knowledge-handoff-report.v1");
7388+
assert_eq!(body["status"], "ready");
7389+
assert_eq!(body["handoff_ready"], true);
7390+
assert_eq!(body["blocking_surfaces"], json!([]));
7391+
assert_eq!(body["continuity_handoff"]["trust_level"], "trusted");
7392+
assert_eq!(
7393+
body["knowledge_flow"]["schema_version"],
7394+
"knowledge-flow-status.v1"
7395+
);
7396+
assert_eq!(
7397+
body["health_continuity"]["schema_version"],
7398+
"health-continuity.v1"
7399+
);
7400+
}
7401+
7402+
#[test]
7403+
fn knowledge_handoff_text_flags_continuity_blockers() {
7404+
let home = tempdir().unwrap();
7405+
7406+
Command::cargo_bin("sonechka")
7407+
.unwrap()
7408+
.env("HOME", home.path())
7409+
.arg("knowledge")
7410+
.arg("handoff")
7411+
.assert()
7412+
.success()
7413+
.stdout(predicate::str::contains("Knowledge handoff"))
7414+
.stdout(predicate::str::contains("status: action-needed"))
7415+
.stdout(predicate::str::contains("handoff ready: no"))
7416+
.stdout(predicate::str::contains("blocking surfaces: continuity"))
7417+
.stdout(predicate::str::contains(
7418+
"recommended command: sonechka health continuity --json --compact",
7419+
))
7420+
.stdout(predicate::str::contains("Continuity handoff"));
7421+
}
7422+
73607423
#[test]
73617424
fn policy_status_reports_models_and_recent_review_evidence() {
73627425
let home = tempdir().unwrap();

0 commit comments

Comments
 (0)