@mc-and-his-agents/loom-installer 0.1.30 → 0.1.31

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 +34 -9
  4. package/payload/plugin/loom/skills/shared/scripts/loom_flow.py +205 -0
  5. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +34 -9
  6. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py +205 -0
  7. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +34 -9
  8. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py +205 -0
  9. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +34 -9
  10. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py +205 -0
  11. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +34 -9
  12. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py +205 -0
  13. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +34 -9
  14. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py +205 -0
  15. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +34 -9
  16. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py +205 -0
  17. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +34 -9
  18. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py +205 -0
  19. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +34 -9
  20. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py +205 -0
  21. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py +34 -9
  22. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py +205 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mc-and-his-agents/loom-installer",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
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": "5c3a0a7339be746f305853154aef71de6eb2dc79",
5
+ "source_commit": "db9b37726bda7d232589b79bad5c20596dd0571f",
6
6
  "source_ref": "main",
7
- "built_at": "2026-04-27T13:52:07+08:00",
7
+ "built_at": "2026-04-27T14:09:35+08:00",
8
8
  "runtime": {
9
9
  "python_minimum": "3.10",
10
10
  "python_recommended": "3.11+"
@@ -633,13 +633,13 @@
633
633
  },
634
634
  {
635
635
  "path": "plugin/loom/skills/shared/scripts/loom_check.py",
636
- "bytes": 321273,
637
- "sha256": "7cb6094384b6d6c6ad1842e9197e9d1211230adbae068f524ea1c8360bc87712"
636
+ "bytes": 322929,
637
+ "sha256": "9a8b31917b244172681a542256994d97de9a7b54869e7e9e7bf5747e8759ae69"
638
638
  },
639
639
  {
640
640
  "path": "plugin/loom/skills/shared/scripts/loom_flow.py",
641
- "bytes": 302884,
642
- "sha256": "e5ff7344c5d62306ee30254113538739137a175350847ed51e9fff8270c77505"
641
+ "bytes": 311901,
642
+ "sha256": "9376481f98f539c0b6aaf72df42e647f320a0e6050e14cd2e1cf0c37240b79cd"
643
643
  },
644
644
  {
645
645
  "path": "plugin/loom/skills/shared/scripts/loom_init.py",
@@ -1223,13 +1223,13 @@
1223
1223
  },
1224
1224
  {
1225
1225
  "path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py",
1226
- "bytes": 321273,
1227
- "sha256": "7cb6094384b6d6c6ad1842e9197e9d1211230adbae068f524ea1c8360bc87712"
1226
+ "bytes": 322929,
1227
+ "sha256": "9a8b31917b244172681a542256994d97de9a7b54869e7e9e7bf5747e8759ae69"
1228
1228
  },
1229
1229
  {
1230
1230
  "path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py",
1231
- "bytes": 302884,
1232
- "sha256": "e5ff7344c5d62306ee30254113538739137a175350847ed51e9fff8270c77505"
1231
+ "bytes": 311901,
1232
+ "sha256": "9376481f98f539c0b6aaf72df42e647f320a0e6050e14cd2e1cf0c37240b79cd"
1233
1233
  },
1234
1234
  {
1235
1235
  "path": "skills/loom-adopt/.loom-runtime/shared/scripts/loom_init.py",
@@ -1843,13 +1843,13 @@
1843
1843
  },
1844
1844
  {
1845
1845
  "path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py",
1846
- "bytes": 321273,
1847
- "sha256": "7cb6094384b6d6c6ad1842e9197e9d1211230adbae068f524ea1c8360bc87712"
1846
+ "bytes": 322929,
1847
+ "sha256": "9a8b31917b244172681a542256994d97de9a7b54869e7e9e7bf5747e8759ae69"
1848
1848
  },
