@mc-and-his-agents/loom-installer 0.1.31 → 0.1.32

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 +49 -0
  4. package/payload/plugin/loom/skills/shared/scripts/loom_flow.py +224 -0
  5. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +49 -0
  6. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py +224 -0
  7. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +49 -0
  8. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py +224 -0
  9. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +49 -0
  10. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py +224 -0
  11. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +49 -0
  12. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py +224 -0
  13. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +49 -0
  14. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py +224 -0
  15. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +49 -0
  16. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py +224 -0
  17. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +49 -0
  18. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py +224 -0
  19. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +49 -0
  20. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py +224 -0
  21. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py +49 -0
  22. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py +224 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mc-and-his-agents/loom-installer",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
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": "db9b37726bda7d232589b79bad5c20596dd0571f",
5
+ "source_commit": "3fb64673745f62b2ea76f770c2f2e06cd6192b76",
6
6
  "source_ref": "main",
7
- "built_at": "2026-04-27T14:09:35+08:00",
7
+ "built_at": "2026-04-27T14:19:25+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": 322929,
637
- "sha256": "9a8b31917b244172681a542256994d97de9a7b54869e7e9e7bf5747e8759ae69"
636
+ "bytes": 326062,
637
+ "sha256": "984cb68a9c5c95e268810b1a6a3c90590bb410b399b2d7ea60efe3da5c377689"
638
638
  },
639
639
  {
640
640
  "path": "plugin/loom/skills/shared/scripts/loom_flow.py",
641
- "bytes": 311901,
642
- "sha256": "9376481f98f539c0b6aaf72df42e647f320a0e6050e14cd2e1cf0c37240b79cd"
641
+ "bytes": 322554,
642
+ "sha256": "02f9b19b65ad580aaee0431a1f5151ac87a4755c95d4045168efb9624f44a558"
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": 322929,
1227
- "sha256": "9a8b31917b244172681a542256994d97de9a7b54869e7e9e7bf5747e8759ae69"
1226
+ "bytes": 326062,
1227
+ "sha256": "984cb68a9c5c95e268810b1a6a3c90590bb410b399b2d7ea60efe3da5c377689"
1228
1228
  },
1229
1229
  {
1230
1230
  "path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py",
1231
- "bytes": 311901,
1232
- "sha256": "9376481f98f539c0b6aaf72df42e647f320a0e6050e14cd2e1cf0c37240b79cd"
1231
+ "bytes": 322554,
1232
+ "sha256": "02f9b19b65ad580aaee0431a1f5151ac87a4755c95d4045168efb9624f44a558"
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": 322929,
1847
- "sha256": "9a8b31917b244172681a542256994d97de9a7b54869e7e9e7bf5747e8759ae69"
1846
+ "bytes": 326062,
1847
+ "sha256": "984cb68a9c5c95e268810b1a6a3c90590bb410b399b2d7ea60efe3da5c377689"
1848
1848
  },
1849
1849
  {
1850
1850
  "path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py",
1851
- "bytes": 311901,
1852
- "sha256": "9376481f98f539c0b6aaf72df42e647f320a0e6050e14cd2e1cf0c37240b79cd"
1851
+ "bytes": 322554,
1852
+ "sha256": "02f9b19b65ad580aaee0431a1f5151ac87a4755c95d4045168efb9624f44a558"
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": 322929,
2467
- "sha256": "9a8b31917b244172681a542256994d97de9a7b54869e7e9e7bf5747e8759ae69"
2466
+ "bytes": 326062,
2467
+ "sha256": "984cb68a9c5c95e268810b1a6a3c90590bb410b399b2d7ea60efe3da5c377689"
2468
2468
  },
2469
2469
  {
2470
2470
  "path": "skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py",
2471
- "bytes": 311901,
2472
- "sha256": "9376481f98f539c0b6aaf72df42e647f320a0e6050e14cd2e1cf0c37240b79cd"
2471
+ "bytes": 322554,
2472
+ "sha256": "02f9b19b65ad580aaee0431a1f5151ac87a4755c95d4045168efb9624f44a558"
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": 322929,
3092
- "sha256": "9a8b31917b244172681a542256994d97de9a7b54869e7e9e7bf5747e8759ae69"
3091
+ "bytes": 326062,
3092
+ "sha256": "984cb68a9c5c95e268810b1a6a3c90590bb410b399b2d7ea60efe3da5c377689"
3093
3093
  },
3094
3094
  {
3095
3095
  "path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py",
3096
- "bytes": 311901,
3097
- "sha256": "9376481f98f539c0b6aaf72df42e647f320a0e6050e14cd2e1cf0c37240b79cd"
3096
+ "bytes": 322554,
3097
+ "sha256": "02f9b19b65ad580aaee0431a1f5151ac87a4755c95d4045168efb9624f44a558"
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": 322929,
3712
- "sha256": "9a8b31917b244172681a542256994d97de9a7b54869e7e9e7bf5747e8759ae69"
3711
+ "bytes": 326062,
3712
+ "sha256": "984cb68a9c5c95e268810b1a6a3c90590bb410b399b2d7ea60efe3da5c377689"
3713
3713
  },
3714
3714
  {
3715
3715
  "path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py",
3716
- "bytes": 311901,
3717
- "sha256": "9376481f98f539c0b6aaf72df42e647f320a0e6050e14cd2e1cf0c37240b79cd"
3716
+ "bytes": 322554,
3717
+ "sha256": "02f9b19b65ad580aaee0431a1f5151ac87a4755c95d4045168efb9624f44a558"
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": 322929,
4332
- "sha256": "9a8b31917b244172681a542256994d97de9a7b54869e7e9e7bf5747e8759ae69"
4331
+ "bytes": 326062,
4332
+ "sha256": "984cb68a9c5c95e268810b1a6a3c90590bb410b399b2d7ea60efe3da5c377689"
4333
4333
  },
4334
4334
  {
4335
4335
  "path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py",
4336
- "bytes": 311901,
4337
- "sha256": "9376481f98f539c0b6aaf72df42e647f320a0e6050e14cd2e1cf0c37240b79cd"
4336
+ "bytes": 322554,
4337
+ "sha256": "02f9b19b65ad580aaee0431a1f5151ac87a4755c95d4045168efb9624f44a558"
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": 322929,
4952
- "sha256": "9a8b31917b244172681a542256994d97de9a7b54869e7e9e7bf5747e8759ae69"
4951
+ "bytes": 326062,
4952
+ "sha256": "984cb68a9c5c95e268810b1a6a3c90590bb410b399b2d7ea60efe3da5c377689"
4953
4953
  },
4954
4954
  {
4955
4955
  "path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py",
4956
- "bytes": 311901,
4957
- "sha256": "9376481f98f539c0b6aaf72df42e647f320a0e6050e14cd2e1cf0c37240b79cd"
4956
+ "bytes": 322554,
4957
+ "sha256": "02f9b19b65ad580aaee0431a1f5151ac87a4755c95d4045168efb9624f44a558"
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": 322929,
5572
- "sha256": "9a8b31917b244172681a542256994d97de9a7b54869e7e9e7bf5747e8759ae69"
5571
+ "bytes": 326062,
5572
+ "sha256": "984cb68a9c5c95e268810b1a6a3c90590bb410b399b2d7ea60efe3da5c377689"
5573
5573
  },
5574
5574
  {
5575
5575
  "path": "skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py",
5576
- "bytes": 311901,
5577
- "sha256": "9376481f98f539c0b6aaf72df42e647f320a0e6050e14cd2e1cf0c37240b79cd"
5576
+ "bytes": 322554,
5577
+ "sha256": "02f9b19b65ad580aaee0431a1f5151ac87a4755c95d4045168efb9624f44a558"
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": 322929,
6192
- "sha256": "9a8b31917b244172681a542256994d97de9a7b54869e7e9e7bf5747e8759ae69"
6191
+ "bytes": 326062,
6192
+ "sha256": "984cb68a9c5c95e268810b1a6a3c90590bb410b399b2d7ea60efe3da5c377689"
6193
6193
  },
6194
6194
  {
6195
6195
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py",
6196
- "bytes": 311901,
6197
- "sha256": "9376481f98f539c0b6aaf72df42e647f320a0e6050e14cd2e1cf0c37240b79cd"
6196
+ "bytes": 322554,
6197
+ "sha256": "02f9b19b65ad580aaee0431a1f5151ac87a4755c95d4045168efb9624f44a558"
6198
6198
  },
6199
6199
  {
6200
6200
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_init.py",
@@ -2539,6 +2539,11 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
2539
2539
  ["python3", "tools/loom_flow.py", "adopt", "verify", "--target", "examples/new-project", "--item", "INIT-0001"],
2540
2540
  {"pass"},
2541
2541
  ),
2542
+ (
2543
+ "carrier-refresh",
2544
+ ["python3", "tools/loom_flow.py", "carrier", "refresh", "--target", "examples/new-project", "--item", "INIT-0001", "--dry-run"],
2545
+ {"pass"},
2546
+ ),
2542
2547
  (
2543
2548
  "governance-profile-status",
2544
2549
  ["python3", "tools/loom_flow.py", "governance-profile", "status", "--target", "examples/new-project"],
@@ -2818,6 +2823,13 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
2818
2823
  failures.append(Failure("daily-execution-cli", "`adopt verify` generated body must pass consumer validation"))
2819
2824
  if not isinstance(bypass, dict) or bypass.get("result") != "pass":
2820
2825
  failures.append(Failure("daily-execution-cli", "`adopt verify` must prove required section deletion blocks"))
2826
+ if label == "carrier-refresh":
2827
+ if payload.get("command") != "carrier" or payload.get("operation") != "refresh":
2828
+ failures.append(Failure("daily-execution-cli", "`carrier refresh` must report command/operation"))
2829
+ if payload.get("schema_version") != "loom-carrier-refresh/v1":
2830
+ failures.append(Failure("daily-execution-cli", "`carrier refresh` must report schema v1"))
2831
+ if not isinstance(payload.get("actions"), list):
2832
+ failures.append(Failure("daily-execution-cli", "`carrier refresh` must include actions"))
2821
2833
  if label in {"governance-profile-status", "governance-profile-upgrade-plan"}:
2822
2834
  if payload.get("command") != "governance-profile":
2823
2835
  failures.append(Failure("daily-execution-cli", f"`{label}` must report `command: governance-profile`"))
@@ -6451,6 +6463,43 @@ def check_adversarial_adoption_fixture(root: Path) -> list[Failure]:
6451
6463
  elif payload.get("result") != "block":
6452
6464
  failures.append(Failure("adversarial-adoption", "runtime provenance drift must block runtime-parity"))
6453
6465
 
6466
+ carrier_refresh_target = base / "carrier-refresh"
6467
+ shutil.copytree(baseline, carrier_refresh_target)
6468
+ carrier_manifest_path = carrier_refresh_target / ".loom/bootstrap/manifest.json"
6469
+ carrier_manifest = load_json_file(carrier_manifest_path)
6470
+ carrier_artifacts = carrier_manifest.get("artifacts") if isinstance(carrier_manifest, dict) else []
6471
+ if isinstance(carrier_artifacts, list):
6472
+ for artifact in carrier_artifacts:
6473
+ if isinstance(artifact, dict) and artifact.get("path") == ".loom/bin/loom_flow.py":
6474
+ artifact["sha256"] = "1" * 64
6475
+ break
6476
+ write_json(carrier_manifest_path, carrier_manifest)
6477
+ dry_run_payload, dry_run_error = load_command_json(
6478
+ root,
6479
+ ["python3", "tools/loom_flow.py", "carrier", "refresh", "--target", str(carrier_refresh_target), "--dry-run"],
6480
+ )
6481
+ if dry_run_error:
6482
+ failures.append(Failure("adversarial-adoption", f"carrier refresh dry-run sample failed: {dry_run_error}"))
6483
+ else:
6484
+ refresh_needed = dry_run_payload.get("refresh_needed")
6485
+ if dry_run_payload.get("result") != "pass" or not isinstance(refresh_needed, list) or not refresh_needed:
6486
+ failures.append(Failure("adversarial-adoption", "carrier refresh dry-run must report runtime provenance refresh-needed"))
6487
+ write_payload, write_error = load_command_json(
6488
+ root,
6489
+ ["python3", "tools/loom_flow.py", "carrier", "refresh", "--target", str(carrier_refresh_target), "--write"],
6490
+ )
6491
+ if write_error:
6492
+ failures.append(Failure("adversarial-adoption", f"carrier refresh write sample failed: {write_error}"))
6493
+ else:
6494
+ after_payload, after_error = load_command_json(
6495
+ root,
6496
+ ["python3", "tools/loom_flow.py", "carrier", "refresh", "--target", str(carrier_refresh_target), "--dry-run"],
6497
+ )
6498
+ if after_error:
6499
+ failures.append(Failure("adversarial-adoption", f"carrier refresh after-write sample failed: {after_error}"))
6500
+ elif after_payload.get("refresh_needed"):
6501
+ failures.append(Failure("adversarial-adoption", "carrier refresh --write must clear runtime provenance drift"))
6502
+
6454
6503
  rollover_target = base / "active-rollover"
6455
6504
  shutil.copytree(baseline, rollover_target)
6456
6505
  payload, error = load_command_json(
@@ -179,6 +179,18 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
179
179
  help="Init-result path relative to the target root",
180
180
  )
181
181
 
182
+ carrier = subparsers.add_parser("carrier", help="Refresh Loom-owned carrier metadata")
183
+ carrier.add_argument("operation", choices=("refresh",))
184
+ carrier.add_argument("--target", required=True, help="Target repository root")
185
+ carrier.add_argument("--item", help="Expected current item id")
186
+ carrier.add_argument(
187
+ "--output",
188
+ default=".loom/bootstrap/init-result.json",
189
+ help="Init-result path relative to the target root",
190
+ )
191
+ carrier.add_argument("--dry-run", action="store_true", default=True, help="Preview refresh actions without writing files; this is the default")
192
+ carrier.add_argument("--write", dest="dry_run", action="store_false", help="Write Loom-owned carrier metadata refreshes")
193
+
182
194
  state = subparsers.add_parser(
183
195
  "state-check",
184
196
  help="Check active-state consistency, checkpoint completeness, and scope overflow signals",
@@ -3891,6 +3903,216 @@ def handle_adopt(args: argparse.Namespace) -> int:
3891
3903
  return emit(adoption_verify_payload(target_root, args.output, args.item))
3892
3904
 
3893
3905
 
3906
+ def runtime_artifact_updates(target_root: Path, payload: dict[str, Any], *, source: str) -> list[dict[str, Any]]:
3907
+ artifacts = payload.get("artifacts")
3908
+ if not isinstance(artifacts, list):
3909
+ artifacts = payload.get("initial_artifacts")
3910
+ if not isinstance(artifacts, list):
3911
+ return []
3912
+ updates: list[dict[str, Any]] = []
3913
+ for artifact in artifacts:
3914
+ if not isinstance(artifact, dict):
3915
+ continue
3916
+ relative = artifact.get("path")
3917
+ if not isinstance(relative, str) or not relative.startswith(".loom/bin/"):
3918
+ continue
3919
+ path, errors = resolve_repo_relative_path(target_root, relative, label=f"{source} artifact path")
3920
+ if errors or path is None or not path.exists() or not path.is_file():
3921
+ updates.append(
3922
+ {
3923
+ "path": relative,
3924
+ "source": source,
3925
+ "status": "block",
3926
+ "missing_inputs": errors or [f"missing runtime artifact: {relative}"],
3927
+ }
3928
+ )
3929
+ continue
3930
+ expected = sha256_file(path)
3931
+ current = artifact.get("sha256")
3932
+ updates.append(
3933
+ {
3934
+ "path": relative,
3935
+ "source": source,
3936
+ "status": "current" if current == expected else "refresh-needed",
3937
+ "current_sha256": current if isinstance(current, str) else None,
3938
+ "expected_sha256": expected,
3939
+ }
3940
+ )
3941
+ return updates
3942
+
3943
+
3944
+ def apply_runtime_artifact_updates(payload: dict[str, Any], updates: list[dict[str, Any]], *, source: str) -> None:
3945
+ artifacts = payload.get("artifacts")
3946
+ if not isinstance(artifacts, list):
3947
+ artifacts = payload.get("initial_artifacts")
3948
+ if not isinstance(artifacts, list):
3949
+ return
3950
+ expected_by_path = {
3951
+ update["path"]: update.get("expected_sha256")
3952
+ for update in updates
3953
+ if update.get("source") == source and update.get("status") == "refresh-needed"
3954
+ }
3955
+ for artifact in artifacts:
3956
+ if not isinstance(artifact, dict):
3957
+ continue
3958
+ relative = artifact.get("path")
3959
+ if isinstance(relative, str) and relative in expected_by_path:
3960
+ artifact["sha256"] = expected_by_path[relative]
3961
+
3962
+
3963
+ def refresh_shadow_evidence_actions(target_root: Path) -> list[dict[str, Any]]:
3964
+ actions: list[dict[str, Any]] = []
3965
+ shadow_root = target_root / ".loom/shadow"
3966
+ if not shadow_root.exists():
3967
+ return actions
3968
+ for path in sorted(shadow_root.glob("*.json")):
3969
+ relative = path.relative_to(target_root).as_posix()
3970
+ try:
3971
+ payload = load_json_file(path)
3972
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
3973
+ actions.append({"path": relative, "kind": "shadow-evidence", "status": "block", "missing_inputs": [str(exc)]})
3974
+ continue
3975
+ if not isinstance(payload, dict):
3976
+ actions.append({"path": relative, "kind": "shadow-evidence", "status": "block", "missing_inputs": ["shadow evidence must be a JSON object"]})
3977
+ continue
3978
+ source_files = payload.get("source_files")
3979
+ if not isinstance(source_files, list) or not source_files:
3980
+ actions.append({"path": relative, "kind": "shadow-evidence", "status": "block", "missing_inputs": ["source_files"]})
3981
+ continue
3982
+ refreshed: dict[str, str] = {}
3983
+ missing: list[str] = []
3984
+ for source in source_files:
3985
+ if not isinstance(source, str):
3986
+ missing.append("source_files entries must be strings")
3987
+ continue
3988
+ source_path, errors = resolve_repo_relative_path(target_root, source, label=f"{relative} source")
3989
+ if errors or source_path is None or not source_path.exists() or source_path.is_dir():
3990
+ missing.extend(errors or [f"missing source file: {source}"])
3991
+ continue
3992
+ refreshed[source] = sha256_file(source_path)
3993
+ if missing:
3994
+ actions.append({"path": relative, "kind": "shadow-evidence", "status": "block", "missing_inputs": missing})
3995
+ continue
3996
+ current = payload.get("source_sha256")
3997
+ actions.append(
3998
+ {
3999
+ "path": relative,
4000
+ "kind": "shadow-evidence",
4001
+ "status": "current" if current == refreshed else "refresh-needed",
4002
+ "expected_source_sha256": refreshed,
4003
+ }
4004
+ )
4005
+ return actions
4006
+
4007
+
4008
+ def apply_shadow_evidence_actions(target_root: Path, actions: list[dict[str, Any]]) -> None:
4009
+ for action in actions:
4010
+ if action.get("kind") != "shadow-evidence" or action.get("status") != "refresh-needed":
4011
+ continue
4012
+ relative = action.get("path")
4013
+ expected = action.get("expected_source_sha256")
4014
+ if not isinstance(relative, str) or not isinstance(expected, dict):
4015
+ continue
4016
+ path, errors = resolve_repo_relative_path(target_root, relative, label="shadow evidence path")
4017
+ if errors or path is None:
4018
+ continue
4019
+ payload = load_json_file(path)
4020
+ if isinstance(payload, dict):
4021
+ payload["source_sha256"] = expected
4022
+ write_json_file(path, payload)
4023
+
4024
+
4025
+ def carrier_refresh_payload(target_root: Path, output_relative: str, expected_item: str | None, *, dry_run: bool) -> dict[str, Any]:
4026
+ runtime_state = runtime_state_payload(target_root)
4027
+ context, context_errors = load_context(target_root, output_relative, expected_item)
4028
+ missing_inputs: list[str] = [f"fact-chain: {message}" for message in context_errors]
4029
+
4030
+ manifest_path, manifest_path_errors = resolve_repo_relative_path(target_root, ".loom/bootstrap/manifest.json", label="bootstrap manifest")
4031
+ init_path, init_path_errors = resolve_repo_relative_path(target_root, output_relative, label="init-result locator")
4032
+ missing_inputs.extend(manifest_path_errors)
4033
+ missing_inputs.extend(init_path_errors)
4034
+ manifest_payload: dict[str, Any] = {}
4035
+ init_payload: dict[str, Any] = {}
4036
+ for label, path in (("manifest", manifest_path), ("init-result", init_path)):
4037
+ if path is None:
4038
+ continue
4039
+ try:
4040
+ payload = load_json_file(path)
4041
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
4042
+ missing_inputs.append(f"invalid {label}: {exc}")
4043
+ continue
4044
+ if label == "manifest":
4045
+ manifest_payload = payload
4046
+ else:
4047
+ init_payload = payload
4048
+
4049
+ actions: list[dict[str, Any]] = []
4050
+ actions.extend(runtime_artifact_updates(target_root, manifest_payload, source="manifest"))
4051
+ actions.extend(runtime_artifact_updates(target_root, init_payload, source="init-result"))
4052
+ actions.extend(refresh_shadow_evidence_actions(target_root))
4053
+ for action in actions:
4054
+ if action.get("status") == "block":
4055
+ missing_inputs.extend(str(message) for message in action.get("missing_inputs", []))
4056
+
4057
+ review_status: dict[str, Any] = {"status": "not_checked"}
4058
+ if not context_errors:
4059
+ assert context
4060
+ review_record, review_path, review_errors = load_review_record(target_root, context["item_id"], context["review_entry"])
4061
+ spec_review_path = default_spec_review_path(context["item_id"])
4062
+ allowed_paths = allowed_post_review_carrier_paths(context, review_path, spec_review_path)
4063
+ if review_errors or review_record is None:
4064
+ review_status = {"status": "missing", "path": review_path, "missing_inputs": review_errors or [f"missing review artifact: {review_path}"]}
4065
+ else:
4066
+ binding, binding_errors = review_head_binding(
4067
+ target_root,
4068
+ reviewed_head=str(review_record.get("reviewed_head", "")),
4069
+ allowed_paths=allowed_paths,
4070
+ )
4071
+ review_status = {"path": review_path, "head_binding": binding, "missing_inputs": binding_errors}
4072
+ if binding.get("status") == "implementation-drift-only":
4073
+ review_status["status"] = "block"
4074
+ missing_inputs.append("review artifact is stale because implementation drift is present")
4075
+ elif binding.get("status") == "carrier-only":
4076
+ review_status["status"] = "refresh-needed"
4077
+ else:
4078
+ review_status["status"] = "current"
4079
+
4080
+ if not dry_run and not missing_inputs:
4081
+ if manifest_path is not None:
4082
+ apply_runtime_artifact_updates(manifest_payload, actions, source="manifest")
4083
+ write_json_file(manifest_path, manifest_payload)
4084
+ if init_path is not None:
4085
+ apply_runtime_artifact_updates(init_payload, actions, source="init-result")
4086
+ write_json_file(init_path, init_payload)
4087
+ apply_shadow_evidence_actions(target_root, actions)
4088
+
4089
+ refresh_needed = [action for action in actions if action.get("status") == "refresh-needed"]
4090
+ result = "block" if missing_inputs else "pass"
4091
+ return {
4092
+ "command": "carrier",
4093
+ "operation": "refresh",
4094
+ "schema_version": "loom-carrier-refresh/v1",
4095
+ "result": result,
4096
+ "summary": (
4097
+ "carrier refresh completed." if result == "pass" and not dry_run
4098
+ else "carrier refresh dry-run completed." if result == "pass"
4099
+ else "carrier refresh found blocking drift."
4100
+ ),
4101
+ "missing_inputs": missing_inputs,
4102
+ "fallback_to": None if result == "pass" else "adoption",
4103
+ "dry_run": dry_run,
4104
+ "runtime_state": runtime_state,
4105
+ "actions": actions,
4106
+ "refresh_needed": refresh_needed,
4107
+ "review": review_status,
4108
+ }
4109
+
4110
+
4111
+ def handle_carrier(args: argparse.Namespace) -> int:
4112
+ target_root = Path(args.target).expanduser().resolve()
4113
+ return emit(carrier_refresh_payload(target_root, args.output, args.item, dry_run=args.dry_run))
4114
+
4115
+
3894
4116
  def host_lifecycle_payload(context: dict[str, Any]) -> dict[str, Any]:
3895
4117
  branch = git_branch(context["target_root"])
3896
4118
  purity = purity_report_from_context(context)
@@ -7281,6 +7503,8 @@ def main(argv: list[str] | None = None) -> int:
7281
7503
  return handle_runtime_state(args)
7282
7504
  if args.command == "adopt":
7283
7505
  return handle_adopt(args)
7506
+ if args.command == "carrier":
7507
+ return handle_carrier(args)
7284
7508
  if args.command == "runtime-evidence":
7285
7509
  return handle_runtime_evidence(args)
7286
7510
  if args.command == "state-check":
@@ -2539,6 +2539,11 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
2539
2539
  ["python3", "tools/loom_flow.py", "adopt", "verify", "--target", "examples/new-project", "--item", "INIT-0001"],
2540
2540
  {"pass"},
2541
2541
  ),
2542
+ (
2543
+ "carrier-refresh",
2544
+ ["python3", "tools/loom_flow.py", "carrier", "refresh", "--target", "examples/new-project", "--item", "INIT-0001", "--dry-run"],
2545
+ {"pass"},
2546
+ ),
2542
2547
  (
2543
2548
  "governance-profile-status",
2544
2549
  ["python3", "tools/loom_flow.py", "governance-profile", "status", "--target", "examples/new-project"],
@@ -2818,6 +2823,13 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
2818
2823
  failures.append(Failure("daily-execution-cli", "`adopt verify` generated body must pass consumer validation"))
2819
2824
  if not isinstance(bypass, dict) or bypass.get("result") != "pass":
2820
2825
  failures.append(Failure("daily-execution-cli", "`adopt verify` must prove required section deletion blocks"))
2826
+ if label == "carrier-refresh":
2827
+ if payload.get("command") != "carrier" or payload.get("operation") != "refresh":
2828
+ failures.append(Failure("daily-execution-cli", "`carrier refresh` must report command/operation"))
2829
+ if payload.get("schema_version") != "loom-carrier-refresh/v1":
2830
+ failures.append(Failure("daily-execution-cli", "`carrier refresh` must report schema v1"))
2831
+ if not isinstance(payload.get("actions"), list):
2832
+ failures.append(Failure("daily-execution-cli", "`carrier refresh` must include actions"))
2821
2833
  if label in {"governance-profile-status", "governance-profile-upgrade-plan"}:
2822
2834
  if payload.get("command") != "governance-profile":
2823
2835
  failures.append(Failure("daily-execution-cli", f"`{label}` must report `command: governance-profile`"))
@@ -6451,6 +6463,43 @@ def check_adversarial_adoption_fixture(root: Path) -> list[Failure]:
6451
6463
  elif payload.get("result") != "block":
6452
6464
  failures.append(Failure("adversarial-adoption", "runtime provenance drift must block runtime-parity"))
6453
6465
 
6466
+ carrier_refresh_target = base / "carrier-refresh"
6467
+ shutil.copytree(baseline, carrier_refresh_target)
6468
+ carrier_manifest_path = carrier_refresh_target / ".loom/bootstrap/manifest.json"
6469
+ carrier_manifest = load_json_file(carrier_manifest_path)
6470
+ carrier_artifacts = carrier_manifest.get("artifacts") if isinstance(carrier_manifest, dict) else []
6471
+ if isinstance(carrier_artifacts, list):
6472
+ for artifact in carrier_artifacts:
6473
+ if isinstance(artifact, dict) and artifact.get("path") == ".loom/bin/loom_flow.py":
6474
+ artifact["sha256"] = "1" * 64
6475
+ break
6476
+ write_json(carrier_manifest_path, carrier_manifest)
6477
+ dry_run_payload, dry_run_error = load_command_json(
6478
+ root,
6479
+ ["python3", "tools/loom_flow.py", "carrier", "refresh", "--target", str(carrier_refresh_target), "--dry-run"],
6480
+ )
6481
+ if dry_run_error:
6482
+ failures.append(Failure("adversarial-adoption", f"carrier refresh dry-run sample failed: {dry_run_error}"))
6483
+ else:
6484
+ refresh_needed = dry_run_payload.get("refresh_needed")
6485
+ if dry_run_payload.get("result") != "pass" or not isinstance(refresh_needed, list) or not refresh_needed:
6486
+ failures.append(Failure("adversarial-adoption", "carrier refresh dry-run must report runtime provenance refresh-needed"))
6487
+ write_payload, write_error = load_command_json(
6488
+ root,
6489
+ ["python3", "tools/loom_flow.py", "carrier", "refresh", "--target", str(carrier_refresh_target), "--write"],
6490
+ )
6491
+ if write_error:
6492
+ failures.append(Failure("adversarial-adoption", f"carrier refresh write sample failed: {write_error}"))
6493
+ else:
6494
+ after_payload, after_error = load_command_json(
6495
+ root,
6496
+ ["python3", "tools/loom_flow.py", "carrier", "refresh", "--target", str(carrier_refresh_target), "--dry-run"],
6497
+ )
6498
+ if after_error:
6499
+ failures.append(Failure("adversarial-adoption", f"carrier refresh after-write sample failed: {after_error}"))
6500
+ elif after_payload.get("refresh_needed"):
6501
+ failures.append(Failure("adversarial-adoption", "carrier refresh --write must clear runtime provenance drift"))
6502
+
6454
6503
  rollover_target = base / "active-rollover"
6455
6504
  shutil.copytree(baseline, rollover_target)
6456
6505
  payload, error = load_command_json(