Coverage for src/keel/contracts.py: 100%

295 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-18 12:05 +0000

1"""Structured command contracts for adapters and parity tests. 

2 

3The contract is intentionally plain JSON-compatible data. Agent adapters can read it before 

4mutating work starts, compare required capabilities with the current runtime, and execute the 

5same command graph without re-deriving keel behavior from prose. 

6""" 

7 

8from __future__ import annotations 

9 

10import re 

11from dataclasses import asdict 

12from pathlib import Path 

13from typing import Any 

14 

15from . import ( 

16 artifacts, 

17 capture, 

18 checkpoint, 

19 closure, 

20 consent, 

21 evidence, 

22 gates, 

23 github_transport, 

24 install, 

25 intake, 

26 ledger, 

27 lock, 

28 model, 

29 orchestrator, 

30 provenance, 

31 runcontrols, 

32 runtime, 

33 stepverifier, 

34 workblock, 

35 workcreation, 

36) 

37from . import config as cfg 

38from . import ship as ship_decisions 

39from .extensions import Extension 

40from .project_commands import get_project_command, list_project_commands 

41 

42SCHEMA_VERSION = "keel.command-contract.v1" 

43 

44_COMPOUND_OVERRIDES: dict[str, str] = { 

45 "s4": "compound", 

46 "s7": "compound", 

47 "s9": "compound", 

48 "s11": "compound", 

49} 

50 

51_STEP_RE = re.compile( 

52 r"^#{2,3}\s+(?P<id>(?:Step\s+[0-9A-Za-z.]+|s\d+))\s*(?:[\u2014-]\s*)?" 

53 r"(?P<name>.*)$" 

54) 

55 

56_BASE_SIDE_EFFECTS: dict[str, tuple[str, ...]] = { 

57 "ship": ("git_worktree", "git_branch", "file_edit", "git_push", "pull_request", "comments", 

58 "reviews", "merge", 

59 "issue_close", "capture"), 

60 "pr-loop": ("file_edit", "git_commit", "git_push", "comments", "reviews", "check_runs"), 

61 "review-cycle": ("file_edit", "comments", "reviews", "git_commit", "git_push"), 

62 "morning": ("issue_read", "pr_read", "report_write"), 

63 "wrap": ("git_commit", "git_push", "pull_request", "session_recap"), 

64 "work-block": ("git_branch", "git_push", "pull_request", "comments", "reviews", "merge", 

65 "deferral_queue", "session_report"), 

66 "overnight": ("git_branch", "git_push", "pull_request", "comments", "reviews", "merge", 

67 "deferral_queue", "session_report"), 

68 "implement": ("git_worktree", "git_branch", "file_edit", "git_commit", "git_push", 

69 "pull_request", "comments"), 

70 "ci-check": ("check_runs",), 

71 "triage": ("labels", "comments"), 

72 "stale-prs": ("comments", "git_checkout", "git_push"), 

73 "regression": ("git_worktree", "issue_write", "labels"), 

74 "review-all-day": ("issue_write", "labels"), 

75 "coverage": ("git_worktree", "git_checkout", "comments", "labels", "issue_write"), 

76 "deps-audit": ("comments", "issue_write"), 

77 "flake-audit": ("issue_write", "comments"), 

78} 

79 

80_DEFAULT_FEEDBACK_WORKFLOWS: dict[str, dict[str, Any]] = { 

81 "pr-loop": { 

82 "posting_mode": "summary", 

83 "posting_owner": "orchestrator", 

84 "reviewer_isolation": { 

85 "shared_with_ship": True, 

86 "codename_prefix": "PR-LOOP", 

87 "no_cross_reading": True, 

88 }, 

89 "inputs": { 

90 "auto_detect_current_branch": True, 

91 "explicit_pr_targets": True, 

92 "reads_review_comments": True, 

93 "reads_issue_conversation_comments": True, 

94 }, 

95 "ci": { 

96 "recheck_after_push": True, 

97 "green_required_to_exit": True, 

98 "degrade_when_logs_unavailable": True, 

99 }, 

100 "fix_loop": { 

101 "budget": 3, 

102 "self_review_before_push": True, 

103 "reviewer_fanout_after_each_push": True, 

104 }, 

105 "completion": { 

106 "marker": None, 

107 "merge": "handoff", 

108 "summary_comment": True, 

109 }, 

110 }, 

111 "review-cycle": { 

112 "posting_mode": "inline", 

113 "posting_owner": "orchestrator", 

114 "reviewer_isolation": { 

115 "shared_with_ship": True, 

116 "codename_prefix": "REVIEW-CYCLE", 

117 "no_cross_reading": True, 

118 }, 

119 "inputs": { 

120 "multi_pr": True, 

121 "sequential_pr_processing": True, 

122 }, 

123 "review": { 

124 "parallel_reviewers_within_pr": True, 

125 "partial_reviewer_failures_degrade": True, 

126 "severity_histogram_source_of_truth": True, 

127 }, 

128 "fix_loop": { 

129 "budget": 3, 

130 "enabled": True, 

131 }, 

132 "completion": { 

133 "marker": "review-cycle-complete", 

134 "marker_after_summary": True, 

135 "merge": "never", 

136 "formal_approval": "never", 

137 }, 

138 }, 

139} 

140 

141_REPORTING_COMMANDS = {"coverage", "deps-audit", "flake-audit"} 

142 

143 

144def available_commands() -> tuple[str, ...]: 

145 """Every packaged adapter command that can expose a structured contract.""" 

146 return tuple(name.removesuffix(".md") for name in install.adapter_names()) 

147 

148 

149def command_graph(command: str, *, profile: str = "standard") -> list[dict[str, Any]]: 

150 """Return the command's step graph as JSON-compatible records. 

151 

152 ``ship`` uses the fixed keel backbone as the canonical graph. When the ``compound`` 

153 profile is selected, the s4/s7/s9/s11 steps are marked as compound overrides. Other 

154 commands expose their adapter step headings, so adapters can still reason about their 

155 command-local sequence without parsing Markdown themselves. 

156 """ 

157 if command == "ship": 

158 compound = profile == "compound" 

159 return [ 

160 { 

161 "step_id": step.id, 

162 "step_name": step.name, 

163 "agentic": step.agentic, 

164 "slot": step.slot, 

165 "source": "backbone", 

166 "profile_step": _COMPOUND_OVERRIDES.get(step.id, "standard") 

167 if compound else "standard", 

168 } 

169 for step in model.BACKBONE 

170 ] 

171 steps = _adapter_steps(command) 

172 return steps if steps else [] 

173 

174 

175def build_command_contract( 

176 *, 

177 command: str, 

178 profile: str = "standard", 

179 config: cfg.ProjectConfig, 

180 loaded: dict[str, list[Extension]], 

181 plan: tuple[orchestrator.PlanItem, ...], 

182 requirement: runtime.CapabilityRequirement, 

183 evaluation: runtime.CapabilityEvaluation, 

184 transport: github_transport.GitHubTransport, 

185 extension_problems: tuple[str, ...] = (), 

186 dry_run: bool = True, 

187 approved_consent_scopes: tuple[str, ...] = (), 

188 consent_approval_source: str = "flag", 

189 consent_mode: str = "explicit", 

190 operator: str | None = None, 

191 target: str | None = None, 

192 reviewer_override: int | None = None, 

193 review_tier: int | None = None, 

194 review_comments: str = "inline", 

195 jury: bool = False, 

196 no_jury: bool = False, 

197 jury_advisory: bool = False, 

198 issue_title: str | None = None, 

199 issue_body: str | None = None, 

200 issue_labels: tuple[str, ...] = (), 

201) -> dict[str, Any]: 

