@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.
Files changed (22) hide show
  1. package/package.json +1 -1
  2. package/payload/manifest.json +42 -42
  3. package/payload/plugin/loom/skills/shared/scripts/loom_check.py +88 -0
  4. package/payload/plugin/loom/skills/shared/scripts/loom_flow.py +183 -4
  5. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +88 -0
  6. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py +183 -4
  7. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +88 -0
  8. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py +183 -4
  9. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +88 -0
  10. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py +183 -4
  11. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +88 -0
  12. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py +183 -4
  13. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +88 -0
  14. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py +183 -4
  15. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +88 -0
  16. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py +183 -4
  17. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +88 -0
  18. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py +183 -4
  19. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +88 -0
  20. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py +183 -4
  21. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py +88 -0
  22. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py +183 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mc-and-his-agents/loom-installer",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Node installer for Loom plugin and single-skill installation surfaces.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,9 +2,9 @@
2
2
  "schema_version": "loom-installer-payload/v1",
3
3
  "loom_version": "0.4.0",
4
4
  "source_repository": "https://github.com/MC-and-his-Agents/Loom",
5
- "source_commit": "3e2a33cf1cb7e519d321cb2617b874e285815c5b",
5
+ "source_commit": "c3f5933d41b009b1e0537356221c3bcd7fd910be",
6
6
  "source_ref": "main",
7
- "built_at": "2026-04-25T13:01:36+08:00",
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": 265994,
632
- "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
631
+ "bytes": 269850,
632
+ "sha256": "15eecced57199f79c76774b2a629cae291c610d30b64b29a31ef60f4bad65ec7"
633
633
  },
634
634
  {
635
635
  "path": "plugin/loom/skills/shared/scripts/loom_flow.py",
636
- "bytes": 279330,
637
- "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265994,
1217
- "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
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": 279330,
1222
- "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265994,
1832
- "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
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": 279330,
1837
- "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265994,
2447
- "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
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": 279330,
2452
- "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265994,
3067
- "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
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": 279330,
3072
- "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265994,
3682
- "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
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": 279330,
3687
- "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265994,
4297
- "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
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": 279330,
4302
- "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265994,
4912
- "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
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": 279330,
4917
- "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265994,
5527
- "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
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": 279330,
5532
- "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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": 265994,
6142
- "sha256": "77334371c586c5e7247e6b781758cbf656594d4d5b8cff33f16b967b43f7811c"
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": 279330,
6147
- "sha256": "ad6d6376a665cdfcd9de2316957e8f36e1ec087de59f29cebe6a6496ac0a555e"
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
- missing_inputs.extend(f"project: {message}" for message in issue_item_errors)
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
- missing_inputs.extend(f"project: {message}" for message in issue_item_errors)
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