@mc-and-his-agents/loom-installer 0.1.11 → 0.1.13

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 (22) hide show
  1. package/package.json +1 -1
  2. package/payload/manifest.json +42 -42
  3. package/payload/plugin/loom/skills/shared/scripts/loom_check.py +156 -16
  4. package/payload/plugin/loom/skills/shared/scripts/loom_flow.py +59 -10
  5. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +156 -16
  6. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py +59 -10
  7. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +156 -16
  8. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py +59 -10
  9. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +156 -16
  10. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py +59 -10
  11. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +156 -16
  12. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py +59 -10
  13. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +156 -16
  14. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py +59 -10
  15. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +156 -16
  16. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py +59 -10
  17. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +156 -16
  18. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py +59 -10
  19. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +156 -16
  20. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py +59 -10
  21. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py +156 -16
  22. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py +59 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mc-and-his-agents/loom-installer",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Node installer for Loom plugin and single-skill installation surfaces.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,9 +2,9 @@
2
2
  "schema_version": "loom-installer-payload/v1",
3
3
  "loom_version": "0.4.0",
4
4
  "source_repository": "https://github.com/MC-and-his-Agents/Loom",
5
- "source_commit": "4f6a4074d02557304018e9f063425f974d6a08cf",
5
+ "source_commit": "b07d54d4963a674da0ff2397f4ad244cb3028584",
6
6
  "source_ref": "main",
7
- "built_at": "2026-04-25T11:49:46+08:00",
7
+ "built_at": "2026-04-25T12:04:56+08:00",
8
8
  "runtime": {
9
9
  "python_minimum": "3.10",
10
10
  "python_recommended": "3.11+"
@@ -628,13 +628,13 @@
628
628
  },
629
629
  {
630
630
  "path": "plugin/loom/skills/shared/scripts/loom_check.py",
631
- "bytes": 253737,
632
- "sha256": "9ada27e8f88d01940e7f70112301f1c6c78ffa362f5159abba2a44ae445266ac"
631
+ "bytes": 260856,
632
+ "sha256": "9f9b145158d659764c853f7234dfb7308c96e78031f43741e431905b91a6b7fc"
633
633
  },
634
634
  {
635
635
  "path": "plugin/loom/skills/shared/scripts/loom_flow.py",
636
- "bytes": 259768,
637
- "sha256": "f11062993fe565160cdc2910e981e9a601d0fcafcbe16dd0072d63acf39d67b0"
636
+ "bytes": 262117,
637
+ "sha256": "ec49996bbebb9a59fee0f906d301c2c66e13e17ec548ef762f2b0e33e9abbe32"
638
638
  },
639
639
  {
640
640
  "path": "plugin/loom/skills/shared/scripts/loom_init.py",
@@ -1213,13 +1213,13 @@
1213
1213
  },
1214
1214
  {
1215
1215
  "path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py",
1216
- "bytes": 253737,
1217
- "sha256": "9ada27e8f88d01940e7f70112301f1c6c78ffa362f5159abba2a44ae445266ac"
1216
+ "bytes": 260856,
1217
+ "sha256": "9f9b145158d659764c853f7234dfb7308c96e78031f43741e431905b91a6b7fc"
1218
1218
  },
1219
1219
  {
1220
1220
  "path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py",
1221
- "bytes": 259768,
1222
- "sha256": "f11062993fe565160cdc2910e981e9a601d0fcafcbe16dd0072d63acf39d67b0"
1221
+ "bytes": 262117,
1222
+ "sha256": "ec49996bbebb9a59fee0f906d301c2c66e13e17ec548ef762f2b0e33e9abbe32"
1223
1223
  },
1224
1224
  {
1225
1225
  "path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_init.py",
@@ -1828,13 +1828,13 @@
1828
1828
  },
1829
1829
  {
1830
1830
  "path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py",
1831
- "bytes": 253737,
1832
- "sha256": "9ada27e8f88d01940e7f70112301f1c6c78ffa362f5159abba2a44ae445266ac"
1831
+ "bytes": 260856,
1832
+ "sha256": "9f9b145158d659764c853f7234dfb7308c96e78031f43741e431905b91a6b7fc"
1833
1833
  },
1834
1834
  {
1835
1835
  "path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py",
1836
- "bytes": 259768,
1837
- "sha256": "f11062993fe565160cdc2910e981e9a601d0fcafcbe16dd0072d63acf39d67b0"
1836
+ "bytes": 262117,
1837
+ "sha256": "ec49996bbebb9a59fee0f906d301c2c66e13e17ec548ef762f2b0e33e9abbe32"
1838
1838
  },
1839
1839
  {
1840
1840
  "path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_init.py",
@@ -2443,13 +2443,13 @@
2443
2443
  },
2444
2444
  {
2445
2445
  "path": "skills/loom-init/.loom-runtime/shared/scripts/loom_check.py",
2446
- "bytes": 253737,
2447
- "sha256": "9ada27e8f88d01940e7f70112301f1c6c78ffa362f5159abba2a44ae445266ac"
2446
+ "bytes": 260856,
2447
+ "sha256": "9f9b145158d659764c853f7234dfb7308c96e78031f43741e431905b91a6b7fc"
2448
2448
  },
2449
2449
  {
2450
2450
  "path": "skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py",
2451
- "bytes": 259768,
2452
- "sha256": "f11062993fe565160cdc2910e981e9a601d0fcafcbe16dd0072d63acf39d67b0"
2451
+ "bytes": 262117,
2452
+ "sha256": "ec49996bbebb9a59fee0f906d301c2c66e13e17ec548ef762f2b0e33e9abbe32"
2453
2453
  },
2454
2454
  {
2455
2455
  "path": "skills/loom-init/.loom-runtime/shared/scripts/loom_init.py",
@@ -3063,13 +3063,13 @@
3063
3063
  },
3064
3064
  {
3065
3065
  "path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py",
3066
- "bytes": 253737,
3067
- "sha256": "9ada27e8f88d01940e7f70112301f1c6c78ffa362f5159abba2a44ae445266ac"
3066
+ "bytes": 260856,
3067
+ "sha256": "9f9b145158d659764c853f7234dfb7308c96e78031f43741e431905b91a6b7fc"
3068
3068
  },
3069
3069
  {
3070
3070
  "path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py",
3071
- "bytes": 259768,
3072
- "sha256": "f11062993fe565160cdc2910e981e9a601d0fcafcbe16dd0072d63acf39d67b0"
3071
+ "bytes": 262117,
3072
+ "sha256": "ec49996bbebb9a59fee0f906d301c2c66e13e17ec548ef762f2b0e33e9abbe32"
3073
3073
  },
3074
3074
  {
3075
3075
  "path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_init.py",
@@ -3678,13 +3678,13 @@
3678
3678
  },
3679
3679
  {
3680
3680
  "path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py",
3681
- "bytes": 253737,
3682
- "sha256": "9ada27e8f88d01940e7f70112301f1c6c78ffa362f5159abba2a44ae445266ac"
3681
+ "bytes": 260856,
3682
+ "sha256": "9f9b145158d659764c853f7234dfb7308c96e78031f43741e431905b91a6b7fc"
3683
3683
  },
3684
3684
  {
3685
3685
  "path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py",
3686
- "bytes": 259768,
3687
- "sha256": "f11062993fe565160cdc2910e981e9a601d0fcafcbe16dd0072d63acf39d67b0"
3686
+ "bytes": 262117,
3687
+ "sha256": "ec49996bbebb9a59fee0f906d301c2c66e13e17ec548ef762f2b0e33e9abbe32"
3688
3688
  },
3689
3689
  {
3690
3690
  "path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_init.py",
@@ -4293,13 +4293,13 @@
4293
4293
  },
4294
4294
  {
4295
4295
  "path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py",
4296
- "bytes": 253737,
4297
- "sha256": "9ada27e8f88d01940e7f70112301f1c6c78ffa362f5159abba2a44ae445266ac"
4296
+ "bytes": 260856,
4297
+ "sha256": "9f9b145158d659764c853f7234dfb7308c96e78031f43741e431905b91a6b7fc"
4298
4298
  },
4299
4299
  {
4300
4300
  "path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py",
4301
- "bytes": 259768,
4302
- "sha256": "f11062993fe565160cdc2910e981e9a601d0fcafcbe16dd0072d63acf39d67b0"
4301
+ "bytes": 262117,
4302
+ "sha256": "ec49996bbebb9a59fee0f906d301c2c66e13e17ec548ef762f2b0e33e9abbe32"
4303
4303
  },