1849
1849
  {
1850
1850
  "path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py",
1851
- "bytes": 302884,
1852
- "sha256": "e5ff7344c5d62306ee30254113538739137a175350847ed51e9fff8270c77505"
1851
+ "bytes": 311901,
1852
+ "sha256": "9376481f98f539c0b6aaf72df42e647f320a0e6050e14cd2e1cf0c37240b79cd"
1853
1853
  },
1854
1854
  {
1855
1855
  "path": "skills/loom-handoff/.loom-runtime/shared/scripts/loom_init.py",
@@ -2463,13 +2463,13 @@
2463
2463
  },
2464
2464
  {
2465
2465
  "path": "skills/loom-init/.loom-runtime/shared/scripts/loom_check.py",
2466
- "bytes": 321273,
2467
- "sha256": "7cb6094384b6d6c6ad1842e9197e9d1211230adbae068f524ea1c8360bc87712"
2466
+ "bytes": 322929,
2467
+ "sha256": "9a8b31917b244172681a542256994d97de9a7b54869e7e9e7bf5747e8759ae69"
2468
2468
  },
2469
2469
  {
2470
2470
  "path": "skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py",
2471
- "bytes": 302884,
2472
- "sha256": "e5ff7344c5d62306ee30254113538739137a175350847ed51e9fff8270c77505"
2471
+ "bytes": 311901,
2472
+ "sha256": "9376481f98f539c0b6aaf72df42e647f320a0e6050e14cd2e1cf0c37240b79cd"
2473
2473
  },
2474
2474
  {
2475
2475
  "path": "skills/loom-init/.loom-runtime/shared/scripts/loom_init.py",
@@ -3088,13 +3088,13 @@
3088
3088
  },
3089
3089
  {
3090
3090
  "path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py",
3091
- "bytes": 321273,
3092
- "sha256": "7cb6094384b6d6c6ad1842e9197e9d1211230adbae068f524ea1c8360bc87712"
3091
+ "bytes": 322929,
3092
+ "sha256": "9a8b31917b244172681a542256994d97de9a7b54869e7e9e7bf5747e8759ae69"
3093
3093
  },
3094
3094
  {
3095
3095
  "path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py",
3096
- "bytes": 302884,
3097
- "sha256": "e5ff7344c5d62306ee30254113538739137a175350847ed51e9fff8270c77505"
3096
+ "bytes": 311901,
3097
+ "sha256": "9376481f98f539c0b6aaf72df42e647f320a0e6050e14cd2e1cf0c37240b79cd"
3098
3098
  },
3099
3099
  {
3100
3100
  "path": "skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_init.py",
@@ -3708,13 +3708,13 @@
3708
3708
  },
3709
3709
  {
3710
3710
  "path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py",
3711
- "bytes": 321273,
3712
- "sha256": "7cb6094384b6d6c6ad1842e9197e9d1211230adbae068f524ea1c8360bc87712"
3711
+ "bytes": 322929,
3712
+ "sha256": "9a8b31917b244172681a542256994d97de9a7b54869e7e9e7bf5747e8759ae69"
3713
3713
  },
3714
3714
  {
3715
3715
  "path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py",
3716
- "bytes": 302884,
3717
- "sha256": "e5ff7344c5d62306ee30254113538739137a175350847ed51e9fff8270c77505"
3716
+ "bytes": 311901,
3717
+ "sha256": "9376481f98f539c0b6aaf72df42e647f320a0e6050e14cd2e1cf0c37240b79cd"
3718
3718
  },
3719
3719
  {
3720
3720
  "path": "skills/loom-pre-review/.loom-runtime/shared/scripts/loom_init.py",
@@ -4328,13 +4328,13 @@
4328
4328
  },
4329
4329
  {
4330
4330
  "path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py",
4331
- "bytes": 321273,
4332
- "sha256": "7cb6094384b6d6c6ad1842e9197e9d1211230adbae068f524ea1c8360bc87712"
4331
+ "bytes": 322929,
4332
+ "sha256": "9a8b31917b244172681a542256994d97de9a7b54869e7e9e7bf5747e8759ae69"
4333
4333
  },
4334
4334
  {
4335
4335
  "path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py",
4336
- "bytes": 302884,
4337
- "sha256": "e5ff7344c5d62306ee30254113538739137a175350847ed51e9fff8270c77505"
4336
+ "bytes": 311901,
4337
+ "sha256": "9376481f98f539c0b6aaf72df42e647f320a0e6050e14cd2e1cf0c37240b79cd"
4338
4338
  },
4339
4339
  {
4340
4340
  "path": "skills/loom-resume/.loom-runtime/shared/scripts/loom_init.py",
@@ -4948,13 +4948,13 @@
4948
4948
  },
4949
4949
  {
4950
4950
  "path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py",
4951
- "bytes": 321273,
4952
- "sha256": "7cb6094384b6d6c6ad1842e9197e9d1211230adbae068f524ea1c8360bc87712"
4951
+ "bytes": 322929,
4952
+ "sha256": "9a8b31917b244172681a542256994d97de9a7b54869e7e9e7bf5747e8759ae69"
4953
4953
  },
4954
4954
  {
4955
4955
  "path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py",
4956
- "bytes": 302884,
4957
- "sha256": "e5ff7344c5d62306ee30254113538739137a175350847ed51e9fff8270c77505"
4956
+ "bytes": 311901,
4957
+ "sha256": "9376481f98f539c0b6aaf72df42e647f320a0e6050e14cd2e1cf0c37240b79cd"
4958
4958
  },
4959
4959
  {
4960
4960
  "path": "skills/loom-retire/.loom-runtime/shared/scripts/loom_init.py",
@@ -5568,13 +5568,13 @@
5568
5568
  },
5569
5569
  {
5570
5570
  "path": "skills/loom-review/.loom-runtime/shared/scripts/loom_check.py",
5571
- "bytes": 321273,
5572
- "sha256": "7cb6094384b6d6c6ad1842e9197e9d1211230adbae068f524ea1c8360bc87712"
5571
+ "bytes": 322929,
5572
+ "sha256": "9a8b31917b244172681a542256994d97de9a7b54869e7e9e7bf5747e8759ae69"
5573
5573
  },
5574
5574
  {
5575
5575
  "path": "skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py",
5576
- "bytes": 302884,
5577
- "sha256": "e5ff7344c5d62306ee30254113538739137a175350847ed51e9fff8270c77505"
5576
+ "bytes": 311901,
5577
+ "sha256": "9376481f98f539c0b6aaf72df42e647f320a0e6050e14cd2e1cf0c37240b79cd"
5578
5578
  },
5579
5579
  {
5580
5580
  "path": "skills/loom-review/.loom-runtime/shared/scripts/loom_init.py",
@@ -6188,13 +6188,13 @@
6188
6188
  },
6189
6189
  {
6190
6190
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py",
6191
- "bytes": 321273,
6192
- "sha256": "7cb6094384b6d6c6ad1842e9197e9d1211230adbae068f524ea1c8360bc87712"
6191
+ "bytes": 322929,
6192
+ "sha256": "9a8b31917b244172681a542256994d97de9a7b54869e7e9e7bf5747e8759ae69"
6193
6193
  },
6194
6194
  {
6195
6195
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py",
6196
- "bytes": 302884,
6197
- "sha256": "e5ff7344c5d62306ee30254113538739137a175350847ed51e9fff8270c77505"
6196
+ "bytes": 311901,
6197
+ "sha256": "9376481f98f539c0b6aaf72df42e647f320a0e6050e14cd2e1cf0c37240b79cd"
6198
6198
  },
6199
6199
  {
6200
6200
  "path": "skills/loom-spec-review/.loom-runtime/shared/scripts/loom_init.py",
@@ -2534,6 +2534,11 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
2534
2534
  ],
2535
2535
  {"pass"},
2536
2536
  ),
2537
+ (
2538
+ "adopt-verify",
2539
+ ["python3", "tools/loom_flow.py", "adopt", "verify", "--target", "examples/new-project", "--item", "INIT-0001"],
2540
+ {"pass"},
2541
+ ),
2537
2542
  (
2538
2543
  "governance-profile-status",
2539
2544
  ["python3", "tools/loom_flow.py", "governance-profile", "status", "--target", "examples/new-project"],
@@ -2798,6 +2803,21 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
2798
2803
  context="`runtime-parity validate`",
2799
2804
  payload=payload,
2800
2805
  )
2806
+ if label == "adopt-verify":
2807
+ if payload.get("command") != "adopt" or payload.get("operation") != "verify":
2808
+ failures.append(Failure("daily-execution-cli", "`adopt verify` must report command/operation"))
2809
+ if payload.get("schema_version") != "loom-adoption-verify/v1":
2810
+ failures.append(Failure("daily-execution-cli", "`adopt verify` must report schema v1"))
2811
+ roundtrip = payload.get("producer_consumer_roundtrip")
2812
+ if not isinstance(roundtrip, dict):
2813
+ failures.append(Failure("daily-execution-cli", "`adopt verify` must include producer_consumer_roundtrip"))
2814
+ else:
2815
+ consumer = roundtrip.get("consumer")
2816
+ bypass = roundtrip.get("bypass_check")
2817
+ if not isinstance(consumer, dict) or consumer.get("result") != "pass":
2818
+ failures.append(Failure("daily-execution-cli", "`adopt verify` generated body must pass consumer validation"))
2819
+ if not isinstance(bypass, dict) or bypass.get("result") != "pass":
2820
+ failures.append(Failure("daily-execution-cli", "`adopt verify` must prove required section deletion blocks"))
2801
2821
  if label in {"governance-profile-status", "governance-profile-upgrade-plan"}:
2802
2822
  if payload.get("command") != "governance-profile":
2803
2823
  failures.append(Failure("daily-execution-cli", f"`{label}` must report `command: governance-profile`"))
@@ -6543,15 +6563,20 @@ def check_node_installer(root: Path) -> list[Failure]:
6543
6563
  ["npm", "test"],
6544
6564
  ["npm", "pack", "--dry-run"],
6545
6565
  )
6546
- for args in commands:
6547
- try:
6548
- result = run_command(root, args, cwd=package_root, timeout_seconds=300)
6549
- except subprocess.TimeoutExpired:
6550
- failures.append(Failure(category, f"`{' '.join(args)}` timed out"))
6551
- continue
6552
- if result.returncode != 0:
6553
- detail = result.stderr.strip() or result.stdout.strip() or "command failed without output"
6554
- failures.append(Failure(category, f"`{' '.join(args)}` failed: {detail}"))
6566
+ with tempfile.TemporaryDirectory(prefix="loom-check-npm-cache-") as cache_dir:
6567
+ npm_env = {
6568
+ "npm_config_cache": cache_dir,
6569
+ "NPM_CONFIG_CACHE": cache_dir,
6570
+ }
6571
+ for args in commands:
6572
+ try:
6573
+ result = run_command(root, args, cwd=package_root, env=npm_env, timeout_seconds=300)
6574
+ except subprocess.TimeoutExpired:
6575
+ failures.append(Failure(category, f"`{' '.join(args)}` timed out"))
6576
+ continue
6577
+ if result.returncode != 0:
6578
+ detail = result.stderr.strip() or result.stdout.strip() or "command failed without output"
6579
+ failures.append(Failure(category, f"`{' '.join(args)}` failed: {detail}"))
6555
6580
  return failures
6556
6581
 
6557
6582
 
@@ -36,6 +36,14 @@ PR_TEMPLATE_SECTIONS = (
36
36
  "## Risks And Follow-ups",
37
37
  "## Related Work",
38
38
  )
