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

307 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-02 20:26 +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 collections.abc import Mapping 

12from dataclasses import asdict 

13from pathlib import Path 

14from typing import Any 

15 

16from . import ( 

17 agents, 

18 artifacts, 

19 capture, 

20 checkpoint, 

21 closure, 

22 consent, 

23 evidence, 

24 gates, 

25 github_transport, 

26 install, 

27 intake, 

28 ledger, 

29 lock, 

30 loop, 

31 model, 

32 orchestrator, 

33 provenance, 

34 runcontrols, 

35 runtime, 

36 stepverifier, 

37 tdd, 

38 team, 

39 workblock, 

40 workcreation, 

41) 

42from . import config as cfg 

43from . import ship as ship_decisions 

44from .extensions import Extension 

45from .project_commands import get_project_command, list_project_commands 

46 

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

48 

49_COMPOUND_OVERRIDES: dict[str, str] = { 

50 "s4": "compound", 

51 "s7": "compound", 

52 "s9": "compound", 

53 "s11": "compound", 

54} 

55 

56_STEP_RE = re.compile( 

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

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

59) 

60 

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

62 "ship": ( 

63 "git_worktree", 

64 "git_branch", 

65 "file_edit", 

66 "git_push", 

67 "pull_request", 

68 "comments", 

69 "reviews", 

70 "merge", 

71 "issue_close", 

72 "capture", 

73 ), 

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

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

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

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

78 "work-block": ( 

79 "git_branch", 

80 "git_push", 

81 "pull_request", 

82 "comments", 

83 "reviews", 

84 "merge", 

85 "deferral_queue", 

86 "session_report", 

87 ), 

88 "overnight": ( 

89 "git_branch", 

90 "git_push", 

91 "pull_request", 

92 "comments", 

93 "reviews", 

94 "merge", 

95 "deferral_queue", 

96 "session_report", 

97 ), 

98 "implement": ( 

99 "git_worktree", 

100 "git_branch", 

101 "file_edit", 

102 "git_commit", 

103 "git_push", 

104 "pull_request", 

105 "comments", 

106 ), 

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

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

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

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

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

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

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

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

115} 

116 

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

118 "pr-loop": { 

119 "posting_mode": "summary", 

120 "posting_owner": "orchestrator", 

121 "reviewer_isolation": { 

122 "shared_with_ship": True, 

123 "codename_prefix": "PR-LOOP", 

124 "no_cross_reading": True, 

125 }, 

126 "inputs": { 

127 "auto_detect_current_branch": True, 

128 "explicit_pr_targets": True, 

129 "reads_review_comments": True, 

130 "reads_issue_conversation_comments": True, 

131 }, 

132 "ci": { 

133 "recheck_after_push": True, 

134 "green_required_to_exit": True, 

135 "degrade_when_logs_unavailable": True, 

136 }, 

137 "fix_loop": { 

138 "budget": 3, 

139 "self_review_before_push": True, 

140 "reviewer_fanout_after_each_push": True, 

141 }, 

142 "completion": { 

143 "marker": None, 

144 "merge": "handoff", 

145 "summary_comment": True, 

146 }, 

147 }, 

148 "review-cycle": { 

149 "posting_mode": "inline", 

150 "posting_owner": "orchestrator", 

151 "reviewer_isolation": { 

152 "shared_with_ship": True, 

153 "codename_prefix": "REVIEW-CYCLE", 

154 "no_cross_reading": True, 

155 }, 

156 "inputs": { 

157 "multi_pr": True, 

158 "sequential_pr_processing": True, 

159 }, 

160 "review": { 

161 "parallel_reviewers_within_pr": True, 

162 "partial_reviewer_failures_degrade": True, 

163 "severity_histogram_source_of_truth": True, 

164 }, 

165 "fix_loop": { 

166 "budget": 3, 

167 "enabled": True, 

168 }, 

169 "completion": { 

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

171 "marker_after_summary": True, 

172 "merge": "never", 

173 "formal_approval": "never", 

174 }, 

175 }, 

176} 

177 

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

179 

180 

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

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

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

184 

185 

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

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

188 

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

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

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

192 command-local sequence without parsing Markdown themselves. 

193 """ 

194 if command == "ship": 

195 compound = profile == "compound" 

196 return [ 

197 { 

198 "step_id": step.id, 

199 "step_name": step.name, 

200 "agentic": step.agentic, 

201 "slot": step.slot, 

202 "source": "backbone", 

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

204 if compound 

205 else "standard", 

206 } 

207 for step in model.BACKBONE 

208 ] 

209 steps = _adapter_steps(command) 

210 return steps if steps else [] 

211 

212 

213def build_command_contract( 

214 *, 

215 command: str, 

216 profile: str = "standard", 

217 config: cfg.ProjectConfig, 

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

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

220 requirement: runtime.CapabilityRequirement, 

221 evaluation: runtime.CapabilityEvaluation, 

222 transport: github_transport.GitHubTransport, 

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

224 dry_run: bool = True, 

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

226 consent_approval_source: str = "flag", 

227 consent_mode: str = "explicit", 

228 operator: str | None = None, 

229 target: str | None = None, 

230 reviewer_override: int | None = None, 

231 review_tier: int | None = None, 

232 review_comments: str = "inline", 

233 jury: bool = False, 

234 no_jury: bool = False, 

235 jury_advisory: bool = False, 

236 issue_title: str | None = None, 

237 issue_body: str | None = None, 

238 #: The retrieved past learnings for this task (#1155), measured by the caller 

239 #: because reading a directory is I/O. The **plan** contract carries it as well 

240 #: as ship's: s4 composes the implement brief from what `keel plan` printed, 

241 #: long before s5 runs `keel ship`, so wiring it into ship alone meant the brief 

242 #: this feature exists for never saw a lesson. 

243 learnings: dict[str, Any] | None = None, 

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

245 role: str | None = None, 

246 delegate: str | None = None, 

247 review_delegates: tuple[str, ...] = (), 

248 host_agent: str = agents.HOST_DEFAULT, 

249 tdd_override: bool = False, 

250 loop_override: bool = False, 

251 #: An explicit `--max-iterations` budget for this run, which outranks both the 

252 #: flag and the knob and publishes its own source (#1173). 

253 loop_budget: int | None = None, 

254 effort: str | None = None, 

255 team_profile: str | None = None, 

256 jury_availability: Mapping[str, Any] | None = None, 

257) -> dict[str, Any]: 

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

259 

260 ``tdd_override`` is the per-run ``--tdd`` flag. The resolved s4 profile is published as 

261 ``implement_mode`` — a sibling of ``workflow_profile``, because ``tdd`` is an s4 profile 

262 the same way ``compound`` is a workflow one — and is what selects the ``tdd-order`` gate 

263 in the contract's ``gates`` list.""" 

