status.py python
573 lines 22.0 KB
Raw
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 6 days ago
1 """``overseer status`` command (§K4.4)."""
2
3 from __future__ import annotations
4
5 from argparse import Namespace
6 from pathlib import Path
7
8 from adapters.config import load_config
9 from adapters.errors import ConfigError, ReadError
10 from cli.context import CliContext
11 from cli.digest import sha256_hex
12 from cli.drift import compute_drift
13 from cli.footprint import resolve_footprint
14 from cli.kit_root import kit_version
15 from cli.output import CommandReport
16 from cli.paths import is_within_repo, resolve_config_path, resolve_repo_root
17 from cli.sanitize import format_config_error, sanitize_text
18 from cli.vcs_status import read_vcs_status, vcs_report
19 from cli.version_lock import LockError, lock_path, read_version_lock
20 from cli.commands.route import routing_policy_status
21 from tools.cost_awareness.format import format_cost_awareness_lines
22 from tools.cost_awareness.surface import build_cost_awareness_report, cost_awareness_payload
23 from adapters.factory import create_adapter
24 from tools.footprint_coverage import check_footprint_coverage
25 from tools.footprint_integrity import check_footprint_integrity
26 from tools.governance_freshness import check_governance_freshness
27 from tools.governance_gates import scan_governance_gates
28 from tools.governance_gates.format import format_pending_gate_lines, pending_gates_payload
29 from tools.land_closeout import check_land_closeout, land_closeout_payload
30 from tools.muse_sync import check_muse_sync
31 from tools.substrate_health import check_substrate
32 from tools.verification_evidence_gate import (
33 build_verification_evidence_gate,
34 format_verification_evidence_gate_line,
35 verification_evidence_gate_payload,
36 )
37 from tools.independent_second_reviewer import (
38 build_independent_second_reviewer_gate,
39 format_independent_second_reviewer_gate_line,
40 independent_second_reviewer_gate_payload,
41 )
42 from tools.optional_feature_tips import (
43 build_optional_feature_tips,
44 optional_feature_tips_payload,
45 )
46 from tools.workspace import build_status_report
47
48
49 GOVERNANCE_SYNC_MARKER = "last_governance_sync"
50 IDE_WORKSPACE_HINT = (
51 "ide: open the repo root (folder containing .overseer) so .cursor/rules load"
52 )
53
54
55 def _read_governance_sync_marker(repo_root: Path) -> str | None:
56 """Return timestamp line only (backward-compatible; tip fields live under governance_freshness)."""
57 marker = repo_root / ".overseer" / GOVERNANCE_SYNC_MARKER
58 if not marker.is_file():
59 return None
60 text = marker.read_text(encoding="utf-8")
61 line = text.splitlines()[0].strip() if text.strip() else ""
62 return line or None
63
64
65 def _governance_freshness_payload(report) -> dict:
66 return {
67 "state": report.state,
68 "ok": report.ok,
69 "message": report.message,
70 "remediation": report.remediation,
71 "d1": report.d1,
72 "d2": report.d2,
73 "marker_present": report.marker_present,
74 "marker_r1": report.marker_r1,
75 "actual_r1": report.actual_r1,
76 }
77
78
79 def _compute_footprint_integrity(
80 repo_root: Path,
81 lock,
82 rendered,
83 ) -> tuple[str, list[str]]:
84 """Recompute kit-only digest; report preserved-living paths separately (§K6.4)."""
85 from cli.version_lock import ORIGIN_PRESERVED, compute_lock_digest, entry_origin
86
87 prior = {entry.path: entry for entry in lock.footprint}
88 preserved_living: list[str] = []
89 kit_entries = []
90 for item in rendered:
91 entry = prior.get(item.destination)
92 origin = entry_origin(entry) if entry is not None else "kit"
93 dest = repo_root / item.destination
94 if dest.is_file():
95 content = dest.read_bytes()
96 else:
97 content = b""
98 digest_hex = sha256_hex(content)
99 if origin == ORIGIN_PRESERVED:
100 preserved_living.append(item.destination)
101 continue
102 from cli.version_lock import FootprintEntry, ORIGIN_KIT
103
104 kit_entries.append(
105 FootprintEntry(
106 path=item.destination,
107 source=item.source,
108 sha256=digest_hex,
109 origin=ORIGIN_KIT,
110 )
111 )
112 computed = compute_lock_digest(kit_entries)
113 integrity = "ok" if computed == lock.footprint_digest else "mismatch"
114 return integrity, preserved_living
115
116
117 def _lock_summary(lock) -> dict:
118 return {
119 "lock_version": lock.lock_version,
120 "kit_version": lock.kit_version,
121 "config_version": lock.config_version,
122 "footprint_digest": lock.footprint_digest,
123 "installed_at": lock.installed_at,
124 "synced_at": lock.synced_at,
125 }
126
127
128 def _exit_code_from_conditions(
129 *,
130 config_error: bool,
131 integrity: str | None,
132 drift_status: str | None,
133 use_exit_code: bool,
134 substrate_ok: bool = True,
135 muse_sync_ok: bool = True,
136 footprint_self_integrity_ok: bool = True,
137 footprint_coverage_ok: bool = True,
138 verification_evidence_gate_ok: bool = True,
139 independent_second_reviewer_gate_ok: bool = True,
140 governance_freshness_ok: bool = True,
141 land_closeout_ok: bool = True,
142 workspace_ok: bool = True,
143 ) -> int:
144 """Apply frozen precedence: 2 > 6 > 35 > 3 > 0 (§MR.7.2).
145
146 §KH2.5: ``muse_sync_ok`` folds into the 2 tier. §KH3.5: ``footprint_self_integrity_ok``
147 (declared-but-absent kit-owned files) also folds into the same 2 tier, distinct from and
148 independent of the opt-in ``--check-footprint`` content-digest ``integrity`` tier (6).
149 §GFG.6: ``governance_freshness_ok`` (D1/D2 drift or stale marker) folds into the same 2 tier.
150 §PMHF.6.1: ``land_closeout_ok`` folds into the same 2 tier; ``land_a_in_progress``
151 has ``ok=True`` so waiting for merge never false-fails status.
152 §MR.7.2: workspace relay failure is ``35`` and overrides drift ``3`` / clean ``0`` without
153 overriding config/substrate/muse_sync/footprint-self-integrity/governance-freshness (``2``)
154 or lock integrity (``6``).
155 """
156 if not use_exit_code:
157 return 0
158 if (
159 config_error
160 or not substrate_ok
161 or not muse_sync_ok
162 or not footprint_self_integrity_ok
163 or not footprint_coverage_ok
164 or not verification_evidence_gate_ok
165 or not independent_second_reviewer_gate_ok
166 or not governance_freshness_ok
167 or not land_closeout_ok
168 ):
169 return 2
170 if integrity == "mismatch":
171 return 6
172 if not workspace_ok:
173 return 35
174 if drift_status in {"behind", "ahead"}:
175 return 3
176 return 0
177
178
179 def run_status(args: Namespace, ctx: CliContext) -> int:
180 """Execute ``overseer status``."""
181 report = CommandReport()
182 repo_root = resolve_repo_root(cwd=ctx.cwd, repo_arg=args.repo, command="status")
183 overseer_dir = repo_root / ".overseer"
184
185 if not overseer_dir.is_dir():
186 payload = {
187 "initialized": False,
188 "warnings": [],
189 }
190 if ctx.output.json_mode:
191 ctx.output.emit_json(payload)
192 else:
193 ctx.output.emit("not initialized")
194 return 0
195
196 config_path = resolve_config_path(repo_root, args.config)
197 if not is_within_repo(repo_root, config_path):
198 ctx.output.error("refused: config path outside repo root")
199 return 4
200
201 config_error = False
202 lock_error = False
203
204 try:
205 config = load_config(config_path)
206 except ConfigError as exc:
207 config_error = True
208 ctx.output.error(format_config_error(exc, repo_root))
209 payload = {
210 "initialized": True,
211 "error": str(exc),
212 "warnings": report.warnings,
213 }
214 if ctx.output.json_mode:
215 ctx.output.emit_json(payload)
216 return 2
217
218 substrate = check_substrate(config, repo_root)
219 if not substrate.ok:
220 report.add_warning(f"substrate: {substrate.state} — {substrate.message}")
221 if substrate.remediation:
222 report.add_warning(f"substrate-remediation: {substrate.remediation}")
223
224 gate_scan = None
225 if config.governance_gates.remind and "status" in config.governance_gates.surfaces:
226 gate_scan = scan_governance_gates(config, repo_root)
227 if gate_scan.pending:
228 for line in format_pending_gate_lines(gate_scan):
229 report.add_warning(line)
230
231 lock_file = lock_path(repo_root)
232 lock = None
233 try:
234 lock = read_version_lock(lock_file)
235 except LockError as exc:
236 lock_error = True
237 report.add_warning(str(exc))
238
239 rendered: list = []
240 integrity: str | None = None
241 preserved_living: list[str] = []
242 if lock is not None:
243 try:
244 rendered = resolve_footprint(config, kit=ctx.kit)
245 except ConfigError as exc:
246 config_error = True
247 ctx.output.error(format_config_error(exc, repo_root))
248 return 2
249
250 if args.check_footprint:
251 integrity, preserved_living = _compute_footprint_integrity(repo_root, lock, rendered)
252 if integrity == "mismatch":
253 report.add_warning("footprint_integrity: mismatch")
254 for path in preserved_living:
255 report.add_warning(f"preserved-living: {path}")
256
257 footprint_self_integrity = check_footprint_integrity(repo_root, lock=lock)
258 if not footprint_self_integrity.ok:
259 report.add_warning(
260 f"footprint_self_integrity: {footprint_self_integrity.state} — "
261 f"{footprint_self_integrity.message}"
262 )
263 if footprint_self_integrity.remediation:
264 report.add_warning(
265 f"footprint_self_integrity-remediation: {footprint_self_integrity.remediation}"
266 )
267
268 footprint_coverage = check_footprint_coverage(
269 repo_root,
270 config,
271 lock=lock,
272 rendered=rendered if rendered else None,
273 kit=ctx.kit,
274 )
275 if not footprint_coverage.ok:
276 report.add_warning(
277 f"footprint_coverage: {footprint_coverage.state} — {footprint_coverage.message}"
278 )
279 if footprint_coverage.remediation:
280 report.add_warning(
281 f"footprint_coverage-remediation: {footprint_coverage.remediation}"
282 )
283
284 drift = compute_drift(
285 cli_version=kit_version(),
286 lock=lock,
287 rendered=rendered,
288 repo_root=repo_root,
289 )
290 if drift["status"] in {"behind", "ahead"}:
291 report.add_warning(f"drift: {drift['status']}")
292
293 vcs_result = read_vcs_status(config, repo_root, ctx.runner)
294 if isinstance(vcs_result, ReadError):
295 ctx.output.error(sanitize_text(str(vcs_result), repo_root))
296 payload = {
297 "initialized": True,
298 "substrate": _substrate_payload(substrate),
299 "vcs": {
300 "error": sanitize_text(str(vcs_result), repo_root),
301 "command": sanitize_text(vcs_result.command, repo_root),
302 },
303 "warnings": report.warnings,
304 }
305 if ctx.output.json_mode:
306 ctx.output.emit_json(payload)
307 return 2
308
309 muse_sync = check_muse_sync(config, vcs_result)
310 if not muse_sync.ok:
311 report.add_warning(f"muse_sync: {muse_sync.state} — {muse_sync.message}")
312 if muse_sync.remediation:
313 report.add_warning(f"muse_sync-remediation: {muse_sync.remediation}")
314
315 governance_freshness = check_governance_freshness(
316 config,
317 repo_root,
318 adapter=create_adapter(config, repo_root, runner=ctx.runner),
319 runner=ctx.runner,
320 )
321 if not governance_freshness.ok:
322 report.add_warning(
323 f"governance_freshness: {governance_freshness.state} — {governance_freshness.message}"
324 )
325 if governance_freshness.remediation:
326 report.add_warning(
327 f"governance_freshness-remediation: {governance_freshness.remediation}"
328 )
329
330 # §PMHF.6.1: always-on land closeout probe; never invokes gh from status.
331 land_closeout = check_land_closeout(
332 config,
333 repo_root,
334 runner=ctx.runner,
335 probe_merged_pr=False,
336 freshness=governance_freshness,
337 )
338 if not land_closeout.ok:
339 report.add_warning(f"land_closeout: {land_closeout.state} — {land_closeout.message}")
340 if land_closeout.remediation:
341 report.add_warning(f"land_closeout-remediation: {land_closeout.remediation}")
342
343 model_routing_status = routing_policy_status(config, repo_root, kit_root=ctx.kit)
344 if model_routing_status.get("enabled") and not model_routing_status.get("valid"):
345 violation = model_routing_status.get("violation") or "invalid"
346 report.add_warning(f"model_routing: invalid — {violation}")
347
348 cost_report = None
349 if config.cost_awareness.enabled and "status" in config.cost_awareness.surfaces:
350 cost_report = build_cost_awareness_report(config, repo_root, kit_root=ctx.kit)
351 if cost_report.invalid:
352 violation = cost_report.violation or "invalid"
353 report.add_warning(f"cost_awareness: invalid — {violation}")
354 elif cost_report.exit_code == 31:
355 ctx.output.error(cost_report.violation or "routing policy file missing or unreadable")
356 payload = {
357 "initialized": True,
358 "cost_awareness": cost_awareness_payload(cost_report),
359 "warnings": report.warnings,
360 }
361 if ctx.output.json_mode:
362 ctx.output.emit_json(payload)
363 return 31
364 else:
365 for line in format_cost_awareness_lines(cost_report):
366 report.add_warning(line)
367
368 handover_path = repo_root / config.repo.root_relative_docs / config.docs.handover
369 roadmap_path = repo_root / config.repo.root_relative_docs / config.docs.roadmap
370 handover_text = handover_path.read_text(encoding="utf-8") if handover_path.is_file() else None
371 roadmap_text = roadmap_path.read_text(encoding="utf-8") if roadmap_path.is_file() else None
372 verification_evidence_gate = build_verification_evidence_gate(
373 config,
374 repo_root,
375 handover_text=handover_text,
376 roadmap_text=roadmap_text,
377 )
378 ve_line = format_verification_evidence_gate_line(verification_evidence_gate)
379 if ve_line:
380 report.add_warning(ve_line)
381
382 independent_second_reviewer_gate = build_independent_second_reviewer_gate(
383 config,
384 repo_root,
385 handover_text=handover_text,
386 roadmap_text=roadmap_text,
387 )
388 isr_line = format_independent_second_reviewer_gate_line(independent_second_reviewer_gate)
389 if isr_line:
390 report.add_warning(isr_line)
391
392 optional_feature_tips = build_optional_feature_tips(config)
393
394 payload = {
395 "initialized": True,
396 "kit_version": kit_version(),
397 "substrate": _substrate_payload(substrate),
398 "muse_sync": _muse_sync_payload(muse_sync),
399 "footprint_self_integrity": _footprint_self_integrity_payload(footprint_self_integrity),
400 "footprint_coverage": _footprint_coverage_payload(footprint_coverage),
401 "ide_workspace_hint": IDE_WORKSPACE_HINT,
402 "optional_feature_tips": optional_feature_tips_payload(optional_feature_tips),
403 "governance_freshness": _governance_freshness_payload(governance_freshness),
404 "land_closeout": land_closeout_payload(land_closeout),
405 "lock": _lock_summary(lock) if lock else None,
406 "drift": drift,
407 "footprint_integrity": integrity,
408 "preserved_living": preserved_living if args.check_footprint else [],
409 "vcs": vcs_report(vcs_result, config),
410 "last_governance_sync": _read_governance_sync_marker(repo_root),
411 "governance_gates": pending_gates_payload(gate_scan)
412 if gate_scan is not None
413 else {"enabled": False, "suppressed": False, "active_phases": [], "pending": []},
414 "model_routing": model_routing_status,
415 "warnings": report.warnings,
416 }
417 if cost_report is not None:
418 payload["cost_awareness"] = cost_awareness_payload(cost_report)
419 ve_payload = verification_evidence_gate_payload(verification_evidence_gate)
420 if ve_payload is not None:
421 payload["verification_evidence_gate"] = ve_payload
422 isr_payload = independent_second_reviewer_gate_payload(independent_second_reviewer_gate)
423 if isr_payload is not None:
424 payload["independent_second_reviewer_gate"] = isr_payload
425 if lock_error:
426 payload["lock_error"] = True
427
428 workspace_report = None
429 workspace_ok = True
430 if getattr(args, "workspace", False):
431 workspace_report = build_status_report(config, repo_root)
432 payload["workspace"] = workspace_report.to_json()
433 # S9: single-repo green must not imply workspace.ok; workspace failure
434 # contributes exit 35 only when --exit-code is set.
435 if workspace_report.configured and not workspace_report.ok:
436 workspace_ok = False
437 report.add_warning(f"workspace: {workspace_report.state}")
438
439 exit_code = _exit_code_from_conditions(
440 config_error=config_error,
441 integrity=integrity if args.check_footprint else None,
442 drift_status=drift["status"],
443 use_exit_code=args.exit_code,
444 substrate_ok=substrate.ok,
445 muse_sync_ok=muse_sync.ok,
446 footprint_self_integrity_ok=footprint_self_integrity.ok,
447 footprint_coverage_ok=footprint_coverage.ok,
448 verification_evidence_gate_ok=verification_evidence_gate.ok,
449 independent_second_reviewer_gate_ok=independent_second_reviewer_gate.ok,
450 governance_freshness_ok=governance_freshness.ok,
451 land_closeout_ok=land_closeout.ok,
452 workspace_ok=workspace_ok,
453 )
454 if lock_error and args.exit_code:
455 exit_code = 6 if exit_code in {0, 3, 35} else max(exit_code, 6)
456
457 if ctx.output.json_mode:
458 ctx.output.emit_json(payload)
459 else:
460 ctx.output.emit(f"kit_version: {payload['kit_version']}")
461 if not substrate.ok:
462 ctx.output.emit(f"substrate: {substrate.state} — {substrate.message}")
463 if substrate.remediation:
464 ctx.output.emit(f"substrate-remediation: {substrate.remediation}")
465 if not muse_sync.ok:
466 ctx.output.emit(f"muse_sync: {muse_sync.state} — {muse_sync.message}")
467 if muse_sync.remediation:
468 ctx.output.emit(f"muse_sync-remediation: {muse_sync.remediation}")
469 if not governance_freshness.ok:
470 ctx.output.emit(
471 f"governance_freshness: {governance_freshness.state} — "
472 f"{governance_freshness.message}"
473 )
474 if governance_freshness.remediation:
475 ctx.output.emit(
476 f"governance_freshness-remediation: {governance_freshness.remediation}"
477 )
478 if not land_closeout.ok:
479 ctx.output.emit(
480 f"land_closeout: {land_closeout.state} — {land_closeout.message}"
481 )
482 if land_closeout.remediation:
483 ctx.output.emit(f"land_closeout-remediation: {land_closeout.remediation}")
484 if lock:
485 ctx.output.emit(f"lock kit_version: {lock.kit_version}")
486 ctx.output.emit(f"drift: {drift['status']}")
487 if integrity:
488 ctx.output.emit(f"footprint_integrity: {integrity}")
489 if model_routing_status.get("enabled"):
490 if model_routing_status.get("valid"):
491 ctx.output.emit("model_routing: valid")
492 else:
493 violation = model_routing_status.get("violation") or "invalid"
494 ctx.output.emit(f"model_routing: invalid — {violation}")
495 if gate_scan is not None and gate_scan.pending:
496 ctx.output.emit("")
497 for line in format_pending_gate_lines(gate_scan):
498 ctx.output.emit(line)
499 if cost_report is not None and config.cost_awareness.enabled:
500 if cost_report.invalid:
501 violation = cost_report.violation or "invalid"
502 ctx.output.emit(f"cost_awareness: invalid — {violation}")
503 else:
504 for line in format_cost_awareness_lines(cost_report):
505 ctx.output.emit(line)
506 if not footprint_coverage.ok:
507 ctx.output.emit(
508 f"footprint_coverage: {footprint_coverage.state} — {footprint_coverage.message}"
509 )
510 if footprint_coverage.remediation:
511 ctx.output.emit(
512 f"footprint_coverage-remediation: {footprint_coverage.remediation}"
513 )
514 ctx.output.emit(IDE_WORKSPACE_HINT)
515 for line in optional_feature_tips:
516 ctx.output.emit(line)
517 if ve_line:
518 ctx.output.emit(ve_line)
519 ctx.output.emit(f"vcs.regime: {vcs_result.regime}")
520 ctx.output.emit(f"vcs.branch: {vcs_result.branch}")
521 ctx.output.emit(f"vcs.dirty: {vcs_result.dirty}")
522 if workspace_report is not None:
523 ctx.output.emit(f"workspace.configured: {str(workspace_report.configured).lower()}")
524 ctx.output.emit(f"workspace.ok: {str(workspace_report.ok).lower()}")
525 ctx.output.emit(f"workspace.state: {workspace_report.state}")
526 if workspace_report.authoritative_handover:
527 ctx.output.emit(
528 f"authoritative_handover: {workspace_report.authoritative_handover}"
529 )
530 for member in workspace_report.members:
531 base = member.get("handover_basename") or "?"
532 ctx.output.emit(f"workspace.member {member['id']}: handover={base}")
533
534 return exit_code
535
536
537 def _substrate_payload(substrate) -> dict:
538 return {
539 "state": substrate.state,
540 "ok": substrate.ok,
541 "missing": list(substrate.missing),
542 "remediation": substrate.remediation,
543 "message": substrate.message,
544 }
545
546
547 def _muse_sync_payload(muse_sync) -> dict:
548 return {
549 "state": muse_sync.state,
550 "ok": muse_sync.ok,
551 "remediation": muse_sync.remediation,
552 "message": muse_sync.message,
553 }
554
555
556 def _footprint_self_integrity_payload(report) -> dict:
557 return {
558 "state": report.state,
559 "ok": report.ok,
560 "missing": list(report.missing),
561 "remediation": report.remediation,
562 "message": report.message,
563 }
564
565
566 def _footprint_coverage_payload(report) -> dict:
567 return {
568 "state": report.state,
569 "ok": report.ok,
570 "missing": list(report.missing),
571 "remediation": report.remediation,
572 "message": report.message,
573 }
File History 2 commits
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 6 days ago
sha256:4671b7f787ddbe63ced31c895b688c77ab495653b65a730b423329f26b3c1439 feat: K1-P1 complete — agent provenance, build-verification… Sonnet 4.6 patch 58 days ago