@mc-and-his-agents/loom-installer 0.1.15 → 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 +29 -0
  4. package/payload/plugin/loom/skills/shared/scripts/loom_flow.py +34 -2
  5. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +29 -0
  6. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py +34 -2
  7. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +29 -0
  8. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py +34 -2
  9. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +29 -0
  10. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py +34 -2
  11. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +29 -0
  12. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py +34 -2
  13. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +29 -0
  14. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py +34 -2
  15. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +29 -0
  16. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py +34 -2
  17. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +29 -0
  18. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py +34 -2
  19. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +29 -0
  20. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py +34 -2
  21. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py +29 -0
  22. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py +34 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mc-and-his-agents/loom-installer",
3
- "version": "0.1.15",
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": "3e2a33cf1cb7e519d321cb2617b874e285815c5b",
5
+ "source_commit": "fbbe9a3659bbaf753f47b38aa230ab122c26227b",
6
6
  "source_ref": "main",
7
- "built_at": "2026-04-25T13:01:36+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": 265994,
632
- "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
631
+ "bytes": 267202,
632
+ "sha256": "213e64f5d974ae8b4dfbce97af96b88405b30cce3ab01d3d3ebf6a653aba4f4f"
633
633
  },
634
634
  {
635
635
  "path": "plugin/loom/skills/shared/scripts/loom_flow.py",
636
- "bytes": 279330,
637
- "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265994,
1217
- "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
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": 279330,
1222
- "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265994,
1832
- "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
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": 279330,
1837
- "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265994,
2447
- "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
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": 279330,
2452
- "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265994,
3067
- "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
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": 279330,
3072
- "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265994,
3682
- "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
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": 279330,
3687
- "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265994,
4297
- "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
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": 279330,
4302
- "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265994,
4912
- "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
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": 279330,
4917
- "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265994,
5527
- "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
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": 279330,
5532
- "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265994,
6142
- "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
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": 279330,
6147
- "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
6146
+ "bytes": 281098,
6147
+ "sha256": "85012c8824e33f4e7816e451d3aa8d38eec63f4006ae98c657676f2640923184"
6148
6148
  },
6149
6149
  {
6150
6150
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_init.py",
@@ -95,6 +95,7 @@ CORE_DOCS = (
95
95
  "docs/evidence/validations/validation-closeout-reconciliation-blocking-gate.md",
96
96
  "docs/evidence/validations/validation-github-profile-binding-orchestration.md",
97
97
  "docs/evidence/validations/validation-github-profile-drift-reconciliation.md",
98
+ "docs/evidence/validations/validation-github-profile-graphql-budget-guard.md",
98
99
  "docs/evidence/validations/validation-loom-core-runtime-parity.md",
99
100
  "docs/evidence/validations/validation-shadow-parity-blocking-gate.md",
100
101
  "docs/evidence/validations/validation-syvert-strong-governance-parity.md",
@@ -273,6 +274,8 @@ def iter_markdown_files(root: Path) -> list[Path]:
273
274
  relative = path.relative_to(root).as_posix()
274
275
  if any(relative == part or relative.startswith(f"{part}/") for part in skipped_parts):
275
276
  continue
277
+ if any(part.startswith(".payload-build-") for part in path.relative_to(root).parts):
278
+ continue
276
279
  if relative.startswith("packages/loom-installer/payload/"):
277
280
  continue
278
281
  results.append(path)
@@ -5559,6 +5562,31 @@ def check_generated_artifacts_untracked(root: Path) -> list[Failure]:
5559
5562
  ]
5560
5563
 
5561
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
+
5562
5590
  def is_within(path: Path, root: Path) -> bool:
5563
5591
  try:
5564
5592
  path.relative_to(root)
@@ -5596,6 +5624,7 @@ def collect_failures(root: Path) -> list[Failure]:
5596
5624
  failures.extend(check_repo_interop_contracts(root))
5597
5625
  failures.extend(check_node_installer(root))
5598
5626
  failures.extend(check_generated_artifacts_untracked(root))
5627
+ failures.extend(check_github_cli_budget(root))
5599
5628
  failures.extend(check_markdown_links(root))
5600
5629
  return failures
5601
5630
 
@@ -926,6 +926,21 @@ def gh_graphql(root: Path, query: str, variables: dict[str, Any]) -> tuple[dict[
926
926
  return data, []
927
927
 
928
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
+
929
944
  def git_dirty_entries(root: Path) -> list[dict[str, str]]:
930
945
  result = run_git(root, ["status", "--porcelain=v1"])
931
946
  if result is None or result.returncode != 0:
@@ -3946,6 +3961,7 @@ query($id: ID!) {
3946
3961
  "id": entry.get("id"),
3947
3962
  "content": {"number": None, "type": "Issue"},
3948
3963
  "status": status_name,
3964
+ "budget_guard": graphql_budget_guard("project_v2_item_field_values"),
3949
3965
  }, []
3950
3966
  return None, []
3951
3967
 
@@ -4022,6 +4038,7 @@ query($owner:String!, $name:String!, $number:Int!) {
4022
4038
  issue = repository.get("issue")
4023
4039
  if not isinstance(issue, dict):
4024
4040
  return None, [f"issue #{issue_number} is missing from GraphQL payload"]
4041
+ issue["budget_guard"] = graphql_budget_guard("native_parent_sub_issue_tree")
4025
4042
  return issue, []
4026
4043
 
4027
4044
 
@@ -4131,6 +4148,7 @@ def reconciliation_audit_payload(
4131
4148
  "status": "unavailable",
4132
4149
  "reason": "GraphQL-only parent/sub-issue tree could not be read.",
4133
4150
  "errors": issue_tree_errors,
4151
+ "budget_guard": graphql_budget_guard("native_parent_sub_issue_tree", issue_tree_errors),
4134
4152
  }
4135
4153
  elif issue_tree is not None:
4136
4154
  issue_payload = {**issue_payload, **issue_tree}
@@ -4261,16 +4279,21 @@ def reconciliation_audit_payload(
4261
4279
  "status": "unavailable",
4262
4280
  "reason": "GitHub ProjectV2 CLI owner resolution is unavailable in this environment.",
4263
4281
  "errors": project_errors,
4282
+ "budget_guard": graphql_budget_guard("project_v2_status_surface", project_errors),
4264
4283
  }
4265
4284
  else:
4266
4285
  missing_inputs.extend(f"project: {message}" for message in project_errors)
4267
4286
  else:
4268
4287
  items = project_context["items"]
4269
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
4270
4290
  if issue_item is None and issue_id is not None and issue_number is not None:
4271
4291
  issue_item, issue_item_errors = project_item_for_issue(target_root, issue_id, project_number)
4272
4292
  if issue_item_errors:
4273
- 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
+ )
4274
4297
  pr_item = find_project_item(items, pr_number, "pr") if pr_number is not None else None
4275
4298
  project_payload = {
4276
4299
  "number": project_number,
@@ -4280,6 +4303,8 @@ def reconciliation_audit_payload(
4280
4303
  "issue_item": issue_item,
4281
4304
  "pr_item": pr_item,
4282
4305
  }
4306
+ if issue_item_budget_guard is not None:
4307
+ project_payload["issue_item_budget_guard"] = issue_item_budget_guard
4283
4308
 
4284
4309
  if issue_number is not None:
4285
4310
  expected_done = issue_payload is not None and (issue_payload.get("state") == "CLOSED" or merged_issue_open)
@@ -4872,16 +4897,21 @@ def closeout_payload(
4872
4897
  "status": "unavailable",
4873
4898
  "reason": "GitHub ProjectV2 CLI owner resolution is unavailable in this environment.",
4874
4899
  "errors": project_errors,
4900
+ "budget_guard": graphql_budget_guard("project_v2_status_surface", project_errors),
4875
4901
  }
4876
4902
  else:
4877
4903
  missing_inputs.extend(f"project: {message}" for message in project_errors)
4878
4904
  else:
4879
4905
  items = project_context["items"]
4880
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
4881
4908
  if issue_item is None and issue_id is not None and issue_number is not None:
4882
4909
  issue_item, issue_item_errors = project_item_for_issue(target_root, issue_id, project_number)
4883
4910
  if issue_item_errors:
4884
- 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
+ )
4885
4915
  pr_item = find_project_item(items, pr_number, "pr") if pr_number is not None else None
4886
4916
  if issue_number is not None and issue_item is None:
4887
4917
  missing_inputs.append("issue is missing from project")
@@ -4893,6 +4923,8 @@ def closeout_payload(
4893
4923
  "issue_item": issue_item,
4894
4924
  "pr_item": pr_item,
4895
4925
  }
4926
+ if issue_item_budget_guard is not None:
4927
+ project_payload["issue_item_budget_guard"] = issue_item_budget_guard
4896
4928
  for label, item in (("issue", issue_item), ("pr", pr_item)):
4897
4929
  if item is None:
4898
4930
  continue
@@ -95,6 +95,7 @@ CORE_DOCS = (
95
95
  "docs/evidence/validations/validation-closeout-reconciliation-blocking-gate.md",
96
96
  "docs/evidence/validations/validation-github-profile-binding-orchestration.md",
97
97
  "docs/evidence/validations/validation-github-profile-drift-reconciliation.md",
98
+ "docs/evidence/validations/validation-github-profile-graphql-budget-guard.md",
98
99
  "docs/evidence/validations/validation-loom-core-runtime-parity.md",
99
100
  "docs/evidence/validations/validation-shadow-parity-blocking-gate.md",
100
101
  "docs/evidence/validations/validation-syvert-strong-governance-parity.md",
@@ -273,6 +274,8 @@ def iter_markdown_files(root: Path) -> list[Path]:
273
274
  relative = path.relative_to(root).as_posix()
274
275
  if any(relative == part or relative.startswith(f"{part}/") for part in skipped_parts):
275
276
  continue
277
+ if any(part.startswith(".payload-build-") for part in path.relative_to(root).parts):
278
+ continue
276
279
  if relative.startswith("packages/loom-installer/payload/"):
277
280
  continue
278
281
  results.append(path)
@@ -5559,6 +5562,31 @@ def check_generated_artifacts_untracked(root: Path) -> list[Failure]:
5559
5562
  ]
5560
5563
 
5561
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
+
5562
5590
  def is_within(path: Path, root: Path) -> bool:
5563
5591
  try:
5564
5592
  path.relative_to(root)
@@ -5596,6 +5624,7 @@ def collect_failures(root: Path) -> list[Failure]:
5596
5624
  failures.extend(check_repo_interop_contracts(root))
5597
5625
  failures.extend(check_node_installer(root))
5598
5626
  failures.extend(check_generated_artifacts_untracked(root))
5627
+ failures.extend(check_github_cli_budget(root))
5599
5628
  failures.extend(check_markdown_links(root))
5600
5629
  return failures
5601
5630
 
@@ -926,6 +926,21 @@ def gh_graphql(root: Path, query: str, variables: dict[str, Any]) -> tuple[dict[
926
926
  return data, []
927
927
 
928
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
+
929
944
  def git_dirty_entries(root: Path) -> list[dict[str, str]]:
930
945
  result = run_git(root, ["status", "--porcelain=v1"])
931
946
  if result is None or result.returncode != 0:
@@ -3946,6 +3961,7 @@ query($id: ID!) {
3946
3961
  "id": entry.get("id"),
3947
3962
  "content": {"number": None, "type": "Issue"},
3948
3963
  "status": status_name,
3964
+ "budget_guard": graphql_budget_guard("project_v2_item_field_values"),
3949
3965
  }, []
3950
3966
  return None, []
3951
3967
 
@@ -4022,6 +4038,7 @@ query($owner:String!, $name:String!, $number:Int!) {
4022
4038
  issue = repository.get("issue")
4023
4039
  if not isinstance(issue, dict):
4024
4040
  return None, [f"issue #{issue_number} is missing from GraphQL payload"]
4041
+ issue["budget_guard"] = graphql_budget_guard("native_parent_sub_issue_tree")
4025
4042
  return issue, []
4026
4043
 
4027
4044
 
@@ -4131,6 +4148,7 @@ def reconciliation_audit_payload(
4131
4148
  "status": "unavailable",
4132
4149
  "reason": "GraphQL-only parent/sub-issue tree could not be read.",
4133
4150
  "errors": issue_tree_errors,
4151
+ "budget_guard": graphql_budget_guard("native_parent_sub_issue_tree", issue_tree_errors),
4134
4152
  }
4135
4153
  elif issue_tree is not None:
4136
4154
  issue_payload = {**issue_payload, **issue_tree}
@@ -4261,16 +4279,21 @@ def reconciliation_audit_payload(
4261
4279
  "status": "unavailable",
4262
4280
  "reason": "GitHub ProjectV2 CLI owner resolution is unavailable in this environment.",
4263
4281
  "errors": project_errors,
4282
+ "budget_guard": graphql_budget_guard("project_v2_status_surface", project_errors),
4264
4283
  }
4265
4284
  else:
4266
4285
  missing_inputs.extend(f"project: {message}" for message in project_errors)
4267
4286
  else:
4268
4287
  items = project_context["items"]
4269
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
4270
4290
  if issue_item is None and issue_id is not None and issue_number is not None:
4271
4291
  issue_item, issue_item_errors = project_item_for_issue(target_root, issue_id, project_number)
4272
4292
  if issue_item_errors:
4273
- 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
+ )
4274
4297
  pr_item = find_project_item(items, pr_number, "pr") if pr_number is not None else None
4275
4298
  project_payload = {
4276
4299
  "number": project_number,
@@ -4280,6 +4303,8 @@ def reconciliation_audit_payload(
4280
4303
  "issue_item": issue_item,
4281
4304
  "pr_item": pr_item,
4282
4305
  }
4306
+ if issue_item_budget_guard is not None:
4307
+ project_payload["issue_item_budget_guard"] = issue_item_budget_guard
4283
4308
 
4284
4309
  if issue_number is not None:
4285
4310
  expected_done = issue_payload is not None and (issue_payload.get("state") == "CLOSED" or merged_issue_open)
@@ -4872,16 +4897,21 @@ def closeout_payload(
4872
4897
  "status": "unavailable",
4873
4898
  "reason": "GitHub ProjectV2 CLI owner resolution is unavailable in this environment.",
4874
4899
  "errors": project_errors,
4900
+ "budget_guard": graphql_budget_guard("project_v2_status_surface", project_errors),
4875
4901
  }
4876
4902
  else:
4877
4903
  missing_inputs.extend(f"project: {message}" for message in project_errors)
4878
4904
  else:
4879
4905
  items = project_context["items"]
4880
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
4881
4908
  if issue_item is None and issue_id is not None and issue_number is not None:
4882
4909
  issue_item, issue_item_errors = project_item_for_issue(target_root, issue_id, project_number)
4883
4910
  if issue_item_errors:
4884
- 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
+ )
4885
4915
  pr_item = find_project_item(items, pr_number, "pr") if pr_number is not None else None
4886
4916
  if issue_number is not None and issue_item is None:
4887
4917
  missing_inputs.append("issue is missing from project")
@@ -4893,6 +4923,8 @@ def closeout_payload(
4893
4923
  "issue_item": issue_item,
4894
4924
  "pr_item": pr_item,
4895
4925
  }
4926
+ if issue_item_budget_guard is not None:
4927
+ project_payload["issue_item_budget_guard"] = issue_item_budget_guard
4896
4928
  for label, item in (("issue", issue_item), ("pr", pr_item)):
4897
4929
  if item is None:
4898
4930
  continue
@@ -95,6 +95,7 @@ CORE_DOCS = (
95
95
  "docs/evidence/validations/validation-closeout-reconciliation-blocking-gate.md",
96
96
  "docs/evidence/validations/validation-github-profile-binding-orchestration.md",
97
97
  "docs/evidence/validations/validation-github-profile-drift-reconciliation.md",
98
+ "docs/evidence/validations/validation-github-profile-graphql-budget-guard.md",
98
99
  "docs/evidence/validations/validation-loom-core-runtime-parity.md",
99
100
  "docs/evidence/validations/validation-shadow-parity-blocking-gate.md",
100
101
  "docs/evidence/validations/validation-syvert-strong-governance-parity.md",
@@ -273,6 +274,8 @@ def iter_markdown_files(root: Path) -> list[Path]:
273
274
  relative = path.relative_to(root).as_posix()
274
275
  if any(relative == part or relative.startswith(f"{part}/") for part in skipped_parts):
275
276
  continue
277
+ if any(part.startswith(".payload-build-") for part in path.relative_to(root).parts):
278
+ continue
276
279
  if relative.startswith("packages/loom-installer/payload/"):
277
280
  continue
278
281
  results.append(path)
@@ -5559,6 +5562,31 @@ def check_generated_artifacts_untracked(root: Path) -> list[Failure]:
5559
5562
  ]
5560
5563
 
5561
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
+
5562
5590
  def is_within(path: Path, root: Path) -> bool:
5563
5591
  try:
5564
5592
  path.relative_to(root)
@@ -5596,6 +5624,7 @@ def collect_failures(root: Path) -> list[Failure]:
5596
5624
  failures.extend(check_repo_interop_contracts(root))
5597
5625
  failures.extend(check_node_installer(root))
5598
5626
  failures.extend(check_generated_artifacts_untracked(root))
5627
+ failures.extend(check_github_cli_budget(root))
5599
5628
  failures.extend(check_markdown_links(root))
5600
5629
  return failures
5601
5630