@mc-and-his-agents/loom-installer 0.1.12 → 0.1.14

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 +178 -4
  4. package/payload/plugin/loom/skills/shared/scripts/loom_flow.py +286 -3
  5. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +178 -4
  6. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py +286 -3
  7. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +178 -4
  8. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py +286 -3
  9. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +178 -4
  10. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py +286 -3
  11. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +178 -4
  12. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py +286 -3
  13. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +178 -4
  14. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py +286 -3
  15. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +178 -4
  16. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py +286 -3
  17. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +178 -4
  18. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py +286 -3
  19. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +178 -4
  20. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py +286 -3
  21. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py +178 -4
  22. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py +286 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mc-and-his-agents/loom-installer",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
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": "f11f0f24401eca30d21786c41e39a74be05bf342",
5
+ "source_commit": "7f500dab49df607146abae440b849dad8f90cf27",
6
6
  "source_ref": "main",
7
- "built_at": "2026-04-25T11:57:32+08:00",
7
+ "built_at": "2026-04-25T12:49:57+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": 256345,
632
- "sha256": "5a515322557bf66f21c37e138e3e0302be450842abdf5b42a5c9b94a62012b95"
631
+ "bytes": 265359,
632
+ "sha256": "8d94670aca94c6166294e145d497eb9f9ba6bb1345258d131ea8722cb071d786"
633
633
  },
634
634
  {
635
635
  "path": "plugin/loom/skills/shared/scripts/loom_flow.py",
636
- "bytes": 260571,
637
- "sha256": "47c0437b00572be5cf84125fd5ccde1449afb41c93c493942e6c4c1dbf3462cb"
636
+ "bytes": 274468,
637
+ "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
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": 256345,
1217
- "sha256": "5a515322557bf66f21c37e138e3e0302be450842abdf5b42a5c9b94a62012b95"
1216
+ "bytes": 265359,
1217
+ "sha256": "8d94670aca94c6166294e145d497eb9f9ba6bb1345258d131ea8722cb071d786"
1218
1218
  },
1219
1219
  {
1220
1220
  "path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py",
1221
- "bytes": 260571,
1222
- "sha256": "47c0437b00572be5cf84125fd5ccde1449afb41c93c493942e6c4c1dbf3462cb"
1221
+ "bytes": 274468,
1222
+ "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
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": 256345,
1832
- "sha256": "5a515322557bf66f21c37e138e3e0302be450842abdf5b42a5c9b94a62012b95"
1831
+ "bytes": 265359,
1832
+ "sha256": "8d94670aca94c6166294e145d497eb9f9ba6bb1345258d131ea8722cb071d786"
1833
1833
  },
1834
1834
  {
1835
1835
  "path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py",
1836
- "bytes": 260571,
1837
- "sha256": "47c0437b00572be5cf84125fd5ccde1449afb41c93c493942e6c4c1dbf3462cb"
1836
+ "bytes": 274468,
1837
+ "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
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": 256345,
2447
- "sha256": "5a515322557bf66f21c37e138e3e0302be450842abdf5b42a5c9b94a62012b95"
2446
+ "bytes": 265359,
2447
+ "sha256": "8d94670aca94c6166294e145d497eb9f9ba6bb1345258d131ea8722cb071d786"
2448
2448
  },
2449
2449
  {
2450
2450
  "path": "skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py",
2451
- "bytes": 260571,
2452
- "sha256": "47c0437b00572be5cf84125fd5ccde1449afb41c93c493942e6c4c1dbf3462cb"
2451
+ "bytes": 274468,
2452
+ "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
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": 256345,
3067
- "sha256": "5a515322557bf66f21c37e138e3e0302be450842abdf5b42a5c9b94a62012b95"
3066
+ "bytes": 265359,
3067
+ "sha256": "8d94670aca94c6166294e145d497eb9f9ba6bb1345258d131ea8722cb071d786"
3068
3068
  },
3069
3069
  {
3070
3070
  "path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py",
3071
- "bytes": 260571,
3072
- "sha256": "47c0437b00572be5cf84125fd5ccde1449afb41c93c493942e6c4c1dbf3462cb"
3071
+ "bytes": 274468,
3072
+ "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
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": 256345,
3682
- "sha256": "5a515322557bf66f21c37e138e3e0302be450842abdf5b42a5c9b94a62012b95"
3681
+ "bytes": 265359,
3682
+ "sha256": "8d94670aca94c6166294e145d497eb9f9ba6bb1345258d131ea8722cb071d786"
3683
3683
  },
3684
3684
  {
3685
3685
  "path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py",
3686
- "bytes": 260571,
3687
- "sha256": "47c0437b00572be5cf84125fd5ccde1449afb41c93c493942e6c4c1dbf3462cb"
3686
+ "bytes": 274468,
3687
+ "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
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": 256345,
4297
- "sha256": "5a515322557bf66f21c37e138e3e0302be450842abdf5b42a5c9b94a62012b95"
4296
+ "bytes": 265359,
4297
+ "sha256": "8d94670aca94c6166294e145d497eb9f9ba6bb1345258d131ea8722cb071d786"
4298
4298
  },
4299
4299
  {
4300
4300
  "path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py",
4301
- "bytes": 260571,
4302
- "sha256": "47c0437b00572be5cf84125fd5ccde1449afb41c93c493942e6c4c1dbf3462cb"
4301
+ "bytes": 274468,
4302
+ "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
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": 256345,
4912
- "sha256": "5a515322557bf66f21c37e138e3e0302be450842abdf5b42a5c9b94a62012b95"
4911
+ "bytes": 265359,
4912
+ "sha256": "8d94670aca94c6166294e145d497eb9f9ba6bb1345258d131ea8722cb071d786"
4913
4913
  },
4914
4914
  {
4915
4915
  "path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py",
4916
- "bytes": 260571,
4917
- "sha256": "47c0437b00572be5cf84125fd5ccde1449afb41c93c493942e6c4c1dbf3462cb"
4916
+ "bytes": 274468,
4917
+ "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
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": 256345,
5527
- "sha256": "5a515322557bf66f21c37e138e3e0302be450842abdf5b42a5c9b94a62012b95"
5526
+ "bytes": 265359,
5527
+ "sha256": "8d94670aca94c6166294e145d497eb9f9ba6bb1345258d131ea8722cb071d786"
5528
5528
  },
5529
5529
  {
5530
5530
  "path": "skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py",
5531
- "bytes": 260571,
5532
- "sha256": "47c0437b00572be5cf84125fd5ccde1449afb41c93c493942e6c4c1dbf3462cb"
5531
+ "bytes": 274468,
5532
+ "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
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": 256345,
6142
- "sha256": "5a515322557bf66f21c37e138e3e0302be450842abdf5b42a5c9b94a62012b95"
6141
+ "bytes": 265359,
6142
+ "sha256": "8d94670aca94c6166294e145d497eb9f9ba6bb1345258d131ea8722cb071d786"
6143
6143
  },
6144
6144
  {
6145
6145
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py",
6146
- "bytes": 260571,
6147
- "sha256": "47c0437b00572be5cf84125fd5ccde1449afb41c93c493942e6c4c1dbf3462cb"
6146
+ "bytes": 274468,
6147
+ "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
6148
6148
  },
