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

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 +39 -1
  4. package/payload/plugin/loom/skills/shared/scripts/loom_flow.py +140 -5
  5. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +39 -1
  6. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py +140 -5
  7. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +39 -1
  8. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py +140 -5
  9. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +39 -1
  10. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py +140 -5
  11. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +39 -1
  12. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py +140 -5
  13. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +39 -1
  14. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py +140 -5
  15. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +39 -1
  16. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py +140 -5
  17. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +39 -1
  18. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py +140 -5
  19. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +39 -1
  20. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py +140 -5
  21. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py +39 -1
  22. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py +140 -5
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.16",
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": "fbbe9a3659bbaf753f47b38aa230ab122c26227b",
6
6
  "source_ref": "main",
7
- "built_at": "2026-04-25T12:49:57+08:00",
7
+ "built_at": "2026-04-25T13:09:15+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": 267202,
632
+ "sha256": "213e64f5d974ae8b4dfbce97af96b88405b30cce3ab01d3d3ebf6a653aba4f4f"
633
633
  },
634
634
  {
635
635
  "path": "plugin/loom/skills/shared/scripts/loom_flow.py",
636
- "bytes": 274468,
637
- "sha256": "95a417012fce9a5c628d1e6e6d1428c875d2a9f483174ca93a5ad9944d09c398"
636
+ "bytes": 281098,
637
+ "sha256": "85012c8824e33f4e7816e451d3aa8d38eec63f4006ae98c657676f2640923184"
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": 267202,
1217
+ "sha256": "213e64f5d974ae8b4dfbce97af96b88405b30cce3ab01d3d3ebf6a653aba4f4f"
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": 281098,
1222
+ "sha256": "85012c8824e33f4e7816e451d3aa8d38eec63f4006ae98c657676f2640923184"
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": 267202,
1832
+ "sha256": "213e64f5d974ae8b4dfbce97af96b88405b30cce3ab01d3d3ebf6a653aba4f4f"
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": 281098,
1837
+ "sha256": "85012c8824e33f4e7816e451d3aa8d38eec63f4006ae98c657676f2640923184"
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": 267202,
2447
+ "sha256": "213e64f5d974ae8b4dfbce97af96b88405b30cce3ab01d3d3ebf6a653aba4f4f"
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": 281098,
2452
+ "sha256": "85012c8824e33f4e7816e451d3aa8d38eec63f4006ae98c657676f2640923184"
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": 267202,
3067
+ "sha256": "213e64f5d974ae8b4dfbce97af96b88405b30cce3ab01d3d3ebf6a653aba4f4f"
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": 281098,
3072
+ "sha256": "85012c8824e33f4e7816e451d3aa8d38eec63f4006ae98c657676f2640923184"
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": 267202,
3682
+ "sha256": "213e64f5d974ae8b4dfbce97af96b88405b30cce3ab01d3d3ebf6a653aba4f4f"
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": 281098,
3687
+ "sha256": "85012c8824e33f4e7816e451d3aa8d38eec63f4006ae98c657676f2640923184"
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": 267202,
4297
+ "sha256": "213e64f5d974ae8b4dfbce97af96b88405b30cce3ab01d3d3ebf6a653aba4f4f"
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": 281098,
4302
+ "sha256": "85012c8824e33f4e7816e451d3aa8d38eec63f4006ae98c657676f2640923184"
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": 267202,
4912
+ "sha256": "213e64f5d974ae8b4dfbce97af96b88405b30cce3ab01d3d3ebf6a653aba4f4f"
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": 281098,
4917
+ "sha256": "85012c8824e33f4e7816e451d3aa8d38eec63f4006ae98c657676f2640923184"
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": 267202,
5527
+ "sha256": "213e64f5d974ae8b4dfbce97af96b88405b30cce3ab01d3d3ebf6a653aba4f4f"
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": 281098,
5532
+ "sha256": "85012c8824e33f4e7816e451d3aa8d38eec63f4006ae98c657676f2640923184"
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": 267202,
6142
+ "sha256": "213e64f5d974ae8b4dfbce97af96b88405b30cce3ab01d3d3ebf6a653aba4f4f"
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": 281098,
6147
+ "sha256": "85012c8824e33f4e7816e451d3aa8d38eec63f4006ae98c657676f2640923184"
6148
6148
  },
6149
6149
  {
6150
6150
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_init.py",
@@ -94,6 +94,8 @@ 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",
98
+ "docs/evidence/validations/validation-github-profile-graphql-budget-guard.md",
97
99
  "docs/evidence/validations/validation-loom-core-runtime-parity.md",
98
100
  "docs/evidence/validations/validation-shadow-parity-blocking-gate.md",
99
101
  "docs/evidence/validations/validation-syvert-strong-governance-parity.md",
@@ -272,6 +274,8 @@ def iter_markdown_files(root: Path) -> list[Path]:
272
274
  relative = path.relative_to(root).as_posix()
273
275
  if any(relative == part or relative.startswith(f"{part}/") for part in skipped_parts):
274
276
  continue
277
+ if any(part.startswith(".payload-build-") for part in path.relative_to(root).parts):
278
+ continue
275
279
  if relative.startswith("packages/loom-installer/payload/"):
276
280
  continue
277
281
  results.append(path)
@@ -1138,7 +1142,7 @@ def require_reconciliation_payload(
1138
1142
  continue
1139
1143
  if finding.get("category") not in {"drift", "gate_failure"}:
1140
1144
  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"}:
1145
+ 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
1146
  failures.append(Failure(category, f"{context} reconciliation finding kind must stay within the stable contract"))
1143
1147
  if finding.get("severity") not in {"warn", "fix-needed", "block"}:
1144
1148
  failures.append(Failure(category, f"{context} reconciliation finding severity must stay within the stable contract"))
@@ -1150,6 +1154,12 @@ def require_reconciliation_payload(
1150
1154
  failures.append(Failure(category, f"{context} reconciliation findings must include `evidence`"))
1151
1155
  if not isinstance(finding.get("recommended_action"), str) or not finding.get("recommended_action"):
1152
1156
  failures.append(Failure(category, f"{context} reconciliation findings must include non-empty `recommended_action`"))
1157
+ binding = payload.get("binding")
1158
+ if binding is not None:
1159
+ if not isinstance(binding, dict):
1160
+ failures.append(Failure(category, f"{context} binding must be an object when present"))
1161
+ elif binding.get("schema_version") != "loom-github-binding/v1":
1162
+ failures.append(Failure(category, f"{context} binding must use `loom-github-binding/v1`"))
1153
1163
 
1154
1164
 
1155
1165
  def require_closeout_reconciliation_contract(
@@ -4824,6 +4834,8 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
4824
4834
  ("merged_but_open", "fix-needed", "reconciliation-sync"),
4825
4835
  ("absorbed_but_open", "fix-needed", "reconciliation-sync"),
4826
4836
  ("parent_drift", "block", "manual-reconciliation"),
4837
+ ("binding_failure", "block", "manual-reconciliation"),
4838
+ ("merge_signal_drift", "block", "manual-reconciliation"),
4827
4839
  ("host_signal_drift", "block", "manual-reconciliation"),
4828
4840
  ]
4829
4841
  for kind, reconciliation_result_value, fallback_to in valid_reconciliation_samples:
@@ -5550,6 +5562,31 @@ def check_generated_artifacts_untracked(root: Path) -> list[Failure]:
5550
5562
  ]
5551
5563
 
5552
5564
 
5565
+ def check_github_cli_budget(root: Path) -> list[Failure]:
5566
+ failures: list[Failure] = []
5567
+ forbidden = tuple(f"gh {kind} view" for kind in ("repo", "issue", "pr"))
5568
+ search_roots = [root / "skills/shared/scripts", root / "tools"]
5569
+ for search_root in search_roots:
5570
+ if not search_root.exists():
5571
+ continue
5572
+ for path in search_root.rglob("*.py"):
5573
+ if path.name == "loom_check.py":
5574
+ continue
5575
+ try:
5576
+ text = path.read_text(encoding="utf-8")
5577
+ except OSError:
5578
+ continue
5579
+ for needle in forbidden:
5580
+ if needle in text:
5581
+ failures.append(
5582
+ Failure(
5583
+ "github-api-budget",
5584
+ f"`{needle}` must not be used in high-frequency implementation path `{path.relative_to(root)}`",
5585
+ )
5586
+ )
5587
+ return failures
5588
+
5589
+
5553
5590
  def is_within(path: Path, root: Path) -> bool:
5554
5591
  try:
5555
5592
  path.relative_to(root)
@@ -5587,6 +5624,7 @@ def collect_failures(root: Path) -> list[Failure]:
5587
5624
  failures.extend(check_repo_interop_contracts(root))
5588
5625
  failures.extend(check_node_installer(root))
5589
5626
  failures.extend(check_generated_artifacts_untracked(root))
5627
+ failures.extend(check_github_cli_budget(root))
5590
5628
  failures.extend(check_markdown_links(root))
5591
5629
  return failures
5592
5630
 
@@ -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")
@@ -920,6 +926,21 @@ def gh_graphql(root: Path, query: str, variables: dict[str, Any]) -> tuple[dict[
920
926
  return data, []
921
927
 
922
928
 
929
+ def graphql_budget_guard(scope: str, errors: list[str] | None = None) -> dict[str, Any]:
930
+ return {
931
+ "graphql_only": True,
932
+ "budget_scope": scope,
933
+ "status": "unavailable" if errors else "guarded",
934
+ "errors": list(errors or []),
935
+ "fallback_to": "manual-reconciliation" if errors else None,
936
+ "recommended_action": (
937
+ "Retry this GraphQL-only host read with explicit operator intent, or continue with REST-backed issue/PR evidence when ProjectV2/native sub-issue data is not required."
938
+ if errors
939
+ else "Use this GraphQL-only host read sparingly; high-frequency repo, issue, and PR reads must stay on REST."
940
+ ),
941
+ }
942
+
943
+
923
944
  def git_dirty_entries(root: Path) -> list[dict[str, str]]:
924
945
  result = run_git(root, ["status", "--porcelain=v1"])
925
946
  if result is None or result.returncode != 0:
@@ -3586,6 +3607,7 @@ def github_binding_payload(
3586
3607
  branch_name: str | None,
3587
3608
  sync: bool,
3588
3609
  dry_run: bool,
3610
+ require_complete_chain: bool = True,
3589
3611
  ) -> dict[str, Any]:
3590
3612
  detected_owner, detected_repo = detect_github_repo(target_root)
3591
3613
  owner = owner or detected_owner
@@ -3744,7 +3766,19 @@ def github_binding_payload(
3744
3766
  "findings": findings,
3745
3767
  "repair_plan": repair_plan if sync or dry_run else [],
3746
3768
  }
3747
- chain_complete = all(entry.get("status") == "present" for entry in binding["chain"])
3769
+ if require_complete_chain:
3770
+ chain_complete = all(entry.get("status") == "present" for entry in binding["chain"])
3771
+ else:
3772
+ required_edges = []
3773
+ if issue_number is not None and pr_number is not None:
3774
+ required_edges.append(("work_item", "implementation_pr"))
3775
+ if pr_number is not None and pr_payload is not None and pr_payload.get("state") == "MERGED":
3776
+ required_edges.extend([("implementation_pr", "merge_commit"), ("merge_commit", "target_branch")])
3777
+ chain_complete = all(
3778
+ entry.get("status") == "present"
3779
+ for entry in binding["chain"]
3780
+ if (entry.get("from"), entry.get("to")) in required_edges
3781
+ )
3748
3782
  if not chain_complete and "binding_chain" not in missing_inputs:
3749
3783
  missing_inputs.append("binding_chain")
3750
3784
  result = "pass" if not missing_inputs and not findings and chain_complete else "block"
@@ -3927,6 +3961,7 @@ query($id: ID!) {
3927
3961
  "id": entry.get("id"),
3928
3962
  "content": {"number": None, "type": "Issue"},
3929
3963
  "status": status_name,
3964
+ "budget_guard": graphql_budget_guard("project_v2_item_field_values"),
3930
3965
  }, []
3931
3966
  return None, []
3932
3967
 
@@ -4003,6 +4038,7 @@ query($owner:String!, $name:String!, $number:Int!) {
4003
4038
  issue = repository.get("issue")
4004
4039
  if not isinstance(issue, dict):
4005
4040
  return None, [f"issue #{issue_number} is missing from GraphQL payload"]
4041
+ issue["budget_guard"] = graphql_budget_guard("native_parent_sub_issue_tree")
4006
4042
  return issue, []
4007
4043
 
4008
4044
 
@@ -4050,9 +4086,12 @@ def reconciliation_result(findings: list[dict[str, Any]]) -> str:
4050
4086
  def reconciliation_audit_payload(
4051
4087
  *,
4052
4088
  target_root: Path,
4089
+ phase_number: int | None,
4090
+ fr_number: int | None,
4053
4091
  issue_number: int | None,
4054
4092
  pr_number: int | None,
4055
4093
  project_number: int | None,
4094
+ branch_name: str | None,
4056
4095
  owner: str,
4057
4096
  repo_name: str,
4058
4097
  ) -> tuple[dict[str, Any], list[str]]:
@@ -4062,6 +4101,36 @@ def reconciliation_audit_payload(
4062
4101
  if issue_number is None and pr_number is None and project_number is None:
4063
4102
  missing_inputs.append("issue/pr/project")
4064
4103
 
4104
+ binding_payload = github_binding_payload(
4105
+ target_root=target_root,
4106
+ owner=owner,
4107
+ repo_name=repo_name,
4108
+ phase_number=phase_number,
4109
+ fr_number=fr_number,
4110
+ issue_number=issue_number,
4111
+ pr_number=pr_number,
4112
+ branch_name=branch_name,
4113
+ sync=False,
4114
+ dry_run=False,
4115
+ require_complete_chain=False,
4116
+ )
4117
+ binding = binding_payload.get("binding") if isinstance(binding_payload.get("binding"), dict) else None
4118
+ binding_findings = binding.get("findings") if isinstance(binding, dict) else None
4119
+ if isinstance(binding_findings, list):
4120
+ for finding in binding_findings:
4121
+ if isinstance(finding, dict):
4122
+ findings.append(
4123
+ make_reconciliation_finding(
4124
+ kind="binding_failure",
4125
+ severity="block",
4126
+ subject=str(finding.get("subject") or "github profile binding"),
4127
+ evidence={"binding": finding.get("evidence", {}), "binding_result": binding_payload.get("result")},
4128
+ recommended_action="repair the GitHub profile binding chain before reconciliation or closeout.",
4129
+ category="gate_failure",
4130
+ fallback_to="manual-reconciliation",
4131
+ )
4132
+ )
4133
+
4065
4134
  issue_payload: dict[str, Any] | None = None
4066
4135
  issue_id: str | None = None
4067
4136
  parent_payload: dict[str, Any] | None = None
@@ -4079,6 +4148,7 @@ def reconciliation_audit_payload(
4079
4148
  "status": "unavailable",
4080
4149
  "reason": "GraphQL-only parent/sub-issue tree could not be read.",
4081
4150
  "errors": issue_tree_errors,
4151
+ "budget_guard": graphql_budget_guard("native_parent_sub_issue_tree", issue_tree_errors),
4082
4152
  }
4083
4153
  elif issue_tree is not None:
4084
4154
  issue_payload = {**issue_payload, **issue_tree}
@@ -4100,6 +4170,22 @@ def reconciliation_audit_payload(
4100
4170
  if isinstance(oid, str) and oid:
4101
4171
  merge_commit_sha = oid
4102
4172
  merge_commit_in_main = contains_merged_commit(target_root, merge_commit_sha)
4173
+ if pr_payload.get("state") == "MERGED" and (not merge_commit_sha or not merge_commit_in_main):
4174
+ findings.append(
4175
+ make_reconciliation_finding(
4176
+ kind="merge_signal_drift",
4177
+ severity="block",
4178
+ subject=f"PR #{pr_number} merge signal",
4179
+ evidence={
4180
+ "pr_state": pr_payload.get("state"),
4181
+ "merge_commit": merge_commit_sha,
4182
+ "merge_commit_in_main": merge_commit_in_main,
4183
+ },
4184
+ recommended_action="repair or re-read the merge commit basis before closeout.",
4185
+ category="drift",
4186
+ fallback_to="manual-reconciliation",
4187
+ )
4188
+ )
4103
4189
 
4104
4190
  merged_issue_open = False
4105
4191
  if issue_payload is not None and pr_payload is not None:
@@ -4187,14 +4273,27 @@ def reconciliation_audit_payload(
4187
4273
  if project_number is not None:
4188
4274
  project_context, project_errors = project_status_context(target_root, owner, project_number)
4189
4275
  if project_errors:
4190
- missing_inputs.extend(f"project: {message}" for message in project_errors)
4276
+ if any("unknown owner type" in message for message in project_errors):
4277
+ project_payload = {
4278
+ "number": project_number,
4279
+ "status": "unavailable",
4280
+ "reason": "GitHub ProjectV2 CLI owner resolution is unavailable in this environment.",
4281
+ "errors": project_errors,
4282
+ "budget_guard": graphql_budget_guard("project_v2_status_surface", project_errors),
4283
+ }
4284
+ else:
4285
+ missing_inputs.extend(f"project: {message}" for message in project_errors)
4191
4286
  else:
4192
4287
  items = project_context["items"]
4193
4288
  issue_item = find_project_item(items, issue_number, "issue") if issue_number is not None else None
4289
+ issue_item_budget_guard: dict[str, Any] | None = None
4194
4290
  if issue_item is None and issue_id is not None and issue_number is not None:
4195
4291
  issue_item, issue_item_errors = project_item_for_issue(target_root, issue_id, project_number)
4196
4292
  if issue_item_errors:
4197
- missing_inputs.extend(f"project: {message}" for message in issue_item_errors)
4293
+ issue_item_budget_guard = graphql_budget_guard(
4294
+ "project_v2_issue_item_lookup",
4295
+ issue_item_errors,
4296
+ )
4198
4297
  pr_item = find_project_item(items, pr_number, "pr") if pr_number is not None else None
4199
4298
  project_payload = {
4200
4299
  "number": project_number,
@@ -4204,6 +4303,8 @@ def reconciliation_audit_payload(
4204
4303
  "issue_item": issue_item,
4205
4304
  "pr_item": pr_item,
4206
4305
  }
4306
+ if issue_item_budget_guard is not None:
4307
+ project_payload["issue_item_budget_guard"] = issue_item_budget_guard
4207
4308
 
4208
4309
  if issue_number is not None:
4209
4310
  expected_done = issue_payload is not None and (issue_payload.get("state") == "CLOSED" or merged_issue_open)
@@ -4304,6 +4405,7 @@ def reconciliation_audit_payload(
4304
4405
  "parent": parent_payload,
4305
4406
  "pr": pr_payload,
4306
4407
  "project": project_payload,
4408
+ "binding": binding,
4307
4409
  "findings": findings,
4308
4410
  },
4309
4411
  [],
@@ -4699,9 +4801,12 @@ def runtime_parity_payload(
4699
4801
  def closeout_payload(
4700
4802
  *,
4701
4803
  target_root: Path,
4804
+ phase_number: int | None,
4805
+ fr_number: int | None,
4702
4806
  issue_number: int | None,
4703
4807
  pr_number: int | None,
4704
4808
  project_number: int | None,
4809
+ branch_name: str | None,
4705
4810
  owner: str,
4706
4811
  repo_name: str,
4707
4812
  skip_gate: bool,
@@ -4733,9 +4838,12 @@ def closeout_payload(
4733
4838
  if issue_number is not None or pr_number is not None or project_number is not None:
4734
4839
  reconciliation_payload, reconciliation_errors = reconciliation_audit_payload(
4735
4840
  target_root=target_root,
4841
+ phase_number=phase_number,
4842
+ fr_number=fr_number,
4736
4843
  issue_number=issue_number,
4737
4844
  pr_number=pr_number,
4738
4845
  project_number=project_number,
4846
+ branch_name=branch_name,
4739
4847
  owner=owner,
4740
4848
  repo_name=repo_name,
4741
4849
  )
@@ -4783,14 +4891,27 @@ def closeout_payload(
4783
4891
  if project_number is not None:
4784
4892
  project_context, project_errors = project_status_context(target_root, owner, project_number)
4785
4893
  if project_errors:
4786
- missing_inputs.extend(f"project: {message}" for message in project_errors)
4894
+ if any("unknown owner type" in message for message in project_errors):
4895
+ project_payload = {
4896
+ "number": project_number,
4897
+ "status": "unavailable",
4898
+ "reason": "GitHub ProjectV2 CLI owner resolution is unavailable in this environment.",
4899
+ "errors": project_errors,
4900
+ "budget_guard": graphql_budget_guard("project_v2_status_surface", project_errors),
4901
+ }
4902
+ else:
4903
+ missing_inputs.extend(f"project: {message}" for message in project_errors)
4787
4904
  else:
4788
4905
  items = project_context["items"]
4789
4906
  issue_item = find_project_item(items, issue_number, "issue") if issue_number is not None else None
4907
+ issue_item_budget_guard: dict[str, Any] | None = None
4790
4908
  if issue_item is None and issue_id is not None and issue_number is not None:
4791
4909
  issue_item, issue_item_errors = project_item_for_issue(target_root, issue_id, project_number)
4792
4910
  if issue_item_errors:
4793
- missing_inputs.extend(f"project: {message}" for message in issue_item_errors)
4911
+ issue_item_budget_guard = graphql_budget_guard(
4912
+ "project_v2_issue_item_lookup",
4913
+ issue_item_errors,
4914
+ )
4794
4915
  pr_item = find_project_item(items, pr_number, "pr") if pr_number is not None else None
4795
4916
  if issue_number is not None and issue_item is None:
4796
4917
  missing_inputs.append("issue is missing from project")
@@ -4802,6 +4923,8 @@ def closeout_payload(
4802
4923
  "issue_item": issue_item,
4803
4924
  "pr_item": pr_item,
4804
4925
  }
4926
+ if issue_item_budget_guard is not None:
4927
+ project_payload["issue_item_budget_guard"] = issue_item_budget_guard
4805
4928
  for label, item in (("issue", issue_item), ("pr", pr_item)):
4806
4929
  if item is None:
4807
4930
  continue
@@ -4882,9 +5005,12 @@ def handle_closeout(args: argparse.Namespace) -> int:
4882
5005
 
4883
5006
  payload, errors = closeout_payload(
4884
5007
  target_root=target_root,
5008
+ phase_number=args.phase,
5009
+ fr_number=args.fr,
4885
5010
  issue_number=args.issue,
4886
5011
  pr_number=args.pr,
4887
5012
  project_number=args.project,
5013
+ branch_name=args.branch,
4888
5014
  owner=owner,
4889
5015
  repo_name=repo_name,
4890
5016
  skip_gate=args.skip_gate,
@@ -4988,9 +5114,12 @@ def handle_closeout(args: argparse.Namespace) -> int:
4988
5114
 
4989
5115
  refreshed_payload, errors = closeout_payload(
4990
5116
  target_root=target_root,
5117
+ phase_number=args.phase,
5118
+ fr_number=args.fr,
4991
5119
  issue_number=args.issue,
4992
5120
  pr_number=args.pr,
4993
5121
  project_number=args.project,
5122
+ branch_name=args.branch,
4994
5123
  owner=owner,
4995
5124
  repo_name=repo_name,
4996
5125
  skip_gate=args.skip_gate,
@@ -5070,9 +5199,12 @@ def handle_reconciliation(args: argparse.Namespace) -> int:
5070
5199
 
5071
5200
  payload, errors = reconciliation_audit_payload(
5072
5201
  target_root=target_root,
5202
+ phase_number=args.phase,
5203
+ fr_number=args.fr,
5073
5204
  issue_number=args.issue,
5074
5205
  pr_number=args.pr,
5075
5206
  project_number=args.project,
5207
+ branch_name=args.branch,
5076
5208
  owner=owner,
5077
5209
  repo_name=repo_name,
5078
5210
  )
@@ -5230,9 +5362,12 @@ def handle_reconciliation(args: argparse.Namespace) -> int:
5230
5362
 
5231
5363
  refreshed_payload, refreshed_errors = reconciliation_audit_payload(
5232
5364
  target_root=target_root,
5365
+ phase_number=args.phase,
5366
+ fr_number=args.fr,
5233
5367
  issue_number=args.issue,
5234
5368
  pr_number=args.pr,
5235
5369
  project_number=args.project,
5370
+ branch_name=args.branch,
5236
5371
  owner=owner,
5237
5372
  repo_name=repo_name,
5238
5373
  )