@ccoalm/ccl-skills 0.9.0 → 0.11.0

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.
Files changed (57) hide show
  1. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/SKILL.md +4 -3
  2. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/staged-review-contract.md +23 -0
  3. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/review_gate.py +178 -4
  4. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +127 -0
  5. package/dist/assets/marketplace/plugins/ccl-skills/skills/defect-diagnosis/SKILL.md +2 -1
  6. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/SKILL.md +1 -1
  7. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/SKILL.md +1 -0
  8. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/SKILL.md +11 -1
  9. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/async-lifecycle-and-performance.md +16 -0
  10. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/source-map.md +1 -0
  11. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/SKILL.md +8 -8
  12. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/delivery-lifecycle.md +1 -1
  13. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/dispatch-owner-skills.md +9 -1
  14. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/problem-resolution-and-learning.md +2 -0
  15. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/SKILL.md +1 -1
  16. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/tag-and-prod-pipeline-gate.md +9 -0
  17. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +17 -20
  18. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/attention-budget-ratchet.md +37 -0
  19. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/description-authoring.md +9 -0
  20. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +26 -44
  21. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/eval-routing.md +24 -3
  22. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/external-practice-controls.md +16 -0
  23. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +5 -5
  24. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/rule-consolidation.md +3 -1
  25. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +59 -0
  26. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +2 -2
  27. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/validation-and-landing.md +3 -1
  28. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +35 -0
  29. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-contract-anchors.sh +126 -0
  30. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-size-budget.sh +197 -1
  31. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/contract-anchors.tsv +16 -0
  32. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/eval-routing-bank.rb +210 -36
  33. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/extraction_review_gate.sh +3 -3
  34. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/gate_receipt.py +576 -0
  35. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/impact-chain-gate.rb +35 -6
  36. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/review_ledger_binding.py +476 -0
  37. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_antipattern_grep_panel.sh +80 -0
  38. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_body_compliance_grading.sh +99 -0
  39. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_impact_chain_refscripts.sh +81 -1
  40. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +28 -0
  41. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_size_budget.sh +251 -0
  42. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_contract_anchors.sh +196 -0
  43. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_eval_routing_bank_grader_diagnostics.sh +222 -0
  44. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_extraction_review_gate.sh +16 -10
  45. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_frozen_case_sanctity.sh +178 -0
  46. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_frozen_case_sanctity_selfproof.sh +108 -0
  47. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_gate_receipt.sh +431 -0
  48. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_pinned_phrase_mutation_walk.sh +151 -0
  49. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_review_ledger_binding.sh +336 -0
  50. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_routing_bank_integrity.sh +86 -5
  51. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_routing_pointer_integrity.sh +41 -1
  52. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_extraction_review_state.sh +141 -28
  53. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py +139 -38
  54. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/SKILL.md +1 -1
  55. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/SKILL.md +9 -8
  56. package/dist/assets/release.json +110 -45
  57. package/package.json +1 -1
@@ -140,11 +140,12 @@ def make_fixture(
140
140
  receipt_mutators=None,
141
141
  completion=True,
142
142
  completion_mutator=None,
143
- budget=2,
143
+ budget=1,
144
144
  base_shas=None,
145
145
  closeout=None,
146
146
  unmatched=0,
147
147
  open_last=False,
148
+ succession=False,
148
149
  ):
149
150
  if receipt_findings is None:
150
151
  receipt_findings = [[finding(1)], []]