39
+ ADOPTION_PR_BODY_SECTIONS = (*PR_TEMPLATE_SECTIONS, "## Review Artifacts")
40
+ ADOPTION_REVIEW_ARTIFACT_LABELS = (
41
+ "Active Work Item",
42
+ "Active Recovery Entry",
43
+ "Status Surface",
44
+ "Review Record",
45
+ "Spec Review Record",
46
+ )
39
47
 
40
48
  OWNED_TEMP_ROOTS = (
41
49
  ".loom/.tmp",
@@ -161,6 +169,16 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
161
169
  help="Init-result path relative to the target root",
162
170
  )
163
171
 
172
+ adopt = subparsers.add_parser("adopt", help="Validate Loom downstream adoption contracts")
173
+ adopt.add_argument("operation", choices=("verify",))
174
+ adopt.add_argument("--target", required=True, help="Target repository root")
175
+ adopt.add_argument("--item", help="Expected current item id")
176
+ adopt.add_argument(
177
+ "--output",
178
+ default=".loom/bootstrap/init-result.json",
179
+ help="Init-result path relative to the target root",
180
+ )
181
+
164
182
  state = subparsers.add_parser(
165
183
  "state-check",
166
184
  help="Check active-state consistency, checkpoint completeness, and scope overflow signals",
@@ -2353,6 +2371,96 @@ def check_pr_template(target_root: Path) -> tuple[dict[str, Any], list[str]]:
2353
2371
  }, missing
2354
2372
 
2355
2373
 
2374
+ def render_adoption_pr_body(context: dict[str, Any]) -> str:
2375
+ item_id = context["item_id"]
2376
+ review_record = context["review_entry"]
2377
+ spec_review_record = default_spec_review_path(item_id)
2378
+ return (
2379
+ "## Summary\n\n"
2380
+ f"- Problem: Adopt Loom governance carriers for `{item_id}`.\n"
2381
+ "- Scope: Loom-owned carrier and review metadata only.\n\n"
2382
+ "## Validation\n\n"
2383
+ "- [x] Verified by Loom adoption round-trip.\n\n"
2384
+ "## Risks And Follow-ups\n\n"
2385
+ "- Risks: None identified by the generated adoption body.\n"
2386
+ "- Follow-ups: Keep repo-specific gates repo-owned.\n\n"
2387
+ "## Related Work\n\n"
2388
+ f"- Issue: {item_id}\n"
2389
+ f"- Spec / plan: .loom/specs/{item_id}/spec.md\n\n"
2390
+ "## Review Artifacts\n\n"
2391
+ f"- Active Work Item: {context['report']['fact_chain']['entry_points']['work_item']}\n"
2392
+ f"- Active Recovery Entry: {context['report']['fact_chain']['entry_points']['recovery_entry']}\n"
2393
+ f"- Status Surface: {context['report']['fact_chain']['entry_points']['status_surface']}\n"
2394
+ f"- Review Record: {review_record}\n"
2395
+ f"- Spec Review Record: {spec_review_record}\n"
2396
+ )
2397
+
2398
+
2399
+ def adoption_pr_body_sections(body: str) -> dict[str, str]:
2400
+ sections: dict[str, list[str]] = {}
2401
+ current: str | None = None
2402
+ for line in body.splitlines():
2403
+ if line.startswith("## "):
2404
+ current = line.strip()
2405
+ sections[current] = []
2406
+ continue
2407
+ if current is not None:
2408
+ sections[current].append(line.rstrip())
2409
+ return {section: "\n".join(lines).strip() for section, lines in sections.items()}
2410
+
2411
+
2412
+ def parse_review_artifact_locators(section: str) -> tuple[dict[str, str], list[str]]:
2413
+ locators: dict[str, str] = {}
2414
+ errors: list[str] = []
2415
+ for raw_line in section.splitlines():
2416
+ stripped = raw_line.strip()
2417
+ if not stripped:
2418
+ continue
2419
+ match = re.match(r"^- ([^:]+):\s*(.+?)\s*$", stripped)
2420
+ if not match:
2421
+ errors.append(f"invalid Review Artifacts bullet: {stripped}")
2422
+ continue
2423
+ label = match.group(1).strip()
2424
+ value = match.group(2).strip().strip("`")
2425
+ if label in locators:
2426
+ errors.append(f"duplicate Review Artifacts field: {label}")
2427
+ continue
2428
+ locators[label] = value
2429
+ for label in ADOPTION_REVIEW_ARTIFACT_LABELS:
2430
+ if label not in locators:
2431
+ errors.append(f"Review Artifacts missing `{label}`")
2432
+ return locators, errors
2433
+
2434
+
2435
+ def validate_adoption_pr_body(body: str, *, target_root: Path) -> dict[str, Any]:
2436
+ sections = adoption_pr_body_sections(body)
2437
+ missing_sections = [section for section in ADOPTION_PR_BODY_SECTIONS if section not in sections]
2438
+ missing_inputs: list[str] = [f"PR body missing section: {section}" for section in missing_sections]
2439
+ artifact_section = sections.get("## Review Artifacts", "")
2440
+ locators, locator_errors = parse_review_artifact_locators(artifact_section)
2441
+ missing_inputs.extend(locator_errors)
2442
+
2443
+ locator_status: dict[str, dict[str, Any]] = {}
2444
+ for label, locator in locators.items():
2445
+ path, errors = resolve_repo_relative_path(target_root, locator, label=f"Review Artifacts `{label}`")
2446
+ exists = bool(path and path.exists() and path.is_file())
2447
+ if errors:
2448
+ missing_inputs.extend(errors)
2449
+ elif not exists:
2450
+ missing_inputs.append(f"Review Artifacts `{label}` points to missing file: {locator}")
2451
+ locator_status[label] = {
2452
+ "locator": locator,
2453
+ "status": "present" if exists and not errors else "missing",
2454
+ }
2455
+
2456
+ return {
2457
+ "result": "pass" if not missing_inputs else "block",
2458
+ "missing_inputs": missing_inputs,
2459
+ "sections": {section: section in sections for section in ADOPTION_PR_BODY_SECTIONS},
2460
+ "review_artifacts": locator_status,
2461
+ }
2462
+
2463
+
2356
2464
  def active_workspace_conflicts(target_root: Path, item_id: str, workspace_entry: str) -> list[str]:
2357
2465
  work_items_dir = target_root / ".loom/work-items"
2358
2466
  if not work_items_dir.exists():
@@ -3688,6 +3796,101 @@ def handle_runtime_state(args: argparse.Namespace) -> int:
3688
3796
  )
3689
3797
 
3690
3798
 
3799
+ def adoption_verify_payload(target_root: Path, output_relative: str, expected_item: str | None) -> dict[str, Any]:
3800
+ runtime_state = runtime_state_payload(target_root)
3801
+ context, context_errors = load_context(target_root, output_relative, expected_item)
3802
+ pr_template, pr_template_errors = check_pr_template(target_root)
3803
+ governance_surface = build_governance_surface(target_root)
3804
+
3805
+ if context_errors:
3806
+ return {
3807
+ "command": "adopt",
3808
+ "operation": "verify",
3809
+ "schema_version": "loom-adoption-verify/v1",
3810
+ "result": "block",
3811
+ "summary": "adoption verify could not read the Loom fact chain.",
3812
+ "missing_inputs": [f"fact-chain: {message}" for message in context_errors],
3813
+ "fallback_to": "adoption",
3814
+ "runtime_state": runtime_state,
3815
+ "governance_surface": governance_surface,
3816
+ "pr_template": pr_template,
3817
+ }
3818
+
3819
+ produced_body = render_adoption_pr_body(context)
3820
+ produced_validation = validate_adoption_pr_body(produced_body, target_root=target_root)
3821
+ bypass_body = produced_body.replace("\n## Review Artifacts\n\n", "\n## Omitted Review Artifacts\n\n", 1)
3822
+ bypass_validation = validate_adoption_pr_body(bypass_body, target_root=target_root)
3823
+
3824
+ review_record, review_path, review_errors = load_review_record(target_root, context["item_id"], context["review_entry"])
3825
+ spec_review_record, spec_review_path, spec_review_errors = load_review_record(
3826
+ target_root,
3827
+ context["item_id"],
3828
+ default_spec_review_path(context["item_id"]),
3829
+ )
3830
+ review_missing = list(review_errors)
3831
+ if review_record is None and not review_errors:
3832
+ review_missing.append(f"missing review artifact: {review_path}")
3833
+ spec_review_missing = list(spec_review_errors)
3834
+ if spec_review_record is None and not spec_review_errors:
3835
+ spec_review_missing.append(f"missing spec review artifact: {spec_review_path}")
3836
+
3837
+ missing_inputs: list[str] = []
3838
+ if runtime_state.get("result") != "pass":
3839
+ missing_inputs.extend(str(message) for message in runtime_state.get("missing_inputs", []))
3840
+ missing_inputs.extend(pr_template_errors)
3841
+ missing_inputs.extend(produced_validation["missing_inputs"])
3842
+ missing_inputs.extend(review_missing)
3843
+ missing_inputs.extend(spec_review_missing)
3844
+ if bypass_validation["result"] != "block":
3845
+ missing_inputs.append("consumer bypass check failed: removing Review Artifacts must block")
3846
+
3847
+ result = "pass" if not missing_inputs else "block"
3848
+ return {
3849
+ "command": "adopt",
3850
+ "operation": "verify",
3851
+ "schema_version": "loom-adoption-verify/v1",
3852
+ "result": result,
3853
+ "summary": (
3854
+ "downstream adoption producer/consumer round-trip is valid."
3855
+ if result == "pass"
3856
+ else "downstream adoption round-trip has blocking contract gaps."
3857
+ ),
3858
+ "missing_inputs": missing_inputs,
3859
+ "fallback_to": None if result == "pass" else "adoption",
3860
+ "runtime_state": runtime_state,
3861
+ "governance_surface": governance_surface,
3862
+ "pr_template": pr_template,
3863
+ "producer_consumer_roundtrip": {
3864
+ "producer": {
3865
+ "status": "pass",
3866
+ "body_sections": list(ADOPTION_PR_BODY_SECTIONS),
3867
+ },
3868
+ "consumer": produced_validation,
3869
+ "bypass_check": {
3870
+ "scenario": "Review Artifacts section omitted",
3871
+ "result": "pass" if bypass_validation["result"] == "block" else "block",
3872
+ "consumer_result": bypass_validation["result"],
3873
+ "missing_inputs": bypass_validation["missing_inputs"],
3874
+ },
3875
+ },
3876
+ "reviews": {
3877
+ "implementation": {
3878
+ "path": review_path,
3879
+ "status": "present" if review_record is not None and not review_errors else "missing",
3880
+ },
3881
+ "spec": {
3882
+ "path": spec_review_path,
3883
+ "status": "present" if spec_review_record is not None and not spec_review_errors else "missing",
3884
+ },
3885
+ },
3886
+ }
3887
+
3888
+
3889
+ def handle_adopt(args: argparse.Namespace) -> int:
3890
+ target_root = Path(args.target).expanduser().resolve()
3891
+ return emit(adoption_verify_payload(target_root, args.output, args.item))
3892
+
3893
+
3691
3894
  def host_lifecycle_payload(context: dict[str, Any]) -> dict[str, Any]:
3692
3895
  branch = git_branch(context["target_root"])
3693
3896
  purity = purity_report_from_context(context)
@@ -7076,6 +7279,8 @@ def main(argv: list[str] | None = None) -> int:
7076
7279
  return handle_fact_chain(args)
7077
7280
  if args.command == "runtime-state":
7078
7281
  return handle_runtime_state(args)
7282
+ if args.command == "adopt":
7283
+ return handle_adopt(args)
7079
7284
  if args.command == "runtime-evidence":
7080
7285
  return handle_runtime_evidence(args)
7081
7286
  if args.command == "state-check":
@@ -2534,6 +2534,11 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
2534
2534
  ],
2535
2535
  {"pass"},
2536
2536
  ),
2537
+ (
2538
+ "adopt-verify",
2539
+ ["python3", "tools/loom_flow.py", "adopt", "verify", "--target", "examples/new-project", "--item", "INIT-0001"],
2540
+ {"pass"},
2541
+ ),
2537
2542
  (
2538
2543
  "governance-profile-status",
2539
2544
  ["python3", "tools/loom_flow.py", "governance-profile", "status", "--target", "examples/new-project"],
@@ -2798,6 +2803,21 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
2798
2803
  context="`runtime-parity validate`",
2799
2804
  payload=payload,
2800
2805
  )
