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

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 +13 -0
  4. package/payload/plugin/loom/skills/shared/scripts/loom_flow.py +242 -1
  5. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +13 -0
  6. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py +242 -1
  7. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +13 -0
  8. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py +242 -1
  9. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +13 -0
  10. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py +242 -1
  11. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +13 -0
  12. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py +242 -1
  13. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +13 -0
  14. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py +242 -1
  15. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +13 -0
  16. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py +242 -1
  17. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +13 -0
  18. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py +242 -1
  19. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +13 -0
  20. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py +242 -1
  21. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py +13 -0
  22. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py +242 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mc-and-his-agents/loom-installer",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
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": "3fb64673745f62b2ea76f770c2f2e06cd6192b76",
5
+ "source_commit": "d27c46eb73b6d01b374b1b0df34287faffa5e858",
6
6
  "source_ref": "main",
7
- "built_at": "2026-04-27T14:19:25+08:00",
7
+ "built_at": "2026-04-27T14:34:16+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": 326062,
637
- "sha256": "984cb68a9c5c95e268810b1a6a3c90590bb410b399b2d7ea60efe3da5c377689"
636
+ "bytes": 327014,
637
+ "sha256": "d2210ea508da857c7e885b6ce6c318a934d982653a5a0c600ade1212641c755a"
638
638
  },
639
639
  {
640
640
  "path": "plugin/loom/skills/shared/scripts/loom_flow.py",
641
- "bytes": 322554,
642
- "sha256": "02f9b19b65ad580aaee0431a1f5151ac87a4755c95d4045168efb9624f44a558"
641
+ "bytes": 332808,
642
+ "sha256": "174f6f787703e98462a6231eab451c66a53d8448ad26081d8e30e1a2ee3ac0e3"
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": 326062,
1227
- "sha256": "984cb68a9c5c95e268810b1a6a3c90590bb410b399b2d7ea60efe3da5c377689"
1226
+ "bytes": 327014,
1227
+ "sha256": "d2210ea508da857c7e885b6ce6c318a934d982653a5a0c600ade1212641c755a"
1228
1228
  },
1229
1229
  {
1230
1230
  "path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py",
1231
- "bytes": 322554,
1232
- "sha256": "02f9b19b65ad580aaee0431a1f5151ac87a4755c95d4045168efb9624f44a558"
1231
+ "bytes": 332808,
1232
+ "sha256": "174f6f787703e98462a6231eab451c66a53d8448ad26081d8e30e1a2ee3ac0e3"
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": 326062,
1847
- "sha256": "984cb68a9c5c95e268810b1a6a3c90590bb410b399b2d7ea60efe3da5c377689"
1846
+ "bytes": 327014,
1847
+ "sha256": "d2210ea508da857c7e885b6ce6c318a934d982653a5a0c600ade1212641c755a"
1848
1848
  },
1849
1849
  {
1850
1850
  "path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py",
1851
- "bytes": 322554,
1852
- "sha256": "02f9b19b65ad580aaee0431a1f5151ac87a4755c95d4045168efb9624f44a558"
1851
+ "bytes": 332808,
1852
+ "sha256": "174f6f787703e98462a6231eab451c66a53d8448ad26081d8e30e1a2ee3ac0e3"
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": 326062,
2467
- "sha256": "984cb68a9c5c95e268810b1a6a3c90590bb410b399b2d7ea60efe3da5c377689"
2466
+ "bytes": 327014,
2467
+ "sha256": "d2210ea508da857c7e885b6ce6c318a934d982653a5a0c600ade1212641c755a"
2468
2468
  },
2469
2469
  {
2470
2470
  "path": "skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py",
2471
- "bytes": 322554,
2472
- "sha256": "02f9b19b65ad580aaee0431a1f5151ac87a4755c95d4045168efb9624f44a558"
2471
+ "bytes": 332808,
2472
+ "sha256": "174f6f787703e98462a6231eab451c66a53d8448ad26081d8e30e1a2ee3ac0e3"
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": 326062,
3092
- "sha256": "984cb68a9c5c95e268810b1a6a3c90590bb410b399b2d7ea60efe3da5c377689"
3091
+ "bytes": 327014,
3092
+ "sha256": "d2210ea508da857c7e885b6ce6c318a934d982653a5a0c600ade1212641c755a"
3093
3093
  },
3094
3094
  {
3095
3095
  "path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py",
3096
- "bytes": 322554,
3097
- "sha256": "02f9b19b65ad580aaee0431a1f5151ac87a4755c95d4045168efb9624f44a558"
3096
+ "bytes": 332808,
3097
+ "sha256": "174f6f787703e98462a6231eab451c66a53d8448ad26081d8e30e1a2ee3ac0e3"
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": 326062,
3712
- "sha256": "984cb68a9c5c95e268810b1a6a3c90590bb410b399b2d7ea60efe3da5c377689"
3711
+ "bytes": 327014,
3712
+ "sha256": "d2210ea508da857c7e885b6ce6c318a934d982653a5a0c600ade1212641c755a"
3713
3713
  },
3714
3714
  {
3715
3715
  "path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py",
3716
- "bytes": 322554,
3717
- "sha256": "02f9b19b65ad580aaee0431a1f5151ac87a4755c95d4045168efb9624f44a558"
3716
+ "bytes": 332808,
3717
+ "sha256": "174f6f787703e98462a6231eab451c66a53d8448ad26081d8e30e1a2ee3ac0e3"
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": 326062,
4332
- "sha256": "984cb68a9c5c95e268810b1a6a3c90590bb410b399b2d7ea60efe3da5c377689"
4331
+ "bytes": 327014,
4332
+ "sha256": "d2210ea508da857c7e885b6ce6c318a934d982653a5a0c600ade1212641c755a"
4333
4333
  },
4334
4334
  {
4335
4335
  "path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py",
4336
- "bytes": 322554,
4337
- "sha256": "02f9b19b65ad580aaee0431a1f5151ac87a4755c95d4045168efb9624f44a558"
4336
+ "bytes": 332808,
4337
+ "sha256": "174f6f787703e98462a6231eab451c66a53d8448ad26081d8e30e1a2ee3ac0e3"
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": 326062,
4952
- "sha256": "984cb68a9c5c95e268810b1a6a3c90590bb410b399b2d7ea60efe3da5c377689"
4951
+ "bytes": 327014,
4952
+ "sha256": "d2210ea508da857c7e885b6ce6c318a934d982653a5a0c600ade1212641c755a"
4953
4953
  },
4954
4954
  {
4955
4955
  "path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py",
4956
- "bytes": 322554,
4957
- "sha256": "02f9b19b65ad580aaee0431a1f5151ac87a4755c95d4045168efb9624f44a558"
4956
+ "bytes": 332808,
4957
+ "sha256": "174f6f787703e98462a6231eab451c66a53d8448ad26081d8e30e1a2ee3ac0e3"
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": 326062,
5572
- "sha256": "984cb68a9c5c95e268810b1a6a3c90590bb410b399b2d7ea60efe3da5c377689"
5571
+ "bytes": 327014,
5572
+ "sha256": "d2210ea508da857c7e885b6ce6c318a934d982653a5a0c600ade1212641c755a"
5573
5573
  },
5574
5574
  {
5575
5575
  "path": "skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py",
5576
- "bytes": 322554,
5577
- "sha256": "02f9b19b65ad580aaee0431a1f5151ac87a4755c95d4045168efb9624f44a558"
5576
+ "bytes": 332808,
5577
+ "sha256": "174f6f787703e98462a6231eab451c66a53d8448ad26081d8e30e1a2ee3ac0e3"
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": 326062,
6192
- "sha256": "984cb68a9c5c95e268810b1a6a3c90590bb410b399b2d7ea60efe3da5c377689"
6191
+ "bytes": 327014,
6192
+ "sha256": "d2210ea508da857c7e885b6ce6c318a934d982653a5a0c600ade1212641c755a"
6193
6193
  },
6194
6194
  {
6195
6195
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py",
6196
- "bytes": 322554,
6197
- "sha256": "02f9b19b65ad580aaee0431a1f5151ac87a4755c95d4045168efb9624f44a558"
6196
+ "bytes": 332808,
6197
+ "sha256": "174f6f787703e98462a6231eab451c66a53d8448ad26081d8e30e1a2ee3ac0e3"
6198
6198
  },
6199
6199
  {
6200
6200
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_init.py",
@@ -2544,6 +2544,11 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
2544
2544
  ["python3", "tools/loom_flow.py", "carrier", "refresh", "--target", "examples/new-project", "--item", "INIT-0001", "--dry-run"],
2545
2545
  {"pass"},
2546
2546
  ),
2547
+ (
2548
+ "host-binding-validate",
2549
+ ["python3", "tools/loom_flow.py", "host-binding", "validate", "--target", ".", "--owner", "MC-and-his-Agents", "--repo", "Loom", "--branch", "main"],
2550
+ {"pass"},
2551
+ ),
2547
2552
  (
2548
2553
  "governance-profile-status",
2549
2554
  ["python3", "tools/loom_flow.py", "governance-profile", "status", "--target", "examples/new-project"],
@@ -2830,6 +2835,14 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
2830
2835
  failures.append(Failure("daily-execution-cli", "`carrier refresh` must report schema v1"))
2831
2836
  if not isinstance(payload.get("actions"), list):
2832
2837
  failures.append(Failure("daily-execution-cli", "`carrier refresh` must include actions"))
2838
+ if label == "host-binding-validate":
2839
+ if payload.get("command") != "host-binding" or payload.get("operation") != "validate":
2840
+ failures.append(Failure("daily-execution-cli", "`host-binding validate` must report command/operation"))
2841
+ if payload.get("schema_version") != "loom-host-binding/v1":
2842
+ failures.append(Failure("daily-execution-cli", "`host-binding validate` must report schema v1"))
2843
+ branch = payload.get("branch")
2844
+ if not isinstance(branch, dict) or branch.get("status") != "present":
2845
+ failures.append(Failure("daily-execution-cli", "`host-binding validate --branch main` must read the branch via REST"))
2833
2846
  if label in {"governance-profile-status", "governance-profile-upgrade-plan"}:
2834
2847
  if payload.get("command") != "governance-profile":
2835
2848
  failures.append(Failure("daily-execution-cli", f"`{label}` must report `command: governance-profile`"))
@@ -13,7 +13,9 @@ import subprocess
13
13
  import sys
14
14
  from pathlib import Path
15
15
  from typing import Any
16
+ from urllib.error import HTTPError, URLError
16
17
  from urllib.parse import quote
18
+ from urllib.request import Request, urlopen
17
19
 
18
20
  from fact_chain_support import (
19
21
  STATUS_FIELDS,
@@ -191,6 +193,17 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
191
193
  carrier.add_argument("--dry-run", action="store_true", default=True, help="Preview refresh actions without writing files; this is the default")
192
194
  carrier.add_argument("--write", dest="dry_run", action="store_false", help="Write Loom-owned carrier metadata refreshes")
193
195
 
196
+ host_binding = subparsers.add_parser("host-binding", help="Validate host issue, PR, branch, and SHA bindings")
197
+ host_binding.add_argument("operation", choices=("validate",))
198
+ host_binding.add_argument("--target", required=True, help="Target repository root")
199
+ host_binding.add_argument("--owner", help="GitHub owner; auto-detected from origin when omitted")
200
+ host_binding.add_argument("--repo", dest="repo_name", help="GitHub repository name; auto-detected from origin when omitted")
201
+ host_binding.add_argument("--issue", type=int, help="GitHub Work Item issue number")
202
+ host_binding.add_argument("--pr", type=int, help="GitHub implementation PR number")
203
+ host_binding.add_argument("--branch", help="GitHub branch name")
204
+ host_binding.add_argument("--head-sha", help="Implementation head SHA to validate")
205
+ host_binding.add_argument("--base-sha", help="Base SHA used for diff validation")
206
+
194
207
  state = subparsers.add_parser(
195
208
  "state-check",
196
209
  help="Check active-state consistency, checkpoint completeness, and scope overflow signals",
@@ -1013,7 +1026,71 @@ def gh_json(root: Path, args: list[str]) -> tuple[dict[str, Any] | None, list[st
1013
1026
 
1014
1027
 
1015
1028
  def gh_rest_json(root: Path, path: str) -> tuple[dict[str, Any] | None, list[str]]:
1016
- return gh_json(root, ["api", path])
1029
+ payload, errors = gh_json(root, ["api", path])
1030
+ if payload is not None or not errors:
1031
+ return payload, errors
1032
+ fallback_payload, fallback_errors = github_public_rest_json(path)
1033
+ if fallback_payload is not None:
1034
+ return fallback_payload, []
1035
+ return None, errors + [f"public REST fallback: {message}" for message in fallback_errors]
1036
+
1037
+
1038
+ def github_public_rest_json(path: str) -> tuple[dict[str, Any] | None, list[str]]:
1039
+ url = f"https://api.github.com/{path.lstrip('/')}"
1040
+ headers = {
1041
+ "Accept": "application/vnd.github+json",
1042
+ "User-Agent": "loom-governance-runtime",
1043
+ }
1044
+ token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
1045
+ if token:
1046
+ headers["Authorization"] = f"Bearer {token}"
1047
+ request = Request(url, headers=headers)
1048
+ try:
1049
+ with urlopen(request, timeout=20) as response:
1050
+ text = response.read().decode("utf-8")
1051
+ except HTTPError as exc:
1052
+ detail = exc.read().decode("utf-8", errors="replace").strip()
1053
+ return None, [f"HTTP {exc.code} {exc.reason}: {detail or url}"]
1054
+ except URLError as exc:
1055
+ return None, [f"REST request failed: {exc.reason}"]
1056
+ except OSError as exc:
1057
+ return None, [f"REST request failed: {exc}"]
1058
+ try:
1059
+ payload = json.loads(text)
1060
+ except json.JSONDecodeError as exc:
1061
+ return None, [f"invalid JSON from public REST endpoint: {exc.msg}"]
1062
+ if not isinstance(payload, dict):
1063
+ return None, ["public REST endpoint did not return a JSON object"]
1064
+ return payload, []
1065
+
1066
+
1067
+ def github_public_rest_list(path: str) -> tuple[list[dict[str, Any]], list[str]]:
1068
+ url = f"https://api.github.com/{path.lstrip('/')}"
1069
+ headers = {
1070
+ "Accept": "application/vnd.github+json",
1071
+ "User-Agent": "loom-governance-runtime",
1072
+ }
1073
+ token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
1074
+ if token:
1075
+ headers["Authorization"] = f"Bearer {token}"
1076
+ request = Request(url, headers=headers)
1077
+ try:
1078
+ with urlopen(request, timeout=20) as response:
1079
+ text = response.read().decode("utf-8")
1080
+ except HTTPError as exc:
1081
+ detail = exc.read().decode("utf-8", errors="replace").strip()
1082
+ return [], [f"HTTP {exc.code} {exc.reason}: {detail or url}"]
1083
+ except URLError as exc:
1084
+ return [], [f"REST request failed: {exc.reason}"]
1085
+ except OSError as exc:
1086
+ return [], [f"REST request failed: {exc}"]
1087
+ try:
1088
+ payload = json.loads(text)
1089
+ except json.JSONDecodeError as exc:
1090
+ return [], [f"invalid JSON from public REST endpoint: {exc.msg}"]
1091
+ if not isinstance(payload, list):
1092
+ return [], ["public REST endpoint did not return a list"]
1093
+ return [entry for entry in payload if isinstance(entry, dict)], []
1017
1094
 
1018
1095
 
1019
1096
  def github_issue_state(value: Any) -> str:
@@ -4113,6 +4190,168 @@ def handle_carrier(args: argparse.Namespace) -> int:
4113
4190
  return emit(carrier_refresh_payload(target_root, args.output, args.item, dry_run=args.dry_run))
4114
4191
 
4115
4192
 
4193
+ def github_commit_pulls(root: Path, owner: str, repo_name: str, head_sha: str) -> tuple[list[dict[str, Any]], list[str]]:
4194
+ path = f"repos/{owner}/{repo_name}/commits/{head_sha}/pulls"
4195
+ result = run_process(
4196
+ [
4197
+ "gh",
4198
+ "api",
4199
+ path,
4200
+ "-H",
4201
+ "Accept: application/vnd.github+json",
4202
+ ],
4203
+ root,
4204
+ )
4205
+ if result.returncode != 0:
4206
+ detail = result.stderr.strip() or result.stdout.strip() or "gh api commit pulls failed"
4207
+ pulls, fallback_errors = github_public_rest_list(path)
4208
+ if pulls:
4209
+ return pulls, []
4210
+ return [], [detail, *[f"public REST fallback: {message}" for message in fallback_errors]]
4211
+ try:
4212
+ payload = json.loads(result.stdout)
4213
+ except json.JSONDecodeError as exc:
4214
+ return [], [f"invalid JSON from commit pulls REST endpoint: {exc.msg}"]
4215
+ if not isinstance(payload, list):
4216
+ return [], ["commit pulls REST endpoint did not return a list"]
4217
+ return [entry for entry in payload if isinstance(entry, dict)], []
4218
+
4219
+
4220
+ def host_binding_validate_payload(
4221
+ *,
4222
+ target_root: Path,
4223
+ owner: str | None,
4224
+ repo_name: str | None,
4225
+ issue_number: int | None,
4226
+ pr_number: int | None,
4227
+ branch_name: str | None,
4228
+ head_sha: str | None,
4229
+ base_sha: str | None,
4230
+ ) -> dict[str, Any]:
4231
+ detected_owner, detected_repo = detect_github_repo(target_root)
4232
+ owner = owner or detected_owner
4233
+ repo_name = repo_name or detected_repo
4234
+ missing_inputs: list[str] = []
4235
+ inferences: list[dict[str, Any]] = []
4236
+
4237
+ if not owner or not repo_name:
4238
+ missing_inputs.append("owner/repo")
4239
+
4240
+ inferred_pr = pr_number
4241
+ inferred_branch = branch_name
4242
+ if owner and repo_name and head_sha and inferred_pr is None:
4243
+ pulls, pull_errors = github_commit_pulls(target_root, owner, repo_name, head_sha)
4244
+ if pull_errors:
4245
+ missing_inputs.extend(f"head_sha: {message}" for message in pull_errors)
4246
+ elif len(pulls) == 1:
4247
+ inferred_pr = int(pulls[0].get("number"))
4248
+ head = pulls[0].get("head")
4249
+ if inferred_branch is None and isinstance(head, dict) and isinstance(head.get("ref"), str):
4250
+ inferred_branch = head.get("ref")
4251
+ inferences.append({"from": "head_sha", "to": "pr", "status": "inferred", "pr": inferred_pr})
4252
+ elif len(pulls) > 1:
4253
+ missing_inputs.append("head_sha resolves to multiple PRs; pass --pr explicitly")
4254
+ else:
4255
+ missing_inputs.append("issue_or_pr_binding")
4256
+
4257
+ if inferred_branch is None and head_sha is None and inferred_pr is None and issue_number is None:
4258
+ missing_inputs.append("branch | head-sha | pr | issue")
4259
+
4260
+ branch_payload: dict[str, Any] | None = None
4261
+ branch_errors: list[str] = []
4262
+ if owner and repo_name and inferred_branch:
4263
+ branch_payload, branch_errors = github_branch_payload(target_root, owner, repo_name, inferred_branch)
4264
+ missing_inputs.extend(f"branch: {message}" for message in branch_errors)
4265
+
4266
+ binding_payload = github_binding_payload(
4267
+ target_root=target_root,
4268
+ owner=owner,
4269
+ repo_name=repo_name,
4270
+ phase_number=None,
4271
+ fr_number=None,
4272
+ issue_number=issue_number,
4273
+ pr_number=inferred_pr,
4274
+ branch_name=inferred_branch,
4275
+ sync=False,
4276
+ dry_run=True,
4277
+ require_complete_chain=False,
4278
+ )
4279
+ binding_missing = [
4280
+ message
4281
+ for message in binding_payload.get("missing_inputs", [])
4282
+ if message not in {"work_item issue", "binding_chain"}
4283
+ ]
4284
+ if issue_number is not None or inferred_pr is not None:
4285
+ missing_inputs.extend(str(message) for message in binding_missing)
4286
+ findings = binding_payload.get("binding", {}).get("findings") if isinstance(binding_payload.get("binding"), dict) else []
4287
+ if findings:
4288
+ missing_inputs.append("binding_findings")
4289
+
4290
+ sha_validation: dict[str, Any] = {
4291
+ "head_sha": head_sha,
4292
+ "base_sha": base_sha,
4293
+ "status": "not_requested" if not head_sha else "validated",
4294
+ }
4295
+ if head_sha and branch_payload is not None:
4296
+ branch_head = branch_payload.get("commit", {}).get("sha") if isinstance(branch_payload.get("commit"), dict) else None
4297
+ sha_validation["branch_head_sha"] = branch_head
4298
+ if branch_head and branch_head != head_sha:
4299
+ sha_validation["status"] = "drift"
4300
+ missing_inputs.append("head_sha does not match branch head")
4301
+ if base_sha and head_sha:
4302
+ changed_paths, diff_errors = git_changed_paths(target_root, base_sha, head_sha)
4303
+ sha_validation["diff"] = {"changed_paths": changed_paths, "errors": diff_errors}
4304
+ if diff_errors:
4305
+ missing_inputs.extend(f"diff: {message}" for message in diff_errors)
4306
+
4307
+ result = "pass" if not missing_inputs else "block"
4308
+ return {
4309
+ "command": "host-binding",
4310
+ "operation": "validate",
4311
+ "schema_version": "loom-host-binding/v1",
4312
+ "result": result,
4313
+ "summary": (
4314
+ "host binding inputs are readable and sufficiently bound."
4315
+ if result == "pass"
4316
+ else "host binding inputs are missing or ambiguous."
4317
+ ),
4318
+ "missing_inputs": missing_inputs,
4319
+ "fallback_to": None if result == "pass" else "github-profile-binding",
4320
+ "repository": {"owner": owner, "name": repo_name},
4321
+ "inputs": {
4322
+ "issue": issue_number,
4323
+ "pr": pr_number,
4324
+ "branch": branch_name,
4325
+ "head_sha": head_sha,
4326
+ "base_sha": base_sha,
4327
+ },
4328
+ "inferences": inferences,
4329
+ "binding": binding_payload.get("binding"),
4330
+ "branch": {
4331
+ "name": inferred_branch,
4332
+ "status": "present" if branch_payload is not None else ("unreadable" if branch_errors else "not_requested"),
4333
+ "errors": branch_errors,
4334
+ },
4335
+ "sha_validation": sha_validation,
4336
+ }
4337
+
4338
+
4339
+ def handle_host_binding(args: argparse.Namespace) -> int:
4340
+ target_root = Path(args.target).expanduser().resolve()
4341
+ return emit(
4342
+ host_binding_validate_payload(
4343
+ target_root=target_root,
4344
+ owner=args.owner,
4345
+ repo_name=args.repo_name,
4346
+ issue_number=args.issue,
4347
+ pr_number=args.pr,
4348
+ branch_name=args.branch,
4349
+ head_sha=args.head_sha,
4350
+ base_sha=args.base_sha,
4351
+ )
4352
+ )
4353
+
4354
+
4116
4355
  def host_lifecycle_payload(context: dict[str, Any]) -> dict[str, Any]:
4117
4356
  branch = git_branch(context["target_root"])
4118
4357
  purity = purity_report_from_context(context)
@@ -7505,6 +7744,8 @@ def main(argv: list[str] | None = None) -> int:
7505
7744
  return handle_adopt(args)
7506
7745
  if args.command == "carrier":
7507
7746
  return handle_carrier(args)
7747
+ if args.command == "host-binding":
7748
+ return handle_host_binding(args)
7508
7749
  if args.command == "runtime-evidence":
7509
7750
  return handle_runtime_evidence(args)
7510
7751
  if args.command == "state-check":
@@ -2544,6 +2544,11 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
2544
2544
  ["python3", "tools/loom_flow.py", "carrier", "refresh", "--target", "examples/new-project", "--item", "INIT-0001", "--dry-run"],
2545
2545
  {"pass"},
2546
2546
  ),
2547
+ (
2548
+ "host-binding-validate",
2549
+ ["python3", "tools/loom_flow.py", "host-binding", "validate", "--target", ".", "--owner", "MC-and-his-Agents", "--repo", "Loom", "--branch", "main"],
2550
+ {"pass"},
2551
+ ),
2547
2552
  (
2548
2553
  "governance-profile-status",
2549
2554
  ["python3", "tools/loom_flow.py", "governance-profile", "status", "--target", "examples/new-project"],
@@ -2830,6 +2835,14 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
2830
2835
  failures.append(Failure("daily-execution-cli", "`carrier refresh` must report schema v1"))
2831
2836
  if not isinstance(payload.get("actions"), list):
2832
2837
  failures.append(Failure("daily-execution-cli", "`carrier refresh` must include actions"))
2838
+ if label == "host-binding-validate":
2839
+ if payload.get("command") != "host-binding" or payload.get("operation") != "validate":
2840
+ failures.append(Failure("daily-execution-cli", "`host-binding validate` must report command/operation"))
2841
+ if payload.get("schema_version") != "loom-host-binding/v1":
2842
+ failures.append(Failure("daily-execution-cli", "`host-binding validate` must report schema v1"))
2843
+ branch = payload.get("branch")
2844
+ if not isinstance(branch, dict) or branch.get("status") != "present":
2845
+ failures.append(Failure("daily-execution-cli", "`host-binding validate --branch main` must read the branch via REST"))
2833
2846
  if label in {"governance-profile-status", "governance-profile-upgrade-plan"}:
2834
2847
  if payload.get("command") != "governance-profile":
2835
2848
  failures.append(Failure("daily-execution-cli", f"`{label}` must report `command: governance-profile`"))