202 """Build the stable adapter contract shared by ``plan --json`` and dry-run commands.""" 

203 declared_side_effects = command_side_effects(command, config, requirement, loaded) 

204 graph = command_graph(command, profile=profile) 

205 if not graph and (project_command := get_project_command(config, command)): 

206 graph = [{ 

207 "step_id": f"project-command:{project_command.name}", 

208 "step_name": project_command.name, 

209 "agentic": bool(project_command.agent_role), 

210 "slot": None, 

211 "source": "project_command", 

212 }] 

213 contract = { 

214 "schema_version": SCHEMA_VERSION, 

215 "command": command, 

216 "mode": "dry-run" if dry_run else "live", 

217 "dry_run": dry_run, 

218 "no_mutations": dry_run, 

219 "project": project_as_dict(config), 

220 "workflow_profile": workflow_profile(command, profile=profile), 

221 "graph": graph, 

222 "backbone_plan": orchestrator.plan_as_dict(plan), 

223 "gates": [gate_as_dict(spec) for spec in gates.plan_gates(config, loaded)], 

224 "project_commands": [command.as_dict() for command in list_project_commands(config)], 

225 "extension_hooks": extension_hooks_as_dict(config, loaded), 

226 "extension_problems": list(extension_problems), 

227 "required_capabilities": list(requirement.required), 

228 "optional_capabilities": list(requirement.optional), 

229 "capabilities": evaluation.as_dict(), 

230 "github_transport": transport.as_dict(), 

231 "checkpoint": checkpoint.checkpoint_contract_as_dict(config), 

232 "capture": capture.contract_as_dict(config), 

233 "run_ledger": ledger.ledger_contract_as_dict(config), 

234 "resource_claims": lock.contract_as_dict(), 

235 "side_effects": { 

236 "declared": list(declared_side_effects), 

237 "mutates_in_dry_run": False, 

238 }, 

239 "operator_consent": consent.build_consent_contract( 

240 command=command, 

241 side_effects=declared_side_effects, 

242 dry_run=dry_run, 

243 approved_scopes=approved_consent_scopes, 

244 approval_source=consent_approval_source, 

245 mode=consent_mode, 

246 operator=operator, 

247 target=target, 

248 ), 

249 "agent_output_provenance": provenance.contract_as_dict(), 

250 } 

251 if command == "morning": 

252 contract["morning_contract"] = morning_contract_as_dict( 

253 config=config, 

254 evaluation=evaluation, 

255 transport=transport, 

256 ) 

257 if command in _REPORTING_COMMANDS: 

258 contract["reporting_contract"] = reporting_contract_as_dict( 

259 command=command, 

260 config=config, 

261 transport=transport, 

262 ) 

263 if command in {"wrap", "work-block", "overnight"}: 

264 contract["session_contract"] = session_contract_as_dict( 

265 command=command, 

266 config=config, 

267 transport=transport, 

268 ) 

269 if command in {"regression", "review-all-day"}: 

270 contract["scan_contract"] = scan_contract_as_dict( 

271 command=command, 

272 config=config, 

273 transport=transport, 

274 ) 

275 if command == "triage": 

276 contract["triage_contract"] = { 

277 "scope": "label-only-advisory", 

278 "one_comment_per_issue": True, 

279 "renderer": "keel.artifacts.render_triage_audit", 

280 "marker": artifacts.TRIAGE_AUDIT_MARKER, 

281 } 

282 if command in {"ship", "pr-loop", "review-cycle", "work-block", "overnight"}: 

283 contract["review_merge_contract"] = ship_decisions.resolve_review_contract( 

284 tier=review_tier, 

285 reviewer_override=reviewer_override, 

286 review_comments=review_comments, 

287 gates=config.gates, 

288 policy_pack=config.policy_pack, 

289 jury=jury, 

290 no_jury=no_jury, 

291 jury_advisory=jury_advisory, 

292 ) 

293 if command == "ship": 

294 contract["evidence"] = evidence.contract_as_dict( 

295 contract["review_merge_contract"], 

296 dry_run=dry_run, 

297 ) 

298 contract["step_verification"] = stepverifier.contract_as_dict( 

299 contract["review_merge_contract"], 

300 dry_run=dry_run, 

301 ) 

302 contract["run_controls"] = runcontrols.contract_as_dict() 

303 if command == "ship": 

304 contract["closure_comment"] = closure.contract_as_dict() 

305 contract["artifact_renderers"] = artifacts.contract_as_dict() 

306 if command in {"ship", "implement", "overnight"}: 

307 contract["issue_intake"] = intake.assess_issue( 

308 title=issue_title, 

309 body=issue_body, 

310 labels=issue_labels, 

311 ) 

312 if command in {"pr-loop", "review-cycle"}: 

313 contract["feedback_workflow"] = feedback_workflow_as_dict(config, command) 

314 return contract 

315 

316 

317def workflow_profile(command: str, *, profile: str = "standard") -> dict[str, Any]: 

318 """First-class workflow profile metadata for command variants. 

319 

320 ``ship`` carries the ``standard`` profile by default; selecting the ``compound`` 

321 profile swaps the s4/s7/s9/s11 steps to compound step overrides without forking the 

322 backbone. 

323 """ 

324 if command == "ship" and profile == "compound": 

325 return { 

326 "name": "ship", 

327 "profile": "compound", 

328 "inherits": "ship", 

329 "first_class_variant": True, 

330 "shared_primitives": [ 

331 "select", 

332 "branch", 

333 "worktree", 

334 "guard", 

335 "classify", 

336 "ci", 

337 "test", 

338 "merge_window", 

339 "merge_lock", 

340 "merge", 

341 "capture_marker", 

342 "close", 

343 ], 

344 "step_overrides": { 

345 "s4": { 

346 "step": "implement", 

347 "mode": "compound", 

348 "reason": "compound implement and PR-quality pass", 

349 }, 

350 "s7": { 

351 "step": "review", 

352 "mode": "compound", 

353 "reason": "persona and diff-aware reviewer fan-out", 

354 }, 

355 "s9": { 

356 "step": "fixloop", 

357 "mode": "compound", 

358 "reason": "structured PR-feedback resolution", 

359 }, 

360 "s11": { 

361 "step": "capture", 

362 "mode": "compound", 

363 "reason": "durable-learning capture", 

364 }, 

365 }, 

366 } 

367 if command == "pr-loop": 

368 return { 

369 "name": "pr-loop", 

370 "profile": "feedback-loop", 

371 "inherits": "ship.s6-s9", 

372 "first_class_variant": True, 

373 "shared_primitives": [ 

374 "linked_worktree_preflight", 

375 "github_transport", 

376 "reviewer_isolation", 

377 "ci_recheck", 

378 "fixloop", 

379 "summary_comment", 

380 "operator_consent", 

381 ], 

382 "step_overrides": { 

383 "merge": { 

384 "mode": "handoff", 

385 "reason": "pr-loop exits after feedback and CI are satisfied.", 

386 }, 

387 }, 

388 } 

389 if command == "review-cycle": 

390 return { 

391 "name": "review-cycle", 

392 "profile": "review-feedback", 

393 "inherits": "ship.s7-s9", 

394 "first_class_variant": True, 

395 "shared_primitives": [ 

396 "multi_pr_targets", 

397 "reviewer_isolation", 

398 "posting_mode", 

399 "severity_histogram", 

400 "fixloop", 

401 "completion_marker", 

402 "operator_consent", 

403 ], 

404 "step_overrides": { 

405 "merge": { 

406 "mode": "never", 

407 "reason": "review-cycle never merges or posts formal approval.", 

408 }, 

409 }, 

410 } 

