@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.
- package/package.json +1 -1
- package/payload/manifest.json +42 -42
- package/payload/plugin/loom/skills/shared/scripts/loom_check.py +29 -0
- package/payload/plugin/loom/skills/shared/scripts/loom_flow.py +34 -2
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +29 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py +34 -2
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +29 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py +34 -2
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +29 -0
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py +34 -2
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +29 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py +34 -2
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +29 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py +34 -2
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +29 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py +34 -2
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +29 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py +34 -2
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +29 -0
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py +34 -2
- package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py +29 -0
- package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py +34 -2
package/package.json
CHANGED
package/payload/manifest.json
CHANGED
|
@@ -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": "
|
|
5
|
+
"source_commit": "fbbe9a3659bbaf753f47b38aa230ab122c26227b",
|
|
6
6
|
"source_ref": "main",
|
|
7
|
-
"built_at": "2026-04-25T13:
|
|
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":
|
|
632
|
-
"sha256": "
|
|
631
|
+
"bytes": 267202,
|
|
632
|
+
"sha256": "213e64f5d974ae8b4dfbce97af96b88405b30cce3ab01d3d3ebf6a653aba4f4f"
|
|
633
633
|
},
|
|
634
634
|
{
|
|
635
635
|
"path": "plugin/loom/skills/shared/scripts/loom_flow.py",
|
|
636
|
-
"bytes":
|
|
637
|
-
"sha256": "
|
|
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":
|
|
1217
|
-
"sha256": "
|
|
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":
|
|
1222
|
-
"sha256": "
|
|
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":
|
|
1832
|
-
"sha256": "
|
|
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":
|
|
1837
|
-
"sha256": "
|
|
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":
|
|
2447
|
-
"sha256": "
|
|
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":
|
|
2452
|
-
"sha256": "
|
|
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":
|
|
3067
|
-
"sha256": "
|
|
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":
|
|
3072
|
-
"sha256": "
|
|
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":
|
|
3682
|
-
"sha256": "
|
|
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":
|
|
3687
|
-
"sha256": "
|
|
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":
|
|
4297
|
-
"sha256": "
|
|
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":
|
|
4302
|
-
"sha256": "
|
|
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":
|
|
4912
|
-
"sha256": "
|
|
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":
|
|
4917
|
-
"sha256": "
|
|
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":
|
|
5527
|
-
"sha256": "
|
|
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":
|
|
5532
|
-
"sha256": "
|
|
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":
|
|
6142
|
-
"sha256": "
|
|
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":
|
|
6147
|
-
"sha256": "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|