@mc-and-his-agents/loom-installer 0.1.15 → 0.1.17
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 +88 -0
- package/payload/plugin/loom/skills/shared/scripts/loom_flow.py +183 -4
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +88 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py +183 -4
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +88 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py +183 -4
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +88 -0
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py +183 -4
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +88 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py +183 -4
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +88 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py +183 -4
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +88 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py +183 -4
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +88 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py +183 -4
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +88 -0
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py +183 -4
- package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py +88 -0
- package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py +183 -4
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": "c3f5933d41b009b1e0537356221c3bcd7fd910be",
|
|
6
6
|
"source_ref": "main",
|
|
7
|
-
"built_at": "2026-04-25T13:
|
|
7
|
+
"built_at": "2026-04-25T13:23:16+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": 269850,
|
|
632
|
+
"sha256": "15eecced57199f79c76774b2a629cae291c610d30b64b29a31ef60f4bad65ec7"
|
|
633
633
|
},
|
|
634
634
|
{
|
|
635
635
|
"path": "plugin/loom/skills/shared/scripts/loom_flow.py",
|
|
636
|
-
"bytes":
|
|
637
|
-
"sha256": "
|
|
636
|
+
"bytes": 287204,
|
|
637
|
+
"sha256": "d33bde08ceeafa70ddad41ba1c6f8373e0366da08a9b17484ce0ad3683f83960"
|
|
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": 269850,
|
|
1217
|
+
"sha256": "15eecced57199f79c76774b2a629cae291c610d30b64b29a31ef60f4bad65ec7"
|
|
1218
1218
|
},
|
|
1219
1219
|
{
|
|
1220
1220
|
"path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py",
|
|
1221
|
-
"bytes":
|
|
1222
|
-
"sha256": "
|
|
1221
|
+
"bytes": 287204,
|
|
1222
|
+
"sha256": "d33bde08ceeafa70ddad41ba1c6f8373e0366da08a9b17484ce0ad3683f83960"
|
|
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": 269850,
|
|
1832
|
+
"sha256": "15eecced57199f79c76774b2a629cae291c610d30b64b29a31ef60f4bad65ec7"
|
|
1833
1833
|
},
|
|
1834
1834
|
{
|
|
1835
1835
|
"path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py",
|
|
1836
|
-
"bytes":
|
|
1837
|
-
"sha256": "
|
|
1836
|
+
"bytes": 287204,
|
|
1837
|
+
"sha256": "d33bde08ceeafa70ddad41ba1c6f8373e0366da08a9b17484ce0ad3683f83960"
|
|
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": 269850,
|
|
2447
|
+
"sha256": "15eecced57199f79c76774b2a629cae291c610d30b64b29a31ef60f4bad65ec7"
|
|
2448
2448
|
},
|
|
2449
2449
|
{
|
|
2450
2450
|
"path": "skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py",
|
|
2451
|
-
"bytes":
|
|
2452
|
-
"sha256": "
|
|
2451
|
+
"bytes": 287204,
|
|
2452
|
+
"sha256": "d33bde08ceeafa70ddad41ba1c6f8373e0366da08a9b17484ce0ad3683f83960"
|
|
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": 269850,
|
|
3067
|
+
"sha256": "15eecced57199f79c76774b2a629cae291c610d30b64b29a31ef60f4bad65ec7"
|
|
3068
3068
|
},
|
|
3069
3069
|
{
|
|
3070
3070
|
"path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py",
|
|
3071
|
-
"bytes":
|
|
3072
|
-
"sha256": "
|
|
3071
|
+
"bytes": 287204,
|
|
3072
|
+
"sha256": "d33bde08ceeafa70ddad41ba1c6f8373e0366da08a9b17484ce0ad3683f83960"
|
|
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": 269850,
|
|
3682
|
+
"sha256": "15eecced57199f79c76774b2a629cae291c610d30b64b29a31ef60f4bad65ec7"
|
|
3683
3683
|
},
|
|
3684
3684
|
{
|
|
3685
3685
|
"path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py",
|
|
3686
|
-
"bytes":
|
|
3687
|
-
"sha256": "
|
|
3686
|
+
"bytes": 287204,
|
|
3687
|
+
"sha256": "d33bde08ceeafa70ddad41ba1c6f8373e0366da08a9b17484ce0ad3683f83960"
|
|
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": 269850,
|
|
4297
|
+
"sha256": "15eecced57199f79c76774b2a629cae291c610d30b64b29a31ef60f4bad65ec7"
|
|
4298
4298
|
},
|
|
4299
4299
|
{
|
|
4300
4300
|
"path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py",
|
|
4301
|
-
"bytes":
|
|
4302
|
-
"sha256": "
|
|
4301
|
+
"bytes": 287204,
|
|
4302
|
+
"sha256": "d33bde08ceeafa70ddad41ba1c6f8373e0366da08a9b17484ce0ad3683f83960"
|
|
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": 269850,
|
|
4912
|
+
"sha256": "15eecced57199f79c76774b2a629cae291c610d30b64b29a31ef60f4bad65ec7"
|
|
4913
4913
|
},
|
|
4914
4914
|
{
|
|
4915
4915
|
"path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py",
|
|
4916
|
-
"bytes":
|
|
4917
|
-
"sha256": "
|
|
4916
|
+
"bytes": 287204,
|
|
4917
|
+
"sha256": "d33bde08ceeafa70ddad41ba1c6f8373e0366da08a9b17484ce0ad3683f83960"
|
|
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": 269850,
|
|
5527
|
+
"sha256": "15eecced57199f79c76774b2a629cae291c610d30b64b29a31ef60f4bad65ec7"
|
|
5528
5528
|
},
|
|
5529
5529
|
{
|
|
5530
5530
|
"path": "skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py",
|
|
5531
|
-
"bytes":
|
|
5532
|
-
"sha256": "
|
|
5531
|
+
"bytes": 287204,
|
|
5532
|
+
"sha256": "d33bde08ceeafa70ddad41ba1c6f8373e0366da08a9b17484ce0ad3683f83960"
|
|
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": 269850,
|
|
6142
|
+
"sha256": "15eecced57199f79c76774b2a629cae291c610d30b64b29a31ef60f4bad65ec7"
|
|
6143
6143
|
},
|
|
6144
6144
|
{
|
|
6145
6145
|
"path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py",
|
|
6146
|
-
"bytes":
|
|
6147
|
-
"sha256": "
|
|
6146
|
+
"bytes": 287204,
|
|
6147
|
+
"sha256": "d33bde08ceeafa70ddad41ba1c6f8373e0366da08a9b17484ce0ad3683f83960"
|
|
6148
6148
|
},
|
|
6149
6149
|
{
|
|
6150
6150
|
"path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_init.py",
|
|
@@ -93,8 +93,10 @@ CORE_DOCS = (
|
|
|
93
93
|
"docs/evidence/extraction-ledger.md",
|
|
94
94
|
"docs/evidence/landing-map.md",
|
|
95
95
|
"docs/evidence/validations/validation-closeout-reconciliation-blocking-gate.md",
|
|
96
|
+
"docs/evidence/validations/validation-adoption-maturity-upgrade-automation.md",
|
|
96
97
|
"docs/evidence/validations/validation-github-profile-binding-orchestration.md",
|
|
97
98
|
"docs/evidence/validations/validation-github-profile-drift-reconciliation.md",
|
|
99
|
+
"docs/evidence/validations/validation-github-profile-graphql-budget-guard.md",
|
|
98
100
|
"docs/evidence/validations/validation-loom-core-runtime-parity.md",
|
|
99
101
|
"docs/evidence/validations/validation-shadow-parity-blocking-gate.md",
|
|
100
102
|
"docs/evidence/validations/validation-syvert-strong-governance-parity.md",
|
|
@@ -273,6 +275,8 @@ def iter_markdown_files(root: Path) -> list[Path]:
|
|
|
273
275
|
relative = path.relative_to(root).as_posix()
|
|
274
276
|
if any(relative == part or relative.startswith(f"{part}/") for part in skipped_parts):
|
|
275
277
|
continue
|
|
278
|
+
if any(part.startswith(".payload-build-") for part in path.relative_to(root).parts):
|
|
279
|
+
continue
|
|
276
280
|
if relative.startswith("packages/loom-installer/payload/"):
|
|
277
281
|
continue
|
|
278
282
|
results.append(path)
|
|
@@ -1326,6 +1330,42 @@ def require_github_binding_payload(
|
|
|
1326
1330
|
failures.append(Failure(category, f"{context}.binding findings must fallback to `github-profile-binding`"))
|
|
1327
1331
|
|
|
1328
1332
|
|
|
1333
|
+
def require_governance_upgrade_payload(
|
|
1334
|
+
failures: list[Failure],
|
|
1335
|
+
*,
|
|
1336
|
+
category: str,
|
|
1337
|
+
context: str,
|
|
1338
|
+
payload: object,
|
|
1339
|
+
) -> None:
|
|
1340
|
+
if not isinstance(payload, dict):
|
|
1341
|
+
failures.append(Failure(category, f"{context} must be an object"))
|
|
1342
|
+
return
|
|
1343
|
+
if payload.get("command") != "governance-profile":
|
|
1344
|
+
failures.append(Failure(category, f"{context} must report `command: governance-profile`"))
|
|
1345
|
+
if payload.get("operation") != "upgrade":
|
|
1346
|
+
failures.append(Failure(category, f"{context} must report `operation: upgrade`"))
|
|
1347
|
+
if payload.get("schema_version") != "loom-governance-upgrade/v1":
|
|
1348
|
+
failures.append(Failure(category, f"{context} schema_version must be `loom-governance-upgrade/v1`"))
|
|
1349
|
+
if payload.get("result") not in {"pass", "block"}:
|
|
1350
|
+
failures.append(Failure(category, f"{context} result must be pass/block"))
|
|
1351
|
+
if payload.get("target_maturity") not in {"standard", "strong"}:
|
|
1352
|
+
failures.append(Failure(category, f"{context} target_maturity must be standard/strong"))
|
|
1353
|
+
if not isinstance(payload.get("dry_run"), bool):
|
|
1354
|
+
failures.append(Failure(category, f"{context} dry_run must be boolean"))
|
|
1355
|
+
actions = payload.get("actions")
|
|
1356
|
+
if not isinstance(actions, list) or not actions:
|
|
1357
|
+
failures.append(Failure(category, f"{context} must include non-empty actions"))
|
|
1358
|
+
return
|
|
1359
|
+
for action in actions:
|
|
1360
|
+
if not isinstance(action, dict):
|
|
1361
|
+
failures.append(Failure(category, f"{context} actions must be objects"))
|
|
1362
|
+
continue
|
|
1363
|
+
if action.get("owner") not in {"loom-owned", "repo-owned", "profile"}:
|
|
1364
|
+
failures.append(Failure(category, f"{context} action owner must stay within the stable set"))
|
|
1365
|
+
if action.get("status") not in {"planned", "present"}:
|
|
1366
|
+
failures.append(Failure(category, f"{context} action status must be planned/present"))
|
|
1367
|
+
|
|
1368
|
+
|
|
1329
1369
|
def require_review_record_contract(
|
|
1330
1370
|
failures: list[Failure],
|
|
1331
1371
|
*,
|
|
@@ -2312,6 +2352,21 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
|
|
|
2312
2352
|
["python3", "tools/loom_flow.py", "governance-profile", "upgrade-plan", "--target", "examples/new-project"],
|
|
2313
2353
|
{"pass", "block"},
|
|
2314
2354
|
),
|
|
2355
|
+
(
|
|
2356
|
+
"governance-profile-upgrade",
|
|
2357
|
+
[
|
|
2358
|
+
"python3",
|
|
2359
|
+
"tools/loom_flow.py",
|
|
2360
|
+
"governance-profile",
|
|
2361
|
+
"upgrade",
|
|
2362
|
+
"--target",
|
|
2363
|
+
"examples/new-project",
|
|
2364
|
+
"--to",
|
|
2365
|
+
"standard",
|
|
2366
|
+
"--dry-run",
|
|
2367
|
+
],
|
|
2368
|
+
{"pass"},
|
|
2369
|
+
),
|
|
2315
2370
|
(
|
|
2316
2371
|
"governance-profile-binding",
|
|
2317
2372
|
["python3", "tools/loom_flow.py", "governance-profile", "binding", "--target", "."],
|
|
@@ -2571,6 +2626,13 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
|
|
|
2571
2626
|
context="`governance-profile binding`",
|
|
2572
2627
|
payload=payload,
|
|
2573
2628
|
)
|
|
2629
|
+
if label == "governance-profile-upgrade":
|
|
2630
|
+
require_governance_upgrade_payload(
|
|
2631
|
+
failures,
|
|
2632
|
+
category="daily-execution-cli",
|
|
2633
|
+
context="`governance-profile upgrade`",
|
|
2634
|
+
payload=payload,
|
|
2635
|
+
)
|
|
2574
2636
|
if label == "flow-pre-review":
|
|
2575
2637
|
require_runtime_state_payload(
|
|
2576
2638
|
failures,
|
|
@@ -5559,6 +5621,31 @@ def check_generated_artifacts_untracked(root: Path) -> list[Failure]:
|
|
|
5559
5621
|
]
|
|
5560
5622
|
|
|
5561
5623
|
|
|
5624
|
+
def check_github_cli_budget(root: Path) -> list[Failure]:
|
|
5625
|
+
failures: list[Failure] = []
|
|
5626
|
+
forbidden = tuple(f"gh {kind} view" for kind in ("repo", "issue", "pr"))
|
|
5627
|
+
search_roots = [root / "skills/shared/scripts", root / "tools"]
|
|
5628
|
+
for search_root in search_roots:
|
|
5629
|
+
if not search_root.exists():
|
|
5630
|
+
continue
|
|
5631
|
+
for path in search_root.rglob("*.py"):
|
|
5632
|
+
if path.name == "loom_check.py":
|
|
5633
|
+
continue
|
|
5634
|
+
try:
|
|
5635
|
+
text = path.read_text(encoding="utf-8")
|
|
5636
|
+
except OSError:
|
|
5637
|
+
continue
|
|
5638
|
+
for needle in forbidden:
|
|
5639
|
+
if needle in text:
|
|
5640
|
+
failures.append(
|
|
5641
|
+
Failure(
|
|
5642
|
+
"github-api-budget",
|
|
5643
|
+
f"`{needle}` must not be used in high-frequency implementation path `{path.relative_to(root)}`",
|
|
5644
|
+
)
|
|
5645
|
+
)
|
|
5646
|
+
return failures
|
|
5647
|
+
|
|
5648
|
+
|
|
5562
5649
|
def is_within(path: Path, root: Path) -> bool:
|
|
5563
5650
|
try:
|
|
5564
5651
|
path.relative_to(root)
|
|
@@ -5596,6 +5683,7 @@ def collect_failures(root: Path) -> list[Failure]:
|
|
|
5596
5683
|
failures.extend(check_repo_interop_contracts(root))
|
|
5597
5684
|
failures.extend(check_node_installer(root))
|
|
5598
5685
|
failures.extend(check_generated_artifacts_untracked(root))
|
|
5686
|
+
failures.extend(check_github_cli_budget(root))
|
|
5599
5687
|
failures.extend(check_markdown_links(root))
|
|
5600
5688
|
return failures
|
|
5601
5689
|
|
|
@@ -312,8 +312,12 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
|
312
312
|
"governance-profile",
|
|
313
313
|
help="Read Loom governance maturity and upgrade requirements",
|
|
314
314
|
)
|
|
315
|
-
governance_profile.add_argument("operation", choices=("status", "upgrade-plan", "binding"))
|
|
315
|
+
governance_profile.add_argument("operation", choices=("status", "upgrade-plan", "upgrade", "binding"))
|
|
316
316
|
governance_profile.add_argument("--target", required=True, help="Target repository root")
|
|
317
|
+
governance_profile.add_argument("--to", choices=("standard", "strong"), help="Target maturity for governance-profile upgrade")
|
|
318
|
+
governance_profile.add_argument("--dry-run", action="store_true", default=True, help="Preview upgrade actions without writing files; this is the default")
|
|
319
|
+
governance_profile.add_argument("--apply", dest="dry_run", action="store_false", help="Apply Loom-owned scaffold writes")
|
|
320
|
+
governance_profile.add_argument("--force", action="store_true", help="Allow replacement of existing Loom-owned scaffold files during upgrade apply")
|
|
317
321
|
governance_profile.add_argument("--owner", help="GitHub owner; auto-detected from origin when omitted")
|
|
318
322
|
governance_profile.add_argument("--repo", dest="repo_name", help="GitHub repository name; auto-detected from origin when omitted")
|
|
319
323
|
governance_profile.add_argument("--phase", type=int, help="GitHub Phase issue number")
|
|
@@ -322,7 +326,6 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
|
322
326
|
governance_profile.add_argument("--pr", type=int, help="GitHub implementation PR number")
|
|
323
327
|
governance_profile.add_argument("--branch", help="GitHub branch name bound to the work item")
|
|
324
328
|
governance_profile.add_argument("--sync", action="store_true", help="Preview host binding repairs; writes are intentionally disabled in this phase")
|
|
325
|
-
governance_profile.add_argument("--dry-run", action="store_true", help="Preview binding sync actions without changing GitHub state")
|
|
326
329
|
|
|
327
330
|
flow = subparsers.add_parser("flow", help="Run a bundled high-frequency Loom flow")
|
|
328
331
|
flow.add_argument("operation", choices=("pre-review", "review", "spec-review", "resume", "handoff", "merge-ready"))
|
|
@@ -926,6 +929,21 @@ def gh_graphql(root: Path, query: str, variables: dict[str, Any]) -> tuple[dict[
|
|
|
926
929
|
return data, []
|
|
927
930
|
|
|
928
931
|
|
|
932
|
+
def graphql_budget_guard(scope: str, errors: list[str] | None = None) -> dict[str, Any]:
|
|
933
|
+
return {
|
|
934
|
+
"graphql_only": True,
|
|
935
|
+
"budget_scope": scope,
|
|
936
|
+
"status": "unavailable" if errors else "guarded",
|
|
937
|
+
"errors": list(errors or []),
|
|
938
|
+
"fallback_to": "manual-reconciliation" if errors else None,
|
|
939
|
+
"recommended_action": (
|
|
940
|
+
"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."
|
|
941
|
+
if errors
|
|
942
|
+
else "Use this GraphQL-only host read sparingly; high-frequency repo, issue, and PR reads must stay on REST."
|
|
943
|
+
),
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
|
|
929
947
|
def git_dirty_entries(root: Path) -> list[dict[str, str]]:
|
|
930
948
|
result = run_git(root, ["status", "--porcelain=v1"])
|
|
931
949
|
if result is None or result.returncode != 0:
|
|
@@ -3558,6 +3576,141 @@ def governance_profile_payload(target_root: Path, operation: str) -> dict[str, A
|
|
|
3558
3576
|
}
|
|
3559
3577
|
|
|
3560
3578
|
|
|
3579
|
+
UPGRADE_SCAFFOLD: dict[str, dict[str, str]] = {
|
|
3580
|
+
".loom/companion/manifest.json": json.dumps(
|
|
3581
|
+
{
|
|
3582
|
+
"schema_version": "loom-repo-companion-manifest/v1",
|
|
3583
|
+
"companion_entry": ".loom/companion/AGENTS.md",
|
|
3584
|
+
"repo_interface": ".loom/companion/repo-interface.json",
|
|
3585
|
+
},
|
|
3586
|
+
ensure_ascii=False,
|
|
3587
|
+
indent=2,
|
|
3588
|
+
)
|
|
3589
|
+
+ "\n",
|
|
3590
|
+
".loom/companion/repo-interface.json": json.dumps(
|
|
3591
|
+
{
|
|
3592
|
+
"schema_version": "loom-repo-interface/v2",
|
|
3593
|
+
"companion_entry": ".loom/companion/AGENTS.md",
|
|
3594
|
+
"repo_specific_requirements": {"review": [], "merge_ready": [], "closeout": []},
|
|
3595
|
+
"specialized_gates": [],
|
|
3596
|
+
"metadata_contract": {"fields": []},
|
|
3597
|
+
"context_schema": {"fields": []},
|
|
3598
|
+
},
|
|
3599
|
+
ensure_ascii=False,
|
|
3600
|
+
indent=2,
|
|
3601
|
+
)
|
|
3602
|
+
+ "\n",
|
|
3603
|
+
".loom/companion/interop.json": json.dumps(
|
|
3604
|
+
{
|
|
3605
|
+
"schema_version": "loom-repo-interop/v1",
|
|
3606
|
+
"host_adapters": [],
|
|
3607
|
+
"repo_native_carriers": [],
|
|
3608
|
+
"shadow_surfaces": {},
|
|
3609
|
+
},
|
|
3610
|
+
ensure_ascii=False,
|
|
3611
|
+
indent=2,
|
|
3612
|
+
)
|
|
3613
|
+
+ "\n",
|
|
3614
|
+
".loom/companion/AGENTS.md": "# Loom Repo Companion\n\n本文件承接 repo-local governance residue;Loom core 与 GitHub profile 规则仍以上游合同为准。\n",
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
|
|
3618
|
+
def governance_upgrade_actions(target_root: Path, target_level: str, maturity: dict[str, Any]) -> list[dict[str, Any]]:
|
|
3619
|
+
missing_by_level = maturity.get("missing_by_level")
|
|
3620
|
+
missing = missing_by_level.get(target_level, []) if isinstance(missing_by_level, dict) else []
|
|
3621
|
+
actions: list[dict[str, Any]] = []
|
|
3622
|
+
for relative, content in UPGRADE_SCAFFOLD.items():
|
|
3623
|
+
path = target_root / relative
|
|
3624
|
+
owner = "loom-owned" if relative.startswith(".loom/") else "repo-owned"
|
|
3625
|
+
actions.append(
|
|
3626
|
+
{
|
|
3627
|
+
"action": "write_scaffold" if not path.exists() else "keep_existing",
|
|
3628
|
+
"path": relative,
|
|
3629
|
+
"owner": owner,
|
|
3630
|
+
"status": "present" if path.exists() else "planned",
|
|
3631
|
+
"reason": "required by governance profile upgrade path",
|
|
3632
|
+
"bytes": len(content.encode("utf-8")),
|
|
3633
|
+
}
|
|
3634
|
+
)
|
|
3635
|
+
for item in missing if isinstance(missing, list) else []:
|
|
3636
|
+
actions.append(
|
|
3637
|
+
{
|
|
3638
|
+
"action": "satisfy_missing_input",
|
|
3639
|
+
"id": item,
|
|
3640
|
+
"owner": "loom-owned" if str(item) in {"repo_interface", "repo_interop"} else "profile",
|
|
3641
|
+
"status": "planned",
|
|
3642
|
+
"reason": f"`{target_level}` maturity currently reports this missing input.",
|
|
3643
|
+
}
|
|
3644
|
+
)
|
|
3645
|
+
return actions
|
|
3646
|
+
|
|
3647
|
+
|
|
3648
|
+
def governance_profile_upgrade_payload(
|
|
3649
|
+
*,
|
|
3650
|
+
target_root: Path,
|
|
3651
|
+
target_level: str | None,
|
|
3652
|
+
dry_run: bool,
|
|
3653
|
+
force: bool,
|
|
3654
|
+
) -> dict[str, Any]:
|
|
3655
|
+
if target_level is None:
|
|
3656
|
+
return {
|
|
3657
|
+
"command": "governance-profile",
|
|
3658
|
+
"operation": "upgrade",
|
|
3659
|
+
"result": "block",
|
|
3660
|
+
"summary": "governance profile upgrade requires `--to standard` or `--to strong`.",
|
|
3661
|
+
"missing_inputs": ["to"],
|
|
3662
|
+
"fallback_to": "adoption",
|
|
3663
|
+
}
|
|
3664
|
+
base = governance_profile_payload(target_root, "upgrade-plan")
|
|
3665
|
+
maturity = base.get("maturity") if isinstance(base.get("maturity"), dict) else {}
|
|
3666
|
+
actions = governance_upgrade_actions(target_root, target_level, maturity if isinstance(maturity, dict) else {})
|
|
3667
|
+
blockers: list[str] = []
|
|
3668
|
+
written_files: list[str] = []
|
|
3669
|
+
if not dry_run:
|
|
3670
|
+
for action in actions:
|
|
3671
|
+
if action.get("action") != "write_scaffold":
|
|
3672
|
+
continue
|
|
3673
|
+
relative = action.get("path")
|
|
3674
|
+
if not isinstance(relative, str):
|
|
3675
|
+
continue
|
|
3676
|
+
if action.get("owner") != "loom-owned":
|
|
3677
|
+
blockers.append(f"{relative} is repo-owned")
|
|
3678
|
+
continue
|
|
3679
|
+
path = target_root / relative
|
|
3680
|
+
if path.exists() and not force:
|
|
3681
|
+
blockers.append(f"{relative} already exists; use --force to replace Loom-owned scaffold")
|
|
3682
|
+
continue
|
|
3683
|
+
content = UPGRADE_SCAFFOLD.get(relative)
|
|
3684
|
+
if content is None:
|
|
3685
|
+
blockers.append(f"{relative} has no scaffold content")
|
|
3686
|
+
continue
|
|
3687
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
3688
|
+
path.write_text(content, encoding="utf-8")
|
|
3689
|
+
written_files.append(relative)
|
|
3690
|
+
result = "block" if blockers else "pass"
|
|
3691
|
+
return {
|
|
3692
|
+
"command": "governance-profile",
|
|
3693
|
+
"operation": "upgrade",
|
|
3694
|
+
"schema_version": "loom-governance-upgrade/v1",
|
|
3695
|
+
"result": result,
|
|
3696
|
+
"summary": (
|
|
3697
|
+
f"governance profile upgrade toward `{target_level}` produced a dry-run action plan."
|
|
3698
|
+
if dry_run and result == "pass"
|
|
3699
|
+
else f"governance profile upgrade toward `{target_level}` applied Loom-owned scaffold writes."
|
|
3700
|
+
if result == "pass"
|
|
3701
|
+
else f"governance profile upgrade toward `{target_level}` is blocked by unsafe writes."
|
|
3702
|
+
),
|
|
3703
|
+
"missing_inputs": blockers,
|
|
3704
|
+
"fallback_to": None if result == "pass" else "adoption",
|
|
3705
|
+
"target_maturity": target_level,
|
|
3706
|
+
"dry_run": dry_run,
|
|
3707
|
+
"force": force,
|
|
3708
|
+
"actions": actions,
|
|
3709
|
+
"written_files": written_files,
|
|
3710
|
+
"maturity": maturity,
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
|
|
3561
3714
|
def issue_binding_entry(role: str, number: int | None, payload: dict[str, Any] | None, errors: list[str]) -> dict[str, Any]:
|
|
3562
3715
|
status = "present" if payload is not None else "missing"
|
|
3563
3716
|
if errors:
|
|
@@ -3785,6 +3938,15 @@ def github_binding_payload(
|
|
|
3785
3938
|
|
|
3786
3939
|
def handle_governance_profile(args: argparse.Namespace) -> int:
|
|
3787
3940
|
target_root = Path(args.target).expanduser().resolve()
|
|
3941
|
+
if args.operation == "upgrade":
|
|
3942
|
+
return emit(
|
|
3943
|
+
governance_profile_upgrade_payload(
|
|
3944
|
+
target_root=target_root,
|
|
3945
|
+
target_level=args.to,
|
|
3946
|
+
dry_run=args.dry_run,
|
|
3947
|
+
force=args.force,
|
|
3948
|
+
)
|
|
3949
|
+
)
|
|
3788
3950
|
if args.operation == "binding":
|
|
3789
3951
|
return emit(
|
|
3790
3952
|
github_binding_payload(
|
|
@@ -3946,6 +4108,7 @@ query($id: ID!) {
|
|
|
3946
4108
|
"id": entry.get("id"),
|
|
3947
4109
|
"content": {"number": None, "type": "Issue"},
|
|
3948
4110
|
"status": status_name,
|
|
4111
|
+
"budget_guard": graphql_budget_guard("project_v2_item_field_values"),
|
|
3949
4112
|
}, []
|
|
3950
4113
|
return None, []
|
|
3951
4114
|
|
|
@@ -4022,6 +4185,7 @@ query($owner:String!, $name:String!, $number:Int!) {
|
|
|
4022
4185
|
issue = repository.get("issue")
|
|
4023
4186
|
if not isinstance(issue, dict):
|
|
4024
4187
|
return None, [f"issue #{issue_number} is missing from GraphQL payload"]
|
|
4188
|
+
issue["budget_guard"] = graphql_budget_guard("native_parent_sub_issue_tree")
|
|
4025
4189
|
return issue, []
|
|
4026
4190
|
|
|
4027
4191
|
|
|
@@ -4131,6 +4295,7 @@ def reconciliation_audit_payload(
|
|
|
4131
4295
|
"status": "unavailable",
|
|
4132
4296
|
"reason": "GraphQL-only parent/sub-issue tree could not be read.",
|
|
4133
4297
|
"errors": issue_tree_errors,
|
|
4298
|
+
"budget_guard": graphql_budget_guard("native_parent_sub_issue_tree", issue_tree_errors),
|
|
4134
4299
|
}
|
|
4135
4300
|
elif issue_tree is not None:
|
|
4136
4301
|
issue_payload = {**issue_payload, **issue_tree}
|
|
@@ -4261,16 +4426,21 @@ def reconciliation_audit_payload(
|
|
|
4261
4426
|
"status": "unavailable",
|
|
4262
4427
|
"reason": "GitHub ProjectV2 CLI owner resolution is unavailable in this environment.",
|
|
4263
4428
|
"errors": project_errors,
|
|
4429
|
+
"budget_guard": graphql_budget_guard("project_v2_status_surface", project_errors),
|
|
4264
4430
|
}
|
|
4265
4431
|
else:
|
|
4266
4432
|
missing_inputs.extend(f"project: {message}" for message in project_errors)
|
|
4267
4433
|
else:
|
|
4268
4434
|
items = project_context["items"]
|
|
4269
4435
|
issue_item = find_project_item(items, issue_number, "issue") if issue_number is not None else None
|
|
4436
|
+
issue_item_budget_guard: dict[str, Any] | None = None
|
|
4270
4437
|
if issue_item is None and issue_id is not None and issue_number is not None:
|
|
4271
4438
|
issue_item, issue_item_errors = project_item_for_issue(target_root, issue_id, project_number)
|
|
4272
4439
|
if issue_item_errors:
|
|
4273
|
-
|
|
4440
|
+
issue_item_budget_guard = graphql_budget_guard(
|
|
4441
|
+
"project_v2_issue_item_lookup",
|
|
4442
|
+
issue_item_errors,
|
|
4443
|
+
)
|
|
4274
4444
|
pr_item = find_project_item(items, pr_number, "pr") if pr_number is not None else None
|
|
4275
4445
|
project_payload = {
|
|
4276
4446
|
"number": project_number,
|
|
@@ -4280,6 +4450,8 @@ def reconciliation_audit_payload(
|
|
|
4280
4450
|
"issue_item": issue_item,
|
|
4281
4451
|
"pr_item": pr_item,
|
|
4282
4452
|
}
|
|
4453
|
+
if issue_item_budget_guard is not None:
|
|
4454
|
+
project_payload["issue_item_budget_guard"] = issue_item_budget_guard
|
|
4283
4455
|
|
|
4284
4456
|
if issue_number is not None:
|
|
4285
4457
|
expected_done = issue_payload is not None and (issue_payload.get("state") == "CLOSED" or merged_issue_open)
|
|
@@ -4872,16 +5044,21 @@ def closeout_payload(
|
|
|
4872
5044
|
"status": "unavailable",
|
|
4873
5045
|
"reason": "GitHub ProjectV2 CLI owner resolution is unavailable in this environment.",
|
|
4874
5046
|
"errors": project_errors,
|
|
5047
|
+
"budget_guard": graphql_budget_guard("project_v2_status_surface", project_errors),
|
|
4875
5048
|
}
|
|
4876
5049
|
else:
|
|
4877
5050
|
missing_inputs.extend(f"project: {message}" for message in project_errors)
|
|
4878
5051
|
else:
|
|
4879
5052
|
items = project_context["items"]
|
|
4880
5053
|
issue_item = find_project_item(items, issue_number, "issue") if issue_number is not None else None
|
|
5054
|
+
issue_item_budget_guard: dict[str, Any] | None = None
|
|
4881
5055
|
if issue_item is None and issue_id is not None and issue_number is not None:
|
|
4882
5056
|
issue_item, issue_item_errors = project_item_for_issue(target_root, issue_id, project_number)
|
|
4883
5057
|
if issue_item_errors:
|
|
4884
|
-
|
|
5058
|
+
issue_item_budget_guard = graphql_budget_guard(
|
|
5059
|
+
"project_v2_issue_item_lookup",
|
|
5060
|
+
issue_item_errors,
|
|
5061
|
+
)
|
|
4885
5062
|
pr_item = find_project_item(items, pr_number, "pr") if pr_number is not None else None
|
|
4886
5063
|
if issue_number is not None and issue_item is None:
|
|
4887
5064
|
missing_inputs.append("issue is missing from project")
|
|
@@ -4893,6 +5070,8 @@ def closeout_payload(
|
|
|
4893
5070
|
"issue_item": issue_item,
|
|
4894
5071
|
"pr_item": pr_item,
|
|
4895
5072
|
}
|
|
5073
|
+
if issue_item_budget_guard is not None:
|
|
5074
|
+
project_payload["issue_item_budget_guard"] = issue_item_budget_guard
|
|
4896
5075
|
for label, item in (("issue", issue_item), ("pr", pr_item)):
|
|
4897
5076
|
if item is None:
|
|
4898
5077
|
continue
|