4304
4304
  {
4305
4305
  "path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_init.py",
@@ -4908,13 +4908,13 @@
4908
4908
  },
4909
4909
  {
4910
4910
  "path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py",
4911
- "bytes": 253737,
4912
- "sha256": "9ada27e8f88d01940e7f70112301f1c6c78ffa362f5159abba2a44ae445266ac"
4911
+ "bytes": 260856,
4912
+ "sha256": "9f9b145158d659764c853f7234dfb7308c96e78031f43741e431905b91a6b7fc"
4913
4913
  },
4914
4914
  {
4915
4915
  "path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py",
4916
- "bytes": 259768,
4917
- "sha256": "f11062993fe565160cdc2910e981e9a601d0fcafcbe16dd0072d63acf39d67b0"
4916
+ "bytes": 262117,
4917
+ "sha256": "ec49996bbebb9a59fee0f906d301c2c66e13e17ec548ef762f2b0e33e9abbe32"
4918
4918
  },
4919
4919
  {
4920
4920
  "path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_init.py",
@@ -5523,13 +5523,13 @@
5523
5523
  },
5524
5524
  {
5525
5525
  "path": "skills/loom-review/.loom-runtime/shared/scripts/loom_check.py",
5526
- "bytes": 253737,
5527
- "sha256": "9ada27e8f88d01940e7f70112301f1c6c78ffa362f5159abba2a44ae445266ac"
5526
+ "bytes": 260856,
5527
+ "sha256": "9f9b145158d659764c853f7234dfb7308c96e78031f43741e431905b91a6b7fc"
5528
5528
  },
5529
5529
  {
5530
5530
  "path": "skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py",
5531
- "bytes": 259768,
5532
- "sha256": "f11062993fe565160cdc2910e981e9a601d0fcafcbe16dd0072d63acf39d67b0"
5531
+ "bytes": 262117,
5532
+ "sha256": "ec49996bbebb9a59fee0f906d301c2c66e13e17ec548ef762f2b0e33e9abbe32"
5533
5533
  },
5534
5534
  {
5535
5535
  "path": "skills/loom-review/.loom-runtime/shared/scripts/loom_init.py",
@@ -6138,13 +6138,13 @@
6138
6138
  },
6139
6139
  {
6140
6140
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py",
6141
- "bytes": 253737,
6142
- "sha256": "9ada27e8f88d01940e7f70112301f1c6c78ffa362f5159abba2a44ae445266ac"
6141
+ "bytes": 260856,
6142
+ "sha256": "9f9b145158d659764c853f7234dfb7308c96e78031f43741e431905b91a6b7fc"
6143
6143
  },
6144
6144
  {
6145
6145
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py",
6146
- "bytes": 259768,
6147
- "sha256": "f11062993fe565160cdc2910e981e9a601d0fcafcbe16dd0072d63acf39d67b0"
6146
+ "bytes": 262117,
6147
+ "sha256": "ec49996bbebb9a59fee0f906d301c2c66e13e17ec548ef762f2b0e33e9abbe32"
6148
6148
  },
