engineering-process 0.1.0__py3-none-any.whl
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.
- engineering_process/__init__.py +3 -0
- engineering_process/_download_worker.py +73 -0
- engineering_process/_supervisor_posix.py +162 -0
- engineering_process/_supervisor_windows.py +148 -0
- engineering_process/_windows_job.py +419 -0
- engineering_process/bootstrap.py +217 -0
- engineering_process/bundles.py +81 -0
- engineering_process/cli.py +1012 -0
- engineering_process/contracts.py +1274 -0
- engineering_process/distribution.py +90 -0
- engineering_process/environment.py +753 -0
- engineering_process/helper_launch.py +37 -0
- engineering_process/lifecycle.py +1024 -0
- engineering_process/managed.py +89 -0
- engineering_process/markdown.py +78 -0
- engineering_process/publication.py +463 -0
- engineering_process/requirements-build.txt +1 -0
- engineering_process/requirements-dev.txt +10 -0
- engineering_process/requirements-runtime.txt +3 -0
- engineering_process/runner.py +191 -0
- engineering_process/runtime.py +55 -0
- engineering_process/skills.py +118 -0
- engineering_process/supervision.py +72 -0
- engineering_process/syncing.py +398 -0
- engineering_process/tooling.py +891 -0
- engineering_process-0.1.0.data/data/share/engineering-process/bundles.json +47 -0
- engineering_process-0.1.0.data/data/share/engineering-process/examples/change.json +26 -0
- engineering_process-0.1.0.data/data/share/engineering-process/examples/plan.json +28 -0
- engineering_process-0.1.0.data/data/share/engineering-process/examples/project.json +54 -0
- engineering_process-0.1.0.data/data/share/engineering-process/examples/review.json +31 -0
- engineering_process-0.1.0.data/data/share/engineering-process/schemas/change.schema.json +88 -0
- engineering_process-0.1.0.data/data/share/engineering-process/schemas/lifecycle.schema.json +122 -0
- engineering_process-0.1.0.data/data/share/engineering-process/schemas/plan.schema.json +105 -0
- engineering_process-0.1.0.data/data/share/engineering-process/schemas/process-lock.schema.json +39 -0
- engineering_process-0.1.0.data/data/share/engineering-process/schemas/project.schema.json +515 -0
- engineering_process-0.1.0.data/data/share/engineering-process/schemas/review.schema.json +140 -0
- engineering_process-0.1.0.data/data/share/engineering-process/schemas/reviewer-attestation.schema.json +30 -0
- engineering_process-0.1.0.data/data/share/engineering-process/schemas/verification.schema.json +120 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/assess-design/SKILL.md +39 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/build-frontend/SKILL.md +38 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/build-frontend-foundation/SKILL.md +36 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/change-api/SKILL.md +36 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/cross-repo-change/SKILL.md +37 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/define-change-contract/SKILL.md +40 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/design-module/SKILL.md +37 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/evolve-process/SKILL.md +37 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/finish-change/SKILL.md +34 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/govern-ui/SKILL.md +40 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/implement-change/SKILL.md +41 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/implement-module/SKILL.md +36 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/implement-use-case/SKILL.md +36 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/integrate-mcp/SKILL.md +37 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/maintain-docs/SKILL.md +35 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/plan-change/SKILL.md +37 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/publish-change/SKILL.md +38 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/review-change/SKILL.md +49 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/run-change/SKILL.md +51 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/run-change/references/execution.md +88 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/run-project-command/SKILL.md +55 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/specify-use-case/SKILL.md +37 -0
- engineering_process-0.1.0.data/data/share/engineering-process/skills/verify-change/SKILL.md +38 -0
- engineering_process-0.1.0.data/data/share/engineering-process/templates/AGENTS.process.md +17 -0
- engineering_process-0.1.0.data/data/share/engineering-process/templates/PULL_REQUEST_TEMPLATE.md +27 -0
- engineering_process-0.1.0.dist-info/METADATA +387 -0
- engineering_process-0.1.0.dist-info/RECORD +69 -0
- engineering_process-0.1.0.dist-info/WHEEL +5 -0
- engineering_process-0.1.0.dist-info/entry_points.txt +2 -0
- engineering_process-0.1.0.dist-info/licenses/LICENSE +21 -0
- engineering_process-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import sysconfig
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from . import VERSION
|
|
11
|
+
from .bundles import load_bundles
|
|
12
|
+
from .contracts import ContractError, ProcessLock, read_json, validate_process_lock
|
|
13
|
+
from .distribution import asset_root, distribution_digest, skills_root
|
|
14
|
+
from .managed import (
|
|
15
|
+
managed_agents_block,
|
|
16
|
+
managed_agents_visibility_issues,
|
|
17
|
+
merge_managed_agents,
|
|
18
|
+
)
|
|
19
|
+
from .publication import (
|
|
20
|
+
managed_pull_request_block,
|
|
21
|
+
managed_pull_request_visibility_issues,
|
|
22
|
+
merge_managed_pull_request_template,
|
|
23
|
+
validate_project_extensions,
|
|
24
|
+
)
|
|
25
|
+
from .skills import MARKER_NAME, validate_skills
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def default_process_root() -> Path:
|
|
29
|
+
source_root = Path(__file__).resolve().parent.parent
|
|
30
|
+
candidates = (source_root, Path(sysconfig.get_path("data")).resolve())
|
|
31
|
+
for candidate in candidates:
|
|
32
|
+
try:
|
|
33
|
+
asset_root(candidate)
|
|
34
|
+
except ContractError:
|
|
35
|
+
continue
|
|
36
|
+
return candidate
|
|
37
|
+
return candidates[-1]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def process_skills_root(process_root: Path) -> Path:
|
|
41
|
+
return skills_root(process_root)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_lock(project_root: Path) -> ProcessLock:
|
|
45
|
+
path = project_root / ".process" / "process.lock"
|
|
46
|
+
return validate_process_lock(read_json(path), str(path))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _marker(lock: ProcessLock, skill: str) -> dict[str, object]:
|
|
50
|
+
return {
|
|
51
|
+
"schemaVersion": 1,
|
|
52
|
+
"distribution": "engineering-process",
|
|
53
|
+
"version": lock.version,
|
|
54
|
+
"digest": lock.digest,
|
|
55
|
+
"skill": skill,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _read_marker(path: Path) -> dict[str, object] | None:
|
|
60
|
+
marker_path = path / MARKER_NAME
|
|
61
|
+
if not marker_path.is_file():
|
|
62
|
+
return None
|
|
63
|
+
value = read_json(marker_path)
|
|
64
|
+
return value if isinstance(value, dict) else None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _files(path: Path, *, ignore_marker: bool) -> dict[str, bytes]:
|
|
68
|
+
result: dict[str, bytes] = {}
|
|
69
|
+
if not path.is_dir():
|
|
70
|
+
return result
|
|
71
|
+
for item in sorted(candidate for candidate in path.rglob("*") if candidate.is_file()):
|
|
72
|
+
if ignore_marker and item.name == MARKER_NAME:
|
|
73
|
+
continue
|
|
74
|
+
result[item.relative_to(path).as_posix()] = item.read_bytes()
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def managed_parent_issues(project_root: Path) -> list[str]:
|
|
79
|
+
issues: list[str] = []
|
|
80
|
+
for path in (
|
|
81
|
+
project_root / ".process",
|
|
82
|
+
project_root / ".agents",
|
|
83
|
+
project_root / ".agents" / "skills",
|
|
84
|
+
project_root / ".github",
|
|
85
|
+
):
|
|
86
|
+
if path.is_symlink():
|
|
87
|
+
issues.append(f"{path}: managed parent must not be a symlink")
|
|
88
|
+
return issues
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def skill_target_ownership_issues(project_root: Path) -> list[str]:
|
|
92
|
+
target_root = project_root / ".agents" / "skills"
|
|
93
|
+
if target_root.is_symlink():
|
|
94
|
+
return [f"{target_root}: managed skills root must not be a symlink"]
|
|
95
|
+
if not os.path.lexists(target_root):
|
|
96
|
+
return []
|
|
97
|
+
if not target_root.is_dir():
|
|
98
|
+
return [f"{target_root}: managed skills root must be a directory"]
|
|
99
|
+
|
|
100
|
+
issues: list[str] = []
|
|
101
|
+
for target in target_root.iterdir():
|
|
102
|
+
if target.is_symlink():
|
|
103
|
+
issues.append(f"{target}: unmanaged symlink in managed skills root")
|
|
104
|
+
continue
|
|
105
|
+
if target.is_file():
|
|
106
|
+
issues.append(
|
|
107
|
+
f"{target}: unmanaged project skill asset; process capabilities must come "
|
|
108
|
+
"from the pinned engineering-process distribution"
|
|
109
|
+
)
|
|
110
|
+
continue
|
|
111
|
+
if not target.is_dir():
|
|
112
|
+
issues.append(f"{target}: unsupported entry in managed skills root")
|
|
113
|
+
continue
|
|
114
|
+
if not (target / "SKILL.md").is_file():
|
|
115
|
+
continue
|
|
116
|
+
marker = _read_marker(target)
|
|
117
|
+
if not marker or marker.get("distribution") != "engineering-process":
|
|
118
|
+
issues.append(
|
|
119
|
+
f"{target}: unmanaged project skill; process capabilities must come "
|
|
120
|
+
"from the pinned engineering-process distribution"
|
|
121
|
+
)
|
|
122
|
+
return issues
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def selected_skill_target_issues(
|
|
126
|
+
project_root: Path, skills: tuple[str, ...]
|
|
127
|
+
) -> list[str]:
|
|
128
|
+
target_root = project_root / ".agents" / "skills"
|
|
129
|
+
issues: list[str] = []
|
|
130
|
+
for skill in skills:
|
|
131
|
+
target = target_root / skill
|
|
132
|
+
if target.is_symlink():
|
|
133
|
+
issues.append(f"{target}: selected managed skill target must not be a symlink")
|
|
134
|
+
continue
|
|
135
|
+
if not os.path.lexists(target):
|
|
136
|
+
continue
|
|
137
|
+
if not target.is_dir():
|
|
138
|
+
issues.append(f"{target}: selected managed skill target must be a directory")
|
|
139
|
+
continue
|
|
140
|
+
marker = _read_marker(target)
|
|
141
|
+
if not marker or marker.get("distribution") != "engineering-process":
|
|
142
|
+
issues.append(f"{target}: refusing to overwrite an unmanaged skill target")
|
|
143
|
+
return issues
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _pull_request_template_source(process_root: Path) -> tuple[Path, str]:
|
|
147
|
+
path = asset_root(process_root) / "templates" / "PULL_REQUEST_TEMPLATE.md"
|
|
148
|
+
if not path.is_file():
|
|
149
|
+
raise ContractError(f"{path}: missing pull-request template")
|
|
150
|
+
try:
|
|
151
|
+
return path, path.read_text(encoding="utf-8")
|
|
152
|
+
except (OSError, UnicodeError) as error:
|
|
153
|
+
raise ContractError(f"{path}: cannot read pull-request template: {error}") from error
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _agents_source(process_root: Path) -> tuple[Path, str]:
|
|
157
|
+
path = asset_root(process_root) / "templates" / "AGENTS.process.md"
|
|
158
|
+
if not path.is_file():
|
|
159
|
+
raise ContractError(f"{path}: missing agent contract template")
|
|
160
|
+
try:
|
|
161
|
+
return path, path.read_text(encoding="utf-8")
|
|
162
|
+
except (OSError, UnicodeError) as error:
|
|
163
|
+
raise ContractError(f"{path}: cannot read agent contract template: {error}") from error
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _agents_issues(project_root: Path, process_root: Path) -> list[str]:
|
|
167
|
+
_, source = _agents_source(process_root)
|
|
168
|
+
target = project_root / "AGENTS.md"
|
|
169
|
+
if target.is_symlink():
|
|
170
|
+
return [f"{target}: managed agent contract must not be a symlink"]
|
|
171
|
+
if not target.is_file():
|
|
172
|
+
return [f"{target}: missing managed engineering-process agent contract"]
|
|
173
|
+
try:
|
|
174
|
+
current = target.read_text(encoding="utf-8")
|
|
175
|
+
source_block = managed_agents_block(source).strip()
|
|
176
|
+
target_block = managed_agents_block(current).strip()
|
|
177
|
+
except (OSError, UnicodeError, ContractError) as error:
|
|
178
|
+
return [f"{target}: invalid managed agent contract: {error}"]
|
|
179
|
+
if source_block != target_block:
|
|
180
|
+
issues = [
|
|
181
|
+
f"{target}: managed agent contract differs from the pinned distribution"
|
|
182
|
+
]
|
|
183
|
+
else:
|
|
184
|
+
issues = []
|
|
185
|
+
issues.extend(
|
|
186
|
+
f"{target}: {issue}" for issue in managed_agents_visibility_issues(current)
|
|
187
|
+
)
|
|
188
|
+
return issues
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _sync_agents(project_root: Path, process_root: Path) -> None:
|
|
192
|
+
_, source = _agents_source(process_root)
|
|
193
|
+
target = project_root / "AGENTS.md"
|
|
194
|
+
if target.is_symlink():
|
|
195
|
+
raise ContractError(f"{target}: managed agent contract must not be a symlink")
|
|
196
|
+
try:
|
|
197
|
+
current = target.read_text(encoding="utf-8") if target.is_file() else ""
|
|
198
|
+
except (OSError, UnicodeError) as error:
|
|
199
|
+
raise ContractError(f"{target}: cannot read agent contract: {error}") from error
|
|
200
|
+
updated = merge_managed_agents(current, source)
|
|
201
|
+
if current != updated:
|
|
202
|
+
target.write_text(updated, encoding="utf-8")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _pull_request_template_issues(
|
|
206
|
+
project_root: Path, process_root: Path
|
|
207
|
+
) -> list[str]:
|
|
208
|
+
_, source = _pull_request_template_source(process_root)
|
|
209
|
+
target = project_root / ".github" / "PULL_REQUEST_TEMPLATE.md"
|
|
210
|
+
if target.is_symlink():
|
|
211
|
+
return [f"{target}: managed pull-request template must not be a symlink"]
|
|
212
|
+
if not target.is_file():
|
|
213
|
+
return [f"{target}: missing managed pull-request template"]
|
|
214
|
+
try:
|
|
215
|
+
current = target.read_text(encoding="utf-8")
|
|
216
|
+
source_block = managed_pull_request_block(source).strip()
|
|
217
|
+
target_block = managed_pull_request_block(current).strip()
|
|
218
|
+
except (OSError, UnicodeError, ContractError) as error:
|
|
219
|
+
return [f"{target}: invalid managed pull-request template: {error}"]
|
|
220
|
+
issues = [
|
|
221
|
+
f"{target}: managed pull-request template differs from the pinned distribution"
|
|
222
|
+
] if source_block != target_block else []
|
|
223
|
+
issues.extend(
|
|
224
|
+
f"{target}: {issue}"
|
|
225
|
+
for issue in managed_pull_request_visibility_issues(current)
|
|
226
|
+
)
|
|
227
|
+
issues.extend(
|
|
228
|
+
f"{target}: {issue}"
|
|
229
|
+
for issue in validate_project_extensions(current, allow_pending=True)
|
|
230
|
+
)
|
|
231
|
+
return issues
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _sync_pull_request_template(project_root: Path, process_root: Path) -> None:
|
|
235
|
+
_, source = _pull_request_template_source(process_root)
|
|
236
|
+
target = project_root / ".github" / "PULL_REQUEST_TEMPLATE.md"
|
|
237
|
+
if target.is_symlink():
|
|
238
|
+
raise ContractError(f"{target}: managed pull-request template must not be a symlink")
|
|
239
|
+
try:
|
|
240
|
+
current = target.read_text(encoding="utf-8") if target.is_file() else ""
|
|
241
|
+
except (OSError, UnicodeError) as error:
|
|
242
|
+
raise ContractError(f"{target}: cannot read pull-request template: {error}") from error
|
|
243
|
+
updated = merge_managed_pull_request_template(current, source)
|
|
244
|
+
if current != updated:
|
|
245
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
246
|
+
target.write_text(updated, encoding="utf-8")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def synchronized_state(
|
|
250
|
+
project_root: Path,
|
|
251
|
+
process_root: Path,
|
|
252
|
+
lock: ProcessLock,
|
|
253
|
+
) -> list[str]:
|
|
254
|
+
source_root = process_skills_root(process_root)
|
|
255
|
+
target_root = project_root / ".agents" / "skills"
|
|
256
|
+
issues = validate_skills(source_root, lock.skills)
|
|
257
|
+
bundles = load_bundles(process_root, source_root)
|
|
258
|
+
missing_core = sorted(set(bundles["core"]) - set(lock.skills))
|
|
259
|
+
if missing_core:
|
|
260
|
+
issues.append(
|
|
261
|
+
"process.lock omits mandatory core skills: " + ", ".join(missing_core)
|
|
262
|
+
)
|
|
263
|
+
if issues:
|
|
264
|
+
return issues
|
|
265
|
+
if lock.version != VERSION:
|
|
266
|
+
issues.append(
|
|
267
|
+
f"process.lock pins {lock.version}, but processctl is {VERSION}"
|
|
268
|
+
)
|
|
269
|
+
actual_digest = distribution_digest(process_root, lock.skills)
|
|
270
|
+
if actual_digest != lock.digest:
|
|
271
|
+
issues.append(
|
|
272
|
+
f"process.lock digest {lock.digest} does not match source {actual_digest}"
|
|
273
|
+
)
|
|
274
|
+
for skill in lock.skills:
|
|
275
|
+
source = source_root / skill
|
|
276
|
+
target = target_root / skill
|
|
277
|
+
if _read_marker(target) != _marker(lock, skill):
|
|
278
|
+
issues.append(f"{target}: missing or stale managed-skill marker")
|
|
279
|
+
continue
|
|
280
|
+
if _files(source, ignore_marker=True) != _files(target, ignore_marker=True):
|
|
281
|
+
issues.append(f"{target}: content differs from the pinned skill")
|
|
282
|
+
issues.extend(skill_target_ownership_issues(project_root))
|
|
283
|
+
issues.extend(managed_parent_issues(project_root))
|
|
284
|
+
if target_root.is_dir():
|
|
285
|
+
for target in target_root.iterdir():
|
|
286
|
+
if not target.is_dir() or not (target / "SKILL.md").is_file():
|
|
287
|
+
continue
|
|
288
|
+
marker = _read_marker(target)
|
|
289
|
+
if (
|
|
290
|
+
marker
|
|
291
|
+
and marker.get("distribution") == "engineering-process"
|
|
292
|
+
and target.name not in lock.skills
|
|
293
|
+
):
|
|
294
|
+
issues.append(f"{target}: stale managed skill is not present in process.lock")
|
|
295
|
+
issues.extend(_agents_issues(project_root, process_root))
|
|
296
|
+
issues.extend(_pull_request_template_issues(project_root, process_root))
|
|
297
|
+
return issues
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def sync_skills(project_root: Path, process_root: Path, *, check: bool) -> list[str]:
|
|
301
|
+
project_root = project_root.resolve()
|
|
302
|
+
process_root = process_root.resolve()
|
|
303
|
+
lock = load_lock(project_root)
|
|
304
|
+
ownership_issues = [
|
|
305
|
+
*managed_parent_issues(project_root),
|
|
306
|
+
*skill_target_ownership_issues(project_root),
|
|
307
|
+
*selected_skill_target_issues(project_root, lock.skills),
|
|
308
|
+
]
|
|
309
|
+
if ownership_issues and not check:
|
|
310
|
+
raise ContractError("\n".join(ownership_issues))
|
|
311
|
+
issues = synchronized_state(project_root, process_root, lock)
|
|
312
|
+
if check or not issues:
|
|
313
|
+
return issues
|
|
314
|
+
|
|
315
|
+
source_root = process_skills_root(process_root)
|
|
316
|
+
source_issues = validate_skills(source_root, lock.skills)
|
|
317
|
+
bundles = load_bundles(process_root, source_root)
|
|
318
|
+
missing_core = sorted(set(bundles["core"]) - set(lock.skills))
|
|
319
|
+
if source_issues or missing_core:
|
|
320
|
+
details = list(source_issues)
|
|
321
|
+
if missing_core:
|
|
322
|
+
details.append(
|
|
323
|
+
"process.lock omits mandatory core skills: "
|
|
324
|
+
+ ", ".join(missing_core)
|
|
325
|
+
)
|
|
326
|
+
raise ContractError("\n".join(details))
|
|
327
|
+
if lock.version != VERSION:
|
|
328
|
+
raise ContractError(
|
|
329
|
+
f"process.lock pins {lock.version}, but processctl is {VERSION}"
|
|
330
|
+
)
|
|
331
|
+
actual_digest = distribution_digest(process_root, lock.skills)
|
|
332
|
+
if actual_digest != lock.digest:
|
|
333
|
+
raise ContractError(
|
|
334
|
+
f"process.lock digest {lock.digest} does not match source {actual_digest}"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
target_root = project_root / ".agents" / "skills"
|
|
338
|
+
existing_targets = [target_root / skill for skill in lock.skills]
|
|
339
|
+
stale_targets: list[Path] = []
|
|
340
|
+
if target_root.is_dir():
|
|
341
|
+
for target in target_root.iterdir():
|
|
342
|
+
if not target.is_dir():
|
|
343
|
+
continue
|
|
344
|
+
marker = _read_marker(target)
|
|
345
|
+
if marker and marker.get("distribution") == "engineering-process":
|
|
346
|
+
if target.name not in lock.skills:
|
|
347
|
+
stale_targets.append(target)
|
|
348
|
+
|
|
349
|
+
_sync_agents(project_root, process_root)
|
|
350
|
+
_sync_pull_request_template(project_root, process_root)
|
|
351
|
+
|
|
352
|
+
target_root.mkdir(parents=True, exist_ok=True)
|
|
353
|
+
|
|
354
|
+
stage_parent = target_root.parent
|
|
355
|
+
stage = Path(tempfile.mkdtemp(prefix=".engineering-process-stage-", dir=stage_parent))
|
|
356
|
+
staged_skills = stage / "skills"
|
|
357
|
+
backup = stage / "backup"
|
|
358
|
+
staged_skills.mkdir()
|
|
359
|
+
backup.mkdir()
|
|
360
|
+
try:
|
|
361
|
+
for skill in lock.skills:
|
|
362
|
+
target = staged_skills / skill
|
|
363
|
+
shutil.copytree(source_root / skill, target)
|
|
364
|
+
(target / MARKER_NAME).write_text(
|
|
365
|
+
json.dumps(
|
|
366
|
+
_marker(lock, skill),
|
|
367
|
+
ensure_ascii=False,
|
|
368
|
+
indent=2,
|
|
369
|
+
sort_keys=True,
|
|
370
|
+
)
|
|
371
|
+
+ "\n",
|
|
372
|
+
encoding="utf-8",
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
moved: list[Path] = []
|
|
376
|
+
installed: list[Path] = []
|
|
377
|
+
try:
|
|
378
|
+
for target in [*existing_targets, *stale_targets]:
|
|
379
|
+
if target.exists():
|
|
380
|
+
target.rename(backup / target.name)
|
|
381
|
+
moved.append(target)
|
|
382
|
+
for skill in lock.skills:
|
|
383
|
+
target = target_root / skill
|
|
384
|
+
(staged_skills / skill).rename(target)
|
|
385
|
+
installed.append(target)
|
|
386
|
+
except Exception:
|
|
387
|
+
for target in installed:
|
|
388
|
+
if target.exists():
|
|
389
|
+
shutil.rmtree(target)
|
|
390
|
+
for original in moved:
|
|
391
|
+
saved = backup / original.name
|
|
392
|
+
if saved.exists():
|
|
393
|
+
saved.rename(original)
|
|
394
|
+
raise
|
|
395
|
+
finally:
|
|
396
|
+
shutil.rmtree(stage, ignore_errors=True)
|
|
397
|
+
|
|
398
|
+
return synchronized_state(project_root, process_root, lock)
|