"""Forward-deployed engagement lifecycle helpers (KAIZEN-WP-0009 / DEC-FDA-001). File-based Phase 1: ENGAGEMENT.yaml under engagements/pilots// is the source of truth. This module loads, validates, prepares session bundles, and transitions phases — it does not invoke LLMs or touch production hosts. """ from __future__ import annotations import json import re import shutil from dataclasses import dataclass from datetime import date, datetime, timezone from pathlib import Path from typing import Any, Dict, List, Optional, Pattern, Sequence, Tuple import yaml ENGAGEMENT_FILENAME = "ENGAGEMENT.yaml" DEFAULT_PILOTS_DIR = Path("engagements") / "pilots" DEFAULT_ROLES_DIR = Path("roles") VALID_PHASES = ( "requested", "quoting", "funded", "staffing", "ramp_up", "operating", "renewing", "ramp_down", "closed", "cancelled", ) # Allowed forward transitions (plus renewing ↔ operating, and cancelled from most) PHASE_TRANSITIONS: Dict[str, Tuple[str, ...]] = { "requested": ("quoting", "cancelled"), "quoting": ("funded", "requested", "cancelled"), "funded": ("staffing", "cancelled"), "staffing": ("ramp_up", "cancelled"), "ramp_up": ("operating", "cancelled"), "operating": ("renewing", "ramp_down", "cancelled"), "renewing": ("operating", "ramp_down", "cancelled"), "ramp_down": ("closed", "cancelled"), "closed": (), "cancelled": (), } # Sensitive patterns for scrub (heuristic; not a security scanner) _SCRUB_PATTERNS: List[Tuple[str, Pattern]] = [ ("private_key_block", re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----")), ("aws_access_key", re.compile(r"AKIA[0-9A-Z]{16}")), ("bearer_token", re.compile(r"(?i)bearer\s+[a-z0-9\-._~+/]+=*")), ( "ipv4_privateish", re.compile(r"\b(?:10|192\.168|172\.(?:1[6-9]|2\d|3[01]))\.\d{1,3}\.\d{1,3}\b"), ), ("password_assignment", re.compile(r"(?i)(password|passwd|secret)\s*[:=]\s*\S+")), ] # Kai duty catalog (base units before capability tier multiplier) — business model §5.4 DUTY_BASE_KAI: Dict[str, int] = { "short_assist": 100, "standard_review": 400, "deep_assessment": 1200, "ramp_up_package": 5000, "ramp_down_package": 3000, } ACCESS_SURCHARGE_KAI: Dict[str, int] = { "read_only": 0, "host_observe": 100, "privileged_ops": 400, } # Package duties use half tier weight per business model DUTY_HALF_TIER = frozenset({"ramp_up_package", "ramp_down_package"}) class EngagementError(Exception): """Raised when an engagement cannot be loaded or operated on.""" @dataclass class ChecklistItem: item_id: str criterion: str status: str evidence: str @property def done(self) -> bool: return self.status.strip().lower() in ("done", "complete", "yes", "ok") @dataclass class Engagement: """Parsed engagement record plus filesystem paths.""" path: Path data: Dict[str, Any] source_path: Path @property def engagement_id(self) -> str: meta = self.data.get("metadata") or {} return str(meta.get("id") or self.path.name) @property def phase(self) -> str: status = self.data.get("status") or {} return str(status.get("phase") or "requested") @property def role_id(self) -> Optional[str]: spec = self.data.get("spec") or {} role = spec.get("role") or {} rid = role.get("id") return str(rid) if rid else None @property def targets(self) -> List[Dict[str, Any]]: spec = self.data.get("spec") or {} targets = spec.get("targets") or [] return list(targets) if isinstance(targets, list) else [] def rel(self, *parts: str) -> Path: return self.path.joinpath(*parts) def agent_definition_path(self) -> Optional[Path]: spec = self.data.get("spec") or {} ad = spec.get("agent_definition") or {} rel = ad.get("path") if not rel: return None p = self.path / rel return p if p.exists() else p def vault_memory_path(self) -> Path: spec = self.data.get("spec") or {} vault = spec.get("vault") or {} rel = vault.get("memory") or "vault/memory.md" return self.path / rel def checklist_path(self, which: str) -> Optional[Path]: spec = self.data.get("spec") or {} checks = spec.get("checklists") or {} key = "ramp_up" if which in ("ramp_up", "ramp-up", "up") else "ramp_down" if which in ("ramp_down", "ramp-down", "down"): key = "ramp_down" rel = checks.get(key) if not rel: default = ( "checklists/ramp-up-status.md" if key == "ramp_up" else "checklists/ramp-down-status.md" ) rel = default p = self.path / rel return p if p.exists() else None def quote_path(self) -> Optional[Path]: spec = self.data.get("spec") or {} commercial = spec.get("commercial") or {} rel = commercial.get("quote_ref") or "commercial/quote.yaml" p = self.path / rel return p if p.exists() else None def ledger_path(self) -> Path: spec = self.data.get("spec") or {} commercial = spec.get("commercial") or {} rel = commercial.get("ledger_ref") or "commercial/ledger.jsonl" return self.path / rel def today_iso() -> str: return date.today().isoformat() def utc_now_iso() -> str: return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") def text_has_sensitive_content(text: str) -> List[str]: """Return labels of scrub patterns that match text (empty if clean).""" hits: List[str] = [] for label, pattern in _SCRUB_PATTERNS: if pattern.search(text): hits.append(label) return hits def capability_tier(eng: Engagement) -> int: spec = eng.data.get("spec") or {} commercial = spec.get("commercial") or {} tier = commercial.get("capability_tier") if tier is not None: try: return max(1, int(tier)) except (TypeError, ValueError): pass return 4 def compute_duty_kai( duty: str, *, tier: int = 4, access_class: Optional[str] = None, amount_override: Optional[int] = None, ) -> int: """Compute Kai charge for a duty (+ optional access surcharge).""" if amount_override is not None: return max(0, int(amount_override)) if duty not in DUTY_BASE_KAI: raise EngagementError( f"Unknown duty {duty!r}; expected one of: {', '.join(sorted(DUTY_BASE_KAI))}" ) base = DUTY_BASE_KAI[duty] if duty in DUTY_HALF_TIER: amount = int(base * tier / 2) else: amount = base * tier if access_class: if access_class not in ACCESS_SURCHARGE_KAI: raise EngagementError( f"Unknown access class {access_class!r}; " f"expected one of: {', '.join(sorted(ACCESS_SURCHARGE_KAI))}" ) amount += ACCESS_SURCHARGE_KAI[access_class] return amount def find_repo_root(start: Optional[Path] = None) -> Path: """Walk parents for engagements/ or roles/ or .git.""" cur = (start or Path.cwd()).resolve() for candidate in [cur, *cur.parents]: if (candidate / "engagements").is_dir() or (candidate / "roles").is_dir(): return candidate if (candidate / ".git").exists() and ( (candidate / "pyproject.toml").exists() or (candidate / "workplans").is_dir() ): return candidate return cur def list_engagement_dirs(repo_root: Path) -> List[Path]: """Discover engagement directories (pilots first, then other trees).""" found: List[Path] = [] pilots = repo_root / DEFAULT_PILOTS_DIR if pilots.is_dir(): for child in sorted(pilots.iterdir()): if child.is_dir() and (child / ENGAGEMENT_FILENAME).exists(): found.append(child) engagements = repo_root / "engagements" if engagements.is_dir(): for child in sorted(engagements.iterdir()): if child.name == "pilots": continue if child.is_dir() and (child / ENGAGEMENT_FILENAME).exists(): found.append(child) return found def resolve_engagement_dir( engagement_ref: str, repo_root: Optional[Path] = None ) -> Path: """Resolve an engagement id or path to its directory.""" root = (repo_root or find_repo_root()).resolve() ref = Path(engagement_ref) if ref.is_dir() and (ref / ENGAGEMENT_FILENAME).exists(): return ref.resolve() if ref.is_file() and ref.name == ENGAGEMENT_FILENAME: return ref.parent.resolve() # id under pilots/ pilot = root / DEFAULT_PILOTS_DIR / engagement_ref if (pilot / ENGAGEMENT_FILENAME).exists(): return pilot.resolve() # bare engagements/ bare = root / "engagements" / engagement_ref if (bare / ENGAGEMENT_FILENAME).exists(): return bare.resolve() # search by metadata id for d in list_engagement_dirs(root): try: eng = load_engagement(d) except EngagementError: continue if eng.engagement_id == engagement_ref or d.name == engagement_ref: return d raise EngagementError( f"Engagement not found: {engagement_ref!r} " f"(looked under {root / DEFAULT_PILOTS_DIR})" ) def load_engagement(path: Path) -> Engagement: """Load ENGAGEMENT.yaml from a directory (or path to the file).""" path = Path(path) if path.is_file(): source = path base = path.parent else: source = path / ENGAGEMENT_FILENAME base = path if not source.exists(): raise EngagementError(f"Missing {ENGAGEMENT_FILENAME}: {source}") try: data = yaml.safe_load(source.read_text(encoding="utf-8")) except yaml.YAMLError as exc: raise EngagementError(f"Invalid YAML in {source}: {exc}") from exc if not isinstance(data, dict): raise EngagementError(f"{source} must contain a mapping") if data.get("kind") and data.get("kind") != "Engagement": raise EngagementError( f"{source}: kind must be Engagement (got {data.get('kind')})" ) return Engagement(path=base.resolve(), data=data, source_path=source.resolve()) def save_engagement(eng: Engagement) -> None: """Write ENGAGEMENT.yaml back (preserves structure via dump of data).""" eng.source_path.write_text( yaml.safe_dump(eng.data, sort_keys=False, allow_unicode=True), encoding="utf-8", ) def parse_checklist_markdown(text: str) -> List[ChecklistItem]: """Parse checklist tables with columns ID | Criterion | Status | Evidence.""" items: List[ChecklistItem] = [] for line in text.splitlines(): line = line.strip() if not line.startswith("|"): continue cells = [c.strip() for c in line.strip("|").split("|")] if len(cells) < 4: continue item_id, criterion, status, evidence = cells[0], cells[1], cells[2], cells[3] if item_id.lower() in ("id", "---") or set(item_id) <= {"-", ":"}: continue if not re.match(r"^[A-Z]{1,3}-\d+", item_id): continue items.append( ChecklistItem( item_id=item_id, criterion=criterion, status=status, evidence=evidence, ) ) return items def load_checklist( eng: Engagement, which: str ) -> Tuple[Optional[Path], List[ChecklistItem]]: path = eng.checklist_path(which) if path is None: return None, [] items = parse_checklist_markdown(path.read_text(encoding="utf-8")) return path, items def checklist_summary(items: Sequence[ChecklistItem]) -> Dict[str, Any]: done = sum(1 for i in items if i.done) total = len(items) return { "done": done, "total": total, "complete": total > 0 and done == total, "pending": [i.item_id for i in items if not i.done], } def set_checklist_item_status(checklist_path: Path, item_id: str, status: str) -> bool: """Update a checklist row status by item id. Returns True if updated.""" text = checklist_path.read_text(encoding="utf-8") lines = text.splitlines() changed = False out: List[str] = [] for line in lines: stripped = line.strip() if stripped.startswith("|"): cells = [c.strip() for c in stripped.strip("|").split("|")] if len(cells) >= 4 and cells[0] == item_id: cells[2] = status line = "| " + " | ".join(cells) + " |" changed = True out.append(line) if changed: checklist_path.write_text( "\n".join(out) + ("\n" if text.endswith("\n") else ""), encoding="utf-8" ) return changed def set_phase( eng: Engagement, new_phase: str, *, force: bool = False, notes: Optional[str] = None, ) -> Engagement: """Transition engagement phase; updates YAML on disk.""" new_phase = new_phase.strip() if new_phase not in VALID_PHASES: raise EngagementError( f"Invalid phase {new_phase!r}; expected one of: {', '.join(VALID_PHASES)}" ) current = eng.phase if not force and new_phase != current: allowed = PHASE_TRANSITIONS.get(current, ()) if new_phase not in allowed: raise EngagementError( f"Illegal phase transition {current!r} → {new_phase!r}. " f"Allowed: {', '.join(allowed) or '(terminal)'}. Use --force to override." ) status = eng.data.setdefault("status", {}) if not isinstance(status, dict): eng.data["status"] = {} status = eng.data["status"] status["phase"] = new_phase if notes is not None: status["notes"] = notes meta = eng.data.setdefault("metadata", {}) if isinstance(meta, dict): meta["updated"] = today_iso() save_engagement(eng) # Keep agent definition phase in sync if present _sync_agent_definition_phase(eng, new_phase) return load_engagement(eng.path) def _sync_agent_definition_phase(eng: Engagement, phase: str) -> None: ad_path = eng.agent_definition_path() if ad_path is None or not ad_path.exists(): return text = ad_path.read_text(encoding="utf-8") if not text.startswith("---"): return parts = text.split("---", 2) if len(parts) < 3: return try: fm = yaml.safe_load(parts[1]) or {} except yaml.YAMLError: return if not isinstance(fm, dict): return fm["phase"] = phase new_fm = yaml.safe_dump(fm, sort_keys=False, allow_unicode=True).strip() ad_path.write_text(f"---\n{new_fm}\n---{parts[2]}", encoding="utf-8") def validate_engagement(eng: Engagement) -> List[str]: """Return list of validation errors (empty if ok).""" errors: List[str] = [] if eng.data.get("apiVersion") != "kaizen.agentic/v1": errors.append("apiVersion should be kaizen.agentic/v1") if eng.phase not in VALID_PHASES: errors.append(f"unknown phase: {eng.phase}") if not eng.role_id: errors.append("spec.role.id missing") if not eng.targets: errors.append("spec.targets is empty") ad = eng.agent_definition_path() if ad is None: errors.append("spec.agent_definition.path missing") elif not ad.exists(): errors.append(f"agent definition missing: {ad}") mem = eng.vault_memory_path() if not mem.exists(): errors.append(f"vault memory missing: {mem}") for which in ("ramp_up", "ramp_down"): path, items = load_checklist(eng, which) if path is None: errors.append(f"checklist missing: {which}") elif not items: errors.append(f"checklist empty or unparseable: {path}") access = eng.path / "access-plan.md" if not access.exists(): errors.append("access-plan.md missing") return errors def build_prepare_bundle( eng: Engagement, repo_root: Optional[Path] = None ) -> Dict[str, Any]: """Assemble an offline session orientation bundle for this engagement.""" root = (repo_root or find_repo_root(eng.path)).resolve() ad_path = eng.agent_definition_path() agent_prompt = ( ad_path.read_text(encoding="utf-8") if ad_path and ad_path.exists() else None ) mem_path = eng.vault_memory_path() memory = mem_path.read_text(encoding="utf-8") if mem_path.exists() else None access_path = eng.path / "access-plan.md" access_plan = ( access_path.read_text(encoding="utf-8") if access_path.exists() else None ) phase = eng.phase phase_instructions = None role_dir = root / DEFAULT_ROLES_DIR / (eng.role_id or "") if phase == "ramp_up" and (role_dir / "ramp-up.md").exists(): phase_instructions = (role_dir / "ramp-up.md").read_text(encoding="utf-8") elif phase == "ramp_down" and (role_dir / "ramp-down.md").exists(): phase_instructions = (role_dir / "ramp-down.md").read_text(encoding="utf-8") protocols: Dict[str, Optional[str]] = {} role_yaml = role_dir / "ROLE.yaml" if role_yaml.exists(): try: role_data = yaml.safe_load(role_yaml.read_text(encoding="utf-8")) or {} except yaml.YAMLError: role_data = {} for p in (role_data.get("spec") or {}).get("protocols") or []: if not isinstance(p, dict): continue slug = p.get("slug") or p.get("path") rel = p.get("path") content = None if rel: full = root / rel if full.exists(): content = full.read_text(encoding="utf-8") protocols[str(slug)] = content _, ramp_up_items = load_checklist(eng, "ramp_up") _, ramp_down_items = load_checklist(eng, "ramp_down") return { "engagement_id": eng.engagement_id, "phase": phase, "role_id": eng.role_id, "targets": eng.targets, "generated": today_iso(), "path": str(eng.path), "agent_prompt": agent_prompt, "agent_prompt_found": agent_prompt is not None, "memory": memory, "access_plan": access_plan, "phase_instructions": phase_instructions, "protocols": {k: (v is not None) for k, v in protocols.items()}, "protocol_bodies": {k: v for k, v in protocols.items() if v}, "checklist_ramp_up": checklist_summary(ramp_up_items), "checklist_ramp_down": checklist_summary(ramp_down_items), "validation_errors": validate_engagement(eng), "session_close": [ ( f"kaizen-agentic engagement close-session {eng.engagement_id} " "--success --duty standard_review --summary '' " "--time --quality <0-1>" ), "Or manually: update vault memory, reports/, commercial/ledger.jsonl", f"kaizen-agentic engagement checklist {eng.engagement_id}", ], "confidentiality": "client_owned", } def render_prepare_markdown(bundle: Dict[str, Any]) -> str: lines = [ f"# Engagement Prepare: {bundle['engagement_id']}", f"Phase: {bundle['phase']}", f"Role: {bundle.get('role_id')}", f"Generated: {bundle['generated']}", f"Path: {bundle['path']}", "", "## Targets", ] for t in bundle.get("targets") or []: lines.append(f"- {t.get('kind', 'target')}: `{t.get('id')}`") lines.append("") errs = bundle.get("validation_errors") or [] if errs: lines.append("## Validation warnings") for e in errs: lines.append(f"- {e}") lines.append("") ru = bundle.get("checklist_ramp_up") or {} rd = bundle.get("checklist_ramp_down") or {} lines.append("## Checklists") lines.append( f"- ramp_up: {ru.get('done', 0)}/{ru.get('total', 0)}" + (f" pending={ru.get('pending')}" if ru.get("pending") else "") ) lines.append( f"- ramp_down: {rd.get('done', 0)}/{rd.get('total', 0)}" + (f" pending={rd.get('pending')}" if rd.get("pending") else "") ) lines.append("") lines.append("## Access Plan") lines.append(bundle.get("access_plan") or "(missing access-plan.md)") lines.append("") if bundle.get("phase_instructions"): lines.append("## Phase Instructions") lines.append(bundle["phase_instructions"]) lines.append("") lines.append("## Agent Definition") if bundle.get("agent_prompt_found"): lines.append(bundle["agent_prompt"]) else: lines.append("(agent definition not found)") lines.append("") lines.append("## Vault Memory") lines.append(bundle.get("memory") or "(no memory yet)") lines.append("") bodies = bundle.get("protocol_bodies") or {} if bodies: lines.append("## Protocols") for slug, body in bodies.items(): lines.append(f"### {slug}") lines.append(body) lines.append("") else: present = bundle.get("protocols") or {} if present: lines.append("## Protocols (paths only)") for slug, ok in present.items(): lines.append(f"- {slug}: {'loaded' if ok else 'missing'}") lines.append("") lines.append("## Session Close") for cmd in bundle.get("session_close") or []: lines.append(f"- {cmd}") lines.append("") lines.append( "_Confidentiality: operational knowledge is client-owned; " "do not copy vault content into supplier public agents._" ) return "\n".join(lines) def scrub_engagement(eng: Engagement) -> Dict[str, Any]: """Heuristic scan of vault + reports for sensitive patterns.""" hits: List[Dict[str, Any]] = [] scan_roots = [ eng.path / "vault", eng.path / "reports", eng.path / "access-plan.md", ] files: List[Path] = [] for root in scan_roots: if root.is_file(): files.append(root) elif root.is_dir(): files.extend( p for p in root.rglob("*") if p.is_file() and p.suffix in {".md", ".txt", ".yml", ".yaml", ".json", ".jsonl"} ) for fpath in files: try: text = fpath.read_text(encoding="utf-8", errors="replace") except OSError: continue for label, pattern in _SCRUB_PATTERNS: for match in pattern.finditer(text): line_no = text[: match.start()].count("\n") + 1 hits.append( { "file": str(fpath.relative_to(eng.path)), "line": line_no, "pattern": label, "snippet": match.group(0)[:40] + ("…" if len(match.group(0)) > 40 else ""), } ) return { "engagement_id": eng.engagement_id, "files_scanned": len(files), "hits": hits, "clean": len(hits) == 0, "note": "Heuristic only — human review required before contribute_lesson", } def export_handoff(eng: Engagement) -> Path: """Ensure handoff pack exists; return path to handoff directory.""" handoff = eng.path / "vault" / "handoff" handoff.mkdir(parents=True, exist_ok=True) readme = handoff / "README.md" if not readme.exists(): readme.write_text( f"# Handoff — {eng.engagement_id}\n\nGenerated: {today_iso()}\n", encoding="utf-8", ) risks = handoff / "risks.md" if not risks.exists(): risks.write_text( "# Outstanding risks\n\n| Risk | Severity | Status |\n|------|----------|--------|\n", encoding="utf-8", ) # Copy baseline files into handoff/baselines if present baselines = eng.path / "vault" / "baselines" if baselines.is_dir(): dest = handoff / "baselines" dest.mkdir(exist_ok=True) for f in baselines.iterdir(): if f.is_file(): shutil.copy2(f, dest / f.name) return handoff def staff_engagement( *, engagement_id: str, role_id: str, client_id: str, target_id: str, target_kind: str = "host", repo_root: Optional[Path] = None, force: bool = False, ) -> Engagement: """Scaffold a new pilot engagement directory from a Role package.""" root = (repo_root or find_repo_root()).resolve() role_dir = root / DEFAULT_ROLES_DIR / role_id if not role_dir.is_dir(): raise EngagementError(f"Role package not found: {role_dir}") dest = root / DEFAULT_PILOTS_DIR / engagement_id if dest.exists() and not force: if (dest / ENGAGEMENT_FILENAME).exists(): return load_engagement(dest) raise EngagementError(f"Directory exists without engagement: {dest}") dest.mkdir(parents=True, exist_ok=True) (dest / "vault" / "baselines").mkdir(parents=True, exist_ok=True) (dest / "vault" / "session-log").mkdir(parents=True, exist_ok=True) (dest / "vault" / "findings").mkdir(parents=True, exist_ok=True) (dest / "vault" / "handoff").mkdir(parents=True, exist_ok=True) (dest / "checklists").mkdir(parents=True, exist_ok=True) (dest / "commercial").mkdir(parents=True, exist_ok=True) (dest / "reports").mkdir(parents=True, exist_ok=True) role_version = "0.1.0" role_yaml = role_dir / "ROLE.yaml" if role_yaml.exists(): try: rd = yaml.safe_load(role_yaml.read_text(encoding="utf-8")) or {} role_version = str( (rd.get("metadata") or {}).get("version") or role_version ) except yaml.YAMLError: pass # Memory from template mem_tmpl = role_dir / "memory-template.md" mem_path = dest / "vault" / "memory.md" if mem_tmpl.exists(): mem_text = mem_tmpl.read_text(encoding="utf-8") mem_text = mem_text.replace("", engagement_id) mem_text = mem_text.replace( "", f"{client_id}-{target_id}" ) mem_text = mem_text.replace("", today_iso()) mem_path.write_text(mem_text, encoding="utf-8") else: mem_path.write_text( f"---\nagent: {role_id}\nengagement_id: {engagement_id}\n" f'last_updated: "{today_iso()}"\nsession_count: 0\n---\n\n# Memory\n', encoding="utf-8", ) # Agent definition from role template + binding frontmatter ad_tmpl = role_dir / "agent-definition.md" agent_name = f"agent-{role_id}.md" if ad_tmpl.exists(): body = ad_tmpl.read_text(encoding="utf-8") # Replace or inject engagement binding frontmatter if body.startswith("---"): parts = body.split("---", 2) fm = yaml.safe_load(parts[1]) if len(parts) >= 3 else {} if not isinstance(fm, dict): fm = {} fm.update( { "engagement_id": engagement_id, "role_id": role_id, "role_version": role_version, "phase": "staffing", "memory_path": f"engagements/pilots/{engagement_id}/vault/memory.md", "targets": [{"kind": target_kind, "id": target_id}], "confidentiality": "client_owned", "access_classes": ["host_observe", "privileged_ops"], "human_approval_for": [ "privileged_ops", "package_upgrade", "firewall_change", "reboot", ], } ) new_fm = yaml.safe_dump(fm, sort_keys=False, allow_unicode=True).strip() body = f"---\n{new_fm}\n---{parts[2] if len(parts) >= 3 else ''}" (dest / agent_name).write_text(body, encoding="utf-8") else: (dest / agent_name).write_text( ( f"---\nname: {role_id}\nengagement_id: {engagement_id}\n" f"phase: staffing\n---\n\n# {role_id}\n" ), encoding="utf-8", ) # Checklists from role ramp docs (status tables) for which, fname, prefix in ( ("ramp_up", "ramp-up-status.md", "RU"), ("ramp_down", "ramp-down-status.md", "RD"), ): role_check = role_dir / ("ramp-up.md" if which == "ramp_up" else "ramp-down.md") dest_check = dest / "checklists" / fname if role_check.exists(): # Extract table rows from role checklist items = parse_checklist_markdown(role_check.read_text(encoding="utf-8")) lines = [ f"# {which.replace('_', '-').title()} status — {engagement_id}", "", f"Source checklist: `roles/{role_id}/{role_check.name}`", "", "| ID | Criterion | Status | Evidence |", "|----|-----------|--------|----------|", ] for it in items: lines.append( f"| {it.item_id} | {it.criterion} | todo | {it.evidence} |" ) if not items: lines.append( f"| {prefix}-01 | Complete {which} | todo | see role checklist |" ) dest_check.write_text("\n".join(lines) + "\n", encoding="utf-8") else: dest_check.write_text( f"# {which}\n\n| ID | Criterion | Status | Evidence |\n" f"|----|-----------|--------|----------|\n" f"| {prefix}-01 | Complete | todo | |\n", encoding="utf-8", ) (dest / "access-plan.md").write_text( f"# Access plan — {engagement_id}\n\n" f"**Target:** {target_kind} `{target_id}`\n\n" "Secrets: never stored in this tree.\n\n" "## Verification log\n\n" "| Date | Result | Notes |\n" "|------|--------|-------|\n" "| _pending_ | | RU-01 |\n", encoding="utf-8", ) (dest / "schedule.yml").write_text( f"engagement_id: {engagement_id}\ntimezone: Europe/Berlin\nentries: []\n", encoding="utf-8", ) (dest / "vault" / "baselines" / f"{target_id}.md").write_text( f"# Baseline — {target_id}\n\n**Status:** pending\n", encoding="utf-8", ) (dest / "commercial" / "ledger.jsonl").write_text("", encoding="utf-8") (dest / "commercial" / "quote.yaml").write_text( f"apiVersion: kaizen.agentic/v1\nkind: KaiQuote\nmetadata:\n" f' engagement_id: {engagement_id}\n created: "{today_iso()}"\n' f" currency: KAI\nspec:\n total_kai: 0\n notes: [scaffold — fill quote]\n", encoding="utf-8", ) (dest / "request.yaml").write_text( f"apiVersion: kaizen.agentic/v1\nkind: EngagementRequest\n" f'metadata:\n id: engreq-{engagement_id}\n created: "{today_iso()}"\n' f"spec:\n client:\n id: {client_id}\n role:\n id: {role_id}\n" f" targets:\n - kind: {target_kind}\n id: {target_id}\n", encoding="utf-8", ) eng_doc = { "apiVersion": "kaizen.agentic/v1", "kind": "Engagement", "metadata": { "id": engagement_id, "created": today_iso(), "updated": today_iso(), }, "status": { "phase": "staffing", "notes": "Scaffolded by kaizen-agentic engagement staff", }, "spec": { "client": {"id": client_id}, "role": {"id": role_id, "version": role_version}, "targets": [{"kind": target_kind, "id": target_id}], "agent_definition": { "path": agent_name, "derived_from": f"roles/{role_id}/agent-definition.md", }, "vault": {"root": "vault/", "memory": "vault/memory.md"}, "schedule": {"path": "schedule.yml"}, "checklists": { "ramp_up": "checklists/ramp-up-status.md", "ramp_down": "checklists/ramp-down-status.md", }, "policy": { "confidentiality": "client_owned", "contribute_metrics": False, "human_approval_for": [ "privileged_ops", "package_upgrade", "firewall_change", "reboot", ], }, "commercial": { "currency": "kai", "ledger_ref": "commercial/ledger.jsonl", "quote_ref": "commercial/quote.yaml", }, }, } (dest / ENGAGEMENT_FILENAME).write_text( yaml.safe_dump(eng_doc, sort_keys=False, allow_unicode=True), encoding="utf-8", ) return load_engagement(dest) def load_quote(eng: Engagement) -> Optional[Dict[str, Any]]: path = eng.quote_path() if path is None: return None try: data = yaml.safe_load(path.read_text(encoding="utf-8")) except yaml.YAMLError as exc: raise EngagementError(f"Invalid quote YAML: {exc}") from exc return data if isinstance(data, dict) else None def append_session_log(eng: Engagement, line: str) -> Path: """Append one session log line to vault memory under ## Session Log.""" sensitive = text_has_sensitive_content(line) if sensitive: raise EngagementError( f"Session summary rejected — looks sensitive ({', '.join(sensitive)}). " "Use a non-secret one-line outcome." ) mem_path = eng.vault_memory_path() mem_path.parent.mkdir(parents=True, exist_ok=True) if not mem_path.exists(): mem_path.write_text( f"---\nagent: {eng.role_id or 'agent'}\n" f"engagement_id: {eng.engagement_id}\n" f'last_updated: "{today_iso()}"\nsession_count: 0\n---\n\n' f"# Memory\n\n## Session Log\n\n", encoding="utf-8", ) text = mem_path.read_text(encoding="utf-8") # Bump session_count / last_updated in frontmatter when present if text.startswith("---"): parts = text.split("---", 2) if len(parts) >= 3: try: fm = yaml.safe_load(parts[1]) or {} except yaml.YAMLError: fm = {} if isinstance(fm, dict): try: fm["session_count"] = int(fm.get("session_count") or 0) + 1 except (TypeError, ValueError): fm["session_count"] = 1 fm["last_updated"] = today_iso() new_fm = yaml.safe_dump(fm, sort_keys=False, allow_unicode=True).strip() text = f"---\n{new_fm}\n---{parts[2]}" log_line = f"- {today_iso()} · {line.strip()}" if "## Session Log" in text: # Append after the heading (and any existing log lines) idx = text.index("## Session Log") after = text[idx + len("## Session Log") :] # Find next ## heading or end next_h = re.search(r"\n## ", after) if next_h: insert_at = idx + len("## Session Log") + next_h.start() text = text[:insert_at].rstrip() + "\n" + log_line + "\n" + text[insert_at:] else: text = text.rstrip() + "\n" + log_line + "\n" else: text = text.rstrip() + "\n\n## Session Log\n\n" + log_line + "\n" mem_path.write_text(text, encoding="utf-8") return mem_path def append_kai_ledger_entry(eng: Engagement, entry: Dict[str, Any]) -> Path: """Append one KaiLedgerEntry JSON line. Rejects sensitive payload fields.""" for key in ("summary", "notes", "detail", "transcript"): val = entry.get(key) if isinstance(val, str): hits = text_has_sensitive_content(val) if hits: raise EngagementError( f"Ledger field {key!r} rejected — sensitive pattern ({', '.join(hits)})" ) # Never allow raw vault dumps forbidden = {"memory", "agent_prompt", "vault", "secret", "password", "token"} for key in entry: if key.lower() in forbidden: raise EngagementError(f"Ledger must not include field {key!r}") path = eng.ledger_path() path.parent.mkdir(parents=True, exist_ok=True) payload = dict(entry) payload.setdefault("apiVersion", "kaizen.agentic/v1") payload.setdefault("kind", "KaiLedgerEntry") payload.setdefault("engagement_id", eng.engagement_id) payload.setdefault("currency", "KAI") payload.setdefault("created_at", utc_now_iso()) with path.open("a", encoding="utf-8") as handle: handle.write(json.dumps(payload, sort_keys=True)) handle.write("\n") return path def write_session_report( eng: Engagement, *, duty: str, summary: str, success: bool, report_name: Optional[str] = None, ) -> Path: """Write a short client-visible duty report under reports/.""" hits = text_has_sensitive_content(summary) if hits: raise EngagementError( f"Report summary rejected — sensitive pattern ({', '.join(hits)})" ) reports = eng.path / "reports" reports.mkdir(parents=True, exist_ok=True) name = report_name or f"{today_iso()}-{duty.replace('_', '-')}.md" path = reports / name target_ids = ", ".join( str(t.get("id")) for t in eng.targets if isinstance(t, dict) and t.get("id") ) body = ( f"# Session report — {eng.engagement_id}\n\n" f"- **Date:** {today_iso()}\n" f"- **Duty:** {duty}\n" f"- **Targets:** {target_ids or 'n/a'}\n" f"- **Outcome:** {'success' if success else 'failure'}\n" f"- **Phase:** {eng.phase}\n\n" f"## Summary\n\n{summary.strip()}\n\n" f"_Billing metadata only in commercial/ledger.jsonl; " f"no secrets in this report._\n" ) path.write_text(body, encoding="utf-8") return path def close_session( eng: Engagement, *, success: bool, duty: str = "standard_review", summary: str, execution_time_s: Optional[float] = None, quality: Optional[float] = None, access_class: Optional[str] = "host_observe", amount_kai: Optional[int] = None, record_metrics: bool = True, record_ledger: bool = True, write_report: bool = True, agent_name: Optional[str] = None, idempotency_key: Optional[str] = None, ) -> Dict[str, Any]: """Session-close: vault log + optional metrics + Kai duty charge + report. Metrics are stored under the engagement path (``.kaizen/metrics//``) so they stay with the client vault tree. Ledger lines never include secrets. """ from .metrics import MetricsStore # local import avoids circular weight agent = agent_name or eng.role_id or "host-operator" tier = capability_tier(eng) result: Dict[str, Any] = { "engagement_id": eng.engagement_id, "duty": duty, "success": success, "agent": agent, "phase": eng.phase, } # 1) Vault session log target_hint = "" if eng.targets: tid = eng.targets[0].get("id") if isinstance(eng.targets[0], dict) else None if tid: target_hint = f"{tid} · " outcome = "ok" if success else "fail" log_line = f"{target_hint}{duty} · {summary.strip()} · {outcome}" mem_path = append_session_log(eng, log_line) result["memory_path"] = str(mem_path) # 2) Report report_path = None if write_report: report_path = write_session_report( eng, duty=duty, summary=summary, success=success ) result["report_path"] = str(report_path) # 3) Metrics (engagement-scoped) if record_metrics: store = MetricsStore(eng.path, agent) payload: Dict[str, Any] = { "success": success, "engagement_id": eng.engagement_id, "duty": duty, "phase": eng.phase, } if execution_time_s is not None: payload["execution_time_s"] = float(execution_time_s) if quality is not None: payload["quality_score"] = float(quality) recorded = store.append(payload, idempotency_key=idempotency_key) result["metrics_recorded"] = recorded result["metrics_path"] = str(store.executions_path) else: result["metrics_recorded"] = False # 4) Kai ledger if record_ledger: kai = compute_duty_kai( duty, tier=tier, access_class=access_class, amount_override=amount_kai, ) entry_id = f"kai-{utc_now_iso().replace(':', '').replace('-', '')}-{duty}" target_id = None if eng.targets and isinstance(eng.targets[0], dict): target_id = eng.targets[0].get("id") ledger_entry: Dict[str, Any] = { "id": entry_id, "type": "duty_charge", "product": duty, "capability_tier": tier, "amount_kai": kai, "session_ref": ( str(Path(result["report_path"]).relative_to(eng.path)) if result.get("report_path") else None ), "metadata": { "target": target_id, "phase": eng.phase, "success": success, "access_class": access_class, }, } if access_class: ledger_entry["access_surcharge_product"] = access_class # Billing account if present client = (eng.data.get("spec") or {}).get("client") or {} if client.get("billing_account"): ledger_entry["account"] = client["billing_account"] ledger_path = append_kai_ledger_entry(eng, ledger_entry) result["ledger_path"] = str(ledger_path) result["amount_kai"] = kai result["ledger_entry_id"] = entry_id else: result["amount_kai"] = 0 return result