411 if command == "implement": 

412 return { 

413 "name": "implement", 

414 "profile": "standalone-step", 

415 "inherits": "ship.s4", 

416 "first_class_variant": True, 

417 "shared_primitives": [ 

418 "issue_target", 

419 "branch", 

420 "worktree", 

421 "implementer_routing", 

422 "operator_consent", 

423 "handoff", 

424 ], 

425 "step_overrides": {}, 

426 } 

427 if command == "ci-check": 

428 return { 

429 "name": "ci-check", 

430 "profile": "standalone-diagnostic", 

431 "inherits": None, 

432 "first_class_variant": True, 

433 "shared_primitives": [ 

434 "github_transport", 

435 "check_runs", 

436 "latest_run_context", 

437 "log_diagnostics", 

438 "read_only", 

439 "routing_recommendation", 

440 ], 

441 "step_overrides": {}, 

442 } 

443 if command == "morning": 

444 return { 

445 "name": "morning", 

446 "profile": "daily-brief", 

447 "inherits": None, 

448 "first_class_variant": True, 

449 "shared_primitives": [ 

450 "date_window", 

451 "deferral_queue", 

452 "shipped_since", 

453 "github_summary", 

454 "health_providers", 

455 "priority_sources", 

456 "ranked_focus", 

457 "report_output", 

458 ], 

459 "step_overrides": {}, 

460 } 

461 if command == "wrap": 

462 return { 

463 "name": "wrap", 

464 "profile": "session-wrap", 

465 "inherits": None, 

466 "first_class_variant": True, 

467 "shared_primitives": [ 

468 "linked_worktree_preflight", 

469 "base_branch_guard", 

470 "configured_gates", 

471 "conventional_commit", 

472 "ready_pr_create", 

473 "session_recap", 

474 "deferral_queue", 

475 "operator_consent", 

476 ], 

477 "step_overrides": {}, 

478 } 

479 if command == "overnight": 

480 return { 

481 "name": "overnight", 

482 "profile": "session-overnight", 

483 "inherits": "ship", 

484 "first_class_variant": True, 

485 "shared_primitives": [ 

486 "work_block", 

487 "merge_window", 

488 "ship_handoff", 

489 "priority_queue", 

490 "per_issue_worktree", 

491 "no_night_merge", 

492 "blocker_policy", 

493 "session_report", 

494 "deferral_queue", 

495 "stop_conditions", 

496 "operator_consent", 

497 ], 

498 "step_overrides": {}, 

499 } 

500 if command == "work-block": 

501 return { 

502 "name": "work-block", 

503 "profile": "session-work-block-daytime", 

504 "inherits": "ship", 

505 "first_class_variant": True, 

506 "shared_primitives": [ 

507 "work_block", 

508 "queue_snapshot", 

509 "readiness_refresh", 

510 "ship_handoff", 

511 "per_issue_worktree", 

512 "operator_between_item_control", 

513 "progress_snapshot", 

514 "session_report", 

515 "deferral_queue", 

516 "stop_conditions", 

517 "operator_consent", 

518 ], 

519 "step_overrides": {}, 

520 } 

521 if command in _REPORTING_COMMANDS: 

522 return { 

523 "name": command, 

524 "profile": "reporting", 

525 "inherits": None, 

526 "first_class_variant": True, 

527 "shared_primitives": [ 

528 "project_policy", 

529 "github_transport", 

530 "codename_anchor", 

531 "dedupe", 

532 "dry_run_no_mutations", 

533 "ship_handoff", 

534 "operator_consent", 

535 ], 

536 "step_overrides": {}, 

537 } 

538 if command == "regression": 

539 return { 

540 "name": "regression", 

541 "profile": "scan-and-file", 

542 "inherits": None, 

543 "first_class_variant": True, 

544 "shared_primitives": [ 

545 "canonical_base_scan", 

546 "clean_tree_preflight", 

547 "read_only_worktree", 

548 "area_fanout", 

549 "reviewer_isolation", 

550 "confidence_filter", 

551 "dedupe", 

552 "issue_lock", 

553 "issue_create", 

554 "ship_handoff", 

555 "final_report", 

556 "operator_consent", 

557 ], 

558 "step_overrides": {}, 

559 } 

560 if command == "review-all-day": 

561 return { 

562 "name": "review-all-day", 

563 "profile": "time-window-scan", 

564 "inherits": None, 

565 "first_class_variant": True, 

566 "shared_primitives": [ 

567 "merge_window_span", 

568 "remote_ref_scope", 

569 "batch_or_fanout", 

570 "reviewer_isolation", 

571 "diff_truncation", 

572 "finding_filter", 

573 "dedupe", 

574 "issue_prefix", 

575 "issue_create", 

576 "final_report", 

577 "operator_consent", 

578 ], 

579 "step_overrides": {}, 

580 } 

581 if command == "ship": 

582 return { 

583 "name": "ship", 

584 "profile": "standard", 

585 "inherits": None, 

586 "first_class_variant": True, 

587 "shared_primitives": [ 

588 "select", 

589 "branch", 

590 "guard", 

591 "implement", 

592 "classify", 

593 "ci", 

594 "review", 

595 "test", 

596 "fixloop", 

597 "merge", 

598 "capture", 

599 "close", 

600 ], 

601 "step_overrides": {}, 

602 } 

603 return { 

604 "name": command, 

605 "profile": "adapter", 

606 "inherits": None, 

607 "first_class_variant": False, 

608 "shared_primitives": [], 

609 "step_overrides": {}, 

610 } 

611 

612 

613def command_side_effects( 

614 command: str, 

615 config: cfg.ProjectConfig, 

616 requirement: runtime.CapabilityRequirement, 

617 loaded: dict[str, list[Extension]], 

618) -> tuple[str, ...]: 

619 """Return command side effects plus project capability-derived consent effects.""" 

620 effects: list[str] = list(_BASE_SIDE_EFFECTS.get(command, ())) 

621 if project_command := get_project_command(config, command): 

622 effects.extend(project_command.side_effects) 

623 effects.extend(consent.capability_side_effects(requirement.required)) 

624 effects.extend(consent.capability_side_effects(requirement.optional)) 

625 for extensions in loaded.values(): 

626 for ext in extensions: 

627 effects.extend(consent.capability_side_effects(ext.required_capabilities)) 

628 effects.extend(consent.capability_side_effects(ext.optional_capabilities)) 

629 return tuple(dict.fromkeys(effects)) 

630 

631 

632def reporting_contract_as_dict( 

633 *, 

634 command: str, 

635 config: cfg.ProjectConfig, 

636 transport: github_transport.GitHubTransport | None = None, 

637) -> dict[str, Any]: 

638 """Project-neutral reporting parity contract for audit/report adapters.""" 

639 github = transport.as_dict() if transport is not None else {} 

640 base = { 

641 "command": command, 

642 "base_branch": config.base_branch, 

643 "timezone": config.timezone, 

644 "github_transport": github, 

645 "policy_source": "policy_pack + adapter arguments", 

646 "dry_run": { 

647 "mutates": False, 

648 "prints_planned_writes": True, 

649 }, 

650 "handoff": { 

651 "fixes_route_to": "ship", 

652 "auto_applies_fixes": False, 

653 }, 

654 "work_creation_policy": workcreation.contract_as_dict( 

655 near_text_similarity=_near_text_similarity(config), 

656 ), 

657 } 