@@ -166,11 +167,15 @@ def make_fixture(
166
167
  receipt_hashes = []
167
168
  receipt_refs = []
168
169
  for index, findings in enumerate(receipt_findings, start=1):
169
- mode = "review" if index == 1 else "challenge"
170
+ # With a succession the LAST round opens a second chain: its own chain
171
+ # arithmetic restarts at one while its lane position keeps counting.
172
+ is_succession = succession and index == rounds
173
+ chain_position = 1 if is_succession else index
174
+ mode = "challenge" if is_succession else "review" if index == 1 else "challenge"
170
175
  status = "findings" if findings else "passed"
171
- remaining = 3 - index
176
+ remaining = 2 - chain_position
172
177
  if status == "findings":
173
- state = "post_review_budget" if index == 3 else "findings_pending"
178
+ state = "post_review_budget" if chain_position == 2 else "findings_pending"
174
179
  else:
175
180
  state = "reviewed"
176
181
  receipt = {
@@ -179,16 +184,27 @@ def make_fixture(
179
184
  "status": status,
180
185
  "findings": copy.deepcopy(findings),
181
186
  "review_chain_tracked": True,
182
- "review_chain_id": "extraction-chain",
183
- "autonomous_review_index": index,
184
- "autonomous_review_budget": 3,
187
+ "review_chain_id": (
188
+ "extraction-succession" if is_succession else "extraction-chain"
189
+ ),
190
+ "autonomous_review_index": chain_position,
191
+ "autonomous_review_budget": 2,
185
192
  "autonomous_reviews_remaining": remaining,
186
193
  "autonomous_review_allowed": remaining > 0,
187
- "challenge_index": 0 if index == 1 else index - 1,
194
+ "challenge_index": 1 if is_succession else 0 if index == 1 else index - 1,
188
195
  "challenge_budget": budget,
189
- "prior_review_result_sha256": list(receipt_hashes),
190
- "candidate_sha256": CANDIDATE,
191
- "packet_sha256": CANDIDATE,
196
+ "prior_review_result_sha256": [] if is_succession else list(receipt_hashes),
197
+ "predecessor_chain_id": "extraction-chain" if is_succession else None,
198
+ "predecessor_result_sha256": (
199
+ receipt_hashes[-1] if is_succession and receipt_hashes else None
200
+ ),
201
+ "predecessor_candidate_sha256": OTHER_CANDIDATE if is_succession else None,
202
+ "candidate_sha256": (
203
+ OTHER_CANDIDATE if succession and not is_succession else CANDIDATE
204
+ ),
205
+ "packet_sha256": (
206
+ OTHER_CANDIDATE if succession and not is_succession else CANDIDATE
207
+ ),
192
208
  "review_scope": copy.deepcopy(scope),
193
209
  "review_scope_sha256": scope_sha,
194
210
  "wording_only_proof_sha256": None,
@@ -221,7 +237,7 @@ def make_fixture(
221
237
  "review_chain_tracked": True,
222
238
  "review_chain_id": final["review_chain_id"],
223
239
  "autonomous_review_index": final["autonomous_review_index"],
224
- "autonomous_review_budget": 3,
240
+ "autonomous_review_budget": 2,
225
241
  "autonomous_reviews_remaining": final["autonomous_reviews_remaining"],
226
242
  "autonomous_review_allowed": False,
227
243
  "challenge_budget": budget,
@@ -411,9 +427,9 @@ run("float-schema", float_schema["ledger"], 1, "schema_version must be 3")
411
427
 
412
428
  float_budget = make_fixture(
413
429
  "float-budget",
414
- receipt_mutators={2: lambda row: row.update(challenge_budget=2.0)},
430
+ receipt_mutators={2: lambda row: row.update(challenge_budget=1.0)},
415
431
  )
416
- run("float-budget", float_budget["ledger"], 1, "challenge_budget must be 2")
432
+ run("float-budget", float_budget["ledger"], 1, "challenge_budget must be 1")
417
433
 
418
434
  nan_schema = make_fixture("nan-schema")
419
435
  nan_schema["ledger"]["schema_version"] = float("nan")
@@ -493,11 +509,108 @@ scope_mismatch = make_fixture("scope-mismatch", receipt_mutators={2: drift_scope
493
509
  run("scope-mismatch", scope_mismatch["ledger"], 1, "review scope changed")
494
510
 
495
511
  budget_four = make_fixture("budget-four", budget=4)
496
- run("budget-four", budget_four["ledger"], 1, "challenge_budget must be 2")
512
+ run("budget-four", budget_four["ledger"], 1, "challenge_budget must be 1")
497
513
 
498
514
  review_only = make_fixture("review-only", receipt_findings=[[]])
499
515
  run("review-only", review_only["ledger"], 1, "ready requires at least one tracked challenge")
500
516
 
517
+ # A caller must not out-run the wrapper budget by supplying extra receipts: a
518
+ # third receipt under budget 1 claims a negative remaining count and would
519
+ # otherwise reach completion validation as a ready-state budget bypass.
520
+ over_budget = make_fixture("over-budget", receipt_findings=[[], [], []])
521
+ run("over-budget", over_budget["ledger"], 1, "exceeds the wrapper budget")
522
+
523
+ # Chain succession. A fix that edits the reviewed owner package ends its chain, so
524
+ # the landing candidate can only be challenged in a succeeding chain. The lane then
525
+ # spans two chains and three rounds, and the succession round is the only one that
526
+ # binds what actually lands.
527
+ succession_ready = make_fixture(
528
+ "succession-ready", receipt_findings=[[finding(1)], [], []], succession=True
529
+ )
530
+ run("succession-ready", succession_ready["ledger"], 0, "closeout_state=ready_for_human_decision")
531
+
532
+ # The failure this whole mechanism exists to make visible: the fix batch moved the
533
+ # candidate and no round ever inspected what lands.
534
+ succession_absent = make_fixture(
535
+ "succession-absent",
536
+ receipt_findings=[[finding(1)], []],
537
+ receipt_mutators={
538
+ 1: lambda row: row.update(
539
+ candidate_sha256=OTHER_CANDIDATE, packet_sha256=OTHER_CANDIDATE
540
+ ),
541
+ 2: lambda row: row.update(
542
+ candidate_sha256=OTHER_CANDIDATE, packet_sha256=OTHER_CANDIDATE
543
+ ),
544
+ },
545
+ )
546
+ run("succession-absent", succession_absent["ledger"], 1, "does not bind the ledger candidate")
547
+
548
+ # A succession whose predecessor saw the same candidate reviewed nothing new.
549
+ succession_unmoved = make_fixture(
550
+ "succession-unmoved",
551
+ receipt_findings=[[finding(1)], [], []],
552
+ succession=True,
553
+ receipt_mutators={
554
+ 1: lambda row: row.update(candidate_sha256=CANDIDATE, packet_sha256=CANDIDATE),
555
+ 2: lambda row: row.update(candidate_sha256=CANDIDATE, packet_sha256=CANDIDATE),
556
+ 3: lambda row: row.update(predecessor_candidate_sha256=CANDIDATE),
557
+ },
558
+ )
559
+ run(
560
+ "succession-unmoved",
561
+ succession_unmoved["ledger"],
562
+ 1,
563
+ "must bind a candidate the wrapper chain never saw",
564
+ )
565
+
566
+ succession_foreign_terminal = make_fixture(
567
+ "succession-foreign-terminal",
568
+ receipt_findings=[[finding(1)], [], []],
569
+ succession=True,
570
+ receipt_mutators={3: lambda row: row.update(predecessor_result_sha256="f" * 64)},
571
+ )
572
+ run(
573
+ "succession-foreign-terminal",
574
+ succession_foreign_terminal["ledger"],
575
+ 1,
576
+ "does not bind the succeeded chain's terminal receipt",
577
+ )
578
+
579
+ succession_self = make_fixture(
580
+ "succession-self",
581
+ receipt_findings=[[finding(1)], [], []],
582
+ succession=True,
583
+ receipt_mutators={3: lambda row: row.update(review_chain_id="extraction-chain")},
584
+ )
585
+ run("succession-self", succession_self["ledger"], 1, "succeeds its own chain")
586
+
587
+ succession_misplaced = make_fixture(
588
+ "succession-misplaced",
589
+ receipt_findings=[[finding(1)], [], []],
590
+ succession=True,
591
+ receipt_mutators={
592
+ 2: lambda row: row.update(predecessor_chain_id="extraction-chain"),
593
+ },
594
+ )
595
+ run(
596
+ "succession-misplaced",
597
+ succession_misplaced["ledger"],
598
+ 1,
599
+ "must be the lane's final round",
600
+ )
601
+
602
+ succession_over_lane = make_fixture(
603
+ "succession-over-lane",
604
+ receipt_findings=[[finding(1)], [], [], []],
605
+ succession=True,
606
+ )
607
+ run(
608
+ "succession-over-lane",
609
+ succession_over_lane["ledger"],
610
+ 1,
611
+ "controller_receipts must contain one to 3 ordered Agent rounds",
612
+ )
613
+
501
614
  missing_complete = make_fixture("missing-complete")
502
615
  missing_complete["ledger"]["completion_receipt"] = None
503
616
  run("missing-complete", missing_complete["ledger"], 1, "requires a completion receipt")
@@ -537,7 +650,7 @@ run(
537
650
 
538
651
  continuation = make_fixture(
539
652
  "continuation",
540
- receipt_findings=[[finding(1)], [], [finding(2)]],
653
+ receipt_findings=[[finding(1)], [finding(2)]],
541
654
  completion=False,
542
655
  open_last=True,
543
656
  )
@@ -550,10 +663,10 @@ run(
550
663
 
551
664
  continuation_unconsumed_first_drift = make_fixture(
552
665
  "continuation-unconsumed-first-drift",
553
- receipt_findings=[[finding(1)], [], [finding(2)]],
666
+ receipt_findings=[[finding(1)], [finding(2)]],
554
667
  completion=False,
555
668
  open_last=True,
556
- base_shas=[A, A, A, B],
669
+ base_shas=[A, A, B],
557
670
  )
558
671
  run(
559
672
  "continuation-unconsumed-first-drift",
@@ -564,7 +677,7 @@ run(
564
677
 
565
678
  continuation_candidate = make_fixture(
566
679
  "continuation-candidate",
567
- receipt_findings=[[finding(1)], [], [finding(2)]],
680
+ receipt_findings=[[finding(1)], [finding(2)]],
568
681
  completion=False,
569
682
  open_last=True,
570
683
  )
@@ -595,8 +708,8 @@ run(
595
708
 
596
709
  unknown_state = make_fixture(
597
710
  "unknown-state",
598
- receipt_findings=[[finding(1)], [], [finding(2)]],
599
- receipt_mutators={3: lambda row: row.update(review_state="potato")},
711
+ receipt_findings=[[finding(1)], [finding(2)]],
712
+ receipt_mutators={2: lambda row: row.update(review_state="potato")},
600
713
  completion=False,
601
714
  open_last=True,
602
715
  )
@@ -605,10 +718,10 @@ run("unknown-state", unknown_state["ledger"], 1, "unknown controller review_stat
605
718
 
606
719
  passed_continuation = make_fixture(
607
720
  "passed-continuation",
608
- receipt_findings=[[finding(1)], [], []],
721
+ receipt_findings=[[finding(1)], []],
609
722
  completion=False,
610
723
  )
611
- run("passed-continuation", passed_continuation["ledger"], 1, "round 3 findings in post_review_budget")
724
+ run("passed-continuation", passed_continuation["ledger"], 1, "findings in post_review_budget")
612
725
 
613
726
  bad_finding_hash = make_fixture("bad-finding-hash")
614
727
  bad_finding_hash["ledger"]["finding_classes"][0]["occurrences"][0][
@@ -744,7 +857,7 @@ run(
744
857
 
745
858
  historical_open = make_fixture(
746
859
  "historical-open",
747
- receipt_findings=[[finding(1)], [finding(2)], []],
860
+ receipt_findings=[[finding(1), finding(2)], []],
748
861
  )
749
862
  historical_open["ledger"]["finding_classes"][0]["occurrences"][0][
750
863
  "disposition"
@@ -756,7 +869,7 @@ run("historical-open", historical_open["ledger"], 1, "unresolved finding occurre
756
869
 
757
870
  historical_open_linked = make_fixture(
758
871
  "historical-open-linked",
759
- receipt_findings=[[finding(1)], [finding(2)], []],
872
+ receipt_findings=[[finding(1), finding(2)], []],
760
873
  )
761
874
  historical_open_linked_occurrences = historical_open_linked["ledger"][
762
875
  "finding_classes"
@@ -825,7 +938,7 @@ run("needs-human", needs_human["ledger"], 1, "unresolved finding occurrence")
825
938
 
826
939
  third_occurrence = make_fixture(
827
940
  "third-occurrence",
828
- receipt_findings=[[finding(1)], [finding(2)], [finding(3)]],
941
+ receipt_findings=[[finding(1), finding(2)], [finding(3)]],
829
942
  completion=False,
830
943
  unmatched=1,
831
944
  open_last=True,
@@ -1045,7 +1158,7 @@ for closing_disposition in sorted(CLOSED_DISPOSITIONS):
1045
1158
  case_name = f"historical-needs-human-{closing_disposition.replace('_', '-')}"
1046
1159
  historical_needs_human_linked = make_fixture(
1047
1160
  case_name,
1048
- receipt_findings=[[finding(1)], [finding(2)], []],
1161
+ receipt_findings=[[finding(1), finding(2)], []],
1049
1162
  )
1050
1163
  historical_needs_human_occurrences = historical_needs_human_linked["ledger"][
1051
1164
  "finding_classes"
@@ -1128,7 +1241,7 @@ duplicate_completion_result = invoke(
1128
1241
 
1129
1242
  duplicate_sweep = make_fixture(
1130
1243
  "duplicate-sweep",
1131
- receipt_findings=[[finding(1)], [finding(2)], [finding(3)]],
1244
+ receipt_findings=[[finding(1), finding(2)], [finding(3)]],
1132
1245
  completion=False,
1133
1246
  open_last=True,
1134
1247
  )
@@ -30,6 +30,17 @@ TERMINAL_STATES = {
30
30
  }
31
31
  EXTERNAL_REVIEW_STATES = {"reviewed", "findings_pending", "post_review_budget"}
32
32
  KNOWN_REVIEW_STATES = EXTERNAL_REVIEW_STATES | {"self_reviewed"}
33
+ # The extraction wrapper fixes the autonomous lane at one review plus one
34
+ # challenge; every numeric bound below derives from these two constants so a
35
+ # future budget change lands in exactly one place.
36
+ WRAPPER_CHALLENGE_BUDGET = 1
37
+ WRAPPER_AUTONOMOUS_ROUNDS = WRAPPER_CHALLENGE_BUDGET + 1
38
+ # A fix that edits the reviewed owner package ends its chain by construction, so
39
+ # the post-fix candidate is challenged in ONE succeeding chain rather than inside
40
+ # the ended one. The lane therefore spans at most two chains: the wrapper-budget
41
+ # chain plus a single succession challenge bound to the landing candidate.
42
+ LANE_MAX_CHAINS = 2
43
+ LANE_MAX_ROUNDS = WRAPPER_AUTONOMOUS_ROUNDS + 1
33
44
  DISPOSITIONS = {
34
45
  "fixed",
35
46
  "source_refuted",
@@ -272,8 +283,8 @@ def validate_scope(receipt: dict[str, Any], label: str) -> str:
272
283
  ]
273
284
  if len(normalized_risks) != len(set(normalized_risks)):
274
285
  fail(f"{label}.review_scope.risk_tags contains duplicates")
275
- if scope["challenge_budget"] != 2 or type(scope["challenge_budget"]) is not int:
276
- fail(f"{label}.review_scope.challenge_budget must be 2")
286
+ if scope["challenge_budget"] != WRAPPER_CHALLENGE_BUDGET or type(scope["challenge_budget"]) is not int:
287
+ fail(f"{label}.review_scope.challenge_budget must be {WRAPPER_CHALLENGE_BUDGET}")
277
288
  if (
278
289
  scope["wording_only_proof_sha256"] is not None
279
290
  or scope["wording_only_scope_sha256"] is not None
@@ -299,8 +310,10 @@ def validate_controller_receipts(
299
310
  payload: dict[str, Any], ledger_dir: Path
300
311
  ) -> tuple[list[dict[str, Any]], list[str], dict[str, list[str]], str, str]:
301
312
  refs = payload["controller_receipts"]
302
- if not isinstance(refs, list) or not 1 <= len(refs) <= 3:
303
- fail("controller_receipts must contain one to three ordered Agent rounds")
313
+ if not isinstance(refs, list) or not 1 <= len(refs) <= LANE_MAX_ROUNDS:
314
+ fail(
315
+ f"controller_receipts must contain one to {LANE_MAX_ROUNDS} ordered Agent rounds"
316
+ )
304
317
  if payload["autonomous_round"] != len(refs) or type(payload["autonomous_round"]) is not int:
305
318
  fail("autonomous_round must equal the ordered controller receipt count")
306
319
 
@@ -308,11 +321,20 @@ def validate_controller_receipts(
308
321
  receipt_hashes: list[str] = []
309
322
  finding_hashes: dict[str, list[str]] = {}
310
323
  chain_id: str | None = None
324
+ final_chain_id: str | None = None
311
325
  scope_hash: str | None = None
326
+ candidates: list[str] = []
327
+ succession_index: int | None = None
312
328
  ledger_candidate = sha256(
313
329
  payload["candidate_sha256"], "ledger.candidate_sha256"
314
330
  )
315
331
  for expected_index, value in enumerate(refs, start=1):
332
+ # A caller supplying more receipts than the lane can mint would
333
+ # otherwise claim a negative remaining count and reach completion
334
+ # validation as a ready-state budget bypass. The wrapper bound still
335
+ # governs each chain; the lane bound governs their sum.
336
+ if expected_index > LANE_MAX_ROUNDS:
337
+ fail("controller chain exceeds the lane budget")
316
338
  ref = exact_object(
317
339
  value, {"sequence", "file", "sha256"}, f"controller_receipts[{expected_index - 1}]"
318
340
  )
@@ -329,7 +351,28 @@ def validate_controller_receipts(
329
351
  receipt = decode_json(raw, label=f"controller receipt {expected_index}")
330
352
  if receipt.get("schema_version") != 3 or type(receipt.get("schema_version")) is not int:
331
353
  fail(f"controller receipt {expected_index} schema_version must be 3")
332
- expected_mode = "review" if expected_index == 1 else "challenge"
354
+ # A succession round opens a second chain, so its own chain arithmetic
355
+ # restarts at one while its lane position keeps counting. Classify first;
356
+ # every expectation below reads the chain position, not the lane index.
357
+ predecessor_chain = receipt.get("predecessor_chain_id")
358
+ is_succession = predecessor_chain is not None
359
+ if is_succession:
360
+ if expected_index != len(refs):
361
+ fail("a succession round must be the lane's final round")
362
+ if expected_index != WRAPPER_AUTONOMOUS_ROUNDS + 1:
363
+ fail("a succession round may only follow a spent wrapper chain")
364
+ succession_index = expected_index
365
+ chain_position = 1
366
+ else:
367
+ # Only a succession round may sit past the wrapper chain. Without this
368
+ # the caller claims a negative remaining count and reaches completion
369
+ # validation as a ready-state budget bypass.
370
+ if expected_index > WRAPPER_AUTONOMOUS_ROUNDS:
371
+ fail("controller chain exceeds the wrapper budget")
372
+ chain_position = expected_index
373
+ expected_mode = (
374
+ "challenge" if is_succession else "review" if expected_index == 1 else "challenge"
375
+ )
333
376
  if receipt.get("mode") != expected_mode:
334
377
  fail(f"controller receipt {expected_index} must have mode {expected_mode}")
335
378
  if receipt.get("status") not in {"passed", "findings"}:
@@ -339,26 +382,35 @@ def validate_controller_receipts(
339
382
  current_chain = bounded_text(
340
383
  receipt.get("review_chain_id"), f"controller receipt {expected_index}.review_chain_id", 120
341
384
  )
342
- if chain_id is None:
385
+ if is_succession:
386
+ if current_chain == chain_id:
387
+ fail(
388
+ f"controller receipt {expected_index} succeeds its own chain"
389
+ )
390
+ final_chain_id = current_chain
391
+ elif chain_id is None:
343
392
  chain_id = current_chain
393
+ final_chain_id = current_chain
344
394
  elif current_chain != chain_id:
345
395
  fail(f"controller receipt {expected_index} review_chain_id changed")
346
- if receipt.get("autonomous_review_index") != expected_index or type(
396
+ if receipt.get("autonomous_review_index") != chain_position or type(
347
397
  receipt.get("autonomous_review_index")
348
398
  ) is not int:
349
399
  fail(f"controller receipt {expected_index} autonomous_review_index is not contiguous")
350
- expected_challenge_index = 0 if expected_index == 1 else expected_index - 1
400
+ expected_challenge_index = (
401
+ 1 if is_succession else 0 if expected_index == 1 else expected_index - 1
402
+ )
351
403
  if receipt.get("challenge_index") != expected_challenge_index or type(
352
404
  receipt.get("challenge_index")
353
405
  ) is not int:
354
406
  fail(f"controller receipt {expected_index} challenge_index is invalid")
355
- if receipt.get("challenge_budget") != 2 or type(receipt.get("challenge_budget")) is not int:
356
- fail(f"controller receipt {expected_index} challenge_budget must be 2")
357
- if receipt.get("autonomous_review_budget") != 3 or type(
407
+ if receipt.get("challenge_budget") != WRAPPER_CHALLENGE_BUDGET or type(receipt.get("challenge_budget")) is not int:
408
+ fail(f"controller receipt {expected_index} challenge_budget must be {WRAPPER_CHALLENGE_BUDGET}")
409
+ if receipt.get("autonomous_review_budget") != WRAPPER_AUTONOMOUS_ROUNDS or type(
358
410
  receipt.get("autonomous_review_budget")
359
411
  ) is not int:
360
- fail(f"controller receipt {expected_index} autonomous_review_budget must be 3")
361
- expected_remaining = 3 - expected_index
412
+ fail(f"controller receipt {expected_index} autonomous_review_budget must be {WRAPPER_AUTONOMOUS_ROUNDS}")
413
+ expected_remaining = WRAPPER_AUTONOMOUS_ROUNDS - chain_position
362
414
  if receipt.get("autonomous_reviews_remaining") != expected_remaining or type(
363
415
  receipt.get("autonomous_reviews_remaining")
364
416
  ) is not int:
@@ -366,7 +418,8 @@ def validate_controller_receipts(
366
418
  if receipt.get("autonomous_review_allowed") is not (expected_remaining > 0):
367
419
  fail(f"controller receipt {expected_index} autonomous_review_allowed is invalid")
368
420
  prior = receipt.get("prior_review_result_sha256")
369
- if prior != receipt_hashes:
421
+ expected_prior = [] if is_succession else receipt_hashes
422
+ if prior != expected_prior:
370
423
  fail(f"controller receipt {expected_index} prior_review_result_sha256 is not the complete ordered prefix")
371
424
  candidate = sha256(
372
425
  receipt.get("candidate_sha256"), f"controller receipt {expected_index}.candidate_sha256"
@@ -376,13 +429,10 @@ def validate_controller_receipts(
376
429
  )
377
430
  if packet != candidate:
378
431
  fail(f"controller receipt {expected_index} packet_sha256 must equal candidate_sha256")
379
- # Every counted round must have inspected the exact final candidate;
380
- # a chain whose review round saw an earlier candidate is not a review
381
- # of the candidate this ledger closes out.
382
- if candidate != ledger_candidate:
383
- fail(
384
- f"controller receipt {expected_index} does not bind the ledger candidate"
385
- )
432
+ # Which candidate a round must bind depends on its phase, and the phase
433
+ # split is only known once every round is loaded, so this is settled
434
+ # after the loop rather than here.
435
+ candidates.append(candidate)
386
436
  current_scope_hash = validate_scope(receipt, f"controller receipt {expected_index}")
387
437
  if scope_hash is None:
388
438
  scope_hash = current_scope_hash
@@ -415,7 +465,7 @@ def validate_controller_receipts(
415
465
  fail(f"controller receipt {expected_index} has unknown controller review_state {state}")
416
466
  expected_state = (
417
467
  "post_review_budget"
418
- if receipt["status"] == "findings" and expected_index == 3
468
+ if receipt["status"] == "findings" and chain_position == WRAPPER_AUTONOMOUS_ROUNDS
419
469
  else "findings_pending"
420
470
  if receipt["status"] == "findings"
421
471
  else "reviewed"
@@ -429,9 +479,38 @@ def validate_controller_receipts(
429
479
  receipt_hashes.append(receipt_hash)
430
480
  finding_hashes[receipt_hash] = current_findings
431
481
 
432
- if chain_id is None or scope_hash is None:
482
+ if chain_id is None or scope_hash is None or final_chain_id is None:
433
483
  fail("controller receipt chain is empty")
434
- return receipts, receipt_hashes, finding_hashes, chain_id, scope_hash
484
+
485
+ # Phase binding. Without a succession every counted round must have inspected
486
+ # the exact final candidate. With one, the wrapper chain reviewed the candidate
487
+ # the fix batch then moved, and the succession round is the only round that can
488
+ # bind what actually lands — which is the whole point of running it.
489
+ if succession_index is None:
490
+ for position, value in enumerate(candidates, start=1):
491
+ if value != ledger_candidate:
492
+ fail(f"controller receipt {position} does not bind the ledger candidate")
493
+ else:
494
+ succession = receipts[-1]
495
+ succeeded_candidate = sha256(
496
+ succession.get("predecessor_candidate_sha256"),
497
+ "succession receipt.predecessor_candidate_sha256",
498
+ )
499
+ if candidates[-1] != ledger_candidate:
500
+ fail("the succession round does not bind the ledger candidate")
501
+ if succeeded_candidate == ledger_candidate:
502
+ fail("a succession round must bind a candidate the wrapper chain never saw")
503
+ for position, value in enumerate(candidates[:-1], start=1):
504
+ if value != succeeded_candidate:
505
+ fail(f"controller receipt {position} does not bind the succeeded candidate")
506
+ if succession.get("predecessor_chain_id") != chain_id:
507
+ fail("the succession round does not name the chain it succeeds")
508
+ if sha256(
509
+ succession.get("predecessor_result_sha256"),
510
+ "succession receipt.predecessor_result_sha256",
511
+ ) != receipt_hashes[-2]:
512
+ fail("the succession round does not bind the succeeded chain's terminal receipt")
513
+ return receipts, receipt_hashes, finding_hashes, final_chain_id, scope_hash
435
514
 
436
515
 
437
516
  def validate_completion_receipt(
@@ -469,24 +548,30 @@ def validate_completion_receipt(
469
548
  fail("completion receipt cannot close a final external receipt with findings")
470
549
  if receipt.get("review_chain_tracked") is not True or receipt.get("review_chain_id") != chain_id:
471
550
  fail("completion receipt review_chain_id does not match the controller chain")
472
- if receipt.get("challenge_budget") != 2 or type(receipt.get("challenge_budget")) is not int:
473
- fail("completion receipt challenge_budget must be 2")
474
- if receipt.get("autonomous_review_budget") != 3 or type(
551
+ if receipt.get("challenge_budget") != WRAPPER_CHALLENGE_BUDGET or type(receipt.get("challenge_budget")) is not int:
552
+ fail(f"completion receipt challenge_budget must be {WRAPPER_CHALLENGE_BUDGET}")
553
+ if receipt.get("autonomous_review_budget") != WRAPPER_AUTONOMOUS_ROUNDS or type(
475
554
  receipt.get("autonomous_review_budget")
476
555
  ) is not int:
477
- fail("completion receipt autonomous_review_budget must be 3")
478
- if receipt.get("autonomous_review_index") != len(receipts) or type(
556
+ fail(f"completion receipt autonomous_review_budget must be {WRAPPER_AUTONOMOUS_ROUNDS}")
557
+ # The completion checkpoint binds the final external round, so its chain
558
+ # arithmetic is that round's — which is the succeeding chain's when a fix
559
+ # batch ended the wrapper chain, not the lane's round count.
560
+ final_index = final.get("autonomous_review_index")
561
+ if receipt.get("autonomous_review_index") != final_index or type(
479
562
  receipt.get("autonomous_review_index")
480
563
  ) is not int:
481
564
  fail("completion receipt autonomous_review_index does not match the final round")
482
- expected_remaining = 3 - len(receipts)
565
+ expected_remaining = WRAPPER_AUTONOMOUS_ROUNDS - final_index
483
566
  if receipt.get("autonomous_reviews_remaining") != expected_remaining or type(
484
567
  receipt.get("autonomous_reviews_remaining")
485
568
  ) is not int:
486
569
  fail("completion receipt autonomous_reviews_remaining does not match the final round")
487
570
  if receipt.get("autonomous_review_allowed") is not False:
488
571
  fail("completion receipt must disable further autonomous review")
489
- if receipt.get("prior_review_result_sha256") != receipt_hashes[:-1]:
572
+ if receipt.get("prior_review_result_sha256") != final.get(
573
+ "prior_review_result_sha256"
574
+ ):
490
575
  fail("completion receipt prior_review_result_sha256 does not match the final external receipt")
491
576
  if receipt.get("completion_review_result_sha256") != receipt_hashes[-1]:
492
577
  fail("completion receipt completion_review_result_sha256 does not identify the final external receipt")
@@ -940,13 +1025,29 @@ def validate(payload: dict[str, Any], ledger_dir: Path) -> tuple[str, int, int]:
940
1025
  fail("ready_for_human_decision requires no unresolved finding occurrence and no unreviewed delta")
941
1026
  elif closeout == "continuation_authorization_required":
942
1027
  final = receipts[-1]
943
- if (
944
- len(receipts) != 3
945
- or final.get("status") != "findings"
946
- or final.get("review_state") != "post_review_budget"
947
- or final.get("human_decision_required") is not True
948
- ):
949
- fail("continuation_authorization_required requires round 3 findings in post_review_budget")
1028
+ # The lane is spent either at the wrapper chain's last round, or — when a
1029
+ # fix batch moved the candidate — at the succession round that reviewed it.
1030
+ # Only the closeout knows the lane is spent: the controller sizes
1031
+ # human_decision_required from its own chain, where the succession round
1032
+ # is round one of two.
1033
+ succeeded = final.get("predecessor_chain_id") is not None
1034
+ if succeeded:
1035
+ spent = (
1036
+ len(receipts) == LANE_MAX_ROUNDS and final.get("status") == "findings"
1037
+ )
1038
+ else:
1039
+ spent = (
1040
+ len(receipts) == WRAPPER_AUTONOMOUS_ROUNDS
1041
+ and final.get("status") == "findings"
1042
+ and final.get("review_state") == "post_review_budget"
1043
+ and final.get("human_decision_required") is True
1044
+ )
1045
+ if not spent:
1046
+ fail(
1047
+ "continuation_authorization_required requires final-round "
1048
+ f"(round {WRAPPER_AUTONOMOUS_ROUNDS}) findings in post_review_budget, "
1049
+ f"or a succession round (round {LANE_MAX_ROUNDS}) carrying findings"
1050
+ )
950
1051
  elif closeout == "baseline_race" and not delta:
951
1052
  fail("baseline_race requires a non-empty unreviewed_delta")
952
1053
 
@@ -15,7 +15,7 @@ Bitable ownership:
15
15
 
16
16
  - Ambiguous "写测试用例" / "write test cases" requests mean `testing-strategy` first when the user wants test code, E2E/browser/device tests, CI coverage, mocks, or verification evidence. Use this skill only when the deliverable is a structured TC artifact, Feishu/Bitable sync, or source-to-TC generation workflow.
17
17
  - Use `testing-strategy` to decide which test layers (unit / integration / E2E / scenario) are needed *after* the use cases are written.
18
- - Use `go-microservice-dev` / `python-service-dev` / `web-react-dev` / `miniapp-product-dev` / `app-cross-platform-dev` for implementing the test code once test cases are approved.
18
+ - Test-code implementation, once test cases are approved, must go to the owning stack skill: `go-microservice-dev` / `python-service-dev` / `nodejs-service-dev` / `web-react-dev` / `miniapp-product-dev` / `app-cross-platform-dev`.
19
19
  - Use `product-rd-workflow` when the test case work is part of a broader feature delivery plan.
20
20
  - Use `llm-inference-integration` when test cases cover LLM / agent / RAG / inference behavior.
21
21
  - Use `skill-extraction-workflow` to feed lessons from test case generation or Feishu Bitable setup back into this skill after each delivery cycle.
@@ -9,14 +9,15 @@ Use this skill when the task is about test scope, test design, verification evid
9
9
 
10
10
  ## Skill Routing
11
11
 
12
- - Use this skill for test strategy, test pyramid decisions, unit/integration/e2e/scenario boundaries, fixtures, mocks/fakes, regression coverage, and verification evidence.
13
- - Use `go-microservice-dev` for Go-specific implementation details after the test layer is chosen.
14
- - Use `python-service-dev` for Python-specific pytest, async test, fixture, fake client, ruff, mypy/pyright, packaging, or migration-test implementation details after the test layer is chosen.
15
- - Use `app-cross-platform-dev` for Flutter unit/widget/integration tests, Android unit/Compose/instrumented tests, iOS unit/UI tests, mobile device smoke, and app release verification after the test layer is chosen.
16
- - Use `miniapp-product-dev` for WeChat/Alipay/Douyin/Baidu mini-program build, developer-tool preview, real-device smoke, host capability verification, review submission, and release checks after the test layer is chosen.
17
- - Use `web-react-dev` for React unit/component/API-client/browser/E2E implementation, browser automation, accessibility checks, and build/deploy smoke after the test layer is chosen.
18
- - Use `terminal-cli-dev` for command-line, terminal, PTY-backed, ANSI-rendered, keyboard-driven, and full-screen text UI implementation after the test layer is chosen.
19
- - Use `llm-inference-integration` for LLM/agent/RAG evals, replay sets, prompt/model regression checks, shadow runs, and inference-specific observability after the test layer is chosen.
12
+ - Use this skill for test strategy, test pyramid decisions, unit/integration/e2e/scenario boundaries, fixtures, mocks/fakes, regression coverage, and verification evidence. Stack pointers below apply after that choice.
13
+ - Use `go-microservice-dev` for Go-specific implementation details.
14
+ - Use `python-service-dev` for Python-specific pytest, async test, fixture, fake client, ruff, mypy/pyright, packaging, or migration-test implementation details.
15
+ - Node runner, mock/fixture, fake timers, stream and cancellation assertions, child-process lifecycle, and packaged-artifact verification must use `nodejs-service-dev`.
16
+ - Use `app-cross-platform-dev` for Flutter unit/widget/integration tests, Android unit/Compose/instrumented tests, iOS unit/UI tests, mobile device smoke, and app release verification.
17
+ - Use `miniapp-product-dev` for WeChat/Alipay/Douyin/Baidu mini-program build, developer-tool preview, real-device smoke, host capability verification, review submission, and release checks.
18
+ - Use `web-react-dev` for React unit/component/API-client/browser/E2E implementation, browser automation, accessibility checks, and build/deploy smoke.
19
+ - Use `terminal-cli-dev` for the terminal interface contract, PTY/ANSI/keyboard rendering, and full-screen text UI testing.
20
+ - Use `llm-inference-integration` for LLM/agent/RAG evals, replay sets, prompt/model regression checks, shadow runs, and inference-specific observability.
20
21
  - Use `defect-diagnosis` first when a test is failing, flaky, or used to reproduce a bug.
21
22
  - Use `product-rd-workflow` first when testing is part of an end-to-end product delivery plan.
22
23
  - Use `product-ui-ux-design` for visual hierarchy, interaction, state, accessibility, and design acceptance criteria; use this skill for automated proof and test coverage.