6149
6149
  {
6150
6150
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_init.py",
@@ -92,7 +92,9 @@ CORE_DOCS = (
92
92
  "docs/methodology/templates/pull-request.md",
93
93
  "docs/evidence/extraction-ledger.md",
94
94
  "docs/evidence/landing-map.md",
95
+ "docs/evidence/validations/validation-closeout-reconciliation-blocking-gate.md",
95
96
  "docs/evidence/validations/validation-loom-core-runtime-parity.md",
97
+ "docs/evidence/validations/validation-shadow-parity-blocking-gate.md",
96
98
  "docs/evidence/validations/validation-syvert-strong-governance-parity.md",
97
99
  "docs/adoption/rationale.md",
98
100
  "docs/adoption/routing-and-checkpoints.md",
@@ -968,10 +970,17 @@ def require_shadow_parity_payload(
968
970
  return
969
971
  if payload.get("command") != "shadow-parity":
970
972
  failures.append(Failure(category, f"{context} must report `command: shadow-parity`"))
971
- if payload.get("result") not in {"pass", "warn"}:
972
- failures.append(Failure(category, f"{context} result must be `pass` or `warn`"))
973
- if payload.get("fallback_to") is not None:
974
- failures.append(Failure(category, f"{context} fallback_to must remain `null`"))
973
+ mode = payload.get("mode", "validation-only")
974
+ if mode not in {"validation-only", "blocking"}:
975
+ failures.append(Failure(category, f"{context} mode must be `validation-only` or `blocking`"))
976
+ if payload.get("blocking") != (mode == "blocking"):
977
+ failures.append(Failure(category, f"{context} blocking flag must match mode"))
978
+ allowed_results = {"pass", "block"} if mode == "blocking" else {"pass", "warn"}
979
+ if payload.get("result") not in allowed_results:
980
+ failures.append(Failure(category, f"{context} result must stay within the stable mode-specific contract"))
981
+ expected_fallbacks = {"manual-reconciliation"} if payload.get("result") == "block" else {None}
982
+ if payload.get("fallback_to") not in expected_fallbacks:
983
+ failures.append(Failure(category, f"{context} fallback_to must match the shadow parity enforcement mode"))
975
984
  if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
976
985
  failures.append(Failure(category, f"{context} must include non-empty `summary`"))
977
986
  if not isinstance(payload.get("missing_inputs"), list):
@@ -1007,6 +1016,14 @@ def require_shadow_parity_payload(
1007
1016
  failures.append(Failure(category, f"{context} reports[{index}] must declare a known surface"))
1008
1017
  if report.get("result") not in {"match", "mismatch", "unreadable"}:
1009
1018
  failures.append(Failure(category, f"{context} reports[{index}] result must stay within the stable contract"))
1019
+ if report.get("classification") not in {None, "drift", "gate_failure"}:
1020
+ failures.append(Failure(category, f"{context} reports[{index}] classification must stay within the stable contract"))
1021
+ if not isinstance(report.get("blocking"), bool):
1022
+ failures.append(Failure(category, f"{context} reports[{index}] must include boolean `blocking`"))
1023
+ if not isinstance(report.get("recommended_action"), str) or not report.get("recommended_action"):
1024
+ failures.append(Failure(category, f"{context} reports[{index}] must include non-empty `recommended_action`"))
1025
+ if mode == "blocking" and report.get("result") != "match" and report.get("blocking") is not True:
1026
+ failures.append(Failure(category, f"{context} reports[{index}] must block non-matching reports in blocking mode"))
1010
1027
  if not isinstance(report.get("summary"), str) or not report.get("summary"):
1011
1028
  failures.append(Failure(category, f"{context} reports[{index}] must include non-empty `summary`"))
1012
1029
  if not isinstance(report.get("missing_inputs"), list):
@@ -1118,10 +1135,14 @@ def require_reconciliation_payload(
1118
1135
  if not isinstance(finding, dict):
1119
1136
  failures.append(Failure(category, f"{context} reconciliation findings must be JSON objects"))
1120
1137
  continue
1121
- if finding.get("kind") not in {"absorbed_but_open", "parent_drift", "project_drift"}:
1138
+ if finding.get("category") not in {"drift", "gate_failure"}:
1139
+ failures.append(Failure(category, f"{context} reconciliation finding category must stay within the stable taxonomy"))
1140
+ if finding.get("kind") not in {"merged_but_open", "absorbed_but_open", "parent_drift", "project_drift", "host_signal_drift"}:
1122
1141
  failures.append(Failure(category, f"{context} reconciliation finding kind must stay within the stable contract"))
1123
1142
  if finding.get("severity") not in {"warn", "fix-needed", "block"}:
1124
1143
  failures.append(Failure(category, f"{context} reconciliation finding severity must stay within the stable contract"))
1144
+ if finding.get("fallback_to") not in {"reconciliation-sync", "manual-reconciliation", None}:
1145
+ failures.append(Failure(category, f"{context} reconciliation finding fallback_to must stay within the stable contract"))
1125
1146
  if not isinstance(finding.get("subject"), str) or not finding.get("subject"):
1126
1147
  failures.append(Failure(category, f"{context} reconciliation findings must include non-empty `subject`"))
1127
1148
  if not isinstance(finding.get("evidence"), dict):
@@ -4692,18 +4713,20 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
4692
4713
  "summary": "warn",
4693
4714
  "missing_inputs": [],
4694
4715
  "fallback_to": "manual-reconciliation",
4695
- "findings": [
4696
- {
4697
- "kind": "project_drift",
4698
- "severity": "warn",
4699
- "subject": "project 5",
4700
- "evidence": {},
4701
- "recommended_action": "review warning",
4702
- }
4703
- ],
4716
+ "findings": [
4717
+ {
4718
+ "category": "drift",
4719
+ "kind": "project_drift",
4720
+ "severity": "warn",
4721
+ "subject": "project 5",
4722
+ "evidence": {},
4723
+ "recommended_action": "review warning",
4724
+ "fallback_to": "reconciliation-sync",
4725
+ }
4726
+ ],
4727
+ },
4704
4728
  },
4705
- },
4706
- )
4729
+ )
4707
4730
  if warn_payload_failures:
4708
4731
  failures.append(
4709
4732
  Failure(
@@ -4712,6 +4735,50 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
4712
4735
  )
4713
4736
  )
4714
4737
 
4738
+ valid_reconciliation_samples = [
4739
+ ("merged_but_open", "fix-needed", "reconciliation-sync"),
4740
+ ("absorbed_but_open", "fix-needed", "reconciliation-sync"),
4741
+ ("parent_drift", "block", "manual-reconciliation"),
4742
+ ("host_signal_drift", "block", "manual-reconciliation"),
4743
+ ]
4744
+ for kind, reconciliation_result_value, fallback_to in valid_reconciliation_samples:
4745
+ sample_failures = []
4746
+ require_closeout_reconciliation_contract(
4747
+ sample_failures,
4748
+ category="daily-execution-cli",
4749
+ context=f"`closeout-{kind}`",
4750
+ payload={
4751
+ "result": "block",
4752
+ "fallback_to": fallback_to,
4753
+ "reconciliation": {
4754
+ "command": "reconciliation",
4755
+ "operation": "audit",
4756
+ "result": reconciliation_result_value,
4757
+ "summary": kind,
4758
+ "missing_inputs": [] if kind != "host_signal_drift" else ["github control plane"],
4759
+ "fallback_to": "manual-reconciliation",
4760
+ "findings": [
4761
+ {
4762
+ "category": "drift",
4763
+ "kind": kind,
4764
+ "severity": reconciliation_result_value,
4765
+ "subject": "closeout sample",
4766
+ "evidence": {},
4767
+ "recommended_action": "reconcile closeout sample",
4768
+ "fallback_to": fallback_to,
4769
+ }
4770
+ ],
4771
+ },
4772
+ },
4773
+ )
4774
+ if sample_failures:
4775
+ failures.append(
4776
+ Failure(
4777
+ "daily-execution-cli",
4778
+ f"`closeout-{kind}` synthetic payload must satisfy closeout reconciliation validation",
4779
+ )
4780
+ )
4781
+
4715
4782
  return failures
4716
4783
 
4717
4784
 
@@ -5250,6 +5317,23 @@ def check_repo_interop_contracts(root: Path) -> list[Failure]:
5250
5317
  if parity_payload.get("result") != "pass":
5251
5318
  failures.append(Failure("repo-interop", "`shadow-parity` must pass when all declared surfaces match"))
5252
5319
 
5320
+ blocking_match_payload, error = load_command_json(
5321
+ root,
5322
+ ["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(present_target), "--blocking"],
5323
+ )
5324
+ if error:
5325
+ failures.append(Failure("repo-interop", f"`shadow-parity --blocking` match sample failed: {error}"))
5326
+ else:
5327
+ require_shadow_parity_payload(
5328
+ failures,
5329
+ category="repo-interop",
5330
+ context="`shadow-parity --blocking` match sample",
5331
+ payload=blocking_match_payload,
5332
+ expected_reports=4,
5333
+ )
5334
+ if blocking_match_payload.get("result") != "pass":
5335
+ failures.append(Failure("repo-interop", "`shadow-parity --blocking` must pass when all declared surfaces match"))
5336
+
5253
5337
  mismatch_target = base / "mismatch"
5254
5338
  shutil.copytree(present_target, mismatch_target)
5255
5339
  write_json(mismatch_target / ".loom/shadow/review-repo.json", {"decision": "block"})
@@ -5271,6 +5355,62 @@ def check_repo_interop_contracts(root: Path) -> list[Failure]:
5271
5355
  if not isinstance(reports, list) or not reports or reports[0].get("result") != "mismatch":
5272
5356
  failures.append(Failure("repo-interop", "`shadow-parity` mismatch sample must report `mismatch`"))
5273
5357
 
5358
+ blocking_mismatch_payload, error = load_command_json(
5359
+ root,
5360
+ [
5361
+ "python3",
5362
+ "tools/loom_flow.py",
5363
+ "shadow-parity",
5364
+ "--target",
5365
+ str(mismatch_target),
5366
+ "--surface",
5367
+ "review",
5368
+ "--mode",
5369
+ "blocking",
5370
+ ],
5371
+ )
5372
+ if error:
5373
+ failures.append(Failure("repo-interop", f"`shadow-parity --mode blocking` mismatch sample failed: {error}"))
5374
+ else:
5375
+ require_shadow_parity_payload(
5376
+ failures,
5377
+ category="repo-interop",
5378
+ context="`shadow-parity --mode blocking` mismatch sample",
5379
+ payload=blocking_mismatch_payload,
5380
+ expected_reports=1,
5381
+ )
5382
+ if blocking_mismatch_payload.get("result") != "block":
5383
+ failures.append(Failure("repo-interop", "`shadow-parity --mode blocking` must block mismatches"))
5384
+
5385
+ unreadable_target = base / "unreadable"
5386
+ shutil.copytree(present_target, unreadable_target)
5387
+ (unreadable_target / ".loom/shadow/closeout-repo.json").unlink()
5388
+ blocking_unreadable_payload, error = load_command_json(
5389
+ root,
5390
+ [
5391
+ "python3",
5392
+ "tools/loom_flow.py",
5393
+ "shadow-parity",
5394
+ "--target",
5395
+ str(unreadable_target),
5396
+ "--surface",
5397
+ "closeout",
5398
+ "--blocking",
5399
+ ],
5400
+ )
5401
+ if error:
5402
+ failures.append(Failure("repo-interop", f"`shadow-parity --blocking` unreadable sample failed: {error}"))
5403
+ else:
5404
+ require_shadow_parity_payload(
5405
+ failures,
5406
+ category="repo-interop",
5407
+ context="`shadow-parity --blocking` unreadable sample",
5408
+ payload=blocking_unreadable_payload,
5409
+ expected_reports=1,
5410
+ )
5411
+ if blocking_unreadable_payload.get("result") != "block":
5412
+ failures.append(Failure("repo-interop", "`shadow-parity --blocking` must block unreadable surfaces"))
5413
+
5274
5414
  return failures
5275
5415
 
5276
5416
 
@@ -276,6 +276,17 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
276
276
  default=".loom/bootstrap/init-result.json",
277
277
  help="Init-result path relative to the target root",
278
278
  )
279
+ shadow.add_argument(
280
+ "--mode",
281
+ choices=("validation-only", "blocking"),
282
+ default="validation-only",
283
+ help="Shadow parity enforcement mode; defaults to validation-only.",
284
+ )
285
+ shadow.add_argument(
286
+ "--blocking",
287
+ action="store_true",
288
+ help="Shortcut for --mode blocking. This is explicit opt-in and never the default.",
289
+ )
279
290
 
280
291
  runtime_parity = subparsers.add_parser(
281
292
  "runtime-parity",
@@ -543,8 +554,11 @@ def shadow_parity_report(
543
554
  empty_report = {
544
555
  "surface": surface,
545
556
  "result": "unreadable",
557
+ "classification": "gate_failure",
558
+ "blocking": False,
546
559
  "summary": "shadow parity could not be evaluated for this surface.",
547
560
  "missing_inputs": [],
561
+ "recommended_action": "restore the declared Loom and repo-native shadow parity locators before treating this surface as authoritative.",
548
562
  "host_adapters": [],
549
563
  "repo_native_carriers": [],
550
564
  "loom_surface": {
@@ -633,8 +647,11 @@ def shadow_parity_report(
633
647
  return {
634
648
  "surface": surface,
635
649
  "result": "match",
650
+ "classification": None,
651
+ "blocking": False,
636
652
  "summary": "Loom and repo-native surfaces report the same normalized result.",
637
653
  "missing_inputs": [],
654
+ "recommended_action": "no shadow parity action required.",
638
655
  "host_adapters": relevant_host_adapters,
639
656
  "repo_native_carriers": relevant_repo_native_carriers,
640
657
  "loom_surface": loom_surface,
@@ -643,8 +660,11 @@ def shadow_parity_report(
643
660
  return {
644
661
  "surface": surface,
645
662
  "result": "mismatch",
663
+ "classification": "drift",
664
+ "blocking": False,
646
665
  "summary": "Loom and repo-native surfaces disagree on the normalized result.",
647
666
  "missing_inputs": [],
667
+ "recommended_action": "resolve the parity mismatch or explicitly choose the authoritative surface outside repo interop before enabling blocking consumption.",
648
668
  "host_adapters": relevant_host_adapters,
649
669
  "repo_native_carriers": relevant_repo_native_carriers,
650
670
  "loom_surface": loom_surface,
@@ -3748,13 +3768,19 @@ def make_reconciliation_finding(
3748
3768
  subject: str,
3749
3769
  evidence: dict[str, Any],
3750
3770
  recommended_action: str,
3771
+ category: str = "drift",
3772
+ fallback_to: str | None = None,
3751
3773
  ) -> dict[str, Any]:
3774
+ if fallback_to is None:
3775
+ fallback_to = "manual-reconciliation" if severity == "block" else "reconciliation-sync"
3752
3776
  return {
3777
+ "category": category,
3753
3778
  "kind": kind,
3754
3779
  "severity": severity,
3755
3780
  "subject": subject,
3756
3781
  "evidence": evidence,
3757
3782
  "recommended_action": recommended_action,
3783
+ "fallback_to": fallback_to,
3758
3784
  }
3759
3785
 
3760
3786
 
@@ -3824,13 +3850,13 @@ def reconciliation_audit_payload(
3824
3850
  merge_commit_sha = oid
3825
3851
  merge_commit_in_main = contains_merged_commit(target_root, merge_commit_sha)
3826
3852
 
3827
- absorbed_issue = False
3853
+ merged_issue_open = False
3828
3854
  if issue_payload is not None and pr_payload is not None:
3829
3855
  if issue_payload.get("state") == "OPEN" and pr_payload.get("state") == "MERGED" and merge_commit_sha and merge_commit_in_main:
3830
- absorbed_issue = True
3856
+ merged_issue_open = True
3831
3857
  findings.append(
3832
3858
  make_reconciliation_finding(
3833
- kind="absorbed_but_open",
3859
+ kind="merged_but_open",
3834
3860
  severity="fix-needed",
3835
3861
  subject=f"issue #{issue_number}",
3836
3862
  evidence={
@@ -3840,7 +3866,7 @@ def reconciliation_audit_payload(
3840
3866
  "merge_commit": merge_commit_sha,
3841
3867
  "merge_commit_in_main": merge_commit_in_main,
3842
3868
  },
3843
- recommended_action="close the absorbed issue or run reconciliation sync after reviewing the evidence.",
3869
+ recommended_action="close the merged issue or run reconciliation sync after reviewing the evidence.",
3844
3870
  )
3845
3871
  )
3846
3872
 
@@ -3866,7 +3892,7 @@ def reconciliation_audit_payload(
3866
3892
  if child_state == "CLOSED":
3867
3893
  resolved_children.append(child)
3868
3894
  continue
3869
- if child_number == issue_number and absorbed_issue:
3895
+ if child_number == issue_number and merged_issue_open:
3870
3896
  resolved_children.append(child)
3871
3897
  continue
3872
3898
  unresolved_children.append(child)
@@ -3929,7 +3955,7 @@ def reconciliation_audit_payload(
3929
3955
  }
3930
3956
 
3931
3957
  if issue_number is not None:
3932
- expected_done = issue_payload is not None and (issue_payload.get("state") == "CLOSED" or absorbed_issue)
3958
+ expected_done = issue_payload is not None and (issue_payload.get("state") == "CLOSED" or merged_issue_open)
3933
3959
  if issue_item is None:
3934
3960
  project_drift_details.append(
3935
3961
  {
@@ -3994,12 +4020,23 @@ def reconciliation_audit_payload(
3994
4020
  )
3995
4021
 
3996
4022
  if missing_inputs:
4023
+ findings.append(
4024
+ make_reconciliation_finding(
4025
+ kind="host_signal_drift",
4026
+ severity="block",
4027
+ subject="github control plane",
4028
+ evidence={"missing_inputs": missing_inputs},
4029
+ recommended_action="restore readable GitHub issue, PR, project, or repository signals before closeout.",
4030
+ category="drift",
4031
+ fallback_to="manual-reconciliation",
4032
+ )
4033
+ )
3997
4034
  result = "block"
3998
4035
  summary = "reconciliation audit could not complete because required GitHub inputs were missing."
3999
4036
  else:
4000
4037
  result = reconciliation_result(findings)
4001
4038
  summary = (
4002
- "reconciliation audit found no absorbed-but-open, parent-drift, or project-drift findings."
4039
+ "reconciliation audit found no merged-but-open, absorbed-but-open, parent-drift, host-signal-drift, or project-drift findings."
4003
4040
  if result == "pass"
4004
4041
  else "reconciliation audit found GitHub drift that must be reviewed before closeout."
4005
4042
  )
@@ -4037,7 +4074,7 @@ def reconciliation_sync_plan(audit_payload: dict[str, Any]) -> tuple[list[dict[s
4037
4074
  evidence = finding.get("evidence")
4038
4075
  if severity != "fix-needed":
4039
4076
  continue
4040
- if kind == "absorbed_but_open":
4077
+ if kind in {"merged_but_open", "absorbed_but_open"}:
4041
4078
  plan.append(
4042
4079
  {
4043
4080
  "kind": kind,
@@ -6105,6 +6142,7 @@ def handle_shadow_parity(args: argparse.Namespace) -> int:
6105
6142
  governance_surface = build_governance_surface(target_root)
6106
6143
  repo_interop = governance_surface.get("repo_interop")
6107
6144
  requested_surfaces = SHADOW_PARITY_SURFACES if args.surface == "all" else (args.surface,)
6145
+ mode = "blocking" if args.blocking else args.mode
6108
6146
  reports = [
6109
6147
  shadow_parity_report(
6110
6148
  repo_interop,
@@ -6114,9 +6152,18 @@ def handle_shadow_parity(args: argparse.Namespace) -> int:
6114
6152
  for surface in requested_surfaces
6115
6153
  ]
6116
6154
 
6117
- result = "pass" if reports and all(report["result"] == "match" for report in reports) else "warn"
6155
+ all_match = bool(reports) and all(report["result"] == "match" for report in reports)
6156
+ blocking_reports = [report for report in reports if report.get("result") != "match"]
6157
+ if mode == "blocking":
6158
+ result = "pass" if all_match else "block"
6159
+ for report in blocking_reports:
6160
+ report["blocking"] = True
6161
+ else:
6162
+ result = "pass" if all_match else "warn"
6118
6163
  if result == "pass":
6119
6164
  summary = "shadow parity matches across all requested surfaces."
6165
+ elif mode == "blocking":
6166
+ summary = "shadow parity blocking mode found mismatch or unreadable surfaces."
6120
6167
  else:
6121
6168
  summaries = {report["result"] for report in reports}
6122
6169
  if "mismatch" in summaries:
@@ -6133,10 +6180,12 @@ def handle_shadow_parity(args: argparse.Namespace) -> int:
6133
6180
  return emit(
6134
6181
  {
6135
6182
  "command": "shadow-parity",
6183
+ "mode": mode,
6184
+ "blocking": mode == "blocking",
6136
6185
  "result": result,
6137
6186
  "summary": summary,
6138
6187
  "missing_inputs": missing_inputs,
6139
- "fallback_to": None,
6188
+ "fallback_to": "manual-reconciliation" if result == "block" else None,
6140
6189
  "runtime_state": runtime_state,
6141
6190
  "governance_surface": governance_surface,
6142
6191
  "reports": reports,