@mc-and-his-agents/loom-installer 0.1.33 → 0.1.35

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 +104 -22
  4. package/payload/plugin/loom/skills/shared/scripts/loom_flow.py +2 -2
  5. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +104 -22
  6. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py +2 -2
  7. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +104 -22
  8. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py +2 -2
  9. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +104 -22
  10. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py +2 -2
  11. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +104 -22
  12. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py +2 -2
  13. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +104 -22
  14. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py +2 -2
  15. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +104 -22
  16. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py +2 -2
  17. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +104 -22
  18. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py +2 -2
  19. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +104 -22
  20. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py +2 -2
  21. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py +104 -22
  22. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mc-and-his-agents/loom-installer",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
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": "d27c46eb73b6d01b374b1b0df34287faffa5e858",
5
+ "source_commit": "caf16c628cee5a632ace111fa0d192191ea12a9c",
6
6
  "source_ref": "main",
7
- "built_at": "2026-04-27T14:34:16+08:00",
7
+ "built_at": "2026-04-27T15:02:07+08:00",
8
8
  "runtime": {
9
9
  "python_minimum": "3.10",
10
10
  "python_recommended": "3.11+"
@@ -633,13 +633,13 @@
633
633
  },
634
634
  {
635
635
  "path": "plugin/loom/skills/shared/scripts/loom_check.py",
636
- "bytes": 327014,
637
- "sha256": "d2210ea508da857c7e885b6ce6c318a934d982653a5a0c600ade1212641c755a"
636
+ "bytes": 331479,
637
+ "sha256": "a5916d824ef9d6ef2d8f18378758cc072b3af7ba5cb67923b32dea16c2c5786d"
638
638
  },
639
639
  {
640
640
  "path": "plugin/loom/skills/shared/scripts/loom_flow.py",
641
- "bytes": 332808,
642
- "sha256": "174f6f787703e98462a6231eab451c66a53d8448ad26081d8e30e1a2ee3ac0e3"
641
+ "bytes": 332816,
642
+ "sha256": "55f9d625dfb3cf35e7d0f849878700b29a099975831b5caccb3ce87fa4134def"
643
643
  },
644
644
  {
645
645
  "path": "plugin/loom/skills/shared/scripts/loom_init.py",
@@ -1223,13 +1223,13 @@
1223
1223
  },
1224
1224
  {
1225
1225
  "path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py",
1226
- "bytes": 327014,
1227
- "sha256": "d2210ea508da857c7e885b6ce6c318a934d982653a5a0c600ade1212641c755a"
1226
+ "bytes": 331479,
1227
+ "sha256": "a5916d824ef9d6ef2d8f18378758cc072b3af7ba5cb67923b32dea16c2c5786d"
1228
1228
  },
1229
1229
  {
1230
1230
  "path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py",
1231
- "bytes": 332808,
1232
- "sha256": "174f6f787703e98462a6231eab451c66a53d8448ad26081d8e30e1a2ee3ac0e3"
1231
+ "bytes": 332816,
1232
+ "sha256": "55f9d625dfb3cf35e7d0f849878700b29a099975831b5caccb3ce87fa4134def"
1233
1233
  },
1234
1234
  {
1235
1235
  "path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_init.py",
@@ -1843,13 +1843,13 @@
1843
1843
  },
1844
1844
  {
1845
1845
  "path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py",
1846
- "bytes": 327014,
1847
- "sha256": "d2210ea508da857c7e885b6ce6c318a934d982653a5a0c600ade1212641c755a"
1846
+ "bytes": 331479,
1847
+ "sha256": "a5916d824ef9d6ef2d8f18378758cc072b3af7ba5cb67923b32dea16c2c5786d"
1848
1848
  },
1849
1849
  {
1850
1850
  "path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py",
1851
- "bytes": 332808,
1852
- "sha256": "174f6f787703e98462a6231eab451c66a53d8448ad26081d8e30e1a2ee3ac0e3"
1851
+ "bytes": 332816,
1852
+ "sha256": "55f9d625dfb3cf35e7d0f849878700b29a099975831b5caccb3ce87fa4134def"
1853
1853
  },
