@ccoalm/ccl-skills 0.15.0 → 0.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/assets/marketplace/plugins/ccl-skills/agent-context/session-start.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/ccl-skills.ts +80 -4
- package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/commands/ccl-install-skills.md +16 -4
- package/dist/assets/marketplace/plugins/ccl-skills/scripts/owner-dispatch/owner-dispatch.sh +13 -2
- package/dist/assets/marketplace/plugins/ccl-skills/scripts/owner-dispatch/test.sh +53 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/SKILL.md +2 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/SKILL.md +6 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/development-completion.md +26 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/staged-review-contract.md +37 -13
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/AGENTS.md +5 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/codex_review.sh +77 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/kimi_packet_mcp.py +98 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/parse_cli_review.py +48 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/review_gate.py +237 -17
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_cli_review_wrappers.sh +165 -11
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_kimi_packet_mcp.py +143 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_client_compat.py +572 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +65 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/defect-diagnosis/SKILL.md +3 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/SKILL.md +2 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-agent-delegation/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/SKILL.md +2 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/SKILL.md +7 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/alerting-and-on-call.md +8 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/SKILL.md +3 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/SKILL.md +16 -14
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/dual-sidecar-and-traffic-config-center.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/grpc-authority-workaround.md +40 -83
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/mesh-architecture.md +2 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/retry-timeout-circuit-breaker.md +44 -37
- package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/service-discovery-recipe.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/SKILL.md +7 -7
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/delivery-lifecycle.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/design-review-gate-mechanics.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/pre-final-continuation-gate.md +20 -11
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/refactoring-discipline.md +7 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-scope/SKILL.md +8 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +2 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +17 -17
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/external-practice-controls.md +3 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/harness-patterns-and-eval.md +4 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/resume-paused-delivery.md +3 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +24 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/eval-golden-trace.rb +31 -7
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/impact-chain-gate.rb +74 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/skill-behavior-eval.py +103 -21
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_ai_coding_implementation_gates.sh +83 -48
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_body_compliance_grading.sh +80 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_impact_chain_refscripts.sh +74 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_controlled_escalation_pins.sh +3 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_eval_runtime.py +428 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_extraction_review_state.sh +190 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py +106 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/terminal-cli-dev/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/SKILL.md +3 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/SKILL.md +1 -1
- package/dist/assets/release.json +87 -67
- package/dist/claude-adapter.js +14 -7
- package/dist/codex-host.d.ts +2 -4
- package/dist/codex-host.js +40 -19
- package/dist/host-probe.d.ts +27 -0
- package/dist/host-probe.js +51 -0
- package/dist/opencode-adapter.js +24 -19
- package/dist/operations.js +34 -8
- package/dist/unified.d.ts +1 -1
- package/dist/unified.js +18 -9
- package/package.json +1 -1
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import argparse
|
|
7
|
+
import contextlib
|
|
8
|
+
import copy
|
|
9
|
+
import hashlib
|
|
7
10
|
import importlib.util
|
|
11
|
+
import io
|
|
8
12
|
import json
|
|
9
13
|
import os
|
|
10
14
|
from pathlib import Path
|
|
@@ -12,12 +16,14 @@ import subprocess
|
|
|
12
16
|
import sys
|
|
13
17
|
import tempfile
|
|
14
18
|
import unittest
|
|
19
|
+
from unittest import mock
|
|
15
20
|
|
|
16
21
|
|
|
17
22
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
18
23
|
if str(SCRIPT_DIR) not in sys.path:
|
|
19
24
|
sys.path.insert(0, str(SCRIPT_DIR))
|
|
20
25
|
import review_gate
|
|
26
|
+
import kimi_packet_mcp
|
|
21
27
|
|
|
22
28
|
SPEC = importlib.util.spec_from_file_location(
|
|
23
29
|
"parse_cli_review", SCRIPT_DIR / "parse_cli_review.py"
|
|
@@ -505,5 +511,571 @@ class ReviewClientCompatibilityTest(unittest.TestCase):
|
|
|
505
511
|
self.assertFalse(eligibility)
|
|
506
512
|
|
|
507
513
|
|
|
514
|
+
class CodexPacketAuditTest(unittest.TestCase):
|
|
515
|
+
def setUp(self) -> None:
|
|
516
|
+
temporary = tempfile.TemporaryDirectory()
|
|
517
|
+
self.addCleanup(temporary.cleanup)
|
|
518
|
+
self.packet = Path(temporary.name) / "packet.txt"
|
|
519
|
+
self.packet.write_text("candidate\n", encoding="utf-8")
|
|
520
|
+
result = Path(temporary.name) / "result.txt"
|
|
521
|
+
result.write_text("NO_BLOCKING_FINDINGS\n", encoding="utf-8")
|
|
522
|
+
self.digest = hashlib.sha256(self.packet.read_bytes()).hexdigest()
|
|
523
|
+
self.args = argparse.Namespace(
|
|
524
|
+
client="codex",
|
|
525
|
+
mode="review",
|
|
526
|
+
reviewer_family="openai",
|
|
527
|
+
provider="codex-cli",
|
|
528
|
+
model="",
|
|
529
|
+
result_file=str(result),
|
|
530
|
+
packet=str(self.packet),
|
|
531
|
+
packet_sha256=self.digest,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
@staticmethod
|
|
535
|
+
def completed_events(*items: dict) -> list[dict]:
|
|
536
|
+
return [
|
|
537
|
+
{"type": "thread.started", "thread_id": "synthetic-review"},
|
|
538
|
+
{"type": "turn.started"},
|
|
539
|
+
*items,
|
|
540
|
+
{
|
|
541
|
+
"type": "item.completed",
|
|
542
|
+
"item": {
|
|
543
|
+
"id": "verdict",
|
|
544
|
+
"type": "agent_message",
|
|
545
|
+
"text": "NO_BLOCKING_FINDINGS",
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
{"type": "turn.completed"},
|
|
549
|
+
]
|
|
550
|
+
|
|
551
|
+
@staticmethod
|
|
552
|
+
def read_event() -> dict:
|
|
553
|
+
return {
|
|
554
|
+
"type": "item.completed",
|
|
555
|
+
"item": {
|
|
556
|
+
"id": "read-1",
|
|
557
|
+
"type": "mcp_tool_call",
|
|
558
|
+
"server": "code_review_packet",
|
|
559
|
+
"tool": "read_packet",
|
|
560
|
+
"arguments": {"byte_offset": 0, "max_bytes": 46_000},
|
|
561
|
+
"status": "completed",
|
|
562
|
+
"result": {
|
|
563
|
+
"content": [
|
|
564
|
+
{"type": "text", "text": "PACKET_CHUNK 0:10/10\ncandidate\n"}
|
|
565
|
+
],
|
|
566
|
+
"structured_content": None,
|
|
567
|
+
},
|
|
568
|
+
"error": None,
|
|
569
|
+
},
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
def assert_inconclusive(self, events: list[dict], reason: str | None = None) -> None:
|
|
573
|
+
failure = PARSER.audit_codex(self.args, events)
|
|
574
|
+
self.assertIsNotNone(failure)
|
|
575
|
+
self.assertEqual(failure["status"], "inconclusive")
|
|
576
|
+
if reason is not None:
|
|
577
|
+
self.assertEqual(failure["reason_code"], reason)
|
|
578
|
+
|
|
579
|
+
def test_codex_packet_accepts_verified_read(self) -> None:
|
|
580
|
+
failure = PARSER.audit_codex(
|
|
581
|
+
self.args, self.completed_events(self.read_event())
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
self.assertIsNone(failure)
|
|
585
|
+
|
|
586
|
+
def test_codex_packet_accepts_verified_search(self) -> None:
|
|
587
|
+
event = self.read_event()
|
|
588
|
+
arguments = {"query": "candidate", "byte_offset": 0, "limit": 2}
|
|
589
|
+
response = kimi_packet_mcp.search_packet(self.packet, self.digest, arguments)
|
|
590
|
+
self.assertFalse(response["isError"])
|
|
591
|
+
event["item"].update(tool="search_packet", arguments=arguments)
|
|
592
|
+
event["item"]["result"]["content"] = response["content"]
|
|
593
|
+
|
|
594
|
+
failure = PARSER.audit_codex(self.args, self.completed_events(event))
|
|
595
|
+
|
|
596
|
+
self.assertIsNone(failure)
|
|
597
|
+
|
|
598
|
+
def test_codex_packet_accepts_started_updated_completed_read(self) -> None:
|
|
599
|
+
completed = self.read_event()
|
|
600
|
+
started = copy.deepcopy(completed)
|
|
601
|
+
started["type"] = "item.started"
|
|
602
|
+
started["item"].update(status="in_progress", result=None)
|
|
603
|
+
updated = copy.deepcopy(started)
|
|
604
|
+
updated["type"] = "item.updated"
|
|
605
|
+
|
|
606
|
+
failure = PARSER.audit_codex(
|
|
607
|
+
self.args, self.completed_events(started, updated, completed)
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
self.assertIsNone(failure)
|
|
611
|
+
|
|
612
|
+
def test_codex_inline_accepts_completion_without_tools(self) -> None:
|
|
613
|
+
self.args.packet = None
|
|
614
|
+
self.args.packet_sha256 = None
|
|
615
|
+
|
|
616
|
+
self.assertIsNone(PARSER.audit_codex(self.args, self.completed_events()))
|
|
617
|
+
|
|
618
|
+
def test_codex_inline_rejects_packet_tool_without_binding(self) -> None:
|
|
619
|
+
self.args.packet = None
|
|
620
|
+
self.args.packet_sha256 = None
|
|
621
|
+
|
|
622
|
+
self.assert_inconclusive(
|
|
623
|
+
self.completed_events(self.read_event()), "tool_boundary_violation"
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
def test_codex_packet_rejects_unapproved_tool_surfaces(self) -> None:
|
|
627
|
+
read = self.read_event()["item"]
|
|
628
|
+
items = (
|
|
629
|
+
{**read, "server": "other_packet_server"},
|
|
630
|
+
{**read, "tool": "write_packet"},
|
|
631
|
+
{**read, "tool": "read_file"},
|
|
632
|
+
{
|
|
633
|
+
"id": "shell-1",
|
|
634
|
+
"type": "command_execution",
|
|
635
|
+
"command": "printf synthetic",
|
|
636
|
+
"aggregated_output": "synthetic",
|
|
637
|
+
"status": "completed",
|
|
638
|
+
"exit_code": 0,
|
|
639
|
+
},
|
|
640
|
+
)
|
|
641
|
+
for item in items:
|
|
642
|
+
with self.subTest(item_type=item["type"], tool=item.get("tool")):
|
|
643
|
+
event = {"type": "item.completed", "item": item}
|
|
644
|
+
self.assert_inconclusive(
|
|
645
|
+
self.completed_events(event), "tool_boundary_violation"
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
def test_codex_packet_rejects_forged_read_and_search_results(self) -> None:
|
|
649
|
+
for tool, arguments in (
|
|
650
|
+
("read_packet", {"byte_offset": 0, "max_bytes": 46_000}),
|
|
651
|
+
("search_packet", {"query": "candidate", "byte_offset": 0, "limit": 2}),
|
|
652
|
+
):
|
|
653
|
+
with self.subTest(tool=tool):
|
|
654
|
+
event = self.read_event()
|
|
655
|
+
event["item"].update(tool=tool, arguments=arguments)
|
|
656
|
+
event["item"]["result"]["content"] = [
|
|
657
|
+
{"type": "text", "text": "forged packet contents"}
|
|
658
|
+
]
|
|
659
|
+
self.assert_inconclusive(self.completed_events(event))
|
|
660
|
+
|
|
661
|
+
def test_codex_packet_rejects_changed_digest_bound_file(self) -> None:
|
|
662
|
+
event = self.read_event()
|
|
663
|
+
self.packet.write_text("Candidate\n", encoding="utf-8")
|
|
664
|
+
|
|
665
|
+
self.assert_inconclusive(self.completed_events(event))
|
|
666
|
+
|
|
667
|
+
def test_codex_packet_rejects_missing_read_completion(self) -> None:
|
|
668
|
+
event = self.read_event()
|
|
669
|
+
event["type"] = "item.started"
|
|
670
|
+
event["item"].update(status="in_progress", result=None)
|
|
671
|
+
|
|
672
|
+
self.assert_inconclusive(self.completed_events(event))
|
|
673
|
+
|
|
674
|
+
def test_codex_packet_rejects_malformed_and_extra_arguments(self) -> None:
|
|
675
|
+
cases = (
|
|
676
|
+
("read_packet", None),
|
|
677
|
+
("read_packet", []),
|
|
678
|
+
("read_packet", {"byte_offset": 0}),
|
|
679
|
+
("read_packet", {"byte_offset": 0, "max_bytes": 46_000, "path": "other"}),
|
|
680
|
+
("search_packet", {"query": "candidate", "limit": 2}),
|
|
681
|
+
(
|
|
682
|
+
"search_packet",
|
|
683
|
+
{"query": "candidate", "byte_offset": 0, "limit": 2, "path": "other"},
|
|
684
|
+
),
|
|
685
|
+
)
|
|
686
|
+
for tool, arguments in cases:
|
|
687
|
+
with self.subTest(tool=tool, arguments=arguments):
|
|
688
|
+
event = self.read_event()
|
|
689
|
+
event["item"].update(tool=tool, arguments=arguments)
|
|
690
|
+
self.assert_inconclusive(self.completed_events(event))
|
|
691
|
+
|
|
692
|
+
def test_codex_packet_accepts_verified_argument_error_then_retry(self) -> None:
|
|
693
|
+
retry = self.read_event()
|
|
694
|
+
retry["item"]["id"] = "read-retry"
|
|
695
|
+
error = self.read_event()
|
|
696
|
+
error["item"]["arguments"] = {"byte_offset": 11, "max_bytes": 46_000}
|
|
697
|
+
error["item"]["result"]["content"] = [
|
|
698
|
+
{"type": "text", "text": "packet chunk starts beyond end"}
|
|
699
|
+
]
|
|
700
|
+
|
|
701
|
+
failure = PARSER.audit_codex(self.args, self.completed_events(error, retry))
|
|
702
|
+
|
|
703
|
+
self.assertIsNone(failure)
|
|
704
|
+
|
|
705
|
+
def test_codex_packet_transport_failure_stays_inconclusive_after_retry(self) -> None:
|
|
706
|
+
for status, result, error in (
|
|
707
|
+
("failed", None, {"message": "transport closed"}),
|
|
708
|
+
("completed", None, {"message": "transport closed"}),
|
|
709
|
+
("completed", None, None),
|
|
710
|
+
):
|
|
711
|
+
with self.subTest(status=status, error=error):
|
|
712
|
+
failed = self.read_event()
|
|
713
|
+
failed["item"].update(status=status, result=result, error=error)
|
|
714
|
+
retry = self.read_event()
|
|
715
|
+
retry["item"]["id"] = "read-retry"
|
|
716
|
+
self.assert_inconclusive(self.completed_events(failed, retry))
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
class CompletionFindingDispositionTest(unittest.TestCase):
|
|
720
|
+
"""Exercise real receipt and completion validation without external models."""
|
|
721
|
+
|
|
722
|
+
def setUp(self) -> None:
|
|
723
|
+
temporary = tempfile.TemporaryDirectory()
|
|
724
|
+
self.addCleanup(temporary.cleanup)
|
|
725
|
+
self.root = Path(temporary.name)
|
|
726
|
+
self.packet = self.root / "candidate.patch"
|
|
727
|
+
self.packet.write_text(
|
|
728
|
+
"diff --git a/x b/x\n--- a/x\n+++ b/x\n@@ -1 +1 @@\n-a\n+b\n",
|
|
729
|
+
encoding="utf-8",
|
|
730
|
+
)
|
|
731
|
+
self.plan = self.root / "review-plan.json"
|
|
732
|
+
self.write_json(self.plan, {
|
|
733
|
+
"intent": "Preserve bounded completion after source refutation.",
|
|
734
|
+
"acceptance": ["Every finding occurrence is resolved without adding review authority."],
|
|
735
|
+
"self_review": [
|
|
736
|
+
{"concern": concern,
|
|
737
|
+
"conclusion": f"The synthetic completion fixture preserves {concern} boundaries.",
|
|
738
|
+
"evidence_refs": ["fixture"]}
|
|
739
|
+
for concern in ("correctness", "safety", "failure_paths", "tests_evidence", "compatibility")
|
|
740
|
+
],
|
|
741
|
+
"evidence": [{"id": "fixture", "result": "Synthetic exact-candidate completion and history fixture."}],
|
|
742
|
+
})
|
|
743
|
+
self.dispositions = self.root / "dispositions.json"
|
|
744
|
+
self.record_receipts()
|
|
745
|
+
|
|
746
|
+
def record_receipts(self, final_status: str = "findings", *,
|
|
747
|
+
duplicate_findings: bool = False, distinct_finding: bool = False) -> None:
|
|
748
|
+
self.final_review_status = final_status
|
|
749
|
+
self.duplicate_findings = duplicate_findings
|
|
750
|
+
self.distinct_finding = distinct_finding
|
|
751
|
+
self.provider_calls: list[list[str]] = []
|
|
752
|
+
self.profiles: list[dict] = []
|
|
753
|
+
self.receipt_paths = [self.root / "review.json", self.root / "challenge.json"]
|
|
754
|
+
self.receipts = []
|
|
755
|
+
for index, mode in enumerate(("review", "challenge"), 1):
|
|
756
|
+
extra = ["--review-chain-id", "source-refutation-fixture",
|
|
757
|
+
"--autonomous-review-index", str(index)]
|
|
758
|
+
if mode == "challenge":
|
|
759
|
+
extra.extend(["--challenge-index", "1", "--focus", "source-boundary",
|
|
760
|
+
"--prior-review-result-file", str(self.receipt_paths[0])])
|
|
761
|
+
code, result = self.invoke(mode, extra)
|
|
762
|
+
self.assertEqual(code, 0, result)
|
|
763
|
+
self.assertEqual(result["status"], "findings" if mode == "review" else final_status, result)
|
|
764
|
+
self.receipts.append(result)
|
|
765
|
+
self.write_json(self.receipt_paths[index - 1], result)
|
|
766
|
+
self.original_receipt_bytes = [path.read_bytes() for path in self.receipt_paths]
|
|
767
|
+
self.refresh_dispositions()
|
|
768
|
+
|
|
769
|
+
@staticmethod
|
|
770
|
+
def write_json(path: Path, value: object) -> None:
|
|
771
|
+
path.write_text(json.dumps(value, ensure_ascii=False, sort_keys=True), encoding="utf-8")
|
|
772
|
+
|
|
773
|
+
@staticmethod
|
|
774
|
+
def digest(path: Path) -> str:
|
|
775
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
776
|
+
|
|
777
|
+
@staticmethod
|
|
778
|
+
def finding_digest(finding: dict) -> str:
|
|
779
|
+
canonical = json.dumps(finding, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
780
|
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
781
|
+
|
|
782
|
+
def wrapper_result(self, command: list[str], **kwargs: object) -> subprocess.CompletedProcess:
|
|
783
|
+
# Only the provider boundary is replaced. Packet, profile, chain and
|
|
784
|
+
# completion validation execute normally; no subprocess is launched.
|
|
785
|
+
self.assertTrue(command[0].endswith("kimi_review.sh"), command[0])
|
|
786
|
+
self.provider_calls.append(list(command))
|
|
787
|
+
profile_path = Path(command[command.index("--review-profile-file") + 1])
|
|
788
|
+
profile = json.loads(profile_path.read_text(encoding="utf-8"))
|
|
789
|
+
self.profiles.append(profile)
|
|
790
|
+
mode = command[command.index("--mode") + 1]
|
|
791
|
+
payload = {
|
|
792
|
+
"reviewer": "kimi", "reviewer_family": "moonshot", "provider": "kimi-cli",
|
|
793
|
+
"model": "synthetic", "mode": mode, "status": "findings",
|
|
794
|
+
"native_skill_binding": "established",
|
|
795
|
+
"concern_results": [
|
|
796
|
+
{"concern": item["id"], "conclusion": f"Checked {item['id']} against the frozen candidate."}
|
|
797
|
+
for item in profile["required_concerns"]
|
|
798
|
+
],
|
|
799
|
+
"findings": [{"severity": "P1", "file": "x", "line": 1,
|
|
800
|
+
"failure_path": "Synthetic missing guard before an action.",
|
|
801
|
+
"smallest_fix": "Check the guard before the synthetic action."}],
|
|
802
|
+
}
|
|
803
|
+
if mode == "challenge" and self.final_review_status == "passed":
|
|
804
|
+
payload.update(status="passed", findings=[])
|
|
805
|
+
elif self.duplicate_findings:
|
|
806
|
+
payload["findings"].append(dict(reversed(list(payload["findings"][0].items()))))
|
|
807
|
+
if self.distinct_finding:
|
|
808
|
+
payload["findings"].append({**payload["findings"][0],
|
|
809
|
+
"failure_path": "A distinct synthetic failure on the same line."})
|
|
810
|
+
return subprocess.CompletedProcess(command, 0, json.dumps(payload).encode("utf-8"), b"")
|
|
811
|
+
|
|
812
|
+
def arguments(self, mode: str) -> list[str]:
|
|
813
|
+
return ["--mode", mode, "--cwd", str(self.root), "--diff-file", str(self.packet),
|
|
814
|
+
"--implementer-family", "openai", "--review-plan-file", str(self.plan),
|
|
815
|
+
"--challenge-budget", "1"]
|
|
816
|
+
|
|
817
|
+
def invoke(self, mode: str, extra: list[str]) -> tuple[int, dict]:
|
|
818
|
+
output = io.StringIO()
|
|
819
|
+
with (mock.patch.object(review_gate, "run", side_effect=self.wrapper_result),
|
|
820
|
+
mock.patch.object(review_gate, "client_order", return_value=["kimi"]),
|
|
821
|
+
contextlib.redirect_stdout(output)):
|
|
822
|
+
code = review_gate.main(self.arguments(mode) + extra)
|
|
823
|
+
return code, json.loads(output.getvalue())
|
|
824
|
+
|
|
825
|
+
def refresh_dispositions(self) -> dict:
|
|
826
|
+
hashes = [self.digest(path) for path in self.receipt_paths]
|
|
827
|
+
receipts = [json.loads(path.read_text(encoding="utf-8")) for path in self.receipt_paths]
|
|
828
|
+
manifest = {
|
|
829
|
+
"schema_version": 1,
|
|
830
|
+
"candidate_sha256": receipts[-1]["candidate_sha256"],
|
|
831
|
+
"review_result_sha256": hashes,
|
|
832
|
+
"dispositions": [
|
|
833
|
+
{"receipt_sha256": receipt_hash,
|
|
834
|
+
"finding_sha256": self.finding_digest(finding),
|
|
835
|
+
"disposition": "source_refuted",
|
|
836
|
+
"evidence": ["Synthetic source x:1 checks the guard before the action; the reported path is unreachable."]}
|
|
837
|
+
for receipt_hash, receipt in zip(hashes, receipts)
|
|
838
|
+
for finding in {self.finding_digest(item): item for item in receipt["findings"]}.values()
|
|
839
|
+
],
|
|
840
|
+
}
|
|
841
|
+
self.write_json(self.dispositions, manifest)
|
|
842
|
+
return manifest
|
|
843
|
+
|
|
844
|
+
def complete(self, *, include_prior: bool = True) -> tuple[int, dict]:
|
|
845
|
+
extra = ["--completion-review-result-file", str(self.receipt_paths[-1]),
|
|
846
|
+
"--finding-dispositions-file", str(self.dispositions)]
|
|
847
|
+
if include_prior:
|
|
848
|
+
extra.extend(["--prior-review-result-file", str(self.receipt_paths[0])])
|
|
849
|
+
result = self.invoke("complete", extra)
|
|
850
|
+
self.assertEqual(len(self.provider_calls), 2, "completion must never call a reviewer")
|
|
851
|
+
return result
|
|
852
|
+
|
|
853
|
+
def assert_rejected(self, *, include_prior: bool = True) -> dict:
|
|
854
|
+
code, result = self.complete(include_prior=include_prior)
|
|
855
|
+
self.assertEqual(code, 2, result)
|
|
856
|
+
self.assertEqual(result["status"], "inconclusive", result)
|
|
857
|
+
self.assertTrue(result["completion_gated"], result)
|
|
858
|
+
return result
|
|
859
|
+
|
|
860
|
+
def test_source_refuted_findings_validate_bound_dispositions(self) -> None:
|
|
861
|
+
# Keep the narrower validator exercised independently of CLI parsing.
|
|
862
|
+
args = review_gate.build_parser().parse_args(self.arguments("complete") + [
|
|
863
|
+
"--completion-review-result-file", str(self.receipt_paths[-1])])
|
|
864
|
+
args.finding_dispositions_file = str(self.dispositions)
|
|
865
|
+
args.prior_review_result_file = [str(self.receipt_paths[0])]
|
|
866
|
+
result_hash, prior, metadata = review_gate.validate_finding_dispositions(
|
|
867
|
+
args, self.receipts[-1]["candidate_sha256"], self.profiles[-1])
|
|
868
|
+
self.assertEqual(result_hash, self.digest(self.receipt_paths[-1]))
|
|
869
|
+
self.assertEqual(prior["findings"], self.receipts[-1]["findings"])
|
|
870
|
+
self.assertEqual(metadata["completion_basis"], "source_refuted_findings")
|
|
871
|
+
|
|
872
|
+
def test_two_refuted_rounds_complete_without_new_review_or_history_reset(self) -> None:
|
|
873
|
+
code, result = self.complete()
|
|
874
|
+
self.assertEqual(code, 0, result)
|
|
875
|
+
self.assertEqual(result["status"], "passed")
|
|
876
|
+
self.assertFalse(result["completion_gated"])
|
|
877
|
+
self.assertEqual(result["completion_basis"], "source_refuted_findings")
|
|
878
|
+
self.assertEqual(result["finding_dispositions_sha256"], self.digest(self.dispositions))
|
|
879
|
+
manifest = json.loads(self.dispositions.read_text(encoding="utf-8"))
|
|
880
|
+
# A repeated finding in a later round is a separate occurrence even
|
|
881
|
+
# when its canonical finding hash is identical.
|
|
882
|
+
self.assertEqual(len({item["finding_sha256"] for item in manifest["dispositions"]}), 1)
|
|
883
|
+
self.assertEqual(len({item["receipt_sha256"] for item in manifest["dispositions"]}), 2)
|
|
884
|
+
self.assertEqual(result["resolved_finding_occurrences"], [
|
|
885
|
+
{key: item[key] for key in ("receipt_sha256", "finding_sha256")}
|
|
886
|
+
for item in manifest["dispositions"]])
|
|
887
|
+
for field in ("review_chain_id", "review_scope_sha256", "prior_review_result_sha256",
|
|
888
|
+
"prior_challenge_focuses", "autonomous_review_budget", "autonomous_review_index",
|
|
889
|
+
"autonomous_reviews_remaining"):
|
|
890
|
+
self.assertEqual(result[field], self.receipts[-1][field], field)
|
|
891
|
+
self.assertEqual(result["autonomous_review_budget"], 2)
|
|
892
|
+
self.assertEqual(result["autonomous_review_index"], 2)
|
|
893
|
+
self.assertEqual(result["autonomous_reviews_remaining"], 0)
|
|
894
|
+
self.assertFalse(result["autonomous_review_allowed"])
|
|
895
|
+
self.assertEqual(result["next_action"], "complete")
|
|
896
|
+
self.assertEqual([path.read_bytes() for path in self.receipt_paths], self.original_receipt_bytes)
|
|
897
|
+
|
|
898
|
+
def test_dispositions_file_is_required_and_must_exist(self) -> None:
|
|
899
|
+
code, result = self.invoke("complete", [
|
|
900
|
+
"--completion-review-result-file", str(self.receipt_paths[-1])])
|
|
901
|
+
self.assertEqual(code, 2, result)
|
|
902
|
+
self.assertTrue(result["completion_gated"], result)
|
|
903
|
+
self.dispositions.unlink()
|
|
904
|
+
self.assert_rejected()
|
|
905
|
+
|
|
906
|
+
def test_identical_findings_share_identity_without_rewriting_receipts(self) -> None:
|
|
907
|
+
for final_status in ("findings", "passed"):
|
|
908
|
+
with self.subTest(final_status=final_status):
|
|
909
|
+
self.record_receipts(final_status, duplicate_findings=True)
|
|
910
|
+
self.assertEqual(len(self.receipts[0]["findings"]), 2)
|
|
911
|
+
code, result = self.complete()
|
|
912
|
+
self.assertEqual(code, 0, result)
|
|
913
|
+
self.assertEqual(len(result["resolved_finding_occurrences"]),
|
|
914
|
+
2 if final_status == "findings" else 1)
|
|
915
|
+
self.assertEqual([path.read_bytes() for path in self.receipt_paths],
|
|
916
|
+
self.original_receipt_bytes)
|
|
917
|
+
|
|
918
|
+
def test_duplicate_identity_never_hides_a_distinct_finding_or_round(self) -> None:
|
|
919
|
+
self.record_receipts(duplicate_findings=True, distinct_finding=True)
|
|
920
|
+
manifest = self.refresh_dispositions()
|
|
921
|
+
self.assertEqual([len(row["findings"]) for row in self.receipts], [3, 3])
|
|
922
|
+
self.assertEqual(len(manifest["dispositions"]), 4)
|
|
923
|
+
code, result = self.complete()
|
|
924
|
+
self.assertEqual(code, 0, result)
|
|
925
|
+
self.assertEqual(len(result["resolved_finding_occurrences"]), 4)
|
|
926
|
+
for missing in range(4):
|
|
927
|
+
with self.subTest(missing_occurrence=missing):
|
|
928
|
+
incomplete = copy.deepcopy(manifest)
|
|
929
|
+
del incomplete["dispositions"][missing]
|
|
930
|
+
self.write_json(self.dispositions, incomplete)
|
|
931
|
+
self.assert_rejected()
|
|
932
|
+
|
|
933
|
+
def test_passed_final_still_requires_valid_historical_finding_dispositions(self) -> None:
|
|
934
|
+
self.record_receipts(final_status="passed")
|
|
935
|
+
code, result = self.complete()
|
|
936
|
+
self.assertEqual(code, 0, result)
|
|
937
|
+
self.assertEqual(result["completion_basis"], "source_refuted_findings")
|
|
938
|
+
self.assertEqual(len(result["resolved_finding_occurrences"]), 1)
|
|
939
|
+
self.assertEqual(result["resolved_finding_occurrences"][0]["receipt_sha256"],
|
|
940
|
+
self.digest(self.receipt_paths[0]))
|
|
941
|
+
manifest = self.refresh_dispositions()
|
|
942
|
+
manifest["dispositions"][0]["disposition"] = "accepted_risk"
|
|
943
|
+
self.write_json(self.dispositions, manifest)
|
|
944
|
+
rejected = self.assert_rejected()
|
|
945
|
+
self.assertEqual(rejected["next_action"], "resolve_review_findings")
|
|
946
|
+
manifest["dispositions"] = []
|
|
947
|
+
self.write_json(self.dispositions, manifest)
|
|
948
|
+
self.assert_rejected()
|
|
949
|
+
|
|
950
|
+
def test_every_historical_finding_occurrence_must_be_covered_once(self) -> None:
|
|
951
|
+
original = self.refresh_dispositions()
|
|
952
|
+
for missing in (0, 1):
|
|
953
|
+
with self.subTest(missing_round=missing):
|
|
954
|
+
manifest = copy.deepcopy(original)
|
|
955
|
+
del manifest["dispositions"][missing]
|
|
956
|
+
self.write_json(self.dispositions, manifest)
|
|
957
|
+
self.assert_rejected()
|
|
958
|
+
manifest = copy.deepcopy(original)
|
|
959
|
+
manifest["dispositions"].append(copy.deepcopy(manifest["dispositions"][0]))
|
|
960
|
+
self.write_json(self.dispositions, manifest)
|
|
961
|
+
self.assert_rejected()
|
|
962
|
+
|
|
963
|
+
def test_disposition_candidate_receipt_and_finding_hashes_are_bound(self) -> None:
|
|
964
|
+
original = self.refresh_dispositions()
|
|
965
|
+
for field in ("candidate_sha256", "review_result_sha256", "receipt_sha256", "finding_sha256"):
|
|
966
|
+
with self.subTest(field=field):
|
|
967
|
+
manifest = copy.deepcopy(original)
|
|
968
|
+
if field == "review_result_sha256":
|
|
969
|
+
manifest[field][0] = "f" * 64
|
|
970
|
+
elif field == "candidate_sha256":
|
|
971
|
+
manifest[field] = "f" * 64
|
|
972
|
+
else:
|
|
973
|
+
manifest["dispositions"][0][field] = "f" * 64
|
|
974
|
+
self.write_json(self.dispositions, manifest)
|
|
975
|
+
self.assert_rejected()
|
|
976
|
+
|
|
977
|
+
def test_unresolved_or_accepted_risk_cannot_complete(self) -> None:
|
|
978
|
+
original = self.refresh_dispositions()
|
|
979
|
+
for disposition in ("unresolved", "accepted_risk", "accepted_tradeoff", "needs_human_decision"):
|
|
980
|
+
with self.subTest(disposition=disposition):
|
|
981
|
+
manifest = copy.deepcopy(original)
|
|
982
|
+
manifest["dispositions"][0]["disposition"] = disposition
|
|
983
|
+
self.write_json(self.dispositions, manifest)
|
|
984
|
+
self.assert_rejected()
|
|
985
|
+
|
|
986
|
+
def test_source_refutation_requires_bounded_nonempty_evidence(self) -> None:
|
|
987
|
+
original = self.refresh_dispositions()
|
|
988
|
+
for evidence in (None, [], [""], [" \n"], "source x:1", [123], ["x" * 100_000]):
|
|
989
|
+
with self.subTest(evidence_type=type(evidence).__name__, size=len(str(evidence))):
|
|
990
|
+
manifest = copy.deepcopy(original)
|
|
991
|
+
if evidence is None:
|
|
992
|
+
del manifest["dispositions"][0]["evidence"]
|
|
993
|
+
else:
|
|
994
|
+
manifest["dispositions"][0]["evidence"] = evidence
|
|
995
|
+
self.write_json(self.dispositions, manifest)
|
|
996
|
+
self.assert_rejected()
|
|
997
|
+
|
|
998
|
+
def test_evidence_strings_match_downstream_normalization_and_text_boundary(self) -> None:
|
|
999
|
+
original = self.refresh_dispositions()
|
|
1000
|
+
for text in (" leading space", "trailing space ", "line\nbreak", "tab\tinside",
|
|
1001
|
+
"NUL\0inside", "DEL\x7finside", "C1\x85inside", "line\u2028separator",
|
|
1002
|
+
"paragraph\u2029separator", "zero\u200bwidth", "bidi\u202econtrol", "bad\ud800unicode"):
|
|
1003
|
+
with self.subTest(text=ascii(text)):
|
|
1004
|
+
manifest = copy.deepcopy(original)
|
|
1005
|
+
manifest["dispositions"][0]["evidence"] = [text]
|
|
1006
|
+
# Escaped JSON can encode an unpaired surrogate even though the
|
|
1007
|
+
# decoded string cannot be encoded as valid UTF-8 text.
|
|
1008
|
+
self.dispositions.write_text(json.dumps(manifest), encoding="utf-8")
|
|
1009
|
+
self.assert_rejected()
|
|
1010
|
+
|
|
1011
|
+
def test_changed_candidate_cannot_use_previous_refutations(self) -> None:
|
|
1012
|
+
self.packet.write_text(self.packet.read_text(encoding="utf-8").replace("+b\n", "+c\n"), encoding="utf-8")
|
|
1013
|
+
self.assert_rejected()
|
|
1014
|
+
|
|
1015
|
+
def test_stale_historical_candidate_cannot_use_current_refutations(self) -> None:
|
|
1016
|
+
historical = copy.deepcopy(self.receipts[0])
|
|
1017
|
+
historical["candidate_sha256"] = historical["packet_sha256"] = "f" * 64
|
|
1018
|
+
self.write_json(self.receipt_paths[0], historical)
|
|
1019
|
+
final = copy.deepcopy(self.receipts[-1])
|
|
1020
|
+
final["prior_review_result_sha256"][0] = self.digest(self.receipt_paths[0])
|
|
1021
|
+
self.write_json(self.receipt_paths[-1], final)
|
|
1022
|
+
# Refresh every outer receipt reference so stale candidate rejection
|
|
1023
|
+
# cannot be attributed to an incidental raw-receipt hash mismatch.
|
|
1024
|
+
self.refresh_dispositions()
|
|
1025
|
+
self.assert_rejected()
|
|
1026
|
+
|
|
1027
|
+
def test_inconclusive_receipt_cannot_be_refuted_into_a_pass(self) -> None:
|
|
1028
|
+
for index in (0, 1):
|
|
1029
|
+
with self.subTest(round=index):
|
|
1030
|
+
for path, raw in zip(self.receipt_paths, self.original_receipt_bytes):
|
|
1031
|
+
path.write_bytes(raw)
|
|
1032
|
+
receipt = json.loads(self.receipt_paths[index].read_text(encoding="utf-8"))
|
|
1033
|
+
receipt["status"] = "inconclusive"
|
|
1034
|
+
self.write_json(self.receipt_paths[index], receipt)
|
|
1035
|
+
if index == 0:
|
|
1036
|
+
final = copy.deepcopy(self.receipts[-1])
|
|
1037
|
+
final["prior_review_result_sha256"][0] = self.digest(self.receipt_paths[0])
|
|
1038
|
+
self.write_json(self.receipt_paths[-1], final)
|
|
1039
|
+
self.refresh_dispositions()
|
|
1040
|
+
self.assert_rejected()
|
|
1041
|
+
|
|
1042
|
+
def test_forged_or_missing_history_cannot_complete(self) -> None:
|
|
1043
|
+
self.assert_rejected(include_prior=False)
|
|
1044
|
+
receipt = copy.deepcopy(self.receipts[-1])
|
|
1045
|
+
receipt["prior_review_result_sha256"][0] = "f" * 64
|
|
1046
|
+
self.write_json(self.receipt_paths[-1], receipt)
|
|
1047
|
+
self.refresh_dispositions()
|
|
1048
|
+
self.assert_rejected()
|
|
1049
|
+
|
|
1050
|
+
def test_original_receipt_metadata_must_match_the_external_round(self) -> None:
|
|
1051
|
+
for index in (0, 1):
|
|
1052
|
+
for field in ("review_state", "human_decision_required", "gate_required", "gate_triggers"):
|
|
1053
|
+
with self.subTest(round=index, field=field):
|
|
1054
|
+
for path, raw in zip(self.receipt_paths, self.original_receipt_bytes):
|
|
1055
|
+
path.write_bytes(raw)
|
|
1056
|
+
receipt = copy.deepcopy(self.receipts[index])
|
|
1057
|
+
if field == "review_state":
|
|
1058
|
+
receipt[field] = "reviewed"
|
|
1059
|
+
elif field == "human_decision_required":
|
|
1060
|
+
receipt[field] = not receipt[field]
|
|
1061
|
+
elif field == "gate_required":
|
|
1062
|
+
receipt["self_review_gate"]["required"] = False
|
|
1063
|
+
else:
|
|
1064
|
+
receipt["self_review_gate"]["required_triggers"] = []
|
|
1065
|
+
self.write_json(self.receipt_paths[index], receipt)
|
|
1066
|
+
if index == 0:
|
|
1067
|
+
final = copy.deepcopy(self.receipts[-1])
|
|
1068
|
+
final["prior_review_result_sha256"][0] = self.digest(self.receipt_paths[0])
|
|
1069
|
+
self.write_json(self.receipt_paths[-1], final)
|
|
1070
|
+
self.refresh_dispositions()
|
|
1071
|
+
self.assert_rejected()
|
|
1072
|
+
|
|
1073
|
+
def test_receipt_order_is_part_of_the_disposition_binding(self) -> None:
|
|
1074
|
+
manifest = self.refresh_dispositions()
|
|
1075
|
+
manifest["review_result_sha256"].reverse()
|
|
1076
|
+
self.write_json(self.dispositions, manifest)
|
|
1077
|
+
self.assert_rejected()
|
|
1078
|
+
|
|
1079
|
+
|
|
508
1080
|
if __name__ == "__main__":
|
|
509
1081
|
unittest.main()
|
package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh
CHANGED
|
@@ -1233,6 +1233,71 @@ out="$(run_gate --review-plan-file "$WORK/owner-review-plan.json" --allow-fallba
|
|
|
1233
1233
|
check "owner-aware success without a wrapper binding receipt fails closed" \
|
|
1234
1234
|
'[ "$rc" = 2 ] && json_fields "$out" reason_code=binding_mismatch next_action=stop_reviewer_lane && [ "$(printf "%s" "$out" | python3 -c "import json,sys; print(len(json.load(sys.stdin).get(\"reviewed_skills\", [])))")" = 0 ]'
|
|
1235
1235
|
|
|
1236
|
+
# Owner count is independent of concern count: a cross-skill candidate can have
|
|
1237
|
+
# more owners than the eight known concerns without changing the review scope.
|
|
1238
|
+
python3 - "$WORK" <<'PY'
|
|
1239
|
+
import copy
|
|
1240
|
+
import json
|
|
1241
|
+
from pathlib import Path
|
|
1242
|
+
import sys
|
|
1243
|
+
|
|
1244
|
+
work = Path(sys.argv[1])
|
|
1245
|
+
owners = (
|
|
1246
|
+
"requirement-scope", "product-rd-workflow", "platform-service-connectivity",
|
|
1247
|
+
"platform-observability", "skill-extraction-workflow", "python-service-dev",
|
|
1248
|
+
"web-react-dev", "testing-strategy", "terminal-cli-dev",
|
|
1249
|
+
)
|
|
1250
|
+
plan = json.loads((work / "review-plan.json").read_text())
|
|
1251
|
+
patches = []
|
|
1252
|
+
for owner in owners:
|
|
1253
|
+
root = work / owner
|
|
1254
|
+
root.mkdir(exist_ok=True)
|
|
1255
|
+
entrypoint = root / "SKILL.md"
|
|
1256
|
+
if not entrypoint.exists():
|
|
1257
|
+
entrypoint.write_text(f"# Synthetic {owner} owner\n")
|
|
1258
|
+
plan["self_review"].append({
|
|
1259
|
+
"concern": "correctness", "skill": owner,
|
|
1260
|
+
"conclusion": f"The {owner} boundary preserves its stated acceptance behavior.",
|
|
1261
|
+
"evidence_refs": ["e1"],
|
|
1262
|
+
})
|
|
1263
|
+
path = f"skills/{owner}/SKILL.md"
|
|
1264
|
+
patches.append(f"diff --git a/{path} b/{path}\n--- a/{path}\n+++ b/{path}\n@@ -1 +1 @@\n-old\n+new\n")
|
|
1265
|
+
(work / "many-owner-diff.patch").write_text("".join(patches))
|
|
1266
|
+
plans = {"many-owner": plan}
|
|
1267
|
+
missing_owner = copy.deepcopy(plan)
|
|
1268
|
+
missing_owner["self_review"].pop()
|
|
1269
|
+
plans["many-owner-missing-owner"] = missing_owner
|
|
1270
|
+
missing_concern = copy.deepcopy(plan)
|
|
1271
|
+
missing_concern["self_review"] = [row for row in missing_concern["self_review"] if row["concern"] != "safety"]
|
|
1272
|
+
plans["many-owner-missing-concern"] = missing_concern
|
|
1273
|
+
duplicate = copy.deepcopy(plan)
|
|
1274
|
+
duplicate["self_review"].append(copy.deepcopy(duplicate["self_review"][-1]))
|
|
1275
|
+
plans["many-owner-duplicate"] = duplicate
|
|
1276
|
+
default_duplicate = copy.deepcopy(plan)
|
|
1277
|
+
default_duplicate["self_review"].append({**default_duplicate["self_review"][0], "skill": "code-review"})
|
|
1278
|
+
plans["many-owner-default-duplicate"] = default_duplicate
|
|
1279
|
+
for name, value in plans.items():
|
|
1280
|
+
(work / f"{name}-plan.json").write_text(json.dumps(value))
|
|
1281
|
+
PY
|
|
1282
|
+
|
|
1283
|
+
reset_case passed unavailable unavailable
|
|
1284
|
+
out="$(run_gate --diff-file "$WORK/many-owner-diff.patch" --review-plan-file "$WORK/many-owner-plan.json")"; rc=$?
|
|
1285
|
+
check "nine derived owners can assess shared concerns in one explicit plan" \
|
|
1286
|
+
'[ "$rc" = 0 ] && json_fields "$out" status=passed review_plan_source=implementer-supplied owner_selection_source=controller-derived+implementer-declared && [ "$(printf %s "$out" | python3 -c "import json,sys; p=json.load(sys.stdin); print(len(p[\"selected_skills\"]), len(p[\"reviewed_skills\"]))")" = "10 9" ]'
|
|
1287
|
+
printf '%s\n' "$out" >"$WORK/many-owner-review.json"
|
|
1288
|
+
|
|
1289
|
+
reset_case passed unavailable unavailable
|
|
1290
|
+
out="$(run_completion_gate --diff-file "$WORK/many-owner-diff.patch" --review-plan-file "$WORK/many-owner-plan.json" --completion-review-result-file "$WORK/many-owner-review.json")"; rc=$?
|
|
1291
|
+
check "nine-owner explicit self-review closes the exact-candidate completion checkpoint" \
|
|
1292
|
+
'[ "$rc" = 0 ] && [ ! -e "$WORK/state/client_sequence" ] && json_fields "$out" mode=complete status=passed completion_gated=false next_action=complete'
|
|
1293
|
+
|
|
1294
|
+
for invalid_plan in missing-owner missing-concern duplicate default-duplicate; do
|
|
1295
|
+
reset_case passed unavailable unavailable
|
|
1296
|
+
out="$(run_gate --diff-file "$WORK/many-owner-diff.patch" --review-plan-file "$WORK/many-owner-$invalid_plan-plan.json")"; rc=$?
|
|
1297
|
+
check "shared-concern owner plans reject $invalid_plan before provider execution" \
|
|
1298
|
+
'[ "$rc" = 2 ] && [ ! -e "$WORK/state/client_sequence" ] && json_fields "$out" reason_code=self_review_incomplete next_action=deep_self_review'
|
|
1299
|
+
done
|
|
1300
|
+
|
|
1236
1301
|
printf 'diff --git a/skills/testing-strategy/SKILL.md b/skills/testing-strategy/SKILL.md\n--- a/skills/testing-strategy/SKILL.md\n+++ b/skills/testing-strategy/SKILL.md\n@@ -1 +1 @@\n-old\n+new\n' >"$WORK/diff.patch"
|
|
1237
1302
|
reset_case passed unavailable unavailable
|
|
1238
1303
|
out="$(run_gate --allow-fallback-egress)"; rc=$?
|