658 if command == "coverage": 

659 return { 

660 **base, 

661 "target": "pull_request", 

662 "codename_prefix": "COVERAGE-<PR>-", 

663 "renderer": "keel.artifacts.render_coverage_delta", 

664 "marker": artifacts.COVERAGE_DELTA_MARKER, 

665 "idempotency": { 

666 "scope": "one-comment-per-pr", 

667 "find_by_first_line_prefix": "COVERAGE-<PR>-", 

668 "existing_comment": "update-in-place", 

669 "update_unavailable": "do-not-post-duplicate", 

670 }, 

671 "labels": { 

672 "regression": "coverage-regression", 

673 "operation": "idempotent-add-or-remove", 

674 }, 

675 "degradation": { 

676 "unwired_tool": "skip-area", 

677 "coverage_command_failure": "fatal", 

678 }, 

679 "arguments": ["pr", "--base", "--threshold", "--changed", "--open-issues", 

680 "--dry-run"], 

681 } 

682 if command == "deps-audit": 

683 return { 

684 **base, 

685 "target": "daily_tracking_issue", 

686 "tracking_issue_title": "deps-audit: <DATE>", 

687 "codename_prefix": "DEPS-AUDIT-<DATE>-", 

688 "renderer": "keel.artifacts.render_deps_audit", 

689 "marker": artifacts.DEPS_AUDIT_MARKER, 

690 "idempotency": { 

691 "scope": "append-per-run", 

692 "find_tracking_issue_by_exact_title": "deps-audit: <DATE>", 

693 "find_latest_run_by_first_line_prefix": "DEPS-AUDIT-<DATE>-", 

694 "existing_comment": "append-fresh-run-comment", 

695 }, 

696 "degradation": { 

697 "per_ecosystem_failure": "skipped-section", 

698 "argument_failure": "fatal", 

699 }, 

700 "arguments": ["ecosystem", "--severity", "--security-only", "--open-issues", 

701 "--dry-run"], 

702 } 

703 if command == "flake-audit": 

704 return { 

705 **base, 

706 "target": "ci_history_or_local_runs", 

707 "codename_prefix": "FLAKE-AUDIT-<DATE>-", 

708 "renderer": "keel.artifacts.render_flake_audit", 

709 "marker": artifacts.FLAKE_AUDIT_MARKER, 

710 "idempotency": { 

711 "scope": "one-issue-per-flake", 

712 "dedupe_issue_title": "flaky test: <fully.qualified.name>", 

713 "find_run_by_first_line_prefix": "FLAKE-AUDIT-<DATE>-", 

714 }, 

715 "classification": { 

716 "rule": "across-run-disagreement-only", 

717 "minimum_failures": 3, 

718 "consistent_failures": "real-bug-not-flake", 

719 }, 

720 "degradation": { 

721 "artifact_unavailable": "run-level-limitations-section", 

722 "no_ci_or_local_gate": "clean-exit", 

723 }, 

724 "arguments": ["--days", "--runs", "--threshold", "--open-issues", "--dry-run"], 

725 } 

726 return base 

727 

728 

729def project_as_dict(config: cfg.ProjectConfig) -> dict[str, Any]: 

730 """Resolved project config summary safe for adapter planning.""" 

731 return { 

732 "config_hash": cfg.config_hash(config), 

733 "extends": config.extends, 

734 "core_version": config.core_version, 

735 "base_branch": config.base_branch, 

736 "owner": config.owner, 

737 "repo": config.repo, 

738 "platform": config.platform, 

739 "timezone": config.timezone, 

740 "merge_window": config.merge_window, 

741 "merge_window_mode": config.merge_window_mode, 

742 "extensions_dir": config.extensions_dir, 

743 "gates": list(config.gates), 

744 "extensions": {slot: list(files) for slot, files in sorted(config.extensions.items())}, 

745 "policy_pack": config.policy_pack, 

746 "knobs": { 

747 "build_gate_cmd": config.knobs.build_gate_cmd, 

748 "lint_cmd": config.knobs.lint_cmd, 

749 "implementer_agents": dict(sorted(config.knobs.implementer_agents.items())), 

750 **cfg.delegate_profiles_dict(config), 

751 "tier3_globs": list(config.knobs.tier3_globs), 

752 "ci_workflows": dict(sorted(config.knobs.ci_workflows.items())), 

753 "docs_gate_paths": list(config.knobs.docs_gate_paths), 

754 "docs_only_allowlist": list(config.knobs.docs_only_allowlist), 

755 "sot_doc": config.knobs.sot_doc, 

756 "required_capabilities": list(config.knobs.required_capabilities), 

757 "optional_capabilities": list(config.knobs.optional_capabilities), 

758 }, 

759 } 

760 

761 

762def gate_as_dict(spec: gates.GateSpec) -> dict[str, Any]: 

763 """Render a planned gate without losing its capability declarations.""" 

764 return asdict(spec) 

765 

766 

767def extension_hooks_as_dict( 

768 config: cfg.ProjectConfig, loaded: dict[str, list[Extension]] 

769) -> dict[str, list[dict[str, Any]]]: 

770 """Render loaded extension hooks grouped by backbone slot.""" 

771 return { 

772 slot: [ 

773 { 

774 "id": ext.id, 

775 "slot": ext.slot, 

776 "kind": ext.kind, 

777 "mode": ext.mode, 

778 "agent": ext.agent, 

779 "on_fail": ext.on_fail, 

780 "anchorable": ext.anchorable, 

781 "source": ext.source, 

782 "has_run": ext.run is not None, 

783 "has_prompt": ext.prompt is not None or bool(ext.body.strip()), 

784 "required_capabilities": list(ext.required_capabilities), 

785 "optional_capabilities": list(ext.optional_capabilities), 

786 } 

787 for ext in loaded.get(slot, []) 

788 ] 

789 for slot in model.SLOTS 

790 } 

791 

792 

793def ship_result_as_dict( 

794 *, 

795 changed_files: list[str] | None, 

796 outcomes: list[gates.GateOutcome], 

797 verdict, 

798 assessment, 

799 issue_intake: dict[str, Any] | None = None, 

800 run_ledger: dict[str, Any] | None = None, 

801) -> dict[str, Any]: 

802 """Normalized deterministic result record for ``keel ship --json``.""" 

803 closure_comment = None 

804 issue_number = None 

805 pr_number = None 

806 head_sha = None 

807 if isinstance(run_ledger, dict): 

808 record = run_ledger.get("record") 

809 if isinstance(record, dict): 

810 closure_comment = closure.render_closure_comment(record) 

811 issue = record.get("issue") 

812 pull_request = record.get("pull_request") 

813 issue_number = issue.get("number") if isinstance(issue, dict) else None 

814 pr_number = pull_request.get("number") if isinstance(pull_request, dict) else None 

815 head_sha = record.get("head_sha") 

816 head_sha = head_sha if isinstance(head_sha, str) else None 

817 finding_dicts = [_finding_as_dict(finding) for finding in verdict.findings] 

818 testing = _testing_summary(outcomes) 

