@mc-and-his-agents/loom-installer 0.1.21 → 0.1.23

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 (52) hide show
  1. package/package.json +1 -1
  2. package/payload/manifest.json +102 -102
  3. package/payload/plugin/loom/skills/shared/scripts/governance_surface.py +57 -14
  4. package/payload/plugin/loom/skills/shared/scripts/loom_check.py +87 -0
  5. package/payload/plugin/loom/skills/shared/scripts/loom_flow.py +9 -6
  6. package/payload/plugin/loom/skills/shared/scripts/loom_init.py +57 -50
  7. package/payload/plugin/loom/skills/shared/scripts/runtime_state.py +17 -0
  8. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/governance_surface.py +57 -14
  9. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +87 -0
  10. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py +9 -6
  11. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_init.py +57 -50
  12. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/runtime_state.py +17 -0
  13. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/governance_surface.py +57 -14
  14. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +87 -0
  15. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py +9 -6
  16. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_init.py +57 -50
  17. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/runtime_state.py +17 -0
  18. package/payload/skills/loom-init/.loom-runtime/shared/scripts/governance_surface.py +57 -14
  19. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +87 -0
  20. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py +9 -6
  21. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_init.py +57 -50
  22. package/payload/skills/loom-init/.loom-runtime/shared/scripts/runtime_state.py +17 -0
  23. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/governance_surface.py +57 -14
  24. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +87 -0
  25. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py +9 -6
  26. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_init.py +57 -50
  27. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/runtime_state.py +17 -0
  28. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/governance_surface.py +57 -14
  29. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +87 -0
  30. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py +9 -6
  31. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_init.py +57 -50
  32. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/runtime_state.py +17 -0
  33. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/governance_surface.py +57 -14
  34. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +87 -0
  35. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py +9 -6
  36. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_init.py +57 -50
  37. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/runtime_state.py +17 -0
  38. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/governance_surface.py +57 -14
  39. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +87 -0
  40. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py +9 -6
  41. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_init.py +57 -50
  42. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/runtime_state.py +17 -0
  43. package/payload/skills/loom-review/.loom-runtime/shared/scripts/governance_surface.py +57 -14
  44. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +87 -0
  45. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py +9 -6
  46. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_init.py +57 -50
  47. package/payload/skills/loom-review/.loom-runtime/shared/scripts/runtime_state.py +17 -0
  48. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/governance_surface.py +57 -14
  49. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py +87 -0
  50. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py +9 -6
  51. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_init.py +57 -50
  52. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/runtime_state.py +17 -0
@@ -893,18 +893,60 @@ def first_match(directory: Path, suffix: str, root: Path) -> str:
893
893
  return ""
894
894
 
895
895
 
896
+ def existing_locator(root: Path, relative: str | None) -> str:
897
+ if not relative:
898
+ return ""
899
+ return relative if (root / relative).exists() else ""
900
+
901
+
902
+ def active_or_first(root: Path, relative: str | None, directory: Path, suffix: str) -> str:
903
+ return existing_locator(root, relative) or (first_match(directory, suffix, root) if directory.exists() else "")
904
+
905
+
906
+ def current_review_locator(root: Path, review_dir: Path, item_id: str) -> str:
907
+ review_path = review_dir / f"{item_id}.json"
908
+ if review_path.exists():
909
+ return relative_locator(review_path, root)
910
+ return first_match(review_dir, ".json", root) if review_dir.exists() else ""
911
+
912
+
913
+ def active_entry_points(root: Path) -> dict[str, str]:
914
+ init_result = root / ".loom/bootstrap/init-result.json"
915
+ try:
916
+ payload = json.loads(init_result.read_text(encoding="utf-8"))
917
+ except (OSError, json.JSONDecodeError):
918
+ return {}
919
+ if not isinstance(payload, dict):
920
+ return {}
921
+ fact_chain = payload.get("fact_chain")
922
+ if not isinstance(fact_chain, dict):
923
+ return {}
924
+ entry_points = fact_chain.get("entry_points")
925
+ if not isinstance(entry_points, dict):
926
+ return {}
927
+ active: dict[str, str] = {}
928
+ for key in ("current_item_id", "work_item", "recovery_entry", "status_surface"):
929
+ value = entry_points.get(key)
930
+ if isinstance(value, str) and value.strip():
931
+ active[key] = value.strip()
932
+ return active
933
+
934
+
896
935
  def detect_carrier_summary(root: Path, *, repository_mode: str, planning_mode: bool) -> dict[str, dict[str, str]]:
936
+ active = active_entry_points(root)
937
+ active_item_id = active.get("current_item_id") or "INIT-0001"
897
938
  item_dir = root / ".loom/work-items"
898
939
  recovery_dir = root / ".loom/progress"
899
940
  review_dir = root / ".loom/reviews"
900
- status_path = root / ".loom/status/current.md"
901
- spec_path = root / ".loom/specs/INIT-0001/spec.md"
902
- plan_path = root / ".loom/specs/INIT-0001/plan.md"
941
+ status_locator = active.get("status_surface") or ".loom/status/current.md"
942
+ status_path = root / status_locator
943
+ spec_path = root / f".loom/specs/{active_item_id}/spec.md"
944
+ plan_path = root / f".loom/specs/{active_item_id}/plan.md"
903
945
 
904
946
  present_locators = {
905
- "work_item": first_match(item_dir, ".md", root) if item_dir.exists() else "",
906
- "recovery": first_match(recovery_dir, ".md", root) if recovery_dir.exists() else "",
907
- "review": first_match(review_dir, ".json", root) if review_dir.exists() else "",
947
+ "work_item": active_or_first(root, active.get("work_item"), item_dir, ".md"),
948
+ "recovery": active_or_first(root, active.get("recovery_entry"), recovery_dir, ".md"),
949
+ "review": current_review_locator(root, review_dir, active_item_id),
908
950
  "status_surface": relative_locator(status_path, root) if status_path.exists() else "",
909
951
  "spec_path": relative_locator(spec_path, root) if spec_path.exists() else "",
910
952
  "plan_path": relative_locator(plan_path, root) if plan_path.exists() else "",
@@ -922,11 +964,11 @@ def detect_carrier_summary(root: Path, *, repository_mode: str, planning_mode: b
922
964
  return summary
923
965
 
924
966
 
925
- def detect_execution_entry(root: Path, loom_state: str, *, bootstrap_mode: bool) -> str:
967
+ def detect_execution_entry(root: Path, loom_state: str, *, bootstrap_mode: bool, active_item_id: str = "INIT-0001") -> str:
926
968
  if bootstrap_mode:
927
- return "python3 .loom/bin/loom_flow.py flow resume --target . --item INIT-0001"
969
+ return f"python3 .loom/bin/loom_flow.py flow resume --target . --item {active_item_id}"
928
970
  if loom_state == "active":
929
- return f"{command_prefix(root, 'loom_flow.py')} flow resume --target . --item INIT-0001"
971
+ return f"{command_prefix(root, 'loom_flow.py')} flow resume --target . --item {active_item_id}"
930
972
  if loom_state == "partial":
931
973
  return f"{command_prefix(root, 'loom_init.py')} route --target <repo> --task \"请接手当前事项并恢复上下文后继续推进\""
932
974
  return "unknown"
@@ -942,16 +984,16 @@ def detect_validation_entry(loom_state: str, *, bootstrap_mode: bool) -> str:
942
984
  return "unknown"
943
985
 
944
986
 
945
- def detect_review_merge_surface(root: Path, loom_state: str, *, bootstrap_mode: bool) -> dict[str, str]:
987
+ def detect_review_merge_surface(root: Path, loom_state: str, *, bootstrap_mode: bool, active_item_id: str = "INIT-0001") -> dict[str, str]:
946
988
  pr_template = ".github/PULL_REQUEST_TEMPLATE.md" if file_exists(root, ".github/PULL_REQUEST_TEMPLATE.md") else "unknown"
947
989
  validation_surface = ".loom/status/current.md" if file_exists(root, ".loom/status/current.md") else "unknown"
948
990
  if bootstrap_mode and validation_surface == "unknown":
949
991
  validation_surface = ".loom/status/current.md"
950
992
 
951
993
  if bootstrap_mode:
952
- merge_surface = "python3 .loom/bin/loom_flow.py checkpoint merge --target . --item INIT-0001"
994
+ merge_surface = f"python3 .loom/bin/loom_flow.py checkpoint merge --target . --item {active_item_id}"
953
995
  elif loom_state == "active":
954
- merge_surface = f"{command_prefix(root, 'loom_flow.py')} checkpoint merge --target . [--item <id>]"
996
+ merge_surface = f"{command_prefix(root, 'loom_flow.py')} checkpoint merge --target . --item {active_item_id}"
955
997
  else:
956
998
  merge_surface = "unknown"
957
999
  return {
@@ -1207,11 +1249,12 @@ def build_governance_surface(
1207
1249
  loom_state = detect_loom_state(root)
1208
1250
  repository_mode = detect_repository_mode(root, loom_state, scenario_override=scenario_override)
1209
1251
  planning_mode = bootstrap_mode and repository_mode == "new" and loom_state != "active"
1252
+ active_item_id = active_entry_points(root).get("current_item_id", "INIT-0001")
1210
1253
  carrier_summary = detect_carrier_summary(root, repository_mode=repository_mode, planning_mode=planning_mode)
1211
1254
  github_control_plane, github_missing = detect_github_control_plane(root)
1212
- execution_entry = detect_execution_entry(root, loom_state, bootstrap_mode=bootstrap_mode)
1255
+ execution_entry = detect_execution_entry(root, loom_state, bootstrap_mode=bootstrap_mode, active_item_id=active_item_id)
1213
1256
  validation_entry = detect_validation_entry(loom_state, bootstrap_mode=bootstrap_mode)
1214
- review_merge_surface = detect_review_merge_surface(root, loom_state, bootstrap_mode=bootstrap_mode)
1257
+ review_merge_surface = detect_review_merge_surface(root, loom_state, bootstrap_mode=bootstrap_mode, active_item_id=active_item_id)
1215
1258
  repo_interface, repo_interface_missing = detect_repo_interface(root)
1216
1259
  repo_interop, repo_interop_missing = detect_repo_interop(root)
1217
1260
  host_binding = detect_host_binding_surface(
@@ -3468,6 +3468,72 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
3468
3468
  elif payload.get("result") != "pass":
3469
3469
  failures.append(Failure("daily-execution-cli", "`work-item update` must pass on a clean temp copy"))
3470
3470
 
3471
+ payload, error = load_command_json(
3472
+ root,
3473
+ [
3474
+ "python3",
3475
+ "tools/loom_init.py",
3476
+ "verify",
3477
+ "--target",
3478
+ str(authoring_target),
3479
+ ],
3480
+ )
3481
+ if error:
3482
+ failures.append(Failure("daily-execution-cli", f"`loom-init verify` active item rollover failed: {error}"))
3483
+ elif payload.get("ok") is not True:
3484
+ failures.append(Failure("daily-execution-cli", "`loom-init verify` must pass after active item rollover"))
3485
+
3486
+ payload, error = load_command_json(
3487
+ root,
3488
+ [
3489
+ "python3",
3490
+ "tools/loom_status.py",
3491
+ "--target",
3492
+ str(authoring_target),
3493
+ "--item",
3494
+ "NEXT-0001",
3495
+ ],
3496
+ )
3497
+ if error:
3498
+ failures.append(Failure("daily-execution-cli", f"`loom-status` active item rollover failed: {error}"))
3499
+ else:
3500
+ current_item = payload.get("item", {}).get("id") if isinstance(payload.get("item"), dict) else None
3501
+ if current_item != "NEXT-0001":
3502
+ failures.append(Failure("daily-execution-cli", "`loom-status` must report the rolled-over active item"))
3503
+ governance_surface = payload.get("governance_surface")
3504
+ if isinstance(governance_surface, dict):
3505
+ carrier_summary = governance_surface.get("carrier_summary")
3506
+ if isinstance(carrier_summary, dict):
3507
+ work_item = carrier_summary.get("work_item")
3508
+ recovery = carrier_summary.get("recovery")
3509
+ if isinstance(work_item, dict) and work_item.get("locator") != ".loom/work-items/NEXT-0001.md":
3510
+ failures.append(Failure("daily-execution-cli", "`loom-status` carrier summary must point to the active Work Item"))
3511
+ if isinstance(recovery, dict) and recovery.get("locator") != ".loom/progress/NEXT-0001.md":
3512
+ failures.append(Failure("daily-execution-cli", "`loom-status` carrier summary must point to the active recovery entry"))
3513
+ execution_entry = str(governance_surface.get("execution_entry", ""))
3514
+ if "--item NEXT-0001" not in execution_entry:
3515
+ failures.append(Failure("daily-execution-cli", "`loom-status` execution entry must resume the active Work Item"))
3516
+
3517
+ payload, error = load_command_json(
3518
+ root,
3519
+ [
3520
+ "python3",
3521
+ "tools/loom_flow.py",
3522
+ "flow",
3523
+ "spec-review",
3524
+ "--target",
3525
+ str(authoring_target),
3526
+ "--item",
3527
+ "NEXT-0001",
3528
+ ],
3529
+ )
3530
+ if error:
3531
+ failures.append(Failure("daily-execution-cli", f"`flow spec-review` active item rollover failed: {error}"))
3532
+ elif payload.get("result") not in {"block", "fallback"}:
3533
+ failures.append(Failure("daily-execution-cli", "`flow spec-review` must not pass when the active item has no spec suite"))
3534
+ elif ".loom/specs/INIT-0001/spec.md" in json.dumps(payload, ensure_ascii=False):
3535
+ failures.append(Failure("daily-execution-cli", "`flow spec-review` must not fall back to the bootstrap spec suite for a rolled-over active item"))
3536
+
3471
3537
  findings_path = authoring_target / ".loom" / "review-findings.json"
3472
3538
  findings_path.parent.mkdir(parents=True, exist_ok=True)
3473
3539
  findings_path.write_text(
@@ -3711,6 +3777,27 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
3711
3777
  elif payload.get("result") != "block":
3712
3778
  failures.append(Failure("daily-execution-cli", "`bootstrapped runtime-state` must block when the bootstrap manifest drifts"))
3713
3779
 
3780
+ hash_drift_bootstrap = tmp_root / "hash-drift-bootstrapped-target"
3781
+ shutil.copytree(example_target, hash_drift_bootstrap)
3782
+ manifest_path = hash_drift_bootstrap / ".loom" / "bootstrap" / "manifest.json"
3783
+ manifest = load_json_file(manifest_path)
3784
+ if isinstance(manifest, dict):
3785
+ artifacts = manifest.get("artifacts")
3786
+ if isinstance(artifacts, list):
3787
+ for artifact in artifacts:
3788
+ if isinstance(artifact, dict) and artifact.get("path") == ".loom/bin/runtime_state.py":
3789
+ artifact["sha256"] = "0" * 64
3790
+ manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
3791
+ payload, error = load_command_json(
3792
+ root,
3793
+ ["python3", ".loom/bin/loom_init.py", "runtime-state", "--target", "."],
3794
+ cwd=hash_drift_bootstrap,
3795
+ )
3796
+ if error:
3797
+ failures.append(Failure("daily-execution-cli", f"`bootstrapped runtime-state` provenance hash drift failed unexpectedly: {error}"))
3798
+ elif payload.get("result") != "block":
3799
+ failures.append(Failure("daily-execution-cli", "`bootstrapped runtime-state` must block when runtime provenance hashes drift"))
3800
+
3714
3801
  if shutil.which("git") is not None:
3715
3802
  with tempfile.TemporaryDirectory(prefix="loom-check-installed-pre-merge-") as tmp:
3716
3803
  tmp_root = Path(tmp)
@@ -1169,7 +1169,7 @@ def formal_spec_path(context: dict[str, Any]) -> str | None:
1169
1169
  return artifact
1170
1170
 
1171
1171
  fallback = context["target_root"] / ".loom/specs/INIT-0001/spec.md"
1172
- if fallback.exists():
1172
+ if context["item_id"] == "INIT-0001" and fallback.exists():
1173
1173
  return ".loom/specs/INIT-0001/spec.md"
1174
1174
  return None
1175
1175
 
@@ -1182,12 +1182,15 @@ def spec_suite_paths(context: dict[str, Any]) -> dict[str, str]:
1182
1182
  "plan": f".loom/specs/{item_id}/plan.md",
1183
1183
  "implementation_contract": f".loom/specs/{item_id}/implementation-contract.md",
1184
1184
  },
1185
- {
1186
- "spec": ".loom/specs/INIT-0001/spec.md",
1187
- "plan": ".loom/specs/INIT-0001/plan.md",
1188
- "implementation_contract": ".loom/specs/INIT-0001/implementation-contract.md",
1189
- },
1190
1185
  ]
1186
+ if item_id == "INIT-0001":
1187
+ candidates.append(
1188
+ {
1189
+ "spec": ".loom/specs/INIT-0001/spec.md",
1190
+ "plan": ".loom/specs/INIT-0001/plan.md",
1191
+ "implementation_contract": ".loom/specs/INIT-0001/implementation-contract.md",
1192
+ }
1193
+ )
1191
1194
  for suite in candidates:
1192
1195
  if (context["target_root"] / suite["spec"]).exists():
1193
1196
  return suite
@@ -4,6 +4,7 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import argparse
7
+ import hashlib
7
8
  import json
8
9
  import os
9
10
  import re
@@ -27,6 +28,17 @@ TOOL_VERSION = "1.3.0"
27
28
  CONTRACT_VERSION = "1.3.0"
28
29
  WORK_ITEM_ID = "INIT-0001"
29
30
 
31
+ RUNTIME_ARTIFACT_SOURCES = {
32
+ ".loom/bin/loom_init.py": RUNTIME_SOURCE,
33
+ ".loom/bin/fact_chain_support.py": FACT_CHAIN_RUNTIME_SOURCE,
34
+ ".loom/bin/governance_surface.py": GOVERNANCE_RUNTIME_SOURCE,
35
+ ".loom/bin/loom_flow.py": FLOW_RUNTIME_SOURCE,
36
+ ".loom/bin/loom_status.py": STATUS_RUNTIME_SOURCE,
37
+ ".loom/bin/runtime_paths.py": "skills/shared/scripts/runtime_paths.py",
38
+ ".loom/bin/runtime_state.py": "skills/shared/scripts/runtime_state.py",
39
+ ".loom/bin/loom_check.py": CHECK_RUNTIME_SOURCE,
40
+ }
41
+
30
42
  ROOT_BOUNDARY_FILES = (
31
43
  "AGENTS.md",
32
44
  "WORKFLOW.md",
@@ -816,46 +828,14 @@ def initial_artifacts(target_root: Path, install_pr_template: bool, adoption_pat
816
828
  "kind": "capability-map",
817
829
  "source": "generated",
818
830
  },
819
- {
820
- "path": ".loom/bin/loom_init.py",
821
- "kind": "loom-tool",
822
- "source": RUNTIME_SOURCE,
823
- },
824
- {
825
- "path": ".loom/bin/fact_chain_support.py",
826
- "kind": "loom-tool-support",
827
- "source": FACT_CHAIN_RUNTIME_SOURCE,
828
- },
829
- {
830
- "path": ".loom/bin/governance_surface.py",
831
- "kind": "loom-tool-support",
832
- "source": GOVERNANCE_RUNTIME_SOURCE,
833
- },
834
- {
835
- "path": ".loom/bin/loom_flow.py",
836
- "kind": "loom-tool",
837
- "source": FLOW_RUNTIME_SOURCE,
838
- },
839
- {
840
- "path": ".loom/bin/loom_status.py",
841
- "kind": "loom-tool",
842
- "source": STATUS_RUNTIME_SOURCE,
843
- },
844
- {
845
- "path": ".loom/bin/runtime_paths.py",
846
- "kind": "loom-tool-support",
847
- "source": "skills/shared/scripts/runtime_paths.py",
848
- },
849
- {
850
- "path": ".loom/bin/runtime_state.py",
851
- "kind": "loom-tool-support",
852
- "source": "skills/shared/scripts/runtime_state.py",
853
- },
854
- {
855
- "path": ".loom/bin/loom_check.py",
856
- "kind": "loom-tool",
857
- "source": CHECK_RUNTIME_SOURCE,
858
- },
831
+ runtime_artifact(".loom/bin/loom_init.py", "loom-tool", RUNTIME_SOURCE),
832
+ runtime_artifact(".loom/bin/fact_chain_support.py", "loom-tool-support", FACT_CHAIN_RUNTIME_SOURCE),
833
+ runtime_artifact(".loom/bin/governance_surface.py", "loom-tool-support", GOVERNANCE_RUNTIME_SOURCE),
834
+ runtime_artifact(".loom/bin/loom_flow.py", "loom-tool", FLOW_RUNTIME_SOURCE),
835
+ runtime_artifact(".loom/bin/loom_status.py", "loom-tool", STATUS_RUNTIME_SOURCE),
836
+ runtime_artifact(".loom/bin/runtime_paths.py", "loom-tool-support", "skills/shared/scripts/runtime_paths.py"),
837
+ runtime_artifact(".loom/bin/runtime_state.py", "loom-tool-support", "skills/shared/scripts/runtime_state.py"),
838
+ runtime_artifact(".loom/bin/loom_check.py", "loom-tool", CHECK_RUNTIME_SOURCE),
859
839
  ]
860
840
  if uses_attach_only_path(adoption_path):
861
841
  artifacts.extend(
@@ -1362,6 +1342,34 @@ def copy_file(source: Path, target: Path, force: bool) -> bool:
1362
1342
  return write_text(target, content, force=force)
1363
1343
 
1364
1344
 
1345
+ def sha256_file(path: Path) -> str:
1346
+ digest = hashlib.sha256()
1347
+ with path.open("rb") as handle:
1348
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
1349
+ digest.update(chunk)
1350
+ return digest.hexdigest()
1351
+
1352
+
1353
+ def runtime_artifact(path: str, kind: str, source: str) -> dict[str, str]:
1354
+ runtime_sources = {
1355
+ ".loom/bin/loom_init.py": Path(__file__),
1356
+ ".loom/bin/fact_chain_support.py": Path(__file__).with_name("fact_chain_support.py"),
1357
+ ".loom/bin/governance_surface.py": Path(__file__).with_name("governance_surface.py"),
1358
+ ".loom/bin/loom_flow.py": Path(__file__).with_name("loom_flow.py"),
1359
+ ".loom/bin/loom_status.py": Path(__file__).with_name("loom_status.py"),
1360
+ ".loom/bin/runtime_paths.py": Path(__file__).with_name("runtime_paths.py"),
1361
+ ".loom/bin/runtime_state.py": Path(__file__).with_name("runtime_state.py"),
1362
+ ".loom/bin/loom_check.py": Path(__file__).with_name("loom_check.py"),
1363
+ }
1364
+ source_path = runtime_sources[path]
1365
+ return {
1366
+ "path": path,
1367
+ "kind": kind,
1368
+ "source": source,
1369
+ "sha256": sha256_file(source_path),
1370
+ }
1371
+
1372
+
1365
1373
  def manifest_payload(result: dict[str, object]) -> dict[str, object]:
1366
1374
  return {
1367
1375
  "schema_version": "loom-bootstrap-manifest/v1",
@@ -1619,24 +1627,23 @@ def verify_target(target_root: Path, output_path: Path) -> list[str]:
1619
1627
  ):
1620
1628
  if field not in runtime_evidence_report:
1621
1629
  errors.append(f"fact-chain: runtime_evidence is missing `{field}`")
1622
- matching_work_item = None
1623
- for work_item in validated_work_items:
1624
- if work_item.get("id") == current_item_id:
1625
- matching_work_item = work_item
1626
- break
1627
- if matching_work_item is None:
1628
- errors.append(f"init-result is missing the current work item `{current_item_id}`")
1629
- else:
1630
+ bootstrap_work_item = next(
1631
+ (work_item for work_item in validated_work_items if work_item.get("id") == WORK_ITEM_ID),
1632
+ None,
1633
+ )
1634
+ if bootstrap_work_item is None:
1635
+ errors.append(f"init-result is missing the bootstrap work item `{WORK_ITEM_ID}`")
1636
+ elif current_item_id == WORK_ITEM_ID:
1630
1637
  expected_init_fields = {
1631
1638
  "recovery_entry": fact_chain_report["fact_chain"]["entry_points"]["recovery_entry"],
1632
1639
  "validation_entry": fact_chain_report["facts"]["validation_entry"]["value"],
1633
1640
  "workspace_entry": fact_chain_report["facts"]["workspace_entry"]["value"],
1634
1641
  }
1635
1642
  for field, expected_value in expected_init_fields.items():
1636
- actual_value = matching_work_item.get(field)
1643
+ actual_value = bootstrap_work_item.get(field)
1637
1644
  if actual_value != expected_value:
1638
1645
  errors.append(
1639
- f"init-result work item `{current_item_id}` has inconsistent `{field}`: "
1646
+ f"init-result bootstrap work item `{WORK_ITEM_ID}` has inconsistent `{field}`: "
1640
1647
  f"expected `{expected_value}`, got `{actual_value}`"
1641
1648
  )
1642
1649
 
@@ -4,6 +4,7 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import json
7
+ import hashlib
7
8
  import os
8
9
  from pathlib import Path
9
10
  from typing import Any
@@ -85,6 +86,14 @@ def detect_carrier(caller_file: str) -> str | None:
85
86
  return None
86
87
 
87
88
 
89
+ def sha256_file(path: Path) -> str:
90
+ digest = hashlib.sha256()
91
+ with path.open("rb") as handle:
92
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
93
+ digest.update(chunk)
94
+ return digest.hexdigest()
95
+
96
+
88
97
  def _default_scene_for_carrier(carrier: str) -> str:
89
98
  if carrier == "repo-local-wrapper":
90
99
  return "repo-local-demo"
@@ -284,6 +293,14 @@ def _validate_bootstrapped_runtime(caller_file: str) -> tuple[dict[str, Any], li
284
293
  runtime_file = runtime_root / Path(relative).name
285
294
  if not runtime_file.exists():
286
295
  errors.append(f"bootstrapped runtime file is missing: `{relative}`")
296
+ continue
297
+ expected_hash = artifact.get("sha256")
298
+ if not isinstance(expected_hash, str) or not expected_hash.strip():
299
+ errors.append(f"bootstrap runtime artifact `{relative}` must declare sha256 provenance")
300
+ continue
301
+ actual_hash = sha256_file(runtime_file)
302
+ if actual_hash != expected_hash:
303
+ errors.append(f"bootstrap runtime artifact `{relative}` sha256 drifted")
287
304
 
288
305
  status = "pass" if not errors else "block"
289
306
  summary = (
@@ -893,18 +893,60 @@ def first_match(directory: Path, suffix: str, root: Path) -> str:
893
893
  return ""
894
894
 
895
895
 
896
+ def existing_locator(root: Path, relative: str | None) -> str:
897
+ if not relative:
898
+ return ""
899
+ return relative if (root / relative).exists() else ""
900
+
901
+
902
+ def active_or_first(root: Path, relative: str | None, directory: Path, suffix: str) -> str:
903
+ return existing_locator(root, relative) or (first_match(directory, suffix, root) if directory.exists() else "")
904
+
905
+
906
+ def current_review_locator(root: Path, review_dir: Path, item_id: str) -> str:
907
+ review_path = review_dir / f"{item_id}.json"
908
+ if review_path.exists():
909
+ return relative_locator(review_path, root)
910
+ return first_match(review_dir, ".json", root) if review_dir.exists() else ""
911
+
912
+
913
+ def active_entry_points(root: Path) -> dict[str, str]:
914
+ init_result = root / ".loom/bootstrap/init-result.json"
915
+ try:
916
+ payload = json.loads(init_result.read_text(encoding="utf-8"))
917
+ except (OSError, json.JSONDecodeError):
918
+ return {}
919
+ if not isinstance(payload, dict):
920
+ return {}
921
+ fact_chain = payload.get("fact_chain")
922
+ if not isinstance(fact_chain, dict):
923
+ return {}
924
+ entry_points = fact_chain.get("entry_points")
925
+ if not isinstance(entry_points, dict):
926
+ return {}
927
+ active: dict[str, str] = {}
928
+ for key in ("current_item_id", "work_item", "recovery_entry", "status_surface"):
929
+ value = entry_points.get(key)
930
+ if isinstance(value, str) and value.strip():
931
+ active[key] = value.strip()
932
+ return active
933
+
934
+
896
935
  def detect_carrier_summary(root: Path, *, repository_mode: str, planning_mode: bool) -> dict[str, dict[str, str]]:
936
+ active = active_entry_points(root)
937
+ active_item_id = active.get("current_item_id") or "INIT-0001"
897
938
  item_dir = root / ".loom/work-items"
898
939
  recovery_dir = root / ".loom/progress"
899
940
  review_dir = root / ".loom/reviews"
900
- status_path = root / ".loom/status/current.md"
901
- spec_path = root / ".loom/specs/INIT-0001/spec.md"
902
- plan_path = root / ".loom/specs/INIT-0001/plan.md"
941
+ status_locator = active.get("status_surface") or ".loom/status/current.md"
942
+ status_path = root / status_locator
943
+ spec_path = root / f".loom/specs/{active_item_id}/spec.md"
944
+ plan_path = root / f".loom/specs/{active_item_id}/plan.md"
903
945
 
904
946
  present_locators = {
905
- "work_item": first_match(item_dir, ".md", root) if item_dir.exists() else "",
906
- "recovery": first_match(recovery_dir, ".md", root) if recovery_dir.exists() else "",
907
- "review": first_match(review_dir, ".json", root) if review_dir.exists() else "",
947
+ "work_item": active_or_first(root, active.get("work_item"), item_dir, ".md"),
948
+ "recovery": active_or_first(root, active.get("recovery_entry"), recovery_dir, ".md"),
949
+ "review": current_review_locator(root, review_dir, active_item_id),
908
950
  "status_surface": relative_locator(status_path, root) if status_path.exists() else "",
909
951
  "spec_path": relative_locator(spec_path, root) if spec_path.exists() else "",
910
952
  "plan_path": relative_locator(plan_path, root) if plan_path.exists() else "",
@@ -922,11 +964,11 @@ def detect_carrier_summary(root: Path, *, repository_mode: str, planning_mode: b
922
964
  return summary
923
965
 
924
966
 
925
- def detect_execution_entry(root: Path, loom_state: str, *, bootstrap_mode: bool) -> str:
967
+ def detect_execution_entry(root: Path, loom_state: str, *, bootstrap_mode: bool, active_item_id: str = "INIT-0001") -> str:
926
968
  if bootstrap_mode:
927
- return "python3 .loom/bin/loom_flow.py flow resume --target . --item INIT-0001"
969
+ return f"python3 .loom/bin/loom_flow.py flow resume --target . --item {active_item_id}"
928
970
  if loom_state == "active":
929
- return f"{command_prefix(root, 'loom_flow.py')} flow resume --target . --item INIT-0001"
971
+ return f"{command_prefix(root, 'loom_flow.py')} flow resume --target . --item {active_item_id}"
930
972
  if loom_state == "partial":
931
973
  return f"{command_prefix(root, 'loom_init.py')} route --target <repo> --task \"请接手当前事项并恢复上下文后继续推进\""
932
974
  return "unknown"
@@ -942,16 +984,16 @@ def detect_validation_entry(loom_state: str, *, bootstrap_mode: bool) -> str:
942
984
  return "unknown"
943
985
 
944
986
 
945
- def detect_review_merge_surface(root: Path, loom_state: str, *, bootstrap_mode: bool) -> dict[str, str]:
987
+ def detect_review_merge_surface(root: Path, loom_state: str, *, bootstrap_mode: bool, active_item_id: str = "INIT-0001") -> dict[str, str]:
946
988
  pr_template = ".github/PULL_REQUEST_TEMPLATE.md" if file_exists(root, ".github/PULL_REQUEST_TEMPLATE.md") else "unknown"
947
989
  validation_surface = ".loom/status/current.md" if file_exists(root, ".loom/status/current.md") else "unknown"
948
990
  if bootstrap_mode and validation_surface == "unknown":
949
991
  validation_surface = ".loom/status/current.md"
950
992
 
951
993
  if bootstrap_mode:
952
- merge_surface = "python3 .loom/bin/loom_flow.py checkpoint merge --target . --item INIT-0001"
994
+ merge_surface = f"python3 .loom/bin/loom_flow.py checkpoint merge --target . --item {active_item_id}"
953
995
  elif loom_state == "active":
954
- merge_surface = f"{command_prefix(root, 'loom_flow.py')} checkpoint merge --target . [--item <id>]"
996
+ merge_surface = f"{command_prefix(root, 'loom_flow.py')} checkpoint merge --target . --item {active_item_id}"
955
997
  else:
956
998
  merge_surface = "unknown"
957
999
  return {
@@ -1207,11 +1249,12 @@ def build_governance_surface(
1207
1249
  loom_state = detect_loom_state(root)
1208
1250
  repository_mode = detect_repository_mode(root, loom_state, scenario_override=scenario_override)
1209
1251
  planning_mode = bootstrap_mode and repository_mode == "new" and loom_state != "active"
1252
+ active_item_id = active_entry_points(root).get("current_item_id", "INIT-0001")
1210
1253
  carrier_summary = detect_carrier_summary(root, repository_mode=repository_mode, planning_mode=planning_mode)
1211
1254
  github_control_plane, github_missing = detect_github_control_plane(root)
1212
- execution_entry = detect_execution_entry(root, loom_state, bootstrap_mode=bootstrap_mode)
1255
+ execution_entry = detect_execution_entry(root, loom_state, bootstrap_mode=bootstrap_mode, active_item_id=active_item_id)
1213
1256
  validation_entry = detect_validation_entry(loom_state, bootstrap_mode=bootstrap_mode)
1214
- review_merge_surface = detect_review_merge_surface(root, loom_state, bootstrap_mode=bootstrap_mode)
1257
+ review_merge_surface = detect_review_merge_surface(root, loom_state, bootstrap_mode=bootstrap_mode, active_item_id=active_item_id)
1215
1258
  repo_interface, repo_interface_missing = detect_repo_interface(root)
1216
1259
  repo_interop, repo_interop_missing = detect_repo_interop(root)
1217
1260
  host_binding = detect_host_binding_surface(