264 implement_mode = tdd.resolve_mode(config.knobs.implement_mode, flag=tdd_override) 

265 # The s4 iteration policy rides inside `implement_mode` (#1165): it is not a third 

266 # profile but a policy around whichever profile is running, and `wraps` says which. 

267 loop_policy = loop.resolve( 

268 config.knobs.loop, 

269 flag=loop_override, 

270 implement_mode=implement_mode.name, 

271 max_iterations=loop_budget, 

272 ) 

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

274 graph = command_graph(command, profile=profile) 

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

276 graph = [ 

277 { 

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

279 "step_name": project_command.name, 

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

281 "slot": None, 

282 "source": "project_command", 

283 } 

284 ] 

285 contract = { 

286 "schema_version": SCHEMA_VERSION, 

287 "command": command, 

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

289 "dry_run": dry_run, 

290 "no_mutations": dry_run, 

291 "project": project_as_dict(config), 

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

293 "implement_mode": {**implement_mode.as_dict(), "loop": loop_policy.as_dict()}, 

294 "graph": graph, 

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

296 "gates": [ 

297 gate_as_dict(spec) 

298 for spec in gates.plan_gates(config, loaded, implement_mode=implement_mode.name) 

299 ], 

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

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

302 "extension_problems": list(extension_problems), 

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

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

305 "capabilities": evaluation.as_dict(), 

306 "github_transport": transport.as_dict(), 

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

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

309 # The same block `keel ship` publishes, so s4 can open the implement brief 

310 # from the plan it was told not to re-derive. 

311 "learnings": learnings if learnings is not None else capture.learning_retrieval_as_dict(), 

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

313 "resource_claims": lock.contract_as_dict(), 

314 "side_effects": { 

315 "declared": list(declared_side_effects), 

316 "mutates_in_dry_run": False, 

317 }, 

318 "operator_consent": consent.build_consent_contract( 

319 command=command, 

320 side_effects=declared_side_effects, 

321 dry_run=dry_run, 

322 approved_scopes=approved_consent_scopes, 

323 approval_source=consent_approval_source, 

324 mode=consent_mode, 

325 operator=operator, 

326 target=target, 

327 ), 

328 "agent_output_provenance": provenance.contract_as_dict(), 

329 } 

330 if command == "morning": 

331 contract["morning_contract"] = morning_contract_as_dict( 

332 config=config, 

333 evaluation=evaluation, 

334 transport=transport, 

335 ) 

336 if command in _REPORTING_COMMANDS: 

337 contract["reporting_contract"] = reporting_contract_as_dict( 

338 command=command, 

339 config=config, 

340 transport=transport, 

341 ) 

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

343 contract["session_contract"] = session_contract_as_dict( 

344 command=command, 

345 config=config, 

346 transport=transport, 

347 delegation=workblock.delegation_as_dict( 

348 delegate=delegate, 

349 review_delegates=review_delegates, 

350 effort=effort, 

351 team_profile=team_profile, 

352 reviewer_override=reviewer_override, 

353 ), 

354 ) 

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

356 contract["scan_contract"] = scan_contract_as_dict( 

357 command=command, 

358 config=config, 

359 transport=transport, 

360 ) 

361 if command == "triage": 

362 contract["triage_contract"] = { 

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

364 "one_comment_per_issue": True, 

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

366 "marker": artifacts.TRIAGE_AUDIT_MARKER, 

367 } 

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

369 # Resolved once and shared: the reviewer bench the contract publishes and the 

370 # assignment a host dispatches have to be the same answer, or two agents reading 

371 # the same JSON run different teams (#1014). 

372 assignment = team.resolve_assignment( 

373 config.knobs.team, 

374 tier=review_tier, 

375 role=role, 

376 default_count=ship_decisions.reviewer_count(review_tier or 2), 

377 reviewer_override=reviewer_override, 

378 delegate=delegate, 

379 review_delegates=review_delegates, 

380 host_agent=host_agent, 

381 legacy=agents.legacy_team_seats(config), 

382 jury_disabled=no_jury, 

383 jury_advisory=jury_advisory, 

384 team_profile=team_profile, 

385 effort=effort, 

386 # The panel-availability probe, measured by 

387 # `keel.providerprobe.jury_availability` and handed to every resolver 

388 # (#1066). This is one more site that resolves a bench, and it has to see 

389 # the same measurement as the rest or `keel plan` publishes a panel the run 

390 # it plans cannot convene. 

391 jury_availability=jury_availability, 

392 ) 

393 contract["assignment"] = assignment 

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

395 tier=review_tier, 

396 reviewer_override=reviewer_override, 

397 review_comments=review_comments, 

398 gates=config.gates, 

399 policy_pack=config.policy_pack, 

400 jury=jury, 

401 no_jury=no_jury, 

402 jury_advisory=jury_advisory, 

403 require_distinct_vendors=config.knobs.evidence_require_distinct_vendors, 

404 assignment=assignment, 

405 learnings=learnings, 

406 ) 

407 if command == "ship": 

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

409 contract["review_merge_contract"], 

410 dry_run=dry_run, 