819 artifact_bodies = { 

820 "pr_body": artifacts.render_pr_body( 

821 issue_number=issue_number, 

822 issue_intake=issue_intake, 

823 changed_files=changed_files, 

824 testing=testing, 

825 docs_impact=_docs_impact(changed_files), 

826 ), 

827 "issue_update": artifacts.render_issue_update( 

828 issue_number=issue_number, 

829 pull_request=pr_number, 

830 status="ready-for-merge" if not verdict.blocked else "blocked", 

831 summary=assessment.merge.reason, 

832 next_step="Merge when CI and evidence are green." 

833 if not verdict.blocked else "Resolve blocking findings before merge.", 

834 ), 

835 "review_verdict_template": artifacts.render_review_verdict( 

836 reviewer="reviewer", 

837 head_sha=head_sha, 

838 verdict="REQUEST_CHANGES" if verdict.blocked else "LGTM", 

839 scope="Full changed-file diff and keel command contract.", 

840 findings=finding_dicts, 

841 testing="; ".join(testing) if testing else "See PR Testing section.", 

842 ), 

843 "jury_verdict_template": artifacts.render_jury_verdict( 

844 head_sha=head_sha, 

845 participants=("reviewer-a", "reviewer-b", "reviewer-c", "orchestrator"), 

846 verdict="REQUEST_CHANGES" if verdict.blocked else "LGTM", 

847 findings_summary=_finding_summaries(finding_dicts), 

848 remaining_risks="blocking findings present" if verdict.blocked else "none identified", 

849 ), 

850 "extension_result_template": artifacts.render_extension_result( 

851 slot="<slot>", 

852 extension_id="<extension-id>", 

853 status="not-run", 

854 mode="advisory", 

855 summary="Extension result summary goes here.", 

856 ), 

857 } 

858 return { 

859 # None stays None: "could not read the diff" must not report as "0 files". 

860 "changed_files": None if changed_files is None else list(changed_files), 

861 "changed_file_count": None if changed_files is None else len(changed_files), 

862 "changed_files_unreadable": changed_files is None, 

863 "issue_intake": issue_intake, 

864 "run_ledger": run_ledger, 

865 "closure_comment": closure_comment, 

866 "artifact_bodies": artifact_bodies, 

867 "gate_outcomes": [ 

868 { 

869 "gate": outcome.gate, 

870 "ok": outcome.ok, 

871 "skipped": outcome.skipped, 

872 "timed_out": outcome.timed_out, 

873 "error": outcome.error, 

874 "findings": [_finding_as_dict(finding) for finding in outcome.findings], 

875 } 

876 for outcome in outcomes 

877 ], 

878 "verdict": { 

879 "blocked": verdict.blocked, 

880 "counts": dict(verdict.counts), 

881 "findings": [_finding_as_dict(finding) for finding in verdict.findings], 

882 }, 

883 "assessment": { 

884 "tier": assessment.tier, 

885 "reviewers": assessment.reviewers, 

886 "window_open": assessment.window_open, 

887 "ci_ok": assessment.ci_ok, 

888 "merge": { 

889 "action": assessment.merge.action, 

890 "reason": assessment.merge.reason, 

891 }, 

892 "halted": assessment.halted, 

893 "bypassed_window": assessment.bypassed_window, 

894 "review_merge_contract": assessment.review_contract, 

895 }, 

896 } 

897 

898 

899def _testing_summary(outcomes: list[gates.GateOutcome]) -> list[str]: 

900 if not outcomes: 

901 return [] 

902 lines: list[str] = [] 

903 for outcome in outcomes: 

904 if outcome.skipped: 

905 state = "skipped" 

906 elif outcome.ok: 

907 state = "passed" 

908 elif outcome.timed_out: 

909 # Still a blocking outcome — but "failed" would read as a broken test. 

910 state = "timed out" 

911 else: 

912 state = "failed" 

913 suffix = f" ({outcome.error})" if outcome.error else "" 

914 lines.append(f"{outcome.gate}: {state}{suffix}") 

915 return lines 

916 

917 

918def _docs_impact(changed_files: list[str] | None) -> str: 

919 if changed_files is None: 

920 return "Docs Impact: unknown — the changed-file list could not be read from git." 

921 docs = [file for file in changed_files if _is_doc_path(file)] 

922 if docs: 

923 return "Updated docs: " + ", ".join(f"`{file}`" for file in docs) 

924 return "Docs Impact: none — no documentation files changed." 

925 

926 

927def _is_doc_path(file: str) -> bool: 

928 lowered = file.lower() 

929 return ( 

930 "/docs/" in f"/{lowered}" 

931 or lowered.startswith("docs/") 

932 or lowered.endswith((".md", ".mdx", ".rst", ".adoc")) 

933 ) 

934 

935 

936def _finding_summaries(findings: list[dict[str, Any]]) -> list[str]: 

937 return [ 

938 f"{finding['severity']}: {finding['message']}" 

939 for finding in findings 

940 if isinstance(finding.get("severity"), str) and isinstance(finding.get("message"), str) 

941 ] 

942 

943 

944def standalone_result_as_dict( 

945 *, 

946 command: str, 

947 config: cfg.ProjectConfig, 

948 target: str | None = None, 

949 delegate: str | None = None, 

950 transport: github_transport.GitHubTransport | None = None, 

951 evaluation: runtime.CapabilityEvaluation | None = None, 

952) -> dict[str, Any]: 

953 """Deterministic dry-run result records for standalone non-ship commands.""" 

954 if command == "implement": 

955 issue_id = _target_identifier(target) 

956 return { 

957 "command": command, 

958 "target": target, 

959 "base_branch": config.base_branch, 

960 "branch_pattern": f"feature/issue-{issue_id}-<slug>", 

961 "worktree_path_pattern": f"worktrees/issue-{issue_id}", 

962 "implementer": { 

963 "source": "delegate" if delegate else "project-routing-or-host", 

964 "selected": delegate, 

965 "routing_keys": sorted(config.knobs.implementer_agents), 

966 }, 

967 "handoff": { 

968 "opens_pr": True, 

969 "merges": False, 

970 "next_commands": ["ship", "pr-loop"], 

971 }, 

972 } 

973 if command == "ci-check": 

974 resolved = transport.as_dict() if transport is not None else {} 

975 return { 

976 "command": command, 

977 "target": target, 

978 "base_branch": config.base_branch, 

979 "ci_workflows": dict(sorted(config.knobs.ci_workflows.items())), 

980 "latest_run_context": { 

981 "limit": 3, 

982 "selected": "newest available run", 

983 "history": "previous runs used for flake or infra classification", 

984 }, 

985 "diagnostics": { 

986 "read_only": True, 

987 "log_tail": "available when the selected GitHub transport exposes logs", 

988 "classifications": ["real-failure", "flake", "infra-or-quota"], 

989 "proposed_fix_count": 1, 

990 }, 

991 "github_transport": resolved, 

992 "routing": { 

993 "never_direct_merge": True, 

994 "recommendations": ["review-cycle", "pr-loop", "ship", "flake-audit"], 

995 }, 

996 } 

997 if command == "morning": 

998 return { 

999 "command": command, 

1000 "target": target, 

1001 "base_branch": config.base_branch, 

1002 "brief": morning_contract_as_dict( 

1003 config=config, 

1004 evaluation=evaluation, 

1005 transport=transport, 

1006 ), 

1007 "execution": { 

1008 "runs_project_health_commands": False, 

1009 "writes_reports": False, 

1010 "live_work_owner": "adapter-or-extension-after-consent", 

1011 }, 

1012 } 

1013 if command in {"wrap", "work-block", "overnight"}: 

1014 return { 

1015 "command": command, 

1016 "target": target, 

1017 "base_branch": config.base_branch, 

1018 "session": session_contract_as_dict( 

1019 command=command, 

1020 config=config, 

1021 transport=transport, 

1022 ), 

1023 "execution": { 

1024 "runs_gates": False, 

1025 "creates_prs": False, 

1026 "merges": False, 

1027 "writes_reports": False, 

1028 "live_work_owner": "adapter-after-consent", 

1029 }, 

1030 } 

