footprint.py python
177 lines 6.0 KB
Raw
sha256:0e9549ec7b463911bc08b7d586dc320b1ac9b1f5c943ee7e3865dcc6cb0f6f83 chore(governance): sync handover+roadmap to 84db8c8 (drift:… Human 8 hours ago
1 """Vendored footprint resolution per §K4.5."""
2
3 from __future__ import annotations
4
5 from dataclasses import dataclass
6 from pathlib import Path
7
8 from adapters.config import OverseerConfig
9 from adapters.errors import ConfigError
10 from adapters.templating import render_template
11 from cli.docs_paths import join_docs_rel
12 from cli.kit_root import kit_root
13
14 MUSE_BRIDGE_WORKFLOW_DEST = "MUSE-BRIDGE-WORKFLOW.md"
15 MUSE_BRIDGE_DEPLOY_DEST = "scripts/muse-bridge-deploy.sh"
16 SESSION_START_HOOK_DEST = ".cursor/hooks/session-start-next.sh"
17 SESSION_END_HOOK_DEST = ".cursor/hooks/session-end-closeout.sh"
18 EXECUTABLE_FOOTPRINT_DESTINATIONS = frozenset(
19 {
20 MUSE_BRIDGE_DEPLOY_DEST,
21 SESSION_START_HOOK_DEST,
22 SESSION_END_HOOK_DEST,
23 }
24 )
25
26 SESSION_BOOKEND_SPECS: tuple[tuple[str, str], ...] = (
27 ("cursor/hooks/hooks.json", ".cursor/hooks.json"),
28 ("cursor/hooks/session-start-next.sh", SESSION_START_HOOK_DEST),
29 ("cursor/hooks/session-end-closeout.sh", SESSION_END_HOOK_DEST),
30 ("cursor/hooks/README.md", ".cursor/hooks/README.md"),
31 )
32
33
34 @dataclass(frozen=True)
35 class FootprintFile:
36 """One vendored footprint file (destination path, kit source, rendered bytes)."""
37
38 destination: str
39 source: str
40 content: bytes
41
42 @property
43 def text(self) -> str:
44 return self.content.decode("utf-8")
45
46
47 def _docs_path(config: OverseerConfig, doc_name: str) -> str:
48 return join_docs_rel(config.repo.root_relative_docs, doc_name)
49
50
51 def resolve_footprint(config: OverseerConfig, *, kit: Path | None = None) -> list[FootprintFile]:
52 """Resolve the full footprint for ``config``; fail closed on destination collisions."""
53 root = kit or kit_root()
54 files: list[FootprintFile] = []
55
56 template_specs: list[tuple[str, str]] = [
57 ("templates/OVERSEER-HANDOVER.template.md", _docs_path(config, config.docs.handover)),
58 ("templates/ROADMAP.template.md", _docs_path(config, config.docs.roadmap)),
59 (
60 "templates/STANDING-DECISIONS.template.md",
61 ".overseer/STANDING-DECISIONS.reference.md",
62 ),
63 ]
64 if config.docs.coordination:
65 template_specs.append(
66 (
67 "templates/CROSS-REPO-COORDINATION.template.md",
68 _docs_path(config, config.docs.coordination),
69 )
70 )
71
72 if config.vcs.regime == "muse+git-mirror":
73 template_specs.extend(
74 [
75 (
76 "templates/MUSE-BRIDGE-WORKFLOW.template.md",
77 MUSE_BRIDGE_WORKFLOW_DEST,
78 ),
79 (
80 "templates/scripts/muse-bridge-deploy.sh.template",
81 MUSE_BRIDGE_DEPLOY_DEST,
82 ),
83 ]
84 )
85
86 destinations: set[str] = set()
87 for source_rel, dest in template_specs:
88 if dest in destinations:
89 raise ConfigError(
90 f"duplicate footprint destination {dest!r} (config collision)",
91 None,
92 )
93 destinations.add(dest)
94 template_path = root / source_rel
95 rendered = render_template(template_path, config)
96 files.append(
97 FootprintFile(
98 destination=dest,
99 source=source_rel,
100 content=rendered.encode("utf-8"),
101 )
102 )
103
104 policy_dir = root / "policy"
105 for src in sorted(policy_dir.glob("*.yaml")):
106 dest = f".overseer/policy/{src.name}"
107 if dest in destinations:
108 raise ConfigError(f"duplicate footprint destination {dest!r}", None)
109 destinations.add(dest)
110 files.append(
111 FootprintFile(
112 destination=dest,
113 source=f"policy/{src.name}",
114 content=src.read_bytes(),
115 )
116 )
117
118 rules_dir = root / "cursor" / "rules"
119 if rules_dir.is_dir():
120 for src in sorted(rules_dir.iterdir()):
121 if not src.is_file():
122 continue
123 dest = f".cursor/rules/{src.name}"
124 if dest in destinations:
125 raise ConfigError(f"duplicate footprint destination {dest!r}", None)
126 destinations.add(dest)
127 files.append(
128 FootprintFile(
129 destination=dest,
130 source=f"cursor/rules/{src.name}",
131 content=src.read_bytes(),
132 )
133 )
134
135 skills_dir = root / "cursor" / "skills"
136 if skills_dir.is_dir():
137 for src in sorted(skills_dir.rglob("*")):
138 if not src.is_file():
139 continue
140 rel = src.relative_to(skills_dir)
141 content = src.read_bytes()
142 source = f"cursor/skills/{rel.as_posix()}"
143 # Cursor Agent Skills + Claude Code project skills (same bytes).
144 for dest_prefix in (".cursor/skills", ".claude/skills"):
145 dest = f"{dest_prefix}/{rel.as_posix()}"
146 if dest in destinations:
147 raise ConfigError(f"duplicate footprint destination {dest!r}", None)
148 destinations.add(dest)
149 files.append(
150 FootprintFile(
151 destination=dest,
152 source=source,
153 content=content,
154 )
155 )
156
157 if config.session_bookends.enabled:
158 for source_rel, dest in SESSION_BOOKEND_SPECS:
159 if dest in destinations:
160 raise ConfigError(f"duplicate footprint destination {dest!r}", None)
161 destinations.add(dest)
162 source_path = root / source_rel
163 files.append(
164 FootprintFile(
165 destination=dest,
166 source=source_rel,
167 content=source_path.read_bytes(),
168 )
169 )
170
171 files.sort(key=lambda item: item.destination)
172 return files
173
174
175 def footprint_tuples(files: list[FootprintFile]) -> list[tuple[str, str, bytes]]:
176 """Return ``(destination, source, bytes)`` tuples for lock building."""
177 return [(f.destination, f.source, f.content) for f in files]
File History 1 commit
sha256:6abcf1fa82a7a621ccbc945f19acdba5bc0db54569599404a1452fb4a096a199 fix(ISR): default require_independent_second_reviewer to require Human minor 7 hours ago