2806
+ if label == "adopt-verify":
2807
+ if payload.get("command") != "adopt" or payload.get("operation") != "verify":
2808
+ failures.append(Failure("daily-execution-cli", "`adopt verify` must report command/operation"))
2809
+ if payload.get("schema_version") != "loom-adoption-verify/v1":
2810
+ failures.append(Failure("daily-execution-cli", "`adopt verify` must report schema v1"))
2811
+ roundtrip = payload.get("producer_consumer_roundtrip")
2812
+ if not isinstance(roundtrip, dict):
2813
+ failures.append(Failure("daily-execution-cli", "`adopt verify` must include producer_consumer_roundtrip"))
2814
+ else:
2815
+ consumer = roundtrip.get("consumer")
2816
+ bypass = roundtrip.get("bypass_check")
2817
+ if not isinstance(consumer, dict) or consumer.get("result") != "pass":
2818
+ failures.append(Failure("daily-execution-cli", "`adopt verify` generated body must pass consumer validation"))
2819
+ if not isinstance(bypass, dict) or bypass.get("result") != "pass":
2820
+ failures.append(Failure("daily-execution-cli", "`adopt verify` must prove required section deletion blocks"))
2801
2821
  if label in {"governance-profile-status", "governance-profile-upgrade-plan"}:
2802
2822
  if payload.get("command") != "governance-profile":
2803
2823
  failures.append(Failure("daily-execution-cli", f"`{label}` must report `command: governance-profile`"))
@@ -6543,15 +6563,20 @@ def check_node_installer(root: Path) -> list[Failure]:
6543
6563
  ["npm", "test"],
6544
6564
  ["npm", "pack", "--dry-run"],
6545
6565
  )
6546
- for args in commands:
6547
- try:
6548
- result = run_command(root, args, cwd=package_root, timeout_seconds=300)
6549
- except subprocess.TimeoutExpired:
6550
- failures.append(Failure(category, f"`{' '.join(args)}` timed out"))
6551
- continue
6552
- if result.returncode != 0:
6553
- detail = result.stderr.strip() or result.stdout.strip() or "command failed without output"
6554
- failures.append(Failure(category, f"`{' '.join(args)}` failed: {detail}"))
6566
+ with tempfile.TemporaryDirectory(prefix="loom-check-npm-cache-") as cache_dir:
6567
+ npm_env = {
6568
+ "npm_config_cache": cache_dir,
6569
+ "NPM_CONFIG_CACHE": cache_dir,
6570
+ }
6571
+ for args in commands:
6572
+ try:
6573
+ result = run_command(root, args, cwd=package_root, env=npm_env, timeout_seconds=300)
6574
+ except subprocess.TimeoutExpired:
6575
+ failures.append(Failure(category, f"`{' '.join(args)}` timed out"))
6576
+ continue
6577
+ if result.returncode != 0:
6578
+ detail = result.stderr.strip() or result.stdout.strip() or "command failed without output"
6579
+ failures.append(Failure(category, f"`{' '.join(args)}` failed: {detail}"))
6555
6580
  return failures
6556
6581
 
6557
6582