1031 if command in {"pr-loop", "review-cycle"}: 

1032 workflow = feedback_workflow_as_dict(config, command) 

1033 return { 

1034 "command": command, 

1035 "target": target, 

1036 "base_branch": config.base_branch, 

1037 "feedback_workflow": workflow, 

1038 "execution": { 

1039 "commits": False, 

1040 "pushes": False, 

1041 "posts_comments": False, 

1042 "merges": False, 

1043 "live_work_owner": "adapter-after-consent", 

1044 }, 

1045 } 

1046 if command in {"regression", "review-all-day"}: 

1047 return { 

1048 "command": command, 

1049 "target": target, 

1050 "base_branch": config.base_branch, 

1051 "scan": scan_contract_as_dict( 

1052 command=command, 

1053 config=config, 

1054 transport=transport, 

1055 ), 

1056 "execution": { 

1057 "edits_code": False, 

1058 "pushes": False, 

1059 "merges": False, 

1060 "writes_issues": False, 

1061 "live_work_owner": "adapter-after-consent", 

1062 }, 

1063 } 

1064 return {"command": command, "target": target} 

1065 

1066 

1067def feedback_workflow_as_dict(config: cfg.ProjectConfig, command: str) -> dict[str, Any]: 

1068 """Return command-specific feedback-loop policy with project overrides applied.""" 

1069 defaults = _DEFAULT_FEEDBACK_WORKFLOWS.get(command, {}) 

1070 policy = _feedback_workflow_policy(config).get(command, {}) 

1071 return _deep_merge(defaults, policy) 

1072 

1073 

1074def scan_contract_as_dict( 

1075 *, 

1076 command: str, 

1077 config: cfg.ProjectConfig, 

1078 transport: github_transport.GitHubTransport | None = None, 

1079) -> dict[str, Any]: 

1080 """Project-neutral scan-and-file contract for regression and review-all-day.""" 

1081 pack = config.policy_pack or {} 

1082 reports = pack.get("reports") if isinstance(pack.get("reports"), dict) else {} 

1083 scan = pack.get("scan") if isinstance(pack.get("scan"), dict) else {} 

1084 areas = scan.get("areas") if isinstance(scan.get("areas"), dict) else {} 

1085 issue_labels = scan.get("issue_labels") if isinstance(scan.get("issue_labels"), dict) else {} 

1086 github = transport.as_dict() if transport is not None else {} 

1087 base = { 

1088 "timezone": config.timezone, 

1089 "merge_window": config.merge_window, 

1090 "base_branch": config.base_branch, 

1091 "github_transport": github, 

1092 "reports": _report_destinations(reports), 

1093 "project_policy_sources": { 

1094 "scan": "policy_pack.scan", 

1095 "areas": "policy_pack.scan.areas", 

1096 "active_branch_patterns": "policy_pack.scan.active_branch_patterns", 

1097 "risk_globs": "knobs.tier3_globs", 

1098 "labels": "policy_pack.labels + policy_pack.scan.issue_labels", 

1099 "ci_workflows": "knobs.ci_workflows", 

1100 }, 

1101 "write_safety": { 

1102 "dry_run_no_writes": True, 

1103 "orchestrator_only_writes": True, 

1104 "code_mutation": False, 

1105 "pr_mutation": False, 

1106 "issue_write_requires_consent": True, 

1107 }, 

1108 "dedupe": { 

1109 "source": "policy_pack.scan.dedupe + canonical defaults", 

1110 "path_token_boundary": True, # nosec B105 

1111 "type_must_match": True, 

1112 "near_text_similarity": _near_text_similarity(config), 

1113 "open_duplicate": "skip", 

1114 "closed_duplicate": "promote-regression-of", 

1115 "lock": "mkdir", 

1116 }, 

1117 "reviewer_isolation": { 

1118 "parallel": True, 

1119 "no_cross_reading": True, 

1120 "orchestrator_collects_findings": True, 

1121 }, 

1122 "areas": [ 

1123 {"name": name, "paths": list(paths)} 

1124 for name, paths in sorted(areas.items()) 

1125 if isinstance(paths, list) 

1126 ], 

1127 "risk_globs": list(config.knobs.tier3_globs), 

1128 "scan_finding": { 

1129 "renderer": "keel.artifacts.render_scan_finding_issue", 

1130 "marker": artifacts.SCAN_FINDING_MARKER, 

1131 }, 

1132 "issue_labels": { 

1133 key: list(value) 

1134 for key, value in sorted(issue_labels.items()) 

1135 if isinstance(value, list) 

1136 }, 

1137 "work_creation_policy": workcreation.contract_as_dict( 

1138 near_text_similarity=_near_text_similarity(config), 

1139 ), 

1140 } 

1141 if command == "regression": 

1142 base["regression"] = { 

1143 "scan_target": { 

1144 "source": "canonical base head", 

1145 "base_branch": config.base_branch, 

1146 "read_only_worktree": True, 

1147 "clean_tree_preflight": True, 

1148 }, 

1149 "scope": { 

1150 "default": "full", 

1151 "supported": ["full", "changed", "since"], 

1152 }, 

1153 "confidence_filter": { 

1154 "drop": ["low"], 

1155 "downgrade_blocker_when": "medium-confidence", 

1156 "file_only_when": ["high-confidence", "medium-security"], 

1157 }, 

1158 "issue_creation": { 

1159 "one_issue_per_finding": True, 

1160 "route_to": "ship", 

1161 "regression_of_line": "regression-of: #N", 

1162 "labels": issue_labels.get("regression", []), 

1163 }, 

1164 "final_report": [ 

1165 "raw_findings", 

1166 "after_confidence_filter", 

1167 "duplicates_skipped", 

1168 "promoted_regressions", 

1169 "issues_opened", 

1170 "ship_handoffs", 

1171 ], 

1172 } 

1173 elif command == "review-all-day": 

1174 base["review_all_day"] = { 

1175 "span": { 

1176 "timezone": config.timezone, 

1177 "merge_window": config.merge_window, 

1178 "days_are_inclusive_calendar_days": True, 

1179 "n_days_argument_covers_calendar_days": "N+1", 

1180 }, 

1181 "ref_scope": { 

1182 "branches": ["trunk", "active-work-branches"], 

1183 "active_branch_patterns": list(scan.get("active_branch_patterns") or ()), 

1184 "remote_refs_default": True, 

1185 "warn_on_stale_fetch": True, 

1186 }, 

1187 "strategy": { 

1188 "batch_threshold": _scan_int(scan, "batch_threshold", 5), 

1189 "fanout_when_commit_count_gt": _scan_int(scan, "batch_threshold", 5), 

1190 }, 

1191 "diff_truncation": { 

1192 "max_bytes": _scan_int(scan, "large_diff_max_bytes", 200000), 

1193 "boundary": "file", 

1194 }, 

1195 "finding_filter": { 

1196 "skip_minor": True, 

1197 "keep_minor_categories": ["security"], 

1198 "file_categories": ["bug-insert", "regression", "security", "config", 

1199 "test-coverage"], 

1200 }, 

1201 "issue_creation": { 

1202 "title_prefix": "[review-all-day] ", 

1203 "one_issue_per_serious_finding": True, 

1204 "labels": issue_labels.get("review-all-day", []), 

1205 }, 

1206 "final_report": [ 

1207 "commit_range", 

1208 "reviewers", 

1209 "findings", 

1210 "duplicates_skipped", 

1211 "issues_opened", 

1212 ], 

1213 } 

