feat: engagement close-session wires vault, metrics, Kai ledger (WP-0009 T09)
Some checks failed
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 9m26s
ci / test (push) Failing after 4s

Add close_session helpers and CLI to append vault session logs, engagement-
scoped metrics, duty Kai charges, and client reports. Rejects sensitive
summaries. Pilot smoke-closed once; prepare points operators at close-session.
This commit is contained in:
tegwick 2026-07-16 12:09:05 +02:00
parent ad5965be91
commit 4532196546
11 changed files with 588 additions and 14 deletions

View file

@ -25,8 +25,10 @@ from .metrics import MetricsStore, OptimizerStore, performance_summary_markdown
from .optimization import OptimizationLoop, MIN_SAMPLES_FOR_RECOMMENDATIONS
from .engagement_promote import promote_engagement
from .engagement import (
DUTY_BASE_KAI,
EngagementError,
build_prepare_bundle,
close_session,
export_handoff,
find_repo_root,
list_engagement_dirs,
@ -1797,6 +1799,119 @@ def engagement_export_handoff(engagement_ref: str, repo_root: Optional[str]):
click.echo(f"Handoff pack: {handoff}")
@engagement.command("close-session")
@click.argument("engagement_ref")
@click.option("--success", "outcome_success", is_flag=True, help="Session succeeded")
@click.option("--failure", "outcome_failure", is_flag=True, help="Session failed")
@click.option(
"--duty",
type=click.Choice(sorted(DUTY_BASE_KAI.keys())),
default="standard_review",
show_default=True,
help="Duty product for Kai charge",
)
@click.option(
"--summary",
required=True,
help="One-line non-secret outcome (session log + report)",
)
@click.option("--time", "execution_time", type=float, help="Wall time seconds")
@click.option("--quality", type=float, help="Quality score 0.01.0")
@click.option(
"--access-class",
default="host_observe",
show_default=True,
type=click.Choice(["read_only", "host_observe", "privileged_ops", "none"]),
help="Access surcharge for Kai (none = no surcharge)",
)
@click.option(
"--kai-amount",
type=int,
default=None,
help="Override Kai charge (skip catalog formula)",
)
@click.option(
"--no-metrics", is_flag=True, help="Skip .kaizen/metrics under engagement"
)
@click.option("--no-ledger", is_flag=True, help="Skip commercial/ledger.jsonl charge")
@click.option("--no-report", is_flag=True, help="Skip reports/ duty report")
@click.option("--idempotency-key", help="Skip metrics append if key already recorded")
@click.option("--repo-root", default=None, help="Repo root (default: auto-detect)")
@click.option("--json", "as_json", is_flag=True, help="Machine-readable JSON result")
def engagement_close_session(
engagement_ref: str,
outcome_success: bool,
outcome_failure: bool,
duty: str,
summary: str,
execution_time: Optional[float],
quality: Optional[float],
access_class: str,
kai_amount: Optional[int],
no_metrics: bool,
no_ledger: bool,
no_report: bool,
idempotency_key: Optional[str],
repo_root: Optional[str],
as_json: bool,
):
"""Close a duty session: vault log, metrics, Kai ledger, optional report.
Does not invoke LLMs. Rejects summaries that look like secrets. Metrics land
under the engagement tree; ledger lines are billing metadata only.
"""
eng, _root = _resolve_eng(engagement_ref, repo_root)
if outcome_success and outcome_failure:
click.echo("Error: use only one of --success or --failure", err=True)
sys.exit(1)
if not outcome_success and not outcome_failure:
click.echo("Error: specify --success or --failure", err=True)
sys.exit(1)
if quality is not None and not (0.0 <= quality <= 1.0):
click.echo("Error: --quality must be between 0.0 and 1.0", err=True)
sys.exit(1)
access = None if access_class == "none" else access_class
try:
result = close_session(
eng,
success=outcome_success,
duty=duty,
summary=summary,
execution_time_s=execution_time,
quality=quality,
access_class=access,
amount_kai=kai_amount,
record_metrics=not no_metrics,
record_ledger=not no_ledger,
write_report=not no_report,
idempotency_key=idempotency_key,
)
except EngagementError as exc:
click.echo(f"Error: {exc}", err=True)
sys.exit(1)
except ValueError as exc:
click.echo(f"Error: {exc}", err=True)
sys.exit(1)
if as_json:
click.echo(json.dumps(result, indent=2))
return
click.echo(f"Closed session: {result['engagement_id']} ({result['duty']})")
click.echo(f" Memory: {result.get('memory_path')}")
if result.get("report_path"):
click.echo(f" Report: {result['report_path']}")
if result.get("metrics_recorded"):
click.echo(f" Metrics: recorded → {result.get('metrics_path')}")
elif not no_metrics:
click.echo(" Metrics: skipped (duplicate idempotency key)")
if not no_ledger:
click.echo(
f" Ledger: {result.get('amount_kai', 0)} Kai → {result.get('ledger_path')}"
)
@cli.group()
def schedule():
"""Prepare and validate scheduled agent runs (.kaizen/schedule.yml, ADR-005).