411 ) 

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

413 contract["review_merge_contract"], 

414 dry_run=dry_run, 

415 ) 

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

417 if command == "ship": 

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

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

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

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

422 title=issue_title, 

423 body=issue_body, 

424 labels=issue_labels, 

425 ) 

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

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

428 return contract 

429 

430 

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

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

433 

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

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

436 backbone. 

437 """ 

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

439 return { 

440 "name": "ship", 

441 "profile": "compound", 

442 "inherits": "ship", 

443 "first_class_variant": True, 

444 "shared_primitives": [ 

445 "select", 

446 "branch", 

447 "worktree", 

448 "guard", 

449 "classify", 

450 "ci", 

451 "test", 

452 "merge_window", 

453 "merge_lock", 

454 "merge", 

455 "capture_marker", 

456 "close", 

457 ], 

458 "step_overrides": { 

459 "s4": { 

460 "step": "implement", 

461 "mode": "compound", 

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

463 }, 

464 "s7": { 

465 "step": "review", 

466 "mode": "compound", 

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

468 }, 

469 "s9": { 

470 "step": "fixloop", 

471 "mode": "compound", 

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

473 }, 

474 "s11": { 

475 "step": "capture", 

476 "mode": "compound", 

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

478 }, 

479 }, 

480 } 

481 if command == "pr-loop": 

482 return { 

483 "name": "pr-loop", 

484 "profile": "feedback-loop", 

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

486 "first_class_variant": True, 

487 "shared_primitives": [ 

488 "linked_worktree_preflight", 

489 "github_transport", 

490 "reviewer_isolation", 

491 "ci_recheck", 

492 "fixloop", 

493 "summary_comment", 

494 "operator_consent", 

495 ], 

496 "step_overrides": { 

497 "merge": { 

498 "mode": "handoff", 

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

500 }, 

501 }, 

502 } 

503 if command == "review-cycle": 

504 return { 

505 "name": "review-cycle", 

506 "profile": "review-feedback", 

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

508 "first_class_variant": True, 

509 "shared_primitives": [ 

510 "multi_pr_targets", 

511 "reviewer_isolation", 

512 "posting_mode", 

513 "severity_histogram", 

514 "fixloop", 

515 "completion_marker", 

516 "operator_consent", 

517 ], 

518 "step_overrides": { 

519 "merge": { 

520 "mode": "never", 

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

522 }, 

523 }, 

524 } 

525 if command == "implement": 

526 return { 

527 "name": "implement", 

528 "profile": "standalone-step", 

529 "inherits": "ship.s4", 

530 "first_class_variant": True, 

531 "shared_primitives": [ 

532 "issue_target", 

533 "branch", 

534 "worktree", 

535 "implementer_routing", 

536 "operator_consent", 

537 "handoff", 

538 ], 

539 "step_overrides": {}, 

540 } 

541 if command == "ci-check": 

542 return { 

543 "name": "ci-check", 

544 "profile": "standalone-diagnostic", 

545 "inherits": None, 

546 "first_class_variant": True, 

547 "shared_primitives": [ 

548 "github_transport", 

549 "check_runs", 

550 "latest_run_context", 

551 "log_diagnostics", 

552 "read_only", 

553 "routing_recommendation", 

554 ], 

555 "step_overrides": {}, 

556 } 

557 if command == "morning": 

558 return { 

559 "name": "morning", 

560 "profile": "daily-brief", 

561 "inherits": None, 

562 "first_class_variant": True, 

563 "shared_primitives": [ 

564 "date_window", 

565 "deferral_queue", 

566 "shipped_since", 

567 "github_summary", 

568 "health_providers", 

569 "priority_sources", 

570 "ranked_focus", 

571 "report_output", 

572 ], 

573 "step_overrides": {}, 

574 } 

575 if command == "wrap": 

576 return { 

577 "name": "wrap", 

578 "profile": "session-wrap", 

579 "inherits": None, 

580 "first_class_variant": True, 

581 "shared_primitives": [ 

582 "linked_worktree_preflight", 

583 "base_branch_guard", 

584 "configured_gates", 

585 "conventional_commit", 

586 "ready_pr_create", 

587 "session_recap", 

588 "deferral_queue", 

589 "operator_consent", 

590 ], 

591 "step_overrides": {}, 

592 } 

593 if command == "overnight": 

594 return { 

595 "name": "overnight", 

596 "profile": "session-overnight", 

597 "inherits": "ship", 

598 "first_class_variant": True, 

599 "shared_primitives": [ 

600 "work_block", 

601 "merge_window", 

602 "ship_handoff", 

603 "priority_queue", 

604 "per_issue_worktree", 

605 "no_night_merge", 

606 "blocker_policy", 

607 "session_report", 

608 "deferral_queue", 

609 "stop_conditions", 

610 "operator_consent", 

611 ], 

612 "step_overrides": {}, 

613 } 

614 if command == "work-block": 

615 return { 

616 "name": "work-block", 

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

618 "inherits": "ship", 

619 "first_class_variant": True, 

620 "shared_primitives": [ 

621 "work_block", 

622 "queue_snapshot", 

623 "readiness_refresh", 

624 "ship_handoff", 

625 "per_issue_worktree", 

626 "operator_between_item_control", 

627 "progress_snapshot", 

628 "session_report", 

629 "deferral_queue", 

630 "stop_conditions", 

631 "operator_consent", 

632 ], 

633 "step_overrides": {}, 

634 } 

635 if command in _REPORTING_COMMANDS: 

636 return { 

637 "name": command, 

638 "profile": "reporting", 

639 "inherits": None, 

640 "first_class_variant": True, 

641 "shared_primitives": [ 

642 "project_policy", 

643 "github_transport", 

644 "codename_anchor", 

645 "dedupe", 

646 "dry_run_no_mutations", 

647 "ship_handoff", 

648 "operator_consent", 

649 ], 

650 "step_overrides": {}, 

651 } 

652 if command == "regression": 

653 return { 

654 "name": "regression", 

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

656 "inherits": None, 

657 "first_class_variant": True, 

658 "shared_primitives": [ 

659 "canonical_base_scan", 

660 "clean_tree_preflight", 

661 "read_only_worktree", 

662 "area_fanout", 

663 "reviewer_isolation", 

664 "confidence_filter", 

665 "dedupe", 

666 "issue_lock", 

667 "issue_create", 

668 "ship_handoff", 

669 "final_report", 

670 "operator_consent", 

671 ], 

672 "step_overrides": {}, 

673 } 

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

675 return { 

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

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

678 "inherits": None, 

679 "first_class_variant": True, 

680 "shared_primitives": [ 

681 "merge_window_span", 

682 "remote_ref_scope", 

683 "batch_or_fanout", 

684 "reviewer_isolation", 

685 "diff_truncation", 

686 "finding_filter", 

687 "dedupe", 

688 "issue_prefix", 

689 "issue_create", 

690 "final_report", 

691 "operator_consent", 

692 ], 

693 "step_overrides": {}, 

694 } 

695 if command == "ship": 

696 return { 

697 "name": "ship", 

698 "profile": "standard", 

699 "inherits": None, 

700 "first_class_variant": True, 

701 "shared_primitives": [ 

702 "select", 

703 "branch", 

704 "guard", 

705 "implement", 

706 "classify", 

707 "ci", 

708 "review", 

709 "test", 

710 "fixloop", 

711 "merge", 

712 "capture", 

713 "close", 

714 ], 

715 "step_overrides": {}, 

716 } 

717 return { 

718 "name": command, 

719 "profile": "adapter", 

720 "inherits": None, 

721 "first_class_variant": False, 

722 "shared_primitives": [], 

723 "step_overrides": {}, 

724 } 

725 

726 

727def command_side_effects( 

728 command: str, 

729 config: cfg.ProjectConfig, 

730 requirement: runtime.CapabilityRequirement, 

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

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

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

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

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

736 effects.extend(project_command.side_effects) 

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

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

739 for extensions in loaded.values(): 

740 for ext in extensions: 

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

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

743 return tuple(dict.fromkeys(effects)) 

744 

745 

746def reporting_contract_as_dict( 

747 *, 

748 command: str, 

749 config: cfg.ProjectConfig, 

750 transport: github_transport.GitHubTransport | None = None, 

751) -> dict[str, Any]: 

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

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

754 base = { 

755 "command": command, 

756 "base_branch": config.base_branch, 

757 "timezone": config.timezone, 

758 "github_transport": github, 

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

760 "dry_run": { 

761 "mutates": False, 

762 "prints_planned_writes": True, 

763 }, 

764 "handoff": { 

765 "fixes_route_to": "ship", 

766 "auto_applies_fixes": False, 

767 }, 

768 "work_creation_policy": workcreation.contract_as_dict( 

769 near_text_similarity=_near_text_similarity(config), 

770 ), 

771 } 

772 if command == "coverage": 

773 return { 

774 **base, 

775 "target": "pull_request", 

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

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

778 "marker": artifacts.COVERAGE_DELTA_MARKER, 

779 "idempotency": { 

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

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

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

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

784 }, 

785 "labels": { 

786 "regression": "coverage-regression", 

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

788 }, 

789 "degradation": { 

790 "unwired_tool": "skip-area", 

791 "coverage_command_failure": "fatal", 

792 }, 

793 "arguments": ["pr", "--base", "--threshold", "--changed", "--open-issues", "--dry-run"], 

794 } 

795 if command == "deps-audit": 

796 return { 

797 **base, 

798 "target": "daily_tracking_issue", 

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

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

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

802 "marker": artifacts.DEPS_AUDIT_MARKER, 

803 "idempotency": { 

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

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

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

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

808 }, 

809 "degradation": { 

810 "per_ecosystem_failure": "skipped-section", 

811 "argument_failure": "fatal", 

812 }, 

813 "arguments": [ 

814 "ecosystem", 

815 "--severity", 

816 "--security-only", 

817 "--open-issues", 

818 "--dry-run", 

819 ], 

820 } 

821 if command == "flake-audit": 

822 return { 

823 **base, 

824 "target": "ci_history_or_local_runs", 

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

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

827 "marker": artifacts.FLAKE_AUDIT_MARKER, 

828 "idempotency": { 

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

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

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

832 }, 

833 "classification": { 

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

835 "minimum_failures": 3, 

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

837 }, 

838 "degradation": { 

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

840 "no_ci_or_local_gate": "clean-exit", 

841 }, 

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

843 } 

844 return base 

845 

846 

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

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

849 return { 

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

851 "extends": config.extends, 

852 "core_version": config.core_version, 

853 "base_branch": config.base_branch, 

854 "owner": config.owner, 

855 "repo": config.repo, 

856 "platform": config.platform, 

857 "timezone": config.timezone, 

858 "merge_window": config.merge_window, 

859 "merge_window_mode": config.merge_window_mode, 

860 "extensions_dir": config.extensions_dir, 

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

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

863 "policy_pack": config.policy_pack, 

864 "knobs": { 

865 "build_gate_cmd": config.knobs.build_gate_cmd, 

866 "lint_cmd": config.knobs.lint_cmd, 

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

868 **cfg.delegate_profiles_dict(config), 

869 **team.canonical(config.knobs.team), 

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

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

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

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

874 "sot_doc": config.knobs.sot_doc, 

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

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

877 }, 

878 } 

879 

880 

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

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

883 return asdict(spec) 

884 

885 

886def gate_outcome_as_dict(outcome: gates.GateOutcome) -> dict[str, Any]: 

887 """One gate outcome as ``keel ship --json`` and ``keel run-gates --json`` publish it. 