1214 return base 

1215 

1216 

1217def session_contract_as_dict( 

1218 *, 

1219 command: str, 

1220 config: cfg.ProjectConfig, 

1221 transport: github_transport.GitHubTransport | None = None, 

1222) -> dict[str, Any]: 

1223 """Project-neutral session workflow contract for session/work-block commands.""" 

1224 pack = config.policy_pack or {} 

1225 reports = pack.get("reports") if isinstance(pack.get("reports"), dict) else {} 

1226 github = transport.as_dict() if transport is not None else {} 

1227 base = { 

1228 "timezone": config.timezone, 

1229 "merge_window": config.merge_window, 

1230 "merge_window_mode": config.merge_window_mode, 

1231 "base_branch": config.base_branch, 

1232 "github_transport": github, 

1233 "reports": _report_destinations(reports), 

1234 "deferral_queue": _deferral_queue_as_dict(reports), 

1235 "run_ledger": ledger.ledger_contract_as_dict(config), 

1236 "project_policy_sources": { 

1237 "gates": list(config.gates), 

1238 "extensions": {slot: list(files) for slot, files in sorted(config.extensions.items())}, 

1239 "risk_rules": [rule.get("id") for rule in pack.get("risk_rules", []) 

1240 if isinstance(rule, dict)], 

1241 "source_of_truth_doc": config.knobs.sot_doc, 

1242 }, 

1243 } 

1244 if command in {"work-block", "overnight"}: 

1245 base["work_block"] = workblock.contract_as_dict( 

1246 config=config, 

1247 mode="overnight" if command == "overnight" else "daytime", 

1248 transport=github, 

1249 ) 

1250 if command == "wrap": 

1251 base["wrap"] = { 

1252 "workspace_preflight": { 

1253 "must_run_from_linked_worktree": True, 

1254 "abort_on_base_branch": True, 

1255 "git_dir_rule": "main worktree returns .git; linked worktree uses .git/worktrees", 

1256 }, 

1257 "quality_gates": { 

1258 "runner": "keel run-gates", 

1259 "changed_file_policy_source": "policy_pack + extension hooks", 

1260 }, 

1261 "commit": { 

1262 "format": "conventional-commits", 

1263 "supports_closes_issue": True, 

1264 }, 

1265 "pull_request": { 

1266 "ready_not_draft": True, 

1267 "base_branch": config.base_branch, 

1268 "requires_pr_write": True, 

1269 "body_sections": ["Summary", "Docs Impact", "Test Plan"], 

1270 }, 

1271 "recap": { 

1272 "report_key": "session", 

1273 "path": reports.get("session") or reports.get("wrap"), 

1274 "status": "configured" if reports.get("session") or reports.get("wrap") 

1275 else "unconfigured", 

1276 }, 

1277 } 

1278 elif command == "work-block": 

1279 base["daytime"] = { 

1280 "mode_source": { 

1281 "command": "keel work-block", 

1282 "shared_with_overnight": True, 

1283 }, 

1284 "queue": { 

1285 "source": "explicit issue numbers or project queue selector", 

1286 "deterministic_order": ( 

1287 "explicit numbers keep their provided order; selectors sort by policy" 

1288 ), 

1289 }, 

1290 "ship_handoff": { 

1291 "command": "ship", 

1292 "passes_operator_consent_scope": True, 

1293 "per_issue_worktree": True, 

1294 "refreshes_readiness_between_issues": True, 

1295 }, 

1296 "operator_control": { 

1297 "between_items": True, 

1298 "stop_on_consent_gap": True, 

1299 "stop_on_needs_input": True, 

1300 }, 

1301 "report": { 

1302 "day_key": "session", 

1303 "day_path": reports.get("session"), 

1304 "status": "configured" if reports.get("session") else "unconfigured", 

1305 }, 

1306 } 

1307 elif command == "overnight": 

1308 base["overnight"] = { 

1309 "mode_source": { 

1310 "command": "keel window", 

1311 "shared_with_ship": True, 

1312 "timezone": config.timezone, 

1313 "merge_window": config.merge_window, 

1314 }, 

1315 "merge_policy": { 

1316 "day": "ship may merge reviewed CI-green PRs inside the configured window", 

1317 "night": "no merge outside the configured window except true blockers", 

1318 "blocker_source": "ship blocker heuristics + project policy extensions", 

1319 "cannot_weaken_core_window": True, 

1320 }, 

1321 "queue": { 

1322 "source": "project policy or adapter query", 

1323 "tiers": ["T0-blocker", "T1-open-prs", "T2-coverage", "T3-ci-quality", 

1324 "T4-modernization", "T5-docs-backlog"], 

1325 }, 

1326 "ship_handoff": { 

1327 "command": "ship", 

1328 "passes_operator_consent_scope": True, 

1329 "per_issue_worktree": True, 

1330 "shared_work_block_contract": True, 

1331 }, 

1332 "report": { 

1333 "night_key": "overnight", 

1334 "day_key": "session", 

1335 "night_path": reports.get("overnight") or reports.get("morning"), 

1336 "day_path": reports.get("session"), 

1337 "status": "configured" if any( 

1338 reports.get(key) for key in ("overnight", "morning", "session") 

1339 ) else "unconfigured", 

1340 }, 

1341 "stop_conditions": [ 

1342 "merge-window-close", 

1343 "time-budget-exhausted", 

1344 "max-items-reached", 

1345 "hard-blocker", 

1346 "three-consecutive-unresolved-ci-failures", 

1347 "user-cancelled", 

1348 ], 

1349 } 

1350 return base 

1351 

1352 

1353def morning_contract_as_dict( 

1354 *, 

1355 config: cfg.ProjectConfig, 

1356 evaluation: runtime.CapabilityEvaluation | None = None, 

1357 transport: github_transport.GitHubTransport | None = None, 

1358) -> dict[str, Any]: 

1359 """Project-neutral morning briefing contract for adapters and dry-run output.""" 

1360 pack = config.policy_pack or {} 

1361 reports = pack.get("reports") if isinstance(pack.get("reports"), dict) else {} 

1362 health = pack.get("health_providers") if isinstance(pack.get("health_providers"), dict) else {} 

1363 github = transport.as_dict() if transport is not None else {} 

1364 github_status = "available" if github.get("transport") not in {None, "none"} else "degraded" 

1365 return { 

1366 "timezone": config.timezone, 

1367 "merge_window": config.merge_window, 

1368 "base_branch": config.base_branch, 

1369 "sections": [ 

1370 { 

1371 "id": "deferrals", 

1372 "source": "shared_queue", 

1373 "status": "configured" if reports.get("deferrals") else "unconfigured", 

1374 "path": reports.get("deferrals"), 

1375 }, 

1376 { 

1377 "id": "shipped_since_last_brief", 

1378 "source": "github", 

1379 "transport": github.get("transport"), 

1380 "status": github_status, 

1381 }, 

1382 { 

1383 "id": "github_status", 

1384 "source": "github", 

1385 "transport": github.get("transport"), 

1386 "status": github_status, 

1387 }, 

1388 { 

1389 "id": "project_health", 

1390 "source": "policy_pack.health_providers", 

1391 "status": "configured" if health else "unconfigured", 

1392 }, 

1393 { 

1394 "id": "ranked_focus", 

1395 "source": "policy_pack + github", 

1396 "status": "configured" if _priority_sources(config, reports) else "unconfigured", 

1397 }, 

1398 ], 

1399 "health_providers": [ 

1400 _health_provider_as_dict(name, provider, evaluation) 

1401 for name, provider in sorted(health.items()) 

1402 if isinstance(provider, dict) 

1403 ], 

1404 "priority_sources": _priority_sources(config, reports), 

1405 "reports": _report_destinations(reports), 

1406 "deferral_queue": _deferral_queue_as_dict(reports), 

1407 "run_ledger": ledger.ledger_contract_as_dict(config), 

1408 "missing_optional_policy": "unavailable-not-success", 

1409 } 

1410 

1411 

1412def _health_provider_as_dict( 

1413 name: str, 

1414 provider: dict[str, Any], 

1415 evaluation: runtime.CapabilityEvaluation | None, 

1416) -> dict[str, Any]: 

1417 required = tuple(provider.get("required_capabilities") or ()) 

1418 optional = tuple(provider.get("optional_capabilities") or ()) 

1419 missing_required = _missing_capabilities(required, evaluation) 

1420 missing_optional = _missing_capabilities(optional, evaluation) 

1421 status = "available" 

1422 if missing_required: 

1423 status = "blocked" 

1424 elif missing_optional: 

1425 status = "unavailable" 

1426 elif provider.get("command") is None and provider.get("kind") == "project-command": 

1427 status = "unavailable" 

1428 return { 

1429 "name": name, 

1430 "kind": provider.get("kind"), 

1431 "command": provider.get("command"), 

1432 "reports": list(provider.get("reports") or ()), 

1433 "required_capabilities": list(required), 

1434 "optional_capabilities": list(optional), 

1435 "missing_required_capabilities": list(missing_required), 

1436 "missing_optional_capabilities": list(missing_optional), 

1437 "status": status, 

1438 } 

1439 

1440 

1441def _missing_capabilities( 

1442 names: tuple[str, ...], 

1443 evaluation: runtime.CapabilityEvaluation | None, 

1444) -> tuple[str, ...]: 

1445 if evaluation is None: 

1446 return () 

1447 missing = set(evaluation.missing_required) | set(evaluation.missing_optional) 

1448 return tuple(name for name in names if name in missing) 

1449 

1450 

1451def _priority_sources(config: cfg.ProjectConfig, reports: dict[str, Any]) -> list[dict[str, Any]]: 

1452 sources: list[dict[str, Any]] = [] 

1453 if config.knobs.sot_doc: 

1454 sources.append({"id": "source_of_truth", "path": config.knobs.sot_doc}) 

1455 if reports.get("priorities"): 

1456 sources.append({"id": "priorities_report", "path": reports["priorities"]}) 

1457 if config.knobs.ci_workflows: 

1458 sources.append({"id": "ci_workflows", "names": sorted(config.knobs.ci_workflows)}) 

1459 return sources 

1460 

1461 

1462def _report_destinations(reports: dict[str, Any]) -> dict[str, dict[str, Any]]: 

1463 return { 

1464 key: { 

1465 "path": value, 

1466 "write_status": "skipped-in-dry-run", 

1467 } 

1468 for key, value in sorted(reports.items()) 

1469 } 

1470 

1471 

1472def _deferral_queue_as_dict(reports: dict[str, Any]) -> dict[str, Any]: 

1473 return { 

1474 "source": "policy_pack.reports.deferrals", 

1475 "path": reports.get("deferrals"), 

1476 "status": "configured" if reports.get("deferrals") else "unconfigured", 

1477 "shared_with": ["ship", "overnight", "wrap", "morning"], 

1478 } 

1479 

1480 

1481def _feedback_workflow_policy(config: cfg.ProjectConfig) -> dict[str, dict[str, Any]]: 

1482 pack = config.policy_pack or {} 

1483 policy = pack.get("workflow_policies") 

1484 if not isinstance(policy, dict): 

1485 return {} 

1486 return { 

1487 name: value 

1488 for name, value in policy.items() 

1489 if isinstance(name, str) and isinstance(value, dict) 

1490 } 

1491 

1492 

1493def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]: 

1494 merged: dict[str, Any] = {} 

1495 for key, value in base.items(): 

1496 if isinstance(value, dict): 

1497 merged[key] = _deep_merge(value, {}) 

1498 elif isinstance(value, list): 

1499 merged[key] = list(value) 

1500 else: 

1501 merged[key] = value 

1502 for key, value in override.items(): 

1503 if isinstance(value, dict) and isinstance(merged.get(key), dict): 

1504 merged[key] = _deep_merge(merged[key], value) 

1505 elif isinstance(value, list): 

1506 merged[key] = list(value) 

1507 else: 

1508 merged[key] = value 

1509 return merged 

1510 

1511 

1512def _scan_int(scan: dict[str, Any], key: str, default: int) -> int: 

1513 value = scan.get(key) 

1514 return value if isinstance(value, int) else default 

1515 

1516 

1517def _scan_float(scan: dict[str, Any], key: str, default: float) -> float: 

1518 value = scan.get(key) 

1519 return value if isinstance(value, int | float) else default 

1520 

1521 

1522def _near_text_similarity(config: cfg.ProjectConfig) -> float: 

1523 """The project's resolved ``policy_pack.scan.near_text_similarity``. 

1524 

1525 Both contract builders embed a ``work_creation_policy`` block carrying this 

1526 threshold, and the scan contract carries a second copy under ``dedupe``. One 

1527 resolver keeps every copy in agreement — hardcoding the default in 

1528 ``workcreation`` shipped two different values under near-identical keys, agreeing 

1529 only while the project happened to set the knob to the built-in default (#633). 

1530 """ 

1531 pack = config.policy_pack or {} 

1532 scan = pack.get("scan") if isinstance(pack.get("scan"), dict) else {} 

1533 return _scan_float(scan, "near_text_similarity", 

1534 workcreation.DEFAULT_NEAR_TEXT_SIMILARITY) 

1535 

1536 

1537def _target_identifier(target: str | None) -> str: 

1538 if not target: 

1539 return "selected-issue" 

1540 match = re.search(r"#?(\d+)", target) 

1541 if match: 

1542 return match.group(1) 

1543 return re.sub(r"[^a-z0-9]+", "-", target.lower()).strip("-") or "selected-issue" 

1544 

1545 

1546def _adapter_steps(command: str) -> list[dict[str, Any]]: 

1547 path = Path(install.ADAPTERS) / f"{command}.md" 

1548 if not path.exists(): 

1549 return [] 

1550 steps: list[dict[str, Any]] = [] 

1551 for line in path.read_text(encoding="utf-8").splitlines(): 

1552 match = _STEP_RE.match(line) 

1553 if not match: 

1554 continue 

1555 raw_id = match.group("id").strip() 

1556 step_id = raw_id.lower().replace(" ", "-").replace(".", "-") 

1557 steps.append({ 

1558 "step_id": step_id, 

1559 "step_name": match.group("name").strip() or raw_id, 

1560 "agentic": "agent" in match.group("name").lower(), 

1561 "slot": None, 

1562 "source": "adapter", 

1563 }) 

1564 return steps 

1565 

1566 

1567def _finding_as_dict(finding) -> dict[str, Any]: 

1568 tag = provenance.normalize_tag( 

1569 getattr(finding, "provenance", None), 

1570 fallback_agent=finding.source, 

1571 step_id="finding", 

1572 ) 

1573 return { 

1574 "severity": finding.severity, 

1575 "message": finding.message, 

1576 "source": finding.source, 

1577 "path": finding.path, 

1578 "line": finding.line, 

1579 "anchorable": finding.anchorable, 

1580 "provenance": tag, 

1581 }