6149
6149
  {
6150
6150
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_init.py",
@@ -93,7 +93,9 @@ CORE_DOCS = (
93
93
  "docs/evidence/extraction-ledger.md",
94
94
  "docs/evidence/landing-map.md",
95
95
  "docs/evidence/validations/validation-closeout-reconciliation-blocking-gate.md",
96
+ "docs/evidence/validations/validation-github-profile-binding-orchestration.md",
96
97
  "docs/evidence/validations/validation-loom-core-runtime-parity.md",
98
+ "docs/evidence/validations/validation-shadow-parity-blocking-gate.md",
97
99
  "docs/evidence/validations/validation-syvert-strong-governance-parity.md",
98
100
  "docs/adoption/rationale.md",
99
101
  "docs/adoption/routing-and-checkpoints.md",
@@ -969,10 +971,17 @@ def require_shadow_parity_payload(
969
971
  return
970
972
  if payload.get("command") != "shadow-parity":
971
973
  failures.append(Failure(category, f"{context} must report `command: shadow-parity`"))
972
- if payload.get("result") not in {"pass", "warn"}:
973
- failures.append(Failure(category, f"{context} result must be `pass` or `warn`"))
974
- if payload.get("fallback_to") is not None:
975
- failures.append(Failure(category, f"{context} fallback_to must remain `null`"))
974
+ mode = payload.get("mode", "validation-only")
975
+ if mode not in {"validation-only", "blocking"}:
976
+ failures.append(Failure(category, f"{context} mode must be `validation-only` or `blocking`"))
977
+ if payload.get("blocking") != (mode == "blocking"):
978
+ failures.append(Failure(category, f"{context} blocking flag must match mode"))
979
+ allowed_results = {"pass", "block"} if mode == "blocking" else {"pass", "warn"}
980
+ if payload.get("result") not in allowed_results:
981
+ failures.append(Failure(category, f"{context} result must stay within the stable mode-specific contract"))
982
+ expected_fallbacks = {"manual-reconciliation"} if payload.get("result") == "block" else {None}
983
+ if payload.get("fallback_to") not in expected_fallbacks:
984
+ failures.append(Failure(category, f"{context} fallback_to must match the shadow parity enforcement mode"))
976
985
  if not isinstance(payload.get("summary"), str) or not payload.get("summary"):
977
986
  failures.append(Failure(category, f"{context} must include non-empty `summary`"))
978
987
  if not isinstance(payload.get("missing_inputs"), list):
@@ -1008,6 +1017,14 @@ def require_shadow_parity_payload(
1008
1017
  failures.append(Failure(category, f"{context} reports[{index}] must declare a known surface"))
1009
1018
  if report.get("result") not in {"match", "mismatch", "unreadable"}:
1010
1019
  failures.append(Failure(category, f"{context} reports[{index}] result must stay within the stable contract"))
1020
+ if report.get("classification") not in {None, "drift", "gate_failure"}:
1021
+ failures.append(Failure(category, f"{context} reports[{index}] classification must stay within the stable contract"))
1022
+ if not isinstance(report.get("blocking"), bool):
1023
+ failures.append(Failure(category, f"{context} reports[{index}] must include boolean `blocking`"))
1024
+ if not isinstance(report.get("recommended_action"), str) or not report.get("recommended_action"):
1025
+ failures.append(Failure(category, f"{context} reports[{index}] must include non-empty `recommended_action`"))
1026
+ if mode == "blocking" and report.get("result") != "match" and report.get("blocking") is not True:
1027
+ failures.append(Failure(category, f"{context} reports[{index}] must block non-matching reports in blocking mode"))
1011
1028
  if not isinstance(report.get("summary"), str) or not report.get("summary"):
1012
1029
  failures.append(Failure(category, f"{context} reports[{index}] must include non-empty `summary`"))
1013
1030
  if not isinstance(report.get("missing_inputs"), list):
@@ -1230,6 +1247,78 @@ def require_runtime_parity_payload(
1230
1247
  failures.append(Failure(category, f"{context} check `{check.get('name')}` must include `evidence`"))
1231
1248
 
1232
1249
 
1250
+ def require_github_binding_payload(
1251
+ failures: list[Failure],
1252
+ *,
1253
+ category: str,
1254
+ context: str,
1255
+ payload: object,
1256
+ ) -> None:
1257
+ if not isinstance(payload, dict):
1258
+ failures.append(Failure(category, f"{context} must be an object"))
1259
+ return
1260
+ if payload.get("command") != "governance-profile":
1261
+ failures.append(Failure(category, f"{context} must report `command: governance-profile`"))
1262
+ if payload.get("operation") != "binding":
1263
+ failures.append(Failure(category, f"{context} must report `operation: binding`"))
1264
+ if payload.get("schema_version") != "loom-github-binding/v1":
1265
+ failures.append(Failure(category, f"{context} schema_version must be `loom-github-binding/v1`"))
1266
+ if payload.get("result") not in {"pass", "block"}:
1267
+ failures.append(Failure(category, f"{context} result must be pass/block"))
1268
+ if payload.get("fallback_to") not in {None, "github-profile-binding"}:
1269
+ failures.append(Failure(category, f"{context} fallback_to must be null or `github-profile-binding`"))
1270
+ if not isinstance(payload.get("missing_inputs"), list):
1271
+ failures.append(Failure(category, f"{context} missing_inputs must be a list"))
1272
+ binding = payload.get("binding")
1273
+ if not isinstance(binding, dict):
1274
+ failures.append(Failure(category, f"{context} must include `binding` as an object"))
1275
+ return
1276
+ if binding.get("schema_version") != "loom-github-binding/v1":
1277
+ failures.append(Failure(category, f"{context}.binding schema_version must be `loom-github-binding/v1`"))
1278
+ objects = binding.get("objects")
1279
+ expected_objects = {"phase", "fr", "work_item", "branch", "implementation_pr", "merge_commit", "target_branch"}
1280
+ if not isinstance(objects, dict) or set(objects) != expected_objects:
1281
+ failures.append(Failure(category, f"{context}.binding.objects must expose the stable GitHub binding object set"))
1282
+ chain = binding.get("chain")
1283
+ expected_chain = [
1284
+ ("phase", "fr"),
1285
+ ("fr", "work_item"),
1286
+ ("work_item", "implementation_pr"),
1287
+ ("implementation_pr", "merge_commit"),
1288
+ ("merge_commit", "target_branch"),
1289
+ ]
1290
+ if not isinstance(chain, list):
1291
+ failures.append(Failure(category, f"{context}.binding.chain must be a list"))
1292
+ else:
1293
+ actual_chain = [
1294
+ (entry.get("from"), entry.get("to"))
1295
+ for entry in chain
1296
+ if isinstance(entry, dict)
1297
+ ]
1298
+ if actual_chain != expected_chain:
1299
+ failures.append(Failure(category, f"{context}.binding.chain must preserve Phase -> FR -> Work Item -> PR -> merge commit -> target branch order"))
1300
+ for entry in chain:
1301
+ if not isinstance(entry, dict):
1302
+ failures.append(Failure(category, f"{context}.binding.chain entries must be objects"))
1303
+ continue
1304
+ if entry.get("status") not in {"present", "missing"}:
1305
+ failures.append(Failure(category, f"{context}.binding.chain statuses must be present/missing"))
1306
+ findings = binding.get("findings")
1307
+ if not isinstance(findings, list):
1308
+ failures.append(Failure(category, f"{context}.binding.findings must be a list"))
1309
+ return
1310
+ for finding in findings:
1311
+ if not isinstance(finding, dict):
1312
+ failures.append(Failure(category, f"{context}.binding.findings entries must be objects"))
1313
+ continue
1314
+ if finding.get("category") not in {"stale", "drift", "gate_failure"}:
1315
+ failures.append(Failure(category, f"{context}.binding findings must use stable taxonomy categories"))
1316
+ if finding.get("kind") != "binding_failure":
1317
+ failures.append(Failure(category, f"{context}.binding findings must use `binding_failure` for orchestration gaps"))
1318
+ if finding.get("fallback_to") != "github-profile-binding":
1319
+ failures.append(Failure(category, f"{context}.binding findings must fallback to `github-profile-binding`"))
1320
+
1321
+
1233
1322
  def require_review_record_contract(
1234
1323
  failures: list[Failure],
1235
1324
  *,
@@ -2216,6 +2305,11 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
2216
2305
  ["python3", "tools/loom_flow.py", "governance-profile", "upgrade-plan", "--target", "examples/new-project"],
2217
2306
  {"pass", "block"},
2218
2307
  ),
2308
+ (
2309
+ "governance-profile-binding",
2310
+ ["python3", "tools/loom_flow.py", "governance-profile", "binding", "--target", "."],
2311
+ {"block"},
2312
+ ),
2219
2313
  (
2220
2314
  "flow-pre-review",
2221
2315
  [
@@ -2463,6 +2557,13 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
2463
2557
  context=f"`{label}` governance_control_plane",
2464
2558
  payload=control_plane,
2465
2559
  )
2560
+ if label == "governance-profile-binding":
2561
+ require_github_binding_payload(
2562
+ failures,
2563
+ category="daily-execution-cli",
2564
+ context="`governance-profile binding`",
2565
+ payload=payload,
2566
+ )
2466
2567
  if label == "flow-pre-review":
2467
2568
  require_runtime_state_payload(
2468
2569
  failures,
@@ -5301,6 +5402,23 @@ def check_repo_interop_contracts(root: Path) -> list[Failure]:
5301
5402
  if parity_payload.get("result") != "pass":
5302
5403
  failures.append(Failure("repo-interop", "`shadow-parity` must pass when all declared surfaces match"))
5303
5404
 
5405
+ blocking_match_payload, error = load_command_json(
5406
+ root,
5407
+ ["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(present_target), "--blocking"],
5408
+ )
5409
+ if error:
5410
+ failures.append(Failure("repo-interop", f"`shadow-parity --blocking` match sample failed: {error}"))
5411
+ else:
5412
+ require_shadow_parity_payload(
5413
+ failures,
5414
+ category="repo-interop",
5415
+ context="`shadow-parity --blocking` match sample",
5416
+ payload=blocking_match_payload,
5417
+ expected_reports=4,
5418
+ )
5419
+ if blocking_match_payload.get("result") != "pass":
5420
+ failures.append(Failure("repo-interop", "`shadow-parity --blocking` must pass when all declared surfaces match"))
5421
+
5304
5422
  mismatch_target = base / "mismatch"
5305
5423
  shutil.copytree(present_target, mismatch_target)
5306
5424
  write_json(mismatch_target / ".loom/shadow/review-repo.json", {"decision": "block"})
@@ -5322,6 +5440,62 @@ def check_repo_interop_contracts(root: Path) -> list[Failure]:
5322
5440
  if not isinstance(reports, list) or not reports or reports[0].get("result") != "mismatch":
5323
5441
  failures.append(Failure("repo-interop", "`shadow-parity` mismatch sample must report `mismatch`"))
5324
5442
 
5443
+ blocking_mismatch_payload, error = load_command_json(
5444
+ root,
5445
+ [
5446
+ "python3",
5447
+ "tools/loom_flow.py",
5448
+ "shadow-parity",
5449
+ "--target",
5450
+ str(mismatch_target),
5451
+ "--surface",
5452
+ "review",
5453
+ "--mode",
5454
+ "blocking",
5455
+ ],
5456
+ )
5457
+ if error:
5458
+ failures.append(Failure("repo-interop", f"`shadow-parity --mode blocking` mismatch sample failed: {error}"))
5459
+ else:
5460
+ require_shadow_parity_payload(
5461
+ failures,
5462
+ category="repo-interop",
5463
+ context="`shadow-parity --mode blocking` mismatch sample",
5464
+ payload=blocking_mismatch_payload,
5465
+ expected_reports=1,
5466
+ )
5467
+ if blocking_mismatch_payload.get("result") != "block":
5468
+ failures.append(Failure("repo-interop", "`shadow-parity --mode blocking` must block mismatches"))
5469
+
5470
+ unreadable_target = base / "unreadable"
5471
+ shutil.copytree(present_target, unreadable_target)
5472
+ (unreadable_target / ".loom/shadow/closeout-repo.json").unlink()
5473
+ blocking_unreadable_payload, error = load_command_json(
5474
+ root,
5475
+ [
5476
+ "python3",
5477
+ "tools/loom_flow.py",
5478
+ "shadow-parity",
5479
+ "--target",
5480
+ str(unreadable_target),
5481
+ "--surface",
5482
+ "closeout",
5483
+ "--blocking",
5484
+ ],
5485
+ )
5486
+ if error:
5487
+ failures.append(Failure("repo-interop", f"`shadow-parity --blocking` unreadable sample failed: {error}"))
5488
+ else:
5489
+ require_shadow_parity_payload(
5490
+ failures,
5491
+ category="repo-interop",
5492
+ context="`shadow-parity --blocking` unreadable sample",
5493
+ payload=blocking_unreadable_payload,
5494
+ expected_reports=1,
5495
+ )
5496
+ if blocking_unreadable_payload.get("result") != "block":
5497
+ failures.append(Failure("repo-interop", "`shadow-parity --blocking` must block unreadable surfaces"))
5498
+
5325
5499
  return failures
5326
5500
 
5327
5501