@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.
- package/package.json +1 -1
- package/payload/manifest.json +42 -42
- package/payload/plugin/loom/skills/shared/scripts/loom_check.py +39 -1
- package/payload/plugin/loom/skills/shared/scripts/loom_flow.py +140 -5
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +39 -1
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py +140 -5
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +39 -1
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py +140 -5
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +39 -1
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py +140 -5
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +39 -1
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py +140 -5
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +39 -1
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py +140 -5
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +39 -1
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py +140 -5
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +39 -1
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py +140 -5
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +39 -1
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py +140 -5
- package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py +39 -1
- package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py +140 -5
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-
|
|
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",
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
)
|