1854
1854
  {
1855
1855
  "path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_init.py",
@@ -2463,13 +2463,13 @@
2463
2463
  },
2464
2464
  {
2465
2465
  "path": "skills/loom-init/.loom-runtime/shared/scripts/loom_check.py",
2466
- "bytes": 327014,
2467
- "sha256": "d2210ea508da857c7e885b6ce6c318a934d982653a5a0c600ade1212641c755a"
2466
+ "bytes": 331479,
2467
+ "sha256": "a5916d824ef9d6ef2d8f18378758cc072b3af7ba5cb67923b32dea16c2c5786d"
2468
2468
  },
2469
2469
  {
2470
2470
  "path": "skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py",
2471
- "bytes": 332808,
2472
- "sha256": "174f6f787703e98462a6231eab451c66a53d8448ad26081d8e30e1a2ee3ac0e3"
2471
+ "bytes": 332816,
2472
+ "sha256": "55f9d625dfb3cf35e7d0f849878700b29a099975831b5caccb3ce87fa4134def"
2473
2473
  },
2474
2474
  {
2475
2475
  "path": "skills/loom-init/.loom-runtime/shared/scripts/loom_init.py",
@@ -3088,13 +3088,13 @@
3088
3088
  },
3089
3089
  {
3090
3090
  "path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py",
3091
- "bytes": 327014,
3092
- "sha256": "d2210ea508da857c7e885b6ce6c318a934d982653a5a0c600ade1212641c755a"
3091
+ "bytes": 331479,
3092
+ "sha256": "a5916d824ef9d6ef2d8f18378758cc072b3af7ba5cb67923b32dea16c2c5786d"
3093
3093
  },
3094
3094
  {
3095
3095
  "path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py",
3096
- "bytes": 332808,
3097
- "sha256": "174f6f787703e98462a6231eab451c66a53d8448ad26081d8e30e1a2ee3ac0e3"
3096
+ "bytes": 332816,
3097
+ "sha256": "55f9d625dfb3cf35e7d0f849878700b29a099975831b5caccb3ce87fa4134def"
3098
3098
  },
3099
3099
  {
3100
3100
  "path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_init.py",
@@ -3708,13 +3708,13 @@
3708
3708
  },
3709
3709
  {
3710
3710
  "path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py",
3711
- "bytes": 327014,
3712
- "sha256": "d2210ea508da857c7e885b6ce6c318a934d982653a5a0c600ade1212641c755a"
3711
+ "bytes": 331479,
3712
+ "sha256": "a5916d824ef9d6ef2d8f18378758cc072b3af7ba5cb67923b32dea16c2c5786d"
3713
3713
  },
3714
3714
  {
3715
3715
  "path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py",
3716
- "bytes": 332808,
3717
- "sha256": "174f6f787703e98462a6231eab451c66a53d8448ad26081d8e30e1a2ee3ac0e3"
3716
+ "bytes": 332816,
3717
+ "sha256": "55f9d625dfb3cf35e7d0f849878700b29a099975831b5caccb3ce87fa4134def"
3718
3718
  },
3719
3719
  {
3720
3720
  "path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_init.py",
@@ -4328,13 +4328,13 @@
4328
4328
  },
4329
4329
  {
4330
4330
  "path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py",
4331
- "bytes": 327014,
4332
- "sha256": "d2210ea508da857c7e885b6ce6c318a934d982653a5a0c600ade1212641c755a"
4331
+ "bytes": 331479,
4332
+ "sha256": "a5916d824ef9d6ef2d8f18378758cc072b3af7ba5cb67923b32dea16c2c5786d"
4333
4333
  },
4334
4334
  {
4335
4335
  "path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py",
4336
- "bytes": 332808,
4337
- "sha256": "174f6f787703e98462a6231eab451c66a53d8448ad26081d8e30e1a2ee3ac0e3"
4336
+ "bytes": 332816,
4337
+ "sha256": "55f9d625dfb3cf35e7d0f849878700b29a099975831b5caccb3ce87fa4134def"
4338
4338
  },
4339
4339
  {
4340
4340
  "path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_init.py",
@@ -4948,13 +4948,13 @@
4948
4948
  },
4949
4949
  {
4950
4950
  "path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py",
4951
- "bytes": 327014,
4952
- "sha256": "d2210ea508da857c7e885b6ce6c318a934d982653a5a0c600ade1212641c755a"
4951
+ "bytes": 331479,
4952
+ "sha256": "a5916d824ef9d6ef2d8f18378758cc072b3af7ba5cb67923b32dea16c2c5786d"
4953
4953
  },
4954
4954
  {
4955
4955
  "path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py",
4956
- "bytes": 332808,
4957
- "sha256": "174f6f787703e98462a6231eab451c66a53d8448ad26081d8e30e1a2ee3ac0e3"
4956
+ "bytes": 332816,
4957
+ "sha256": "55f9d625dfb3cf35e7d0f849878700b29a099975831b5caccb3ce87fa4134def"
4958
4958
  },
4959
4959
  {
4960
4960
  "path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_init.py",
@@ -5568,13 +5568,13 @@
5568
5568
  },
5569
5569
  {
5570
5570
  "path": "skills/loom-review/.loom-runtime/shared/scripts/loom_check.py",
5571
- "bytes": 327014,
5572
- "sha256": "d2210ea508da857c7e885b6ce6c318a934d982653a5a0c600ade1212641c755a"
5571
+ "bytes": 331479,
5572
+ "sha256": "a5916d824ef9d6ef2d8f18378758cc072b3af7ba5cb67923b32dea16c2c5786d"
5573
5573
  },
5574
5574
  {
5575
5575
  "path": "skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py",
5576
- "bytes": 332808,
5577
- "sha256": "174f6f787703e98462a6231eab451c66a53d8448ad26081d8e30e1a2ee3ac0e3"
5576
+ "bytes": 332816,
5577
+ "sha256": "55f9d625dfb3cf35e7d0f849878700b29a099975831b5caccb3ce87fa4134def"
5578
5578
  },
5579
5579
  {
5580
5580
  "path": "skills/loom-review/.loom-runtime/shared/scripts/loom_init.py",
@@ -6188,13 +6188,13 @@
6188
6188
  },
6189
6189
  {
6190
6190
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py",
6191
- "bytes": 327014,
6192
- "sha256": "d2210ea508da857c7e885b6ce6c318a934d982653a5a0c600ade1212641c755a"
6191
+ "bytes": 331479,
6192
+ "sha256": "a5916d824ef9d6ef2d8f18378758cc072b3af7ba5cb67923b32dea16c2c5786d"
6193
6193
  },
6194
6194
  {
6195
6195
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py",
6196
- "bytes": 332808,
6197
- "sha256": "174f6f787703e98462a6231eab451c66a53d8448ad26081d8e30e1a2ee3ac0e3"
6196
+ "bytes": 332816,
6197
+ "sha256": "55f9d625dfb3cf35e7d0f849878700b29a099975831b5caccb3ce87fa4134def"
6198
6198
  },
6199
6199
  {
6200
6200
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_init.py",
@@ -100,6 +100,7 @@ CORE_DOCS = (
100
100
  "docs/evidence/validations/validation-adoption-gate-rollout.md",
101
101
  "docs/evidence/validations/validation-external-runtime-devendor-migration.md",
102
102
  "docs/evidence/validations/validation-syvert-adversarial-adoption-fixture.md",
103
+ "docs/evidence/validations/validation-zero-friction-adoption-hardening.md",
103
104
  "docs/evidence/validations/validation-github-profile-binding-orchestration.md",
104
105
  "docs/evidence/validations/validation-github-profile-drift-reconciliation.md",
105
106
  "docs/evidence/validations/validation-github-profile-graphql-budget-guard.md",
@@ -6376,12 +6377,47 @@ def check_adversarial_adoption_fixture(root: Path) -> list[Failure]:
6376
6377
  ("shadow-parity", ["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(baseline)], "pass"),
6377
6378
  ("shadow-parity --blocking", ["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(baseline), "--blocking"], "pass"),
6378
6379
  ("flow resume", ["python3", "tools/loom_flow.py", "flow", "resume", "--target", str(baseline), "--item", "INIT-0001"], "pass"),
6380
+ ("adopt verify", ["python3", "tools/loom_flow.py", "adopt", "verify", "--target", str(baseline), "--item", "INIT-0001"], "pass"),
6379
6381
  ):
6380
6382
  payload, error = load_command_json(root, args)
6381
6383
  if error:
6382
6384
  failures.append(Failure("adversarial-adoption", f"`{label}` baseline failed: {error}"))
6383
6385
  elif payload.get("result") != expected:
6384
6386
  failures.append(Failure("adversarial-adoption", f"`{label}` baseline must return `{expected}`"))
6387
+ elif label == "adopt verify":
6388
+ roundtrip = payload.get("producer_consumer_roundtrip")
6389
+ deleted_section = (
6390
+ roundtrip.get("bypass_check")
6391
+ if isinstance(roundtrip, dict)
6392
+ else None
6393
+ )
6394
+ if not isinstance(deleted_section, dict) or deleted_section.get("consumer_result") != "block":
6395
+ failures.append(Failure("adversarial-adoption", "`adopt verify` must prove required Review Artifacts deletion cannot bypass the consumer"))
6396
+
6397
+ sha_only_payload, sha_only_error = load_command_json(
6398
+ root,
6399
+ [
6400
+ "python3",
6401
+ "tools/loom_flow.py",
6402
+ "host-binding",
6403
+ "validate",
6404
+ "--target",
6405
+ str(baseline),
6406
+ "--owner",
6407
+ "MC-and-his-Agents",
6408
+ "--repo",
6409
+ "Loom",
6410
+ "--head-sha",
6411
+ current_head,
6412
+ ],
6413
+ timeout_seconds=60,
6414
+ )
6415
+ if sha_only_error:
6416
+ failures.append(Failure("adversarial-adoption", f"SHA-only host-binding negative sample failed: {sha_only_error}"))
6417
+ else:
6418
+ missing_inputs = sha_only_payload.get("missing_inputs")
6419
+ if sha_only_payload.get("result") != "block" or not isinstance(missing_inputs, list) or not missing_inputs:
6420
+ failures.append(Failure("adversarial-adoption", "SHA-only host-binding must fail closed when REST cannot prove issue or PR binding"))
6385
6421
 
6386
6422
  path_escape_payload, error = load_command_json(
6387
6423
  root,
@@ -6572,28 +6608,66 @@ def check_adversarial_adoption_fixture(root: Path) -> list[Failure]:
6572
6608
  elif payload.get("result") != "block":
6573
6609
  failures.append(Failure("adversarial-adoption", "metadata spoofing must fail closed in the canonical section"))
6574
6610
 
6575
- shadow_target = base / "shadow-broken"
6576
- shutil.copytree(baseline, shadow_target)
6577
- shadow_payload = load_json_file(shadow_target / ".loom/shadow/review-repo.json")
6578
- if isinstance(shadow_payload, dict):
6579
- shadow_payload.pop("source_sha256", None)
6580
- write_json(shadow_target / ".loom/shadow/review-repo.json", shadow_payload)
6581
- warn_payload, warn_error = load_command_json(
6582
- root,
6583
- ["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(shadow_target), "--surface", "review"],
6584
- )
6585
- block_payload, block_error = load_command_json(
6586
- root,
6587
- ["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(shadow_target), "--surface", "review", "--blocking"],
6588
- )
6589
- if warn_error:
6590
- failures.append(Failure("adversarial-adoption", f"shadow evidence validation-only sample failed: {warn_error}"))
6591
- elif warn_payload.get("result") != "warn":
6592
- failures.append(Failure("adversarial-adoption", "broken shadow evidence must warn in validation-only mode"))
6593
- if block_error:
6594
- failures.append(Failure("adversarial-adoption", f"shadow evidence blocking sample failed: {block_error}"))
6595
- elif block_payload.get("result") != "block":
6596
- failures.append(Failure("adversarial-adoption", "broken shadow evidence must block in blocking mode"))
6611
+ def assert_broken_shadow_evidence(
6612
+ label: str,
6613
+ mutate: object,
6614
+ *,
6615
+ expect_validation_warn: bool = True,
6616
+ ) -> None:
6617
+ shadow_target = base / f"shadow-broken-{label}"
6618
+ shutil.copytree(baseline, shadow_target)
6619
+ if callable(mutate):
6620
+ mutate(shadow_target)
6621
+ warn_payload, warn_error = load_command_json(
6622
+ root,
6623
+ ["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(shadow_target), "--surface", "review"],
6624
+ )
6625
+ block_payload, block_error = load_command_json(
6626
+ root,
6627
+ ["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(shadow_target), "--surface", "review", "--blocking"],
6628
+ )
6629
+ if warn_error:
6630
+ failures.append(Failure("adversarial-adoption", f"shadow evidence `{label}` validation-only sample failed: {warn_error}"))
6631
+ elif expect_validation_warn and warn_payload.get("result") != "warn":
6632
+ failures.append(Failure("adversarial-adoption", f"shadow evidence `{label}` must warn in validation-only mode"))
6633
+ if block_error:
6634
+ failures.append(Failure("adversarial-adoption", f"shadow evidence `{label}` blocking sample failed: {block_error}"))
6635
+ elif block_payload.get("result") != "block":
6636
+ failures.append(Failure("adversarial-adoption", f"shadow evidence `{label}` must block in blocking mode"))
6637
+
6638
+ def remove_source_hash(target: Path) -> None:
6639
+ shadow_payload = load_json_file(target / ".loom/shadow/review-repo.json")
6640
+ if isinstance(shadow_payload, dict):
6641
+ shadow_payload.pop("source_sha256", None)
6642
+ write_json(target / ".loom/shadow/review-repo.json", shadow_payload)
6643
+
6644
+ def partial_source_hash(target: Path) -> None:
6645
+ shadow_payload = load_json_file(target / ".loom/shadow/review-repo.json")
6646
+ if isinstance(shadow_payload, dict):
6647
+ shadow_payload["source_files"] = ["native/status/review.json", "host/guardian-review.json"]
6648
+ shadow_payload["source_sha256"] = {"native/status/review.json": sha256_file(target / "native/status/review.json")}
6649
+ write_json(target / ".loom/shadow/review-repo.json", shadow_payload)
6650
+
6651
+ def drift_source_hash(target: Path) -> None:
6652
+ shadow_payload = load_json_file(target / ".loom/shadow/review-repo.json")
6653
+ if isinstance(shadow_payload, dict):
6654
+ shadow_payload["source_sha256"] = {"native/status/review.json": "0" * 64}
6655
+ write_json(target / ".loom/shadow/review-repo.json", shadow_payload)
6656
+
6657
+ def undeclared_shadow_evidence(target: Path) -> None:
6658
+ write_json(
6659
+ target / ".loom/shadow/rogue.json",
6660
+ {
6661
+ "result": "pass",
6662
+ "source_files": ["native/status/review.json"],
6663
+ "source_sha256": {"native/status/review.json": sha256_file(target / "native/status/review.json")},
6664
+ },
6665
+ )
6666
+
6667
+ assert_broken_shadow_evidence("missing-hash", remove_source_hash)
6668
+ assert_broken_shadow_evidence("partial-hash", partial_source_hash)
6669
+ assert_broken_shadow_evidence("hash-drift", drift_source_hash)
6670
+ assert_broken_shadow_evidence("undeclared", undeclared_shadow_evidence, expect_validation_warn=False)
6597
6671
 
6598
6672
  head_before_drift = run_command(root, ["git", "rev-parse", "HEAD"], cwd=baseline, timeout_seconds=30).stdout.strip()
6599
6673
  (baseline / "implementation-drift.txt").write_text("changed after review\n", encoding="utf-8")
@@ -6606,6 +6680,14 @@ def check_adversarial_adoption_fixture(root: Path) -> list[Failure]:
6606
6680
  )
6607
6681
  if not binding_errors or binding_payload.get("status") != "implementation-drift-only":
6608
6682
  failures.append(Failure("adversarial-adoption", "review head binding must classify implementation drift after review"))
6683
+ drift_refresh_payload, drift_refresh_error = load_command_json(
6684
+ root,
6685
+ ["python3", "tools/loom_flow.py", "carrier", "refresh", "--target", str(baseline), "--dry-run"],
6686
+ )
6687
+ if drift_refresh_error:
6688
+ failures.append(Failure("adversarial-adoption", f"carrier refresh implementation-drift sample failed: {drift_refresh_error}"))
6689
+ elif drift_refresh_payload.get("result") != "block":
6690
+ failures.append(Failure("adversarial-adoption", "carrier refresh must block implementation drift instead of refreshing review metadata"))
6609
6691
 
6610
6692
  return failures
6611
6693
 
@@ -4146,9 +4146,9 @@ def carrier_refresh_payload(target_root: Path, output_relative: str, expected_it
4146
4146
  allowed_paths=allowed_paths,
4147
4147
  )
4148
4148
  review_status = {"path": review_path, "head_binding": binding, "missing_inputs": binding_errors}
4149
- if binding.get("status") == "implementation-drift-only":
4149
+ if binding.get("status") in {"implementation-drift-only", "stale"}:
4150
4150
  review_status["status"] = "block"
4151
- missing_inputs.append("review artifact is stale because implementation drift is present")
4151
+ missing_inputs.append("review artifact is stale because non-carrier drift is present")
4152
4152
  elif binding.get("status") == "carrier-only":
4153
4153
  review_status["status"] = "refresh-needed"
4154
4154
  else:
@@ -100,6 +100,7 @@ CORE_DOCS = (
100
100
  "docs/evidence/validations/validation-adoption-gate-rollout.md",
101
101
  "docs/evidence/validations/validation-external-runtime-devendor-migration.md",
102
102
  "docs/evidence/validations/validation-syvert-adversarial-adoption-fixture.md",
103
+ "docs/evidence/validations/validation-zero-friction-adoption-hardening.md",
103
104
  "docs/evidence/validations/validation-github-profile-binding-orchestration.md",
104
105
  "docs/evidence/validations/validation-github-profile-drift-reconciliation.md",
105
106
  "docs/evidence/validations/validation-github-profile-graphql-budget-guard.md",
@@ -6376,12 +6377,47 @@ def check_adversarial_adoption_fixture(root: Path) -> list[Failure]:
6376
6377
  ("shadow-parity", ["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(baseline)], "pass"),
6377
6378
  ("shadow-parity --blocking", ["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(baseline), "--blocking"], "pass"),
6378
6379
  ("flow resume", ["python3", "tools/loom_flow.py", "flow", "resume", "--target", str(baseline), "--item", "INIT-0001"], "pass"),
6380
+ ("adopt verify", ["python3", "tools/loom_flow.py", "adopt", "verify", "--target", str(baseline), "--item", "INIT-0001"], "pass"),
6379
6381
  ):
6380
6382
  payload, error = load_command_json(root, args)
6381
6383
  if error:
6382
6384
  failures.append(Failure("adversarial-adoption", f"`{label}` baseline failed: {error}"))
6383
6385
  elif payload.get("result") != expected:
6384
6386
  failures.append(Failure("adversarial-adoption", f"`{label}` baseline must return `{expected}`"))
6387
+ elif label == "adopt verify":
6388
+ roundtrip = payload.get("producer_consumer_roundtrip")
6389
+ deleted_section = (
6390
+ roundtrip.get("bypass_check")
6391
+ if isinstance(roundtrip, dict)
6392
+ else None
6393
+ )
6394
+ if not isinstance(deleted_section, dict) or deleted_section.get("consumer_result") != "block":
6395
+ failures.append(Failure("adversarial-adoption", "`adopt verify` must prove required Review Artifacts deletion cannot bypass the consumer"))
6396
+
6397
+ sha_only_payload, sha_only_error = load_command_json(
6398
+ root,
6399
+ [
6400
+ "python3",
6401
+ "tools/loom_flow.py",
6402
+ "host-binding",
6403
+ "validate",
6404
+ "--target",
6405
+ str(baseline),
6406
+ "--owner",
6407
+ "MC-and-his-Agents",
6408
+ "--repo",
6409
+ "Loom",
6410
+ "--head-sha",
6411
+ current_head,
6412
+ ],
6413
+ timeout_seconds=60,
6414
+ )
6415
+ if sha_only_error:
6416
+ failures.append(Failure("adversarial-adoption", f"SHA-only host-binding negative sample failed: {sha_only_error}"))
6417
+ else:
6418
+ missing_inputs = sha_only_payload.get("missing_inputs")
6419
+ if sha_only_payload.get("result") != "block" or not isinstance(missing_inputs, list) or not missing_inputs:
6420
+ failures.append(Failure("adversarial-adoption", "SHA-only host-binding must fail closed when REST cannot prove issue or PR binding"))
6385
6421
 
6386
6422
  path_escape_payload, error = load_command_json(
6387
6423
  root,
@@ -6572,28 +6608,66 @@ def check_adversarial_adoption_fixture(root: Path) -> list[Failure]:
6572
6608
  elif payload.get("result") != "block":
6573
6609
  failures.append(Failure("adversarial-adoption", "metadata spoofing must fail closed in the canonical section"))
6574
6610
 
6575
- shadow_target = base / "shadow-broken"
6576
- shutil.copytree(baseline, shadow_target)
6577
- shadow_payload = load_json_file(shadow_target / ".loom/shadow/review-repo.json")
6578
- if isinstance(shadow_payload, dict):
6579
- shadow_payload.pop("source_sha256", None)
6580
- write_json(shadow_target / ".loom/shadow/review-repo.json", shadow_payload)
6581
- warn_payload, warn_error = load_command_json(
6582
- root,
6583
- ["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(shadow_target), "--surface", "review"],
6584
- )
6585
- block_payload, block_error = load_command_json(
6586
- root,
6587
- ["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(shadow_target), "--surface", "review", "--blocking"],
6588
- )
6589
- if warn_error:
6590
- failures.append(Failure("adversarial-adoption", f"shadow evidence validation-only sample failed: {warn_error}"))
6591
- elif warn_payload.get("result") != "warn":
6592
- failures.append(Failure("adversarial-adoption", "broken shadow evidence must warn in validation-only mode"))
6593
- if block_error:
6594
- failures.append(Failure("adversarial-adoption", f"shadow evidence blocking sample failed: {block_error}"))
6595
- elif block_payload.get("result") != "block":
6596
- failures.append(Failure("adversarial-adoption", "broken shadow evidence must block in blocking mode"))
6611
+ def assert_broken_shadow_evidence(
6612
+ label: str,
6613
+ mutate: object,
6614
+ *,
6615
+ expect_validation_warn: bool = True,
6616
+ ) -> None:
6617
+ shadow_target = base / f"shadow-broken-{label}"
6618
+ shutil.copytree(baseline, shadow_target)
6619
+ if callable(mutate):
6620
+ mutate(shadow_target)
6621
+ warn_payload, warn_error = load_command_json(
6622
+ root,
6623
+ ["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(shadow_target), "--surface", "review"],
6624
+ )
6625
+ block_payload, block_error = load_command_json(
6626
+ root,
6627
+ ["python3", "tools/loom_flow.py", "shadow-parity", "--target", str(shadow_target), "--surface", "review", "--blocking"],
6628
+ )
6629
+ if warn_error:
6630
+ failures.append(Failure("adversarial-adoption", f"shadow evidence `{label}` validation-only sample failed: {warn_error}"))
6631
+ elif expect_validation_warn and warn_payload.get("result") != "warn":
6632
+ failures.append(Failure("adversarial-adoption", f"shadow evidence `{label}` must warn in validation-only mode"))
6633
+ if block_error:
6634
+ failures.append(Failure("adversarial-adoption", f"shadow evidence `{label}` blocking sample failed: {block_error}"))
6635
+ elif block_payload.get("result") != "block":
6636
+ failures.append(Failure("adversarial-adoption", f"shadow evidence `{label}` must block in blocking mode"))
6637
+
6638
+ def remove_source_hash(target: Path) -> None:
6639
+ shadow_payload = load_json_file(target / ".loom/shadow/review-repo.json")
6640
+ if isinstance(shadow_payload, dict):
6641
+ shadow_payload.pop("source_sha256", None)
6642
+ write_json(target / ".loom/shadow/review-repo.json", shadow_payload)
6643
+
6644
+ def partial_source_hash(target: Path) -> None:
6645
+ shadow_payload = load_json_file(target / ".loom/shadow/review-repo.json")
6646
+ if isinstance(shadow_payload, dict):
6647
+ shadow_payload["source_files"] = ["native/status/review.json", "host/guardian-review.json"]
6648
+ shadow_payload["source_sha256"] = {"native/status/review.json": sha256_file(target / "native/status/review.json")}
6649
+ write_json(target / ".loom/shadow/review-repo.json", shadow_payload)
6650
+
6651
+ def drift_source_hash(target: Path) -> None:
6652
+ shadow_payload = load_json_file(target / ".loom/shadow/review-repo.json")
6653
+ if isinstance(shadow_payload, dict):
6654
+ shadow_payload["source_sha256"] = {"native/status/review.json": "0" * 64}
6655
+ write_json(target / ".loom/shadow/review-repo.json", shadow_payload)
6656
+
6657
+ def undeclared_shadow_evidence(target: Path) -> None:
6658
+ write_json(
6659
+ target / ".loom/shadow/rogue.json",
6660
+ {
6661
+ "result": "pass",
6662
+ "source_files": ["native/status/review.json"],
6663
+ "source_sha256": {"native/status/review.json": sha256_file(target / "native/status/review.json")},
6664
+ },
6665
+ )
6666
+
6667
+ assert_broken_shadow_evidence("missing-hash", remove_source_hash)
6668
+ assert_broken_shadow_evidence("partial-hash", partial_source_hash)
6669
+ assert_broken_shadow_evidence("hash-drift", drift_source_hash)
6670
+ assert_broken_shadow_evidence("undeclared", undeclared_shadow_evidence, expect_validation_warn=False)
6597
6671
 
6598
6672
  head_before_drift = run_command(root, ["git", "rev-parse", "HEAD"], cwd=baseline, timeout_seconds=30).stdout.strip()
6599
6673
  (baseline / "implementation-drift.txt").write_text("changed after review\n", encoding="utf-8")
@@ -6606,6 +6680,14 @@ def check_adversarial_adoption_fixture(root: Path) -> list[Failure]:
6606
6680
  )
6607
6681
  if not binding_errors or binding_payload.get("status") != "implementation-drift-only":
6608
6682
  failures.append(Failure("adversarial-adoption", "review head binding must classify implementation drift after review"))
6683
+ drift_refresh_payload, drift_refresh_error = load_command_json(
6684
+ root,
6685
+ ["python3", "tools/loom_flow.py", "carrier", "refresh", "--target", str(baseline), "--dry-run"],
6686
+ )
6687
+ if drift_refresh_error:
6688
+ failures.append(Failure("adversarial-adoption", f"carrier refresh implementation-drift sample failed: {drift_refresh_error}"))
6689
+ elif drift_refresh_payload.get("result") != "block":
6690
+ failures.append(Failure("adversarial-adoption", "carrier refresh must block implementation drift instead of refreshing review metadata"))
6609
6691
 
6610
6692
  return failures
6611
6693
 
@@ -4146,9 +4146,9 @@ def carrier_refresh_payload(target_root: Path, output_relative: str, expected_it
4146
4146
  allowed_paths=allowed_paths,
4147
4147
  )
4148
4148
  review_status = {"path": review_path, "head_binding": binding, "missing_inputs": binding_errors}
4149
- if binding.get("status") == "implementation-drift-only":
4149
+ if binding.get("status") in {"implementation-drift-only", "stale"}:
4150
4150
  review_status["status"] = "block"
4151
- missing_inputs.append("review artifact is stale because implementation drift is present")
4151
+ missing_inputs.append("review artifact is stale because non-carrier drift is present")
4152
4152
  elif binding.get("status") == "carrier-only":
4153
4153
  review_status["status"] = "refresh-needed"
4154
4154
  else: