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

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 +10 -1
  4. package/payload/plugin/loom/skills/shared/scripts/loom_flow.py +106 -3
  5. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +10 -1
  6. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py +106 -3
  7. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +10 -1
  8. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py +106 -3
  9. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +10 -1
  10. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py +106 -3
  11. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +10 -1
  12. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py +106 -3
  13. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +10 -1
  14. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py +106 -3
  15. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +10 -1
  16. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py +106 -3
  17. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +10 -1
  18. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py +106 -3
  19. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +10 -1
  20. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py +106 -3
  21. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py +10 -1
  22. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py +106 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mc-and-his-agents/loom-installer",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
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": "7f500dab49df607146abae440b849dad8f90cf27",
5
+ "source_commit": "3e2a33cf1cb7e519d321cb2617b874e285815c5b",
6
6
  "source_ref": "main",
7
- "built_at": "2026-04-25T12:49:57+08:00",
7
+ "built_at": "2026-04-25T13:01:36+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": 265359,
632
- "sha256": "8d94670aca94c6166294e145d497eb9f9ba6bb1345258d131ea8722cb071d786"
631
+ "bytes": 265994,
632
+ "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
633
633
  },
634
634
  {
635
635
  "path": "plugin/loom/skills/shared/scripts/loom_flow.py",
636
- "bytes": 274468,
637
- "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
636
+ "bytes": 279330,
637
+ "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265359,
1217
- "sha256": "8d94670aca94c6166294e145d497eb9f9ba6bb1345258d131ea8722cb071d786"
1216
+ "bytes": 265994,
1217
+ "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
1218
1218
  },
1219
1219
  {
1220
1220
  "path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py",
1221
- "bytes": 274468,
1222
- "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
1221
+ "bytes": 279330,
1222
+ "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265359,
1832
- "sha256": "8d94670aca94c6166294e145d497eb9f9ba6bb1345258d131ea8722cb071d786"
1831
+ "bytes": 265994,
1832
+ "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
1833
1833
  },
1834
1834
  {
1835
1835
  "path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py",
1836
- "bytes": 274468,
1837
- "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
1836
+ "bytes": 279330,
1837
+ "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265359,
2447
- "sha256": "8d94670aca94c6166294e145d497eb9f9ba6bb1345258d131ea8722cb071d786"
2446
+ "bytes": 265994,
2447
+ "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
2448
2448
  },
2449
2449
  {
2450
2450
  "path": "skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py",
2451
- "bytes": 274468,
2452
- "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
2451
+ "bytes": 279330,
2452
+ "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265359,
3067
- "sha256": "8d94670aca94c6166294e145d497eb9f9ba6bb1345258d131ea8722cb071d786"
3066
+ "bytes": 265994,
3067
+ "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
3068
3068
  },
3069
3069
  {
3070
3070
  "path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py",
3071
- "bytes": 274468,
3072
- "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
3071
+ "bytes": 279330,
3072
+ "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265359,
3682
- "sha256": "8d94670aca94c6166294e145d497eb9f9ba6bb1345258d131ea8722cb071d786"
3681
+ "bytes": 265994,
3682
+ "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
3683
3683
  },
3684
3684
  {
3685
3685
  "path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py",
3686
- "bytes": 274468,
3687
- "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
3686
+ "bytes": 279330,
3687
+ "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265359,
4297
- "sha256": "8d94670aca94c6166294e145d497eb9f9ba6bb1345258d131ea8722cb071d786"
4296
+ "bytes": 265994,
4297
+ "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
4298
4298
  },
4299
4299
  {
4300
4300
  "path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py",
4301
- "bytes": 274468,
4302
- "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
4301
+ "bytes": 279330,
4302
+ "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265359,
4912
- "sha256": "8d94670aca94c6166294e145d497eb9f9ba6bb1345258d131ea8722cb071d786"
4911
+ "bytes": 265994,
4912
+ "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
4913
4913
  },
4914
4914
  {
4915
4915
  "path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py",
4916
- "bytes": 274468,
4917
- "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
4916
+ "bytes": 279330,
4917
+ "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265359,
5527
- "sha256": "8d94670aca94c6166294e145d497eb9f9ba6bb1345258d131ea8722cb071d786"
5526
+ "bytes": 265994,
5527
+ "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
5528
5528
  },
5529
5529
  {
5530
5530
  "path": "skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py",
5531
- "bytes": 274468,
5532
- "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
5531
+ "bytes": 279330,
5532
+ "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265359,
6142
- "sha256": "8d94670aca94c6166294e145d497eb9f9ba6bb1345258d131ea8722cb071d786"
6141
+ "bytes": 265994,
6142
+ "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
6143
6143
  },
6144
6144
  {
6145
6145
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py",
6146
- "bytes": 274468,
6147
- "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
6146
+ "bytes": 279330,
6147
+ "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
6148
6148
  },
6149
6149
  {
6150
6150
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_init.py",
@@ -94,6 +94,7 @@ CORE_DOCS = (
94
94
  "docs/evidence/landing-map.md",
95
95
  "docs/evidence/validations/validation-closeout-reconciliation-blocking-gate.md",
96
96
  "docs/evidence/validations/validation-github-profile-binding-orchestration.md",
97
+ "docs/evidence/validations/validation-github-profile-drift-reconciliation.md",
97
98
  "docs/evidence/validations/validation-loom-core-runtime-parity.md",
98
99
  "docs/evidence/validations/validation-shadow-parity-blocking-gate.md",
99
100
  "docs/evidence/validations/validation-syvert-strong-governance-parity.md",
@@ -1138,7 +1139,7 @@ def require_reconciliation_payload(
1138
1139
  continue
1139
1140
  if finding.get("category") not in {"drift", "gate_failure"}:
1140
1141
  failures.append(Failure(category, f"{context} reconciliation finding category must stay within the stable taxonomy"))
1141
- if finding.get("kind") not in {"merged_but_open", "absorbed_but_open", "parent_drift", "project_drift", "host_signal_drift"}:
1142
+ if finding.get("kind") not in {"merged_but_open", "absorbed_but_open", "parent_drift", "project_drift", "host_signal_drift", "binding_failure", "merge_signal_drift"}:
1142
1143
  failures.append(Failure(category, f"{context} reconciliation finding kind must stay within the stable contract"))
1143
1144
  if finding.get("severity") not in {"warn", "fix-needed", "block"}:
1144
1145
  failures.append(Failure(category, f"{context} reconciliation finding severity must stay within the stable contract"))
@@ -1150,6 +1151,12 @@ def require_reconciliation_payload(
1150
1151
  failures.append(Failure(category, f"{context} reconciliation findings must include `evidence`"))
1151
1152
  if not isinstance(finding.get("recommended_action"), str) or not finding.get("recommended_action"):
1152
1153
  failures.append(Failure(category, f"{context} reconciliation findings must include non-empty `recommended_action`"))
1154
+ binding = payload.get("binding")
1155
+ if binding is not None:
1156
+ if not isinstance(binding, dict):
1157
+ failures.append(Failure(category, f"{context} binding must be an object when present"))
1158
+ elif binding.get("schema_version") != "loom-github-binding/v1":
1159
+ failures.append(Failure(category, f"{context} binding must use `loom-github-binding/v1`"))
1153
1160
 
1154
1161
 
1155
1162
  def require_closeout_reconciliation_contract(
@@ -4824,6 +4831,8 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
4824
4831
  ("merged_but_open", "fix-needed", "reconciliation-sync"),
4825
4832
  ("absorbed_but_open", "fix-needed", "reconciliation-sync"),
4826
4833
  ("parent_drift", "block", "manual-reconciliation"),
4834
+ ("binding_failure", "block", "manual-reconciliation"),
4835
+ ("merge_signal_drift", "block", "manual-reconciliation"),
4827
4836
  ("host_signal_drift", "block", "manual-reconciliation"),
4828
4837
  ]
4829
4838
  for kind, reconciliation_result_value, fallback_to in valid_reconciliation_samples:
@@ -247,6 +247,9 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
247
247
  closeout.add_argument("--issue", type=int, help="GitHub issue number to validate or sync")
248
248
  closeout.add_argument("--pr", type=int, help="GitHub pull request number to validate or sync")
249
249
  closeout.add_argument("--project", type=int, help="GitHub project number to validate or sync")
250
+ closeout.add_argument("--phase", type=int, help="GitHub Phase issue number")
251
+ closeout.add_argument("--fr", type=int, help="GitHub FR issue number")
252
+ closeout.add_argument("--branch", help="GitHub branch name bound to the work item")
250
253
  closeout.add_argument("--owner", help="GitHub owner; auto-detected from origin when omitted")
251
254
  closeout.add_argument("--repo", dest="repo_name", help="GitHub repository name; auto-detected from origin when omitted")
252
255
  closeout.add_argument("--comment", help="Optional closeout comment for issue sync")
@@ -258,6 +261,9 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
258
261
  reconciliation.add_argument("--issue", type=int, help="GitHub issue number to audit")
259
262
  reconciliation.add_argument("--pr", type=int, help="GitHub pull request number to audit")
260
263
  reconciliation.add_argument("--project", type=int, help="GitHub project number to audit")
264
+ reconciliation.add_argument("--phase", type=int, help="GitHub Phase issue number")
265
+ reconciliation.add_argument("--fr", type=int, help="GitHub FR issue number")
266
+ reconciliation.add_argument("--branch", help="GitHub branch name bound to the work item")
261
267
  reconciliation.add_argument("--owner", help="GitHub owner; auto-detected from origin when omitted")
262
268
  reconciliation.add_argument("--repo", dest="repo_name", help="GitHub repository name; auto-detected from origin when omitted")
263
269
  reconciliation.add_argument("--comment", help="Optional closeout comment for issue sync")
@@ -3586,6 +3592,7 @@ def github_binding_payload(
3586
3592
  branch_name: str | None,
3587
3593
  sync: bool,
3588
3594
  dry_run: bool,
3595
+ require_complete_chain: bool = True,
3589
3596
  ) -> dict[str, Any]:
3590
3597
  detected_owner, detected_repo = detect_github_repo(target_root)
3591
3598
  owner = owner or detected_owner
@@ -3744,7 +3751,19 @@ def github_binding_payload(
3744
3751
  "findings": findings,
3745
3752
  "repair_plan": repair_plan if sync or dry_run else [],
3746
3753
  }
3747
- chain_complete = all(entry.get("status") == "present" for entry in binding["chain"])
3754
+ if require_complete_chain:
3755
+ chain_complete = all(entry.get("status") == "present" for entry in binding["chain"])
3756
+ else:
3757
+ required_edges = []
3758
+ if issue_number is not None and pr_number is not None:
3759
+ required_edges.append(("work_item", "implementation_pr"))
3760
+ if pr_number is not None and pr_payload is not None and pr_payload.get("state") == "MERGED":
3761
+ required_edges.extend([("implementation_pr", "merge_commit"), ("merge_commit", "target_branch")])
3762
+ chain_complete = all(
3763
+ entry.get("status") == "present"
3764
+ for entry in binding["chain"]
3765
+ if (entry.get("from"), entry.get("to")) in required_edges
3766
+ )
3748
3767
  if not chain_complete and "binding_chain" not in missing_inputs:
3749
3768
  missing_inputs.append("binding_chain")
3750
3769
  result = "pass" if not missing_inputs and not findings and chain_complete else "block"
@@ -4050,9 +4069,12 @@ def reconciliation_result(findings: list[dict[str, Any]]) -> str:
4050
4069
  def reconciliation_audit_payload(
4051
4070
  *,
4052
4071
  target_root: Path,
4072
+ phase_number: int | None,
4073
+ fr_number: int | None,
4053
4074
  issue_number: int | None,
4054
4075
  pr_number: int | None,
4055
4076
  project_number: int | None,
4077
+ branch_name: str | None,
4056
4078
  owner: str,
4057
4079
  repo_name: str,
4058
4080
  ) -> tuple[dict[str, Any], list[str]]:
@@ -4062,6 +4084,36 @@ def reconciliation_audit_payload(
4062
4084
  if issue_number is None and pr_number is None and project_number is None:
4063
4085
  missing_inputs.append("issue/pr/project")
4064
4086
 
4087
+ binding_payload = github_binding_payload(
4088
+ target_root=target_root,
4089
+ owner=owner,
4090
+ repo_name=repo_name,
4091
+ phase_number=phase_number,
4092
+ fr_number=fr_number,
4093
+ issue_number=issue_number,
4094
+ pr_number=pr_number,
4095
+ branch_name=branch_name,
4096
+ sync=False,
4097
+ dry_run=False,
4098
+ require_complete_chain=False,
4099
+ )
4100
+ binding = binding_payload.get("binding") if isinstance(binding_payload.get("binding"), dict) else None
4101
+ binding_findings = binding.get("findings") if isinstance(binding, dict) else None
4102
+ if isinstance(binding_findings, list):
4103
+ for finding in binding_findings:
4104
+ if isinstance(finding, dict):
4105
+ findings.append(
4106
+ make_reconciliation_finding(
4107
+ kind="binding_failure",
4108
+ severity="block",
4109
+ subject=str(finding.get("subject") or "github profile binding"),
4110
+ evidence={"binding": finding.get("evidence", {}), "binding_result": binding_payload.get("result")},
4111
+ recommended_action="repair the GitHub profile binding chain before reconciliation or closeout.",
4112
+ category="gate_failure",
4113
+ fallback_to="manual-reconciliation",
4114
+ )
4115
+ )
4116
+
4065
4117
  issue_payload: dict[str, Any] | None = None
4066
4118
  issue_id: str | None = None
4067
4119
  parent_payload: dict[str, Any] | None = None
@@ -4100,6 +4152,22 @@ def reconciliation_audit_payload(
4100
4152
  if isinstance(oid, str) and oid:
4101
4153
  merge_commit_sha = oid
4102
4154
  merge_commit_in_main = contains_merged_commit(target_root, merge_commit_sha)
4155
+ if pr_payload.get("state") == "MERGED" and (not merge_commit_sha or not merge_commit_in_main):
4156
+ findings.append(
4157
+ make_reconciliation_finding(
4158
+ kind="merge_signal_drift",
4159
+ severity="block",
4160
+ subject=f"PR #{pr_number} merge signal",
4161
+ evidence={
4162
+ "pr_state": pr_payload.get("state"),
4163
+ "merge_commit": merge_commit_sha,
4164
+ "merge_commit_in_main": merge_commit_in_main,
4165
+ },
4166
+ recommended_action="repair or re-read the merge commit basis before closeout.",
4167
+ category="drift",
4168
+ fallback_to="manual-reconciliation",
4169
+ )
4170
+ )
4103
4171
 
4104
4172
  merged_issue_open = False
4105
4173
  if issue_payload is not None and pr_payload is not None:
@@ -4187,7 +4255,15 @@ def reconciliation_audit_payload(
4187
4255
  if project_number is not None:
4188
4256
  project_context, project_errors = project_status_context(target_root, owner, project_number)
4189
4257
  if project_errors:
4190
- missing_inputs.extend(f"project: {message}" for message in project_errors)
4258
+ if any("unknown owner type" in message for message in project_errors):
4259
+ project_payload = {
4260
+ "number": project_number,
4261
+ "status": "unavailable",
4262
+ "reason": "GitHub ProjectV2 CLI owner resolution is unavailable in this environment.",
4263
+ "errors": project_errors,
4264
+ }
4265
+ else:
4266
+ missing_inputs.extend(f"project: {message}" for message in project_errors)
4191
4267
  else:
4192
4268
  items = project_context["items"]
4193
4269
  issue_item = find_project_item(items, issue_number, "issue") if issue_number is not None else None
@@ -4304,6 +4380,7 @@ def reconciliation_audit_payload(
4304
4380
  "parent": parent_payload,
4305
4381
  "pr": pr_payload,
4306
4382
  "project": project_payload,
4383
+ "binding": binding,
4307
4384
  "findings": findings,
4308
4385
  },
4309
4386
  [],
@@ -4699,9 +4776,12 @@ def runtime_parity_payload(
4699
4776
  def closeout_payload(
4700
4777
  *,
4701
4778
  target_root: Path,
4779
+ phase_number: int | None,
4780
+ fr_number: int | None,
4702
4781
  issue_number: int | None,
4703
4782
  pr_number: int | None,
4704
4783
  project_number: int | None,
4784
+ branch_name: str | None,
4705
4785
  owner: str,
4706
4786
  repo_name: str,
4707
4787
  skip_gate: bool,
@@ -4733,9 +4813,12 @@ def closeout_payload(
4733
4813
  if issue_number is not None or pr_number is not None or project_number is not None:
4734
4814
  reconciliation_payload, reconciliation_errors = reconciliation_audit_payload(
4735
4815
  target_root=target_root,
4816
+ phase_number=phase_number,
4817
+ fr_number=fr_number,
4736
4818
  issue_number=issue_number,
4737
4819
  pr_number=pr_number,
4738
4820
  project_number=project_number,
4821
+ branch_name=branch_name,
4739
4822
  owner=owner,
4740
4823
  repo_name=repo_name,
4741
4824
  )
@@ -4783,7 +4866,15 @@ def closeout_payload(
4783
4866
  if project_number is not None:
4784
4867
  project_context, project_errors = project_status_context(target_root, owner, project_number)
4785
4868
  if project_errors:
4786
- missing_inputs.extend(f"project: {message}" for message in project_errors)
4869
+ if any("unknown owner type" in message for message in project_errors):
4870
+ project_payload = {
4871
+ "number": project_number,
4872
+ "status": "unavailable",
4873
+ "reason": "GitHub ProjectV2 CLI owner resolution is unavailable in this environment.",
4874
+ "errors": project_errors,
4875
+ }
4876
+ else:
4877
+ missing_inputs.extend(f"project: {message}" for message in project_errors)
4787
4878
  else:
4788
4879
  items = project_context["items"]
4789
4880
  issue_item = find_project_item(items, issue_number, "issue") if issue_number is not None else None
@@ -4882,9 +4973,12 @@ def handle_closeout(args: argparse.Namespace) -> int:
4882
4973
 
4883
4974
  payload, errors = closeout_payload(
4884
4975
  target_root=target_root,
4976
+ phase_number=args.phase,
4977
+ fr_number=args.fr,
4885
4978
  issue_number=args.issue,
4886
4979
  pr_number=args.pr,
4887
4980
  project_number=args.project,
4981
+ branch_name=args.branch,
4888
4982
  owner=owner,
4889
4983
  repo_name=repo_name,
4890
4984
  skip_gate=args.skip_gate,
@@ -4988,9 +5082,12 @@ def handle_closeout(args: argparse.Namespace) -> int:
4988
5082
 
4989
5083
  refreshed_payload, errors = closeout_payload(
4990
5084
  target_root=target_root,
5085
+ phase_number=args.phase,
5086
+ fr_number=args.fr,
4991
5087
  issue_number=args.issue,
4992
5088
  pr_number=args.pr,
4993
5089
  project_number=args.project,
5090
+ branch_name=args.branch,
4994
5091
  owner=owner,
4995
5092
  repo_name=repo_name,
4996
5093
  skip_gate=args.skip_gate,
@@ -5070,9 +5167,12 @@ def handle_reconciliation(args: argparse.Namespace) -> int:
5070
5167
 
5071
5168
  payload, errors = reconciliation_audit_payload(
5072
5169
  target_root=target_root,
5170
+ phase_number=args.phase,
5171
+ fr_number=args.fr,
5073
5172
  issue_number=args.issue,
5074
5173
  pr_number=args.pr,
5075
5174
  project_number=args.project,
5175
+ branch_name=args.branch,
5076
5176
  owner=owner,
5077
5177
  repo_name=repo_name,
5078
5178
  )
@@ -5230,9 +5330,12 @@ def handle_reconciliation(args: argparse.Namespace) -> int:
5230
5330
 
5231
5331
  refreshed_payload, refreshed_errors = reconciliation_audit_payload(
5232
5332
  target_root=target_root,
5333
+ phase_number=args.phase,
5334
+ fr_number=args.fr,
5233
5335
  issue_number=args.issue,
5234
5336
  pr_number=args.pr,
5235
5337
  project_number=args.project,
5338
+ branch_name=args.branch,
5236
5339
  owner=owner,
5237
5340
  repo_name=repo_name,
5238
5341
  )
@@ -94,6 +94,7 @@ CORE_DOCS = (
94
94
  "docs/evidence/landing-map.md",
95
95
  "docs/evidence/validations/validation-closeout-reconciliation-blocking-gate.md",
96
96
  "docs/evidence/validations/validation-github-profile-binding-orchestration.md",
97
+ "docs/evidence/validations/validation-github-profile-drift-reconciliation.md",
97
98
  "docs/evidence/validations/validation-loom-core-runtime-parity.md",
98
99
  "docs/evidence/validations/validation-shadow-parity-blocking-gate.md",
99
100
  "docs/evidence/validations/validation-syvert-strong-governance-parity.md",
@@ -1138,7 +1139,7 @@ def require_reconciliation_payload(
1138
1139
  continue
1139
1140
  if finding.get("category") not in {"drift", "gate_failure"}:
1140
1141
  failures.append(Failure(category, f"{context} reconciliation finding category must stay within the stable taxonomy"))
1141
- if finding.get("kind") not in {"merged_but_open", "absorbed_but_open", "parent_drift", "project_drift", "host_signal_drift"}:
1142
+ if finding.get("kind") not in {"merged_but_open", "absorbed_but_open", "parent_drift", "project_drift", "host_signal_drift", "binding_failure", "merge_signal_drift"}:
1142
1143
  failures.append(Failure(category, f"{context} reconciliation finding kind must stay within the stable contract"))
1143
1144
  if finding.get("severity") not in {"warn", "fix-needed", "block"}:
1144
1145
  failures.append(Failure(category, f"{context} reconciliation finding severity must stay within the stable contract"))
@@ -1150,6 +1151,12 @@ def require_reconciliation_payload(
1150
1151
  failures.append(Failure(category, f"{context} reconciliation findings must include `evidence`"))
1151
1152
  if not isinstance(finding.get("recommended_action"), str) or not finding.get("recommended_action"):
1152
1153
  failures.append(Failure(category, f"{context} reconciliation findings must include non-empty `recommended_action`"))
1154
+ binding = payload.get("binding")
1155
+ if binding is not None:
1156
+ if not isinstance(binding, dict):
1157
+ failures.append(Failure(category, f"{context} binding must be an object when present"))
1158
+ elif binding.get("schema_version") != "loom-github-binding/v1":
1159
+ failures.append(Failure(category, f"{context} binding must use `loom-github-binding/v1`"))
1153
1160
 
1154
1161
 
1155
1162
  def require_closeout_reconciliation_contract(
@@ -4824,6 +4831,8 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
4824
4831
  ("merged_but_open", "fix-needed", "reconciliation-sync"),
4825
4832
  ("absorbed_but_open", "fix-needed", "reconciliation-sync"),
4826
4833
  ("parent_drift", "block", "manual-reconciliation"),
4834
+ ("binding_failure", "block", "manual-reconciliation"),
4835
+ ("merge_signal_drift", "block", "manual-reconciliation"),
4827
4836
  ("host_signal_drift", "block", "manual-reconciliation"),
4828
4837
  ]
4829
4838
  for kind, reconciliation_result_value, fallback_to in valid_reconciliation_samples: