@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.
- package/package.json +1 -1
- package/payload/manifest.json +102 -102
- package/payload/plugin/loom/skills/shared/scripts/governance_surface.py +57 -14
- package/payload/plugin/loom/skills/shared/scripts/loom_check.py +87 -0
- package/payload/plugin/loom/skills/shared/scripts/loom_flow.py +9 -6
- package/payload/plugin/loom/skills/shared/scripts/loom_init.py +57 -50
- package/payload/plugin/loom/skills/shared/scripts/runtime_state.py +17 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/governance_surface.py +57 -14
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +87 -0
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_flow.py +9 -6
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_init.py +57 -50
- package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/runtime_state.py +17 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/governance_surface.py +57 -14
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +87 -0
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_flow.py +9 -6
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_init.py +57 -50
- package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/runtime_state.py +17 -0
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/governance_surface.py +57 -14
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +87 -0
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_flow.py +9 -6
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_init.py +57 -50
- package/payload/skills/loom-init/.loom-runtime/shared/scripts/runtime_state.py +17 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/governance_surface.py +57 -14
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +87 -0
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_flow.py +9 -6
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_init.py +57 -50
- package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/runtime_state.py +17 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/governance_surface.py +57 -14
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +87 -0
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_flow.py +9 -6
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_init.py +57 -50
- package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/runtime_state.py +17 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/governance_surface.py +57 -14
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +87 -0
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_flow.py +9 -6
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_init.py +57 -50
- package/payload/skills/loom-resume/.loom-runtime/shared/scripts/runtime_state.py +17 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/governance_surface.py +57 -14
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +87 -0
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_flow.py +9 -6
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_init.py +57 -50
- package/payload/skills/loom-retire/.loom-runtime/shared/scripts/runtime_state.py +17 -0
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/governance_surface.py +57 -14
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +87 -0
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_flow.py +9 -6
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_init.py +57 -50
- package/payload/skills/loom-review/.loom-runtime/shared/scripts/runtime_state.py +17 -0
- package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/governance_surface.py +57 -14
- package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py +87 -0
- package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_flow.py +9 -6
- package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_init.py +57 -50
- package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/runtime_state.py +17 -0
|
@@ -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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
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 =
|
|
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 `{
|
|
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
|
-
|
|
901
|
-
|
|
902
|
-
|
|
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":
|
|
906
|
-
"recovery":
|
|
907
|
-
"review":
|
|
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
|
|
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
|
|
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
|
|
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 .
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
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 =
|
|
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 `{
|
|
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 = (
|