888 

889 The severity and whether the gate ran at all travel with it (#1165): ``keel loop 

890 brief`` reads these documents, and without them a failing *soft* gate would hold the 

891 loop open and an unrun blocking gate would read as a pass. ``unconfigured`` says the 

892 gate cannot judge at all — no command, or no gate planned — which the loop stops on 

893 at once rather than iterating against (#1364). 

894 """ 

895 return { 

896 "gate": outcome.gate, 

897 "ok": outcome.ok, 

898 "skipped": outcome.skipped, 

899 "timed_out": outcome.timed_out, 

900 "on_fail": outcome.on_fail, 

901 "not_run": outcome.not_run, 

902 "unconfigured": outcome.unconfigured, 

903 "error": outcome.error, 

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

905 **( 

906 {"reused_from": outcome.reused_from.as_dict()} 

907 if outcome.reused_from is not None 

908 else {} 

909 ), 

910 } 

911 

912 

913def extension_hooks_as_dict( 

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

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

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

917 return { 

918 slot: [ 

919 { 

920 "id": ext.id, 

921 "slot": ext.slot, 

922 "kind": ext.kind, 

923 "mode": ext.mode, 

924 "agent": ext.agent, 

925 "on_fail": ext.on_fail, 

926 "anchorable": ext.anchorable, 

927 "source": ext.source, 

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

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

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

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

932 } 

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

934 ] 

935 for slot in model.SLOTS 

936 } 

937 

938 

939def ship_result_as_dict( 

940 *, 

941 changed_files: list[str] | None, 

942 outcomes: list[gates.GateOutcome], 

943 verdict, 

944 assessment, 

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

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

947 jury_consensus: str | None = None, 

948) -> dict[str, Any]: 

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

950 

951 ``jury_consensus`` is the panel's own verdict (the chair's, as 

952 :func:`keel.jury.jury_verdict` reports it) when the caller has one; the 

953 ``jury_verdict_template`` renders it, and renders 

954 :data:`keel.artifacts.JURY_CONSENSUS_PLACEHOLDER` — which the evidence gate reads as 

955 no approval — when it has none (#1429). 

956 """ 

957 closure_comment = None 

958 issue_number = None 

959 pr_number = None 

960 head_sha = None 

961 run_id = None 

962 implementer_attribution = None 

963 if isinstance(run_ledger, dict): 

964 record = run_ledger.get("record") 

965 if isinstance(record, dict): 

966 closure_comment = closure.render_closure_comment(record) 

967 issue = record.get("issue") 

968 pull_request = record.get("pull_request") 

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

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

971 head_sha = record.get("head_sha") 

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

973 run_id = record.get("run_id") 

974 run_id = run_id if isinstance(run_id, str) else None 

975 # Derived from the record, never from a caller-supplied string: the 

976 # provenance comment and the evidence cross-check must read the same 

977 # implementer through the same helper (#1013). 

978 implementer_attribution = agents.attribution_from_implementer( 

979 evidence.ledger_implementer(record) 

980 ) 

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

982 testing = _testing_summary(outcomes) 

983 artifact_bodies = { 

984 "pr_body": artifacts.render_pr_body( 

985 issue_number=issue_number, 

986 issue_intake=issue_intake, 

987 changed_files=changed_files, 

988 testing=testing, 

989 docs_impact=_docs_impact(changed_files), 

990 ), 

991 "issue_update": artifacts.render_issue_update( 

992 issue_number=issue_number, 

993 pull_request=pr_number, 

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

995 summary=assessment.merge.reason, 

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

997 if not verdict.blocked 

998 else "Resolve blocking findings before merge.", 

999 ), 

1000 "review_verdict_template": artifacts.render_review_verdict( 

1001 reviewer="reviewer", 

1002 head_sha=head_sha, 

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

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

1005 findings=finding_dicts, 

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

1007 ), 

1008 # The consensus is the panel's, never this run's gate verdict (#1429): an unblocked 

1009 # ship says nothing about what the panel concluded, and a template that wrote 

1010 # `LGTM` there approved for a panel that may have rejected the change. Without a 

1011 # consensus it carries a placeholder the evidence gate reads as no approval. 

1012 "jury_verdict_template": artifacts.render_jury_verdict( 

1013 head_sha=head_sha, 

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

1015 verdict=jury_consensus or artifacts.JURY_CONSENSUS_PLACEHOLDER, 

1016 findings_summary=_finding_summaries(finding_dicts), 

1017 remaining_risks=( 

1018 ship_decisions.block_reason(verdict) if verdict.blocked else "none identified" 

1019 ), 

1020 ), 

1021 "extension_result_template": artifacts.render_extension_result( 

1022 slot="<slot>", 

1023 extension_id="<extension-id>", 

1024 status="not-run", 

1025 mode="advisory", 

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

1027 ), 

1028 "ship_provenance": artifacts.render_ship_provenance( 

1029 run_id=run_id, 

1030 issue=issue_number, 

1031 head_sha=head_sha, 

1032 implementer_attribution=implementer_attribution, 

1033 ), 

1034 } 

1035 return { 

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

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

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

1039 "changed_files_unreadable": changed_files is None, 

1040 "issue_intake": issue_intake, 

1041 "run_ledger": run_ledger, 

1042 "closure_comment": closure_comment, 

1043 "artifact_bodies": artifact_bodies, 

1044 "gate_outcomes": [gate_outcome_as_dict(outcome) for outcome in outcomes], 

1045 "verdict": { 

1046 "blocked": verdict.blocked, 

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

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

1049 }, 

1050 "assessment": { 

1051 "tier": assessment.tier, 

1052 "reviewers": assessment.reviewers, 

1053 "window_open": assessment.window_open, 

1054 "ci_ok": assessment.ci_ok, 

1055 "merge": { 

1056 "action": assessment.merge.action, 

1057 "reason": assessment.merge.reason, 

1058 }, 

1059 "halted": assessment.halted, 

1060 "bypassed_window": assessment.bypassed_window, 

1061 "review_merge_contract": assessment.review_contract, 

1062 "assignment": assessment.assignment, 

1063 }, 

1064 } 

1065 

1066 

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

1068 if not outcomes: 

1069 return [] 

1070 lines: list[str] = [] 

1071 for outcome in outcomes: 

1072 if outcome.skipped: 

1073 state = "skipped" 

1074 elif outcome.ok: 

1075 state = "passed" 

1076 elif outcome.timed_out: 

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

1078 state = "timed out" 

1079 else: 

1080 state = "failed" 

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

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

1083 return lines 

1084 

1085 

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

1087 if changed_files is None: 

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

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

1090 if docs: 

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

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

1093 

1094 

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

1096 lowered = file.lower() 

1097 return ( 

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

1099 or lowered.startswith("docs/") 

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

1101 ) 

1102 

1103 

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

1105 return [ 

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

1107 for finding in findings 

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

1109 ] 

1110 

1111 

1112def standalone_result_as_dict( 

1113 *, 

1114 command: str, 

1115 config: cfg.ProjectConfig, 

1116 target: str | None = None, 

1117 delegate: str | None = None, 

1118 transport: github_transport.GitHubTransport | None = None, 

1119 evaluation: runtime.CapabilityEvaluation | None = None, 

1120 delegation: dict[str, Any] | None = None, 

1121) -> dict[str, Any]: 

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

1123 if command == "implement": 

1124 issue_id = _target_identifier(target) 

1125 return { 

1126 "command": command, 

1127 "target": target, 

1128 "base_branch": config.base_branch, 

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

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

1131 "implementer": { 

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

1133 "selected": delegate, 

1134 # Sorted here, not in the rule: the contract's ordering is the contract's 

1135 # business. Which spellings name a role is `agents.known_roles`'. 

1136 "routing_keys": sorted(agents.known_roles(config)), 

1137 }, 

1138 "handoff": { 

1139 "opens_pr": True, 

1140 "merges": False, 

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

1142 }, 

1143 } 

1144 if command == "ci-check": 

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

1146 return { 

1147 "command": command, 

1148 "target": target, 

1149 "base_branch": config.base_branch, 

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

1151 "latest_run_context": { 

1152 "limit": 3, 

1153 "selected": "newest available run", 

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

1155 }, 

1156 "diagnostics": { 

1157 "read_only": True, 

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

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

1160 "proposed_fix_count": 1, 

1161 }, 

1162 "github_transport": resolved, 

1163 "routing": { 

1164 "never_direct_merge": True, 

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

1166 }, 

1167 } 

1168 if command == "morning": 

1169 return { 

1170 "command": command, 

1171 "target": target, 

1172 "base_branch": config.base_branch, 

1173 "brief": morning_contract_as_dict( 

1174 config=config, 

1175 evaluation=evaluation, 

1176 transport=transport, 

1177 ), 

1178 "execution": { 

1179 "runs_project_health_commands": False, 

1180 "writes_reports": False, 

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

1182 }, 

1183 } 

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

1185 return { 

1186 "command": command, 

1187 "target": target, 

1188 "base_branch": config.base_branch, 

1189 "session": session_contract_as_dict( 

1190 command=command, 

1191 config=config, 

1192 transport=transport, 

1193 delegation=delegation, 

1194 ), 

1195 "execution": { 

1196 "runs_gates": False, 

1197 "creates_prs": False, 

1198 "merges": False, 

1199 "writes_reports": False, 

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

1201 }, 

1202 } 

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

1204 workflow = feedback_workflow_as_dict(config, command) 

1205 return { 

1206 "command": command, 

1207 "target": target, 

1208 "base_branch": config.base_branch, 

1209 "feedback_workflow": workflow, 

1210 "execution": { 

1211 "commits": False, 

1212 "pushes": False, 

1213 "posts_comments": False, 

1214 "merges": False, 

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

1216 }, 

1217 } 

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

1219 return { 

1220 "command": command, 

1221 "target": target, 

1222 "base_branch": config.base_branch, 

1223 "scan": scan_contract_as_dict( 

1224 command=command, 

1225 config=config, 

1226 transport=transport, 

1227 ), 

1228 "execution": { 

1229 "edits_code": False, 

1230 "pushes": False, 

1231 "merges": False, 

1232 "writes_issues": False, 

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

1234 }, 

1235 } 

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

1237 

1238 

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

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

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

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

1243 return _deep_merge(defaults, policy) 

1244 

1245 

1246def scan_contract_as_dict( 

1247 *, 

1248 command: str, 

1249 config: cfg.ProjectConfig, 

1250 transport: github_transport.GitHubTransport | None = None, 

1251) -> dict[str, Any]: 

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

1253 pack = config.policy_pack or {} 

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

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

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

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

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

1259 base = { 

1260 "timezone": config.timezone, 

1261 "merge_window": config.merge_window, 

1262 "base_branch": config.base_branch, 

1263 "github_transport": github, 

1264 "reports": _report_destinations(reports), 

1265 "project_policy_sources": { 

1266 "scan": "policy_pack.scan", 

1267 "areas": "policy_pack.scan.areas", 

1268 "active_branch_patterns": "policy_pack.scan.active_branch_patterns", 

1269 "risk_globs": "knobs.tier3_globs", 

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

1271 "ci_workflows": "knobs.ci_workflows", 

1272 }, 

1273 "write_safety": { 

1274 "dry_run_no_writes": True, 

1275 "orchestrator_only_writes": True, 

1276 "code_mutation": False, 

1277 "pr_mutation": False, 

1278 "issue_write_requires_consent": True, 

1279 }, 

1280 "dedupe": { 

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

1282 "path_token_boundary": True, 

1283 "type_must_match": True, 

1284 "near_text_similarity": _near_text_similarity(config), 

1285 "open_duplicate": "skip", 

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

1287 "lock": "mkdir", 

1288 }, 

1289 "reviewer_isolation": { 

1290 "parallel": True, 

1291 "no_cross_reading": True, 

1292 "orchestrator_collects_findings": True, 

1293 }, 

1294 "areas": [ 

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

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

1297 if isinstance(paths, list) 

1298 ], 

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

1300 "scan_finding": { 

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

1302 "marker": artifacts.SCAN_FINDING_MARKER, 

1303 }, 

1304 "issue_labels": { 

1305 key: list(value) 

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

1307 if isinstance(value, list) 

1308 }, 

1309 "work_creation_policy": workcreation.contract_as_dict( 

1310 near_text_similarity=_near_text_similarity(config), 

1311 ), 

1312 } 

1313 if command == "regression": 

1314 base["regression"] = { 

1315 "scan_target": { 

1316 "source": "canonical base head", 

1317 "base_branch": config.base_branch, 

1318 "read_only_worktree": True, 

1319 "clean_tree_preflight": True, 

1320 }, 

1321 "scope": { 

1322 "default": "full", 

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

1324 }, 

1325 "confidence_filter": { 

1326 "drop": ["low"], 

1327 "downgrade_blocker_when": "medium-confidence", 

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

1329 }, 

1330 "issue_creation": { 

1331 "one_issue_per_finding": True, 

1332 "route_to": "ship", 

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

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

1335 }, 

1336 "final_report": [ 

1337 "raw_findings", 

1338 "after_confidence_filter", 

1339 "duplicates_skipped", 

1340 "promoted_regressions", 

1341 "issues_opened", 

1342 "ship_handoffs", 

1343 ], 

1344 } 

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

1346 base["review_all_day"] = { 

1347 "span": { 

1348 "timezone": config.timezone, 

1349 "merge_window": config.merge_window, 

1350 "days_are_inclusive_calendar_days": True, 

1351 "n_days_argument_covers_calendar_days": "N+1", 

1352 }, 

1353 "ref_scope": { 

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

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

1356 "remote_refs_default": True, 

1357 "warn_on_stale_fetch": True, 

1358 }, 

1359 "strategy": { 

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

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

1362 }, 

1363 "diff_truncation": { 

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

1365 "boundary": "file", 

1366 }, 

1367 "finding_filter": { 

1368 "skip_minor": True, 

1369 "keep_minor_categories": ["security"], 

1370 "file_categories": [ 

1371 "bug-insert", 

1372 "regression", 

1373 "security", 

1374 "config", 

1375 "test-coverage", 

1376 ], 

1377 }, 

1378 "issue_creation": { 

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

1380 "one_issue_per_serious_finding": True, 

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

1382 }, 

1383 "final_report": [ 

1384 "commit_range", 

1385 "reviewers", 

1386 "findings", 

1387 "duplicates_skipped", 

1388 "issues_opened", 

1389 ], 

1390 } 

1391 return base 

1392 

1393 

1394def session_contract_as_dict( 

1395 *, 

1396 command: str, 

1397 config: cfg.ProjectConfig, 

1398 transport: github_transport.GitHubTransport | None = None, 

1399 delegation: dict[str, Any] | None = None, 

1400) -> dict[str, Any]: 

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

1402 pack = config.policy_pack or {} 

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

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

1405 base = { 

1406 "timezone": config.timezone, 

1407 "merge_window": config.merge_window, 

1408 "merge_window_mode": config.merge_window_mode, 

1409 "base_branch": config.base_branch, 

1410 "github_transport": github, 

1411 "reports": _report_destinations(reports), 

1412 "deferral_queue": _deferral_queue_as_dict(reports), 

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

1414 "project_policy_sources": { 

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

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

1417 "risk_rules": [ 

1418 rule.get("id") for rule in pack.get("risk_rules", []) if isinstance(rule, dict) 

1419 ], 

1420 "source_of_truth_doc": config.knobs.sot_doc, 

1421 }, 

1422 } 

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

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

1425 config=config, 

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

1427 transport=github, 

1428 delegation=delegation, 

1429 ) 

1430 if command == "wrap": 

1431 base["wrap"] = { 

1432 "workspace_preflight": { 

1433 "must_run_from_linked_worktree": True, 

1434 "abort_on_base_branch": True, 

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

1436 }, 

1437 "quality_gates": { 

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

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

1440 }, 

1441 "commit": { 

1442 "format": "conventional-commits", 

1443 "supports_closes_issue": True, 

1444 }, 

1445 "pull_request": { 

1446 "ready_not_draft": True, 

1447 "base_branch": config.base_branch, 

1448 "requires_pr_write": True, 

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

1450 }, 

1451 "recap": { 

1452 "report_key": "session", 

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

1454 "status": "configured" 

1455 if reports.get("session") or reports.get("wrap") 

1456 else "unconfigured", 

1457 }, 

1458 } 

1459 elif command == "work-block": 

1460 base["daytime"] = { 

1461 "mode_source": { 

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

1463 "shared_with_overnight": True, 

1464 }, 

1465 "queue": { 

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

1467 "deterministic_order": ( 

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

1469 ), 

1470 }, 

1471 "ship_handoff": { 

1472 "command": "ship", 

1473 "passes_operator_consent_scope": True, 

1474 "per_issue_worktree": True, 

1475 "refreshes_readiness_between_issues": True, 

1476 }, 

1477 "operator_control": { 

1478 "between_items": True, 

1479 "stop_on_consent_gap": True, 

1480 "stop_on_needs_input": True, 

1481 }, 

1482 "report": { 

1483 "day_key": "session", 

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

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

1486 }, 

1487 } 

1488 elif command == "overnight": 

1489 base["overnight"] = { 

1490 "mode_source": { 

1491 "command": "keel window", 

1492 "shared_with_ship": True, 

1493 "timezone": config.timezone, 

1494 "merge_window": config.merge_window, 

1495 }, 

1496 "merge_policy": { 

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

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

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

1500 "cannot_weaken_core_window": True, 

1501 }, 

1502 "queue": { 

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

1504 "tiers": [ 

1505 "T0-blocker", 

1506 "T1-open-prs", 

1507 "T2-coverage", 

1508 "T3-ci-quality", 

1509 "T4-modernization", 

1510 "T5-docs-backlog", 

1511 ], 

1512 }, 

1513 "ship_handoff": { 

1514 "command": "ship", 

1515 "passes_operator_consent_scope": True, 

1516 "per_issue_worktree": True, 

1517 "shared_work_block_contract": True, 

1518 }, 

1519 "report": { 

1520 "night_key": "overnight", 

1521 "day_key": "session", 

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

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

1524 "status": ( 

1525 "configured" 

1526 if ( 

1527 reports.get("overnight") or reports.get("morning") or reports.get("session") 

1528 ) 

1529 else "unconfigured" 

1530 ), 

1531 }, 

1532 "stop_conditions": [ 

1533 "merge-window-close", 

1534 "time-budget-exhausted", 

1535 "max-items-reached", 

1536 "hard-blocker", 

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

1538 "user-cancelled", 

1539 ], 

1540 } 

1541 return base 

1542 

1543 

1544def morning_contract_as_dict( 

1545 *, 

1546 config: cfg.ProjectConfig, 

1547 evaluation: runtime.CapabilityEvaluation | None = None, 

1548 transport: github_transport.GitHubTransport | None = None, 

1549) -> dict[str, Any]: 

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

1551 pack = config.policy_pack or {} 

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

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

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

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

1556 return { 

1557 "timezone": config.timezone, 

1558 "merge_window": config.merge_window, 

1559 "base_branch": config.base_branch, 

1560 "sections": [ 

1561 { 

1562 "id": "deferrals", 

1563 "source": "shared_queue", 

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

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

1566 }, 

1567 { 

1568 "id": "shipped_since_last_brief", 

1569 "source": "github", 

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

1571 "status": github_status, 

1572 }, 

1573 { 

1574 "id": "github_status", 

1575 "source": "github", 

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

1577 "status": github_status, 

1578 }, 

1579 { 

1580 "id": "project_health", 

1581 "source": "policy_pack.health_providers", 

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

1583 }, 

1584 { 

1585 "id": "ranked_focus", 

1586 "source": "policy_pack + github", 

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

1588 }, 

1589 ], 

1590 "health_providers": [ 

1591 _health_provider_as_dict(name, provider, evaluation) 

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

1593 if isinstance(provider, dict) 

1594 ], 

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

1596 "reports": _report_destinations(reports), 

1597 "deferral_queue": _deferral_queue_as_dict(reports), 

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

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

1600 } 

1601 

1602 

1603def _health_provider_as_dict( 

1604 name: str, 

1605 provider: dict[str, Any], 

1606 evaluation: runtime.CapabilityEvaluation | None, 

1607) -> dict[str, Any]: 

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

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

1610 missing_required = _missing_capabilities(required, evaluation) 

1611 missing_optional = _missing_capabilities(optional, evaluation) 

1612 status = "available" 

1613 if missing_required: 

1614 status = "blocked" 

1615 elif missing_optional: 

1616 status = "unavailable" 

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

1618 status = "unavailable" 

1619 return { 

1620 "name": name, 

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

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

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

1624 "required_capabilities": list(required), 

1625 "optional_capabilities": list(optional), 

1626 "missing_required_capabilities": list(missing_required), 

1627 "missing_optional_capabilities": list(missing_optional), 

1628 "status": status, 

1629 } 

1630 

1631 

1632def _missing_capabilities( 

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

1634 evaluation: runtime.CapabilityEvaluation | None, 

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

1636 if evaluation is None: 

1637 return () 

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

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

1640 

1641 

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

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

1644 if config.knobs.sot_doc: 

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

1646 if reports.get("priorities"): 

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

1648 if config.knobs.ci_workflows: 

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

1650 return sources 

1651 

1652 

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

1654 return { 

1655 key: { 

1656 "path": value, 

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

1658 } 

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

1660 } 

1661 

1662 

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

1664 return { 

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

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

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

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

1669 } 

1670 

1671 

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

1673 pack = config.policy_pack or {} 

1674 policy = pack.get("workflow_policies") 

1675 if not isinstance(policy, dict): 

1676 return {} 

1677 return { 

1678 name: value 

1679 for name, value in policy.items() 

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

1681 } 

1682 

1683 

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

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

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

1687 if isinstance(value, dict): 

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

1689 elif isinstance(value, list): 

1690 merged[key] = list(value) 

1691 else: 

1692 merged[key] = value 

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

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

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

1696 elif isinstance(value, list): 

1697 merged[key] = list(value) 

1698 else: 

1699 merged[key] = value 

1700 return merged 

1701 

1702 

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

1704 value = scan.get(key) 

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

1706 

1707 

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

1709 value = scan.get(key) 

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

1711 

1712 

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

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

1715 

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

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

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

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

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

1721 """ 

1722 pack = config.policy_pack or {} 

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

1724 return _scan_float(scan, "near_text_similarity", workcreation.DEFAULT_NEAR_TEXT_SIMILARITY) 

1725 

1726 

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

1728 if not target: 

1729 return "selected-issue" 

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

1731 if match: 

1732 return match.group(1) 

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

1734 

1735 

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

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

1738 if not path.exists(): 

1739 return [] 

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

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

1742 match = _STEP_RE.match(line) 

1743 if not match: 

1744 continue 

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

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

1747 steps.append( 

1748 { 

1749 "step_id": step_id, 

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

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

1752 "slot": None, 

1753 "source": "adapter", 

1754 } 

1755 ) 

1756 return steps 

1757 

1758 

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

1760 tag = provenance.normalize_tag( 

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

1762 fallback_agent=finding.source, 

1763 step_id="finding", 

1764 ) 

1765 return { 

1766 "severity": finding.severity, 

1767 "message": finding.message, 

1768 "source": finding.source, 

1769 "path": finding.path, 

1770 "line": finding.line, 

1771 "anchorable": finding.anchorable, 

1772 "provenance": tag, 

1773 }