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,1012 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Callable
|
|
9
|
+
|
|
10
|
+
from . import VERSION
|
|
11
|
+
from .bundles import load_bundles, select_bundles
|
|
12
|
+
from .bootstrap import initialize_project
|
|
13
|
+
from .contracts import (
|
|
14
|
+
ContractError,
|
|
15
|
+
read_json,
|
|
16
|
+
validate_change,
|
|
17
|
+
validate_plan,
|
|
18
|
+
validate_process_lock,
|
|
19
|
+
validate_project,
|
|
20
|
+
validate_review,
|
|
21
|
+
)
|
|
22
|
+
from .distribution import distribution_digest
|
|
23
|
+
from .environment import (
|
|
24
|
+
doctor_environment,
|
|
25
|
+
environment_command_bindings,
|
|
26
|
+
environment_path_entries,
|
|
27
|
+
execute_command,
|
|
28
|
+
require_environment_profile,
|
|
29
|
+
setup_environment,
|
|
30
|
+
)
|
|
31
|
+
from .lifecycle import (
|
|
32
|
+
begin_implementation,
|
|
33
|
+
finish_change,
|
|
34
|
+
lifecycle_environment_issues,
|
|
35
|
+
lifecycle_status,
|
|
36
|
+
register_plan,
|
|
37
|
+
start_change,
|
|
38
|
+
start_review,
|
|
39
|
+
submit_review,
|
|
40
|
+
verify_change,
|
|
41
|
+
)
|
|
42
|
+
from .runner import run_profile
|
|
43
|
+
from .publication import (
|
|
44
|
+
validate_branch,
|
|
45
|
+
validate_commit_range,
|
|
46
|
+
validate_commit_subject,
|
|
47
|
+
validate_pull_request,
|
|
48
|
+
)
|
|
49
|
+
from .skills import validate_skills
|
|
50
|
+
from .syncing import (
|
|
51
|
+
default_process_root,
|
|
52
|
+
load_lock,
|
|
53
|
+
process_skills_root,
|
|
54
|
+
sync_skills,
|
|
55
|
+
synchronized_state,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _root(value: str) -> Path:
|
|
60
|
+
path = Path(value).resolve()
|
|
61
|
+
if not path.is_dir():
|
|
62
|
+
raise argparse.ArgumentTypeError(f"directory does not exist: {value}")
|
|
63
|
+
return path
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _write_json(value: Any) -> None:
|
|
67
|
+
print(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _result(name: str, *, status: str = "passed", **details: Any) -> dict[str, Any]:
|
|
71
|
+
return {"command": name, "status": status, **details}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _emit(args: argparse.Namespace, value: dict[str, Any]) -> None:
|
|
75
|
+
if args.json:
|
|
76
|
+
_write_json(value)
|
|
77
|
+
return
|
|
78
|
+
status = value["status"].upper()
|
|
79
|
+
print(f"{value['command']}: {status}")
|
|
80
|
+
for key, item in value.items():
|
|
81
|
+
if key in {"command", "status"}:
|
|
82
|
+
continue
|
|
83
|
+
if isinstance(item, list):
|
|
84
|
+
for entry in item:
|
|
85
|
+
print(f" {key}: {entry}")
|
|
86
|
+
else:
|
|
87
|
+
print(f" {key}: {item}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def command_project_validate(args: argparse.Namespace) -> int:
|
|
91
|
+
path = args.project_root / ".process" / "project.json"
|
|
92
|
+
project = validate_project(read_json(path), str(path))
|
|
93
|
+
_emit(
|
|
94
|
+
args,
|
|
95
|
+
_result(
|
|
96
|
+
"project validate",
|
|
97
|
+
project=project.identifier,
|
|
98
|
+
profiles=sorted(project.profiles),
|
|
99
|
+
requiredProfiles=list(project.required_profiles),
|
|
100
|
+
environmentProfiles=(
|
|
101
|
+
sorted(project.environment.profiles) if project.environment else []
|
|
102
|
+
),
|
|
103
|
+
managedTools=(
|
|
104
|
+
sorted(project.environment.managed_tools)
|
|
105
|
+
if project.environment
|
|
106
|
+
else []
|
|
107
|
+
),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
return 0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def command_project_init(args: argparse.Namespace) -> int:
|
|
114
|
+
result = initialize_project(
|
|
115
|
+
args.project_root,
|
|
116
|
+
args.process_root,
|
|
117
|
+
manifest_path=args.manifest,
|
|
118
|
+
requested_bundles=args.bundle,
|
|
119
|
+
replace=args.replace,
|
|
120
|
+
)
|
|
121
|
+
_emit(args, _result("project init", **result))
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def command_lock_validate(args: argparse.Namespace) -> int:
|
|
126
|
+
path = args.project_root / ".process" / "process.lock"
|
|
127
|
+
lock = validate_process_lock(read_json(path), str(path))
|
|
128
|
+
bundles = load_bundles(args.process_root, process_skills_root(args.process_root))
|
|
129
|
+
missing_core = sorted(set(bundles["core"]) - set(lock.skills))
|
|
130
|
+
if missing_core:
|
|
131
|
+
raise ContractError(
|
|
132
|
+
f"{path}: omits mandatory core skills: {', '.join(missing_core)}"
|
|
133
|
+
)
|
|
134
|
+
if lock.version != VERSION:
|
|
135
|
+
raise ContractError(
|
|
136
|
+
f"{path}: pins {lock.version}, but processctl is {VERSION}"
|
|
137
|
+
)
|
|
138
|
+
actual_digest = distribution_digest(args.process_root, lock.skills)
|
|
139
|
+
if actual_digest != lock.digest:
|
|
140
|
+
raise ContractError(
|
|
141
|
+
f"{path}: digest {lock.digest} does not match source {actual_digest}"
|
|
142
|
+
)
|
|
143
|
+
_emit(
|
|
144
|
+
args,
|
|
145
|
+
_result(
|
|
146
|
+
"lock validate",
|
|
147
|
+
version=lock.version,
|
|
148
|
+
digest=lock.digest,
|
|
149
|
+
skills=list(lock.skills),
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def command_lock_create(args: argparse.Namespace) -> int:
|
|
156
|
+
lock_path = args.project_root / ".process" / "process.lock"
|
|
157
|
+
if lock_path.exists() and not args.replace:
|
|
158
|
+
raise ContractError(
|
|
159
|
+
f"{lock_path}: already exists; use --replace for an intentional update"
|
|
160
|
+
)
|
|
161
|
+
skills_root = process_skills_root(args.process_root)
|
|
162
|
+
bundles = load_bundles(args.process_root, skills_root)
|
|
163
|
+
requested = select_bundles(bundles, args.bundle)
|
|
164
|
+
skills = tuple(
|
|
165
|
+
sorted(
|
|
166
|
+
{
|
|
167
|
+
skill
|
|
168
|
+
for bundle in requested
|
|
169
|
+
for skill in bundles[bundle]
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
document = {
|
|
174
|
+
"schemaVersion": 1,
|
|
175
|
+
"process": {
|
|
176
|
+
"version": VERSION,
|
|
177
|
+
"digest": distribution_digest(args.process_root, skills),
|
|
178
|
+
},
|
|
179
|
+
"skills": list(skills),
|
|
180
|
+
}
|
|
181
|
+
validate_process_lock(document, str(lock_path))
|
|
182
|
+
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
183
|
+
lock_path.write_text(
|
|
184
|
+
json.dumps(document, ensure_ascii=False, indent=2) + "\n",
|
|
185
|
+
encoding="utf-8",
|
|
186
|
+
)
|
|
187
|
+
_emit(
|
|
188
|
+
args,
|
|
189
|
+
_result(
|
|
190
|
+
"lock create",
|
|
191
|
+
path=str(lock_path),
|
|
192
|
+
version=VERSION,
|
|
193
|
+
digest=document["process"]["digest"],
|
|
194
|
+
skills=list(skills),
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
return 0
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def command_contract_validate(args: argparse.Namespace) -> int:
|
|
201
|
+
document = read_json(args.path)
|
|
202
|
+
validators: dict[str, Callable[[Any, str], None]] = {
|
|
203
|
+
"change": validate_change,
|
|
204
|
+
"plan": validate_plan,
|
|
205
|
+
"review": validate_review,
|
|
206
|
+
}
|
|
207
|
+
validators[args.kind](document, str(args.path))
|
|
208
|
+
_emit(
|
|
209
|
+
args,
|
|
210
|
+
_result(
|
|
211
|
+
"contract validate",
|
|
212
|
+
kind=args.kind,
|
|
213
|
+
path=str(args.path),
|
|
214
|
+
),
|
|
215
|
+
)
|
|
216
|
+
return 0
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _lifecycle_project(args: argparse.Namespace):
|
|
220
|
+
lock = load_lock(args.project_root)
|
|
221
|
+
issues = synchronized_state(args.project_root, args.process_root, lock)
|
|
222
|
+
if issues:
|
|
223
|
+
raise ContractError("\n".join(issues))
|
|
224
|
+
environment_issues = lifecycle_environment_issues(args.project_root)
|
|
225
|
+
if environment_issues:
|
|
226
|
+
raise ContractError("\n".join(environment_issues))
|
|
227
|
+
path = args.project_root / ".process" / "project.json"
|
|
228
|
+
return validate_project(read_json(path), str(path))
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _change_result(command: str, state: dict[str, Any], **details: Any) -> dict[str, Any]:
|
|
232
|
+
return _result(
|
|
233
|
+
command,
|
|
234
|
+
changeId=state["changeId"],
|
|
235
|
+
phase=state["phase"],
|
|
236
|
+
cycle=state["cycle"],
|
|
237
|
+
revision=state["revision"],
|
|
238
|
+
**details,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def command_change_start(args: argparse.Namespace) -> int:
|
|
243
|
+
project = _lifecycle_project(args)
|
|
244
|
+
state = start_change(
|
|
245
|
+
args.project_root,
|
|
246
|
+
project,
|
|
247
|
+
args.contract,
|
|
248
|
+
actor_id=args.actor,
|
|
249
|
+
context_id=args.context,
|
|
250
|
+
kind=args.actor_kind,
|
|
251
|
+
)
|
|
252
|
+
_emit(
|
|
253
|
+
args,
|
|
254
|
+
_change_result(
|
|
255
|
+
"change start",
|
|
256
|
+
state,
|
|
257
|
+
contract=state["contract"]["path"],
|
|
258
|
+
contractDigest=state["contract"]["digest"],
|
|
259
|
+
comparisonBase=state["comparisonBase"],
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
return 0
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def command_change_plan(args: argparse.Namespace) -> int:
|
|
266
|
+
project = _lifecycle_project(args)
|
|
267
|
+
state = register_plan(
|
|
268
|
+
args.project_root,
|
|
269
|
+
project,
|
|
270
|
+
args.change_id,
|
|
271
|
+
args.plan,
|
|
272
|
+
actor_id=args.actor,
|
|
273
|
+
context_id=args.context,
|
|
274
|
+
kind=args.actor_kind,
|
|
275
|
+
)
|
|
276
|
+
_emit(
|
|
277
|
+
args,
|
|
278
|
+
_change_result("change plan", state, plan=state["plan"]["path"]),
|
|
279
|
+
)
|
|
280
|
+
return 0
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def command_change_implement(args: argparse.Namespace) -> int:
|
|
284
|
+
_lifecycle_project(args)
|
|
285
|
+
state = begin_implementation(
|
|
286
|
+
args.project_root,
|
|
287
|
+
args.change_id,
|
|
288
|
+
actor_id=args.actor,
|
|
289
|
+
context_id=args.context,
|
|
290
|
+
kind=args.actor_kind,
|
|
291
|
+
)
|
|
292
|
+
_emit(args, _change_result("change implement", state))
|
|
293
|
+
return 0
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def command_change_verify(args: argparse.Namespace) -> int:
|
|
297
|
+
project = _lifecycle_project(args)
|
|
298
|
+
state, report = verify_change(
|
|
299
|
+
args.project_root,
|
|
300
|
+
project,
|
|
301
|
+
args.change_id,
|
|
302
|
+
args.profile,
|
|
303
|
+
actor_id=args.actor,
|
|
304
|
+
context_id=args.context,
|
|
305
|
+
kind=args.actor_kind,
|
|
306
|
+
)
|
|
307
|
+
_emit(
|
|
308
|
+
args,
|
|
309
|
+
_change_result(
|
|
310
|
+
"change verify",
|
|
311
|
+
state,
|
|
312
|
+
profile=args.profile,
|
|
313
|
+
checkpoint=report["checkpoint"],
|
|
314
|
+
verificationStatus=report["status"],
|
|
315
|
+
report=next(
|
|
316
|
+
item["path"]
|
|
317
|
+
for item in state["verification"]
|
|
318
|
+
if item["profile"] == args.profile
|
|
319
|
+
),
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
return 0
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def command_change_review_start(args: argparse.Namespace) -> int:
|
|
326
|
+
_lifecycle_project(args)
|
|
327
|
+
state, assignment = start_review(
|
|
328
|
+
args.project_root,
|
|
329
|
+
args.change_id,
|
|
330
|
+
actor_id=args.actor,
|
|
331
|
+
context_id=args.context,
|
|
332
|
+
kind=args.actor_kind,
|
|
333
|
+
method=args.method,
|
|
334
|
+
attested_by=args.attested_by,
|
|
335
|
+
evidence=args.attestation_evidence,
|
|
336
|
+
)
|
|
337
|
+
_emit(
|
|
338
|
+
args,
|
|
339
|
+
_change_result(
|
|
340
|
+
"change review start",
|
|
341
|
+
state,
|
|
342
|
+
checkpoint=assignment["checkpoint"],
|
|
343
|
+
comparisonBase=assignment["comparisonBase"],
|
|
344
|
+
request=assignment["path"],
|
|
345
|
+
),
|
|
346
|
+
)
|
|
347
|
+
return 0
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def command_change_review_submit(args: argparse.Namespace) -> int:
|
|
351
|
+
_lifecycle_project(args)
|
|
352
|
+
state = submit_review(args.project_root, args.change_id, args.report)
|
|
353
|
+
_emit(
|
|
354
|
+
args,
|
|
355
|
+
_change_result(
|
|
356
|
+
"change review submit",
|
|
357
|
+
state,
|
|
358
|
+
report=state["review"]["path"],
|
|
359
|
+
),
|
|
360
|
+
)
|
|
361
|
+
return 0
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def command_change_finish(args: argparse.Namespace) -> int:
|
|
365
|
+
_lifecycle_project(args)
|
|
366
|
+
state, completion = finish_change(
|
|
367
|
+
args.project_root,
|
|
368
|
+
args.change_id,
|
|
369
|
+
actor_id=args.actor,
|
|
370
|
+
context_id=args.context,
|
|
371
|
+
kind=args.actor_kind,
|
|
372
|
+
)
|
|
373
|
+
_emit(
|
|
374
|
+
args,
|
|
375
|
+
_change_result(
|
|
376
|
+
"change finish",
|
|
377
|
+
state,
|
|
378
|
+
checkpoint=completion["checkpoint"],
|
|
379
|
+
completion=state["completion"]["path"],
|
|
380
|
+
),
|
|
381
|
+
)
|
|
382
|
+
return 0
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def command_change_status(args: argparse.Namespace) -> int:
|
|
386
|
+
_lifecycle_project(args)
|
|
387
|
+
state = lifecycle_status(args.project_root, args.change_id)
|
|
388
|
+
value = _change_result(
|
|
389
|
+
"change status",
|
|
390
|
+
state,
|
|
391
|
+
status="passed" if state["current"] else "failed",
|
|
392
|
+
current=state["current"],
|
|
393
|
+
issues=state["issues"],
|
|
394
|
+
contract=state["contract"],
|
|
395
|
+
plan=state["plan"],
|
|
396
|
+
implementationActors=state["implementationActors"],
|
|
397
|
+
verification=state["verification"],
|
|
398
|
+
pendingFindings=state["pendingFindings"],
|
|
399
|
+
reviewAssignment=state["reviewAssignment"],
|
|
400
|
+
review=state["review"],
|
|
401
|
+
completion=state["completion"],
|
|
402
|
+
)
|
|
403
|
+
_emit(args, value)
|
|
404
|
+
return 0 if state["current"] else 1
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def command_skills_validate(args: argparse.Namespace) -> int:
|
|
408
|
+
issues = validate_skills(args.root)
|
|
409
|
+
value = _result(
|
|
410
|
+
"skills validate",
|
|
411
|
+
status="failed" if issues else "passed",
|
|
412
|
+
root=str(args.root),
|
|
413
|
+
issues=issues,
|
|
414
|
+
)
|
|
415
|
+
_emit(args, value)
|
|
416
|
+
return 1 if issues else 0
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def command_digest(args: argparse.Namespace) -> int:
|
|
420
|
+
lock = load_lock(args.project_root) if args.project_root else None
|
|
421
|
+
root = process_skills_root(args.process_root)
|
|
422
|
+
if lock and args.bundle:
|
|
423
|
+
raise ContractError("--project-root and --bundle cannot be combined")
|
|
424
|
+
selected = lock.skills if lock else None
|
|
425
|
+
if args.bundle:
|
|
426
|
+
bundles = load_bundles(args.process_root, root)
|
|
427
|
+
requested = select_bundles(bundles, args.bundle)
|
|
428
|
+
selected = tuple(
|
|
429
|
+
sorted(
|
|
430
|
+
{
|
|
431
|
+
skill
|
|
432
|
+
for bundle in requested
|
|
433
|
+
for skill in bundles[bundle]
|
|
434
|
+
}
|
|
435
|
+
)
|
|
436
|
+
)
|
|
437
|
+
digest = distribution_digest(args.process_root, selected or tuple(
|
|
438
|
+
sorted(path.name for path in root.iterdir() if path.is_dir() and (path / "SKILL.md").is_file())
|
|
439
|
+
))
|
|
440
|
+
_emit(
|
|
441
|
+
args,
|
|
442
|
+
_result(
|
|
443
|
+
"digest",
|
|
444
|
+
digest=digest,
|
|
445
|
+
skills=list(selected) if selected else sorted(
|
|
446
|
+
path.name
|
|
447
|
+
for path in root.iterdir()
|
|
448
|
+
if path.is_dir() and (path / "SKILL.md").is_file()
|
|
449
|
+
),
|
|
450
|
+
),
|
|
451
|
+
)
|
|
452
|
+
return 0
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def command_sync(args: argparse.Namespace) -> int:
|
|
456
|
+
issues = sync_skills(
|
|
457
|
+
args.project_root,
|
|
458
|
+
args.process_root,
|
|
459
|
+
check=args.check,
|
|
460
|
+
)
|
|
461
|
+
status = "failed" if issues else "passed"
|
|
462
|
+
_emit(
|
|
463
|
+
args,
|
|
464
|
+
_result(
|
|
465
|
+
"sync check" if args.check else "sync",
|
|
466
|
+
status=status,
|
|
467
|
+
projectRoot=str(args.project_root),
|
|
468
|
+
processRoot=str(args.process_root),
|
|
469
|
+
issues=issues,
|
|
470
|
+
),
|
|
471
|
+
)
|
|
472
|
+
return 1 if issues else 0
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def command_doctor(args: argparse.Namespace) -> int:
|
|
476
|
+
project_path = args.project_root / ".process" / "project.json"
|
|
477
|
+
project = validate_project(read_json(project_path), str(project_path))
|
|
478
|
+
lock = load_lock(args.project_root)
|
|
479
|
+
issues = synchronized_state(args.project_root, args.process_root, lock)
|
|
480
|
+
issues.extend(lifecycle_environment_issues(args.project_root))
|
|
481
|
+
environment = doctor_environment(
|
|
482
|
+
args.project_root,
|
|
483
|
+
project,
|
|
484
|
+
profile=args.profile,
|
|
485
|
+
)
|
|
486
|
+
if environment["status"] == "failed":
|
|
487
|
+
issues.extend(
|
|
488
|
+
f"environment requirement {requirement['id']}: "
|
|
489
|
+
f"{requirement['remediation']}"
|
|
490
|
+
for requirement in environment["requirements"]
|
|
491
|
+
if requirement["status"] != "satisfied"
|
|
492
|
+
)
|
|
493
|
+
value = _result(
|
|
494
|
+
"doctor",
|
|
495
|
+
status="failed" if issues else "passed",
|
|
496
|
+
project=project.identifier,
|
|
497
|
+
processVersion=lock.version,
|
|
498
|
+
profiles=sorted(project.profiles),
|
|
499
|
+
requiredProfiles=list(project.required_profiles),
|
|
500
|
+
skills=list(lock.skills),
|
|
501
|
+
environment=environment,
|
|
502
|
+
issues=issues,
|
|
503
|
+
)
|
|
504
|
+
_emit(args, value)
|
|
505
|
+
return 1 if issues else 0
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def command_setup(args: argparse.Namespace) -> int:
|
|
509
|
+
project_path = args.project_root / ".process" / "project.json"
|
|
510
|
+
project = validate_project(read_json(project_path), str(project_path))
|
|
511
|
+
lock = load_lock(args.project_root)
|
|
512
|
+
issues = synchronized_state(args.project_root, args.process_root, lock)
|
|
513
|
+
if issues:
|
|
514
|
+
raise ContractError("\n".join(issues))
|
|
515
|
+
report = setup_environment(
|
|
516
|
+
args.project_root,
|
|
517
|
+
project,
|
|
518
|
+
profile=args.profile,
|
|
519
|
+
apply=args.apply,
|
|
520
|
+
allowed_mutations=set(args.allow),
|
|
521
|
+
)
|
|
522
|
+
_emit(args, _result("setup", **report))
|
|
523
|
+
return 0 if report["status"] in {"passed", "planned"} else 1
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def command_exec(args: argparse.Namespace) -> int:
|
|
527
|
+
run = args.run[1:] if args.run[:1] == ["--"] else args.run
|
|
528
|
+
if not run:
|
|
529
|
+
raise ContractError("exec requires a command after `--`")
|
|
530
|
+
if args.timeout_seconds < 1 or args.timeout_seconds > 86_400:
|
|
531
|
+
raise ContractError("--timeout-seconds must be from 1 to 86400")
|
|
532
|
+
lock = load_lock(args.project_root)
|
|
533
|
+
issues = synchronized_state(args.project_root, args.process_root, lock)
|
|
534
|
+
if issues:
|
|
535
|
+
raise ContractError("\n".join(issues))
|
|
536
|
+
project_path = args.project_root / ".process" / "project.json"
|
|
537
|
+
project = validate_project(read_json(project_path), str(project_path))
|
|
538
|
+
require_environment_profile(args.project_root, project, profile=args.profile)
|
|
539
|
+
report = execute_command(
|
|
540
|
+
args.project_root,
|
|
541
|
+
identifier="project-command",
|
|
542
|
+
run=tuple(run),
|
|
543
|
+
timeout_seconds=args.timeout_seconds,
|
|
544
|
+
working_directory=args.working_directory,
|
|
545
|
+
path_entries=environment_path_entries(project, profile=args.profile),
|
|
546
|
+
command_bindings=environment_command_bindings(project, profile=args.profile),
|
|
547
|
+
stream_output=not args.json,
|
|
548
|
+
)
|
|
549
|
+
if args.json:
|
|
550
|
+
_emit(args, _result("exec", status=report["status"], execution=report))
|
|
551
|
+
else:
|
|
552
|
+
_emit(
|
|
553
|
+
args,
|
|
554
|
+
_result(
|
|
555
|
+
"exec",
|
|
556
|
+
status=report["status"],
|
|
557
|
+
exitCode=report["exitCode"],
|
|
558
|
+
durationMs=report["durationMs"],
|
|
559
|
+
commandExecuted=report["command"],
|
|
560
|
+
error=report.get("error"),
|
|
561
|
+
),
|
|
562
|
+
)
|
|
563
|
+
return 0 if report["status"] == "passed" else 1
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def command_verify(args: argparse.Namespace) -> int:
|
|
567
|
+
lock = load_lock(args.project_root)
|
|
568
|
+
integration_issues = synchronized_state(
|
|
569
|
+
args.project_root,
|
|
570
|
+
args.process_root,
|
|
571
|
+
lock,
|
|
572
|
+
)
|
|
573
|
+
if integration_issues:
|
|
574
|
+
raise ContractError("\n".join(integration_issues))
|
|
575
|
+
project_path = args.project_root / ".process" / "project.json"
|
|
576
|
+
project = validate_project(read_json(project_path), str(project_path))
|
|
577
|
+
require_environment_profile(args.project_root, project, profile=args.profile)
|
|
578
|
+
report = run_profile(args.project_root, project, args.profile)
|
|
579
|
+
if args.output:
|
|
580
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
581
|
+
args.output.write_text(
|
|
582
|
+
json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
|
|
583
|
+
encoding="utf-8",
|
|
584
|
+
)
|
|
585
|
+
if args.json:
|
|
586
|
+
_write_json(report)
|
|
587
|
+
else:
|
|
588
|
+
print(f"verify {args.profile}: {report['status'].upper()}")
|
|
589
|
+
print(f" project: {report['project']}")
|
|
590
|
+
print(f" checkpoint: {report['checkpoint'] or 'unavailable'}")
|
|
591
|
+
print(f" workspace fingerprint: {report['workspaceFingerprint'] or 'unavailable'}")
|
|
592
|
+
print(
|
|
593
|
+
" source changed during verification: "
|
|
594
|
+
f"{str(report['sourceChangedDuringVerification']).lower()}"
|
|
595
|
+
)
|
|
596
|
+
for check in report["checks"]:
|
|
597
|
+
print(
|
|
598
|
+
f" {check['id']}: {check['status']} "
|
|
599
|
+
f"({check['durationMs']} ms, exit={check['exitCode']})"
|
|
600
|
+
)
|
|
601
|
+
if args.output:
|
|
602
|
+
print(f" report: {args.output}")
|
|
603
|
+
return 0 if report["status"] == "passed" else 1
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _publication_result(
|
|
607
|
+
args: argparse.Namespace, command: str, issues: list[str], **details: Any
|
|
608
|
+
) -> int:
|
|
609
|
+
_emit(
|
|
610
|
+
args,
|
|
611
|
+
_result(
|
|
612
|
+
command,
|
|
613
|
+
status="failed" if issues else "passed",
|
|
614
|
+
**details,
|
|
615
|
+
issues=issues,
|
|
616
|
+
),
|
|
617
|
+
)
|
|
618
|
+
return 1 if issues else 0
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def command_publication_validate_branch(args: argparse.Namespace) -> int:
|
|
622
|
+
issues = validate_branch(args.branch)
|
|
623
|
+
return _publication_result(
|
|
624
|
+
args,
|
|
625
|
+
"publication validate-branch",
|
|
626
|
+
issues,
|
|
627
|
+
branch=args.branch,
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def command_publication_validate_commit(args: argparse.Namespace) -> int:
|
|
632
|
+
issues = validate_commit_subject(args.subject)
|
|
633
|
+
return _publication_result(
|
|
634
|
+
args,
|
|
635
|
+
"publication validate-commit",
|
|
636
|
+
issues,
|
|
637
|
+
subject=args.subject,
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def command_publication_validate_range(args: argparse.Namespace) -> int:
|
|
642
|
+
issues, records = validate_commit_range(
|
|
643
|
+
args.project_root,
|
|
644
|
+
branch=args.branch,
|
|
645
|
+
range_spec=args.range_spec,
|
|
646
|
+
)
|
|
647
|
+
return _publication_result(
|
|
648
|
+
args,
|
|
649
|
+
"publication validate-range",
|
|
650
|
+
issues,
|
|
651
|
+
branch=args.branch,
|
|
652
|
+
range=args.range_spec,
|
|
653
|
+
commits=[commit for commit, _subject in records],
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def command_publication_validate_pr(args: argparse.Namespace) -> int:
|
|
658
|
+
if args.body_file is not None:
|
|
659
|
+
try:
|
|
660
|
+
body = args.body_file.read_text(encoding="utf-8")
|
|
661
|
+
except (OSError, UnicodeError) as error:
|
|
662
|
+
raise ContractError(f"{args.body_file}: cannot read PR body: {error}") from error
|
|
663
|
+
else:
|
|
664
|
+
body = os.environ.get("PR_BODY", "")
|
|
665
|
+
issues = validate_pull_request(
|
|
666
|
+
title=args.title,
|
|
667
|
+
body=body,
|
|
668
|
+
branch=args.branch,
|
|
669
|
+
state=args.state,
|
|
670
|
+
)
|
|
671
|
+
return _publication_result(
|
|
672
|
+
args,
|
|
673
|
+
"publication validate-pr",
|
|
674
|
+
issues,
|
|
675
|
+
branch=args.branch,
|
|
676
|
+
state=args.state,
|
|
677
|
+
title=args.title,
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _add_json(parser: argparse.ArgumentParser) -> None:
|
|
682
|
+
parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _add_project_root(parser: argparse.ArgumentParser) -> None:
|
|
686
|
+
parser.add_argument(
|
|
687
|
+
"--project-root",
|
|
688
|
+
type=_root,
|
|
689
|
+
default=Path.cwd(),
|
|
690
|
+
help="Consumer project root; defaults to the current directory",
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _add_process_root(parser: argparse.ArgumentParser) -> None:
|
|
695
|
+
parser.add_argument(
|
|
696
|
+
"--process-root",
|
|
697
|
+
type=_root,
|
|
698
|
+
default=default_process_root(),
|
|
699
|
+
help="Engineering-process source root; defaults to the installed source",
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def _add_actor(parser: argparse.ArgumentParser) -> None:
|
|
704
|
+
parser.add_argument("--actor", required=True, help="Stable actor identity")
|
|
705
|
+
parser.add_argument("--context", required=True, help="Isolated execution context identity")
|
|
706
|
+
parser.add_argument(
|
|
707
|
+
"--actor-kind",
|
|
708
|
+
choices=("agent", "human"),
|
|
709
|
+
required=True,
|
|
710
|
+
help="Actor type",
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _add_lifecycle_common(parser: argparse.ArgumentParser) -> None:
|
|
715
|
+
_add_project_root(parser)
|
|
716
|
+
_add_process_root(parser)
|
|
717
|
+
_add_json(parser)
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
721
|
+
parser = argparse.ArgumentParser(
|
|
722
|
+
prog="processctl",
|
|
723
|
+
description="Agent-neutral engineering process CLI",
|
|
724
|
+
)
|
|
725
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
726
|
+
|
|
727
|
+
project = commands.add_parser("project", help="Validate project configuration")
|
|
728
|
+
project_commands = project.add_subparsers(dest="project_command", required=True)
|
|
729
|
+
project_validate = project_commands.add_parser("validate")
|
|
730
|
+
_add_project_root(project_validate)
|
|
731
|
+
_add_json(project_validate)
|
|
732
|
+
project_validate.set_defaults(handler=command_project_validate)
|
|
733
|
+
project_init = project_commands.add_parser(
|
|
734
|
+
"init",
|
|
735
|
+
help="Install the portable process from a validated project manifest",
|
|
736
|
+
)
|
|
737
|
+
_add_project_root(project_init)
|
|
738
|
+
_add_process_root(project_init)
|
|
739
|
+
project_init.add_argument(
|
|
740
|
+
"--manifest",
|
|
741
|
+
type=Path,
|
|
742
|
+
help="Source project manifest; defaults to .process/project.json",
|
|
743
|
+
)
|
|
744
|
+
project_init.add_argument(
|
|
745
|
+
"--bundle",
|
|
746
|
+
action="append",
|
|
747
|
+
default=[],
|
|
748
|
+
help="Select one capability bundle; defaults to core and may be repeated",
|
|
749
|
+
)
|
|
750
|
+
project_init.add_argument(
|
|
751
|
+
"--replace",
|
|
752
|
+
action="store_true",
|
|
753
|
+
help="Replace differing managed project and lock files intentionally",
|
|
754
|
+
)
|
|
755
|
+
_add_json(project_init)
|
|
756
|
+
project_init.set_defaults(handler=command_project_init)
|
|
757
|
+
|
|
758
|
+
lock = commands.add_parser("lock", help="Validate process lock")
|
|
759
|
+
lock_commands = lock.add_subparsers(dest="lock_command", required=True)
|
|
760
|
+
lock_validate = lock_commands.add_parser("validate")
|
|
761
|
+
_add_project_root(lock_validate)
|
|
762
|
+
_add_process_root(lock_validate)
|
|
763
|
+
_add_json(lock_validate)
|
|
764
|
+
lock_validate.set_defaults(handler=command_lock_validate)
|
|
765
|
+
lock_create = lock_commands.add_parser("create")
|
|
766
|
+
_add_project_root(lock_create)
|
|
767
|
+
_add_process_root(lock_create)
|
|
768
|
+
lock_create.add_argument(
|
|
769
|
+
"--bundle",
|
|
770
|
+
action="append",
|
|
771
|
+
help="Select one bundle; defaults to core and may be repeated",
|
|
772
|
+
)
|
|
773
|
+
lock_create.add_argument(
|
|
774
|
+
"--replace",
|
|
775
|
+
action="store_true",
|
|
776
|
+
help="Replace an existing lock intentionally",
|
|
777
|
+
)
|
|
778
|
+
_add_json(lock_create)
|
|
779
|
+
lock_create.set_defaults(handler=command_lock_create)
|
|
780
|
+
|
|
781
|
+
contract = commands.add_parser("contract", help="Validate process artifacts")
|
|
782
|
+
contract_commands = contract.add_subparsers(dest="contract_command", required=True)
|
|
783
|
+
contract_validate = contract_commands.add_parser("validate")
|
|
784
|
+
contract_validate.add_argument(
|
|
785
|
+
"--kind", choices=("change", "plan", "review"), required=True
|
|
786
|
+
)
|
|
787
|
+
contract_validate.add_argument("path", type=Path)
|
|
788
|
+
_add_json(contract_validate)
|
|
789
|
+
contract_validate.set_defaults(handler=command_contract_validate)
|
|
790
|
+
|
|
791
|
+
skills = commands.add_parser("skills", help="Validate portable Agent Skills")
|
|
792
|
+
skills_commands = skills.add_subparsers(dest="skills_command", required=True)
|
|
793
|
+
skills_validate = skills_commands.add_parser("validate")
|
|
794
|
+
skills_validate.add_argument(
|
|
795
|
+
"--root",
|
|
796
|
+
type=_root,
|
|
797
|
+
default=process_skills_root(default_process_root()),
|
|
798
|
+
)
|
|
799
|
+
_add_json(skills_validate)
|
|
800
|
+
skills_validate.set_defaults(handler=command_skills_validate)
|
|
801
|
+
|
|
802
|
+
digest = commands.add_parser("digest", help="Calculate the selected distribution digest")
|
|
803
|
+
_add_process_root(digest)
|
|
804
|
+
digest.add_argument("--project-root", type=_root)
|
|
805
|
+
digest.add_argument(
|
|
806
|
+
"--bundle",
|
|
807
|
+
action="append",
|
|
808
|
+
help="Select one bundle; repeat to combine bundles",
|
|
809
|
+
)
|
|
810
|
+
_add_json(digest)
|
|
811
|
+
digest.set_defaults(handler=command_digest)
|
|
812
|
+
|
|
813
|
+
sync = commands.add_parser("sync", help="Synchronize pinned skills into a project")
|
|
814
|
+
_add_project_root(sync)
|
|
815
|
+
_add_process_root(sync)
|
|
816
|
+
sync.add_argument(
|
|
817
|
+
"--check",
|
|
818
|
+
action="store_true",
|
|
819
|
+
help="Check synchronized state without changing files",
|
|
820
|
+
)
|
|
821
|
+
_add_json(sync)
|
|
822
|
+
sync.set_defaults(handler=command_sync)
|
|
823
|
+
|
|
824
|
+
doctor = commands.add_parser("doctor", help="Validate a consumer integration")
|
|
825
|
+
_add_project_root(doctor)
|
|
826
|
+
_add_process_root(doctor)
|
|
827
|
+
doctor.add_argument(
|
|
828
|
+
"--profile",
|
|
829
|
+
help="Environment profile; defaults to environment.defaultProfile",
|
|
830
|
+
)
|
|
831
|
+
_add_json(doctor)
|
|
832
|
+
doctor.set_defaults(handler=command_doctor)
|
|
833
|
+
|
|
834
|
+
setup = commands.add_parser(
|
|
835
|
+
"setup",
|
|
836
|
+
help="Plan or explicitly apply project-declared environment setup",
|
|
837
|
+
)
|
|
838
|
+
_add_project_root(setup)
|
|
839
|
+
_add_process_root(setup)
|
|
840
|
+
setup.add_argument(
|
|
841
|
+
"--profile",
|
|
842
|
+
help="Environment profile; defaults to environment.defaultProfile",
|
|
843
|
+
)
|
|
844
|
+
setup.add_argument(
|
|
845
|
+
"--apply",
|
|
846
|
+
action="store_true",
|
|
847
|
+
help="Execute the complete preflighted setup plan",
|
|
848
|
+
)
|
|
849
|
+
setup.add_argument(
|
|
850
|
+
"--allow",
|
|
851
|
+
action="append",
|
|
852
|
+
default=[],
|
|
853
|
+
choices=("host-configuration", "network", "project-files", "user-files"),
|
|
854
|
+
help="Approve one mutation scope for --apply; repeat as needed",
|
|
855
|
+
)
|
|
856
|
+
_add_json(setup)
|
|
857
|
+
setup.set_defaults(handler=command_setup)
|
|
858
|
+
|
|
859
|
+
execute = commands.add_parser(
|
|
860
|
+
"exec",
|
|
861
|
+
help="Run one project command with its verified managed-tool environment",
|
|
862
|
+
)
|
|
863
|
+
_add_project_root(execute)
|
|
864
|
+
_add_process_root(execute)
|
|
865
|
+
execute.add_argument("--profile", required=True)
|
|
866
|
+
execute.add_argument("--timeout-seconds", type=int, default=3600)
|
|
867
|
+
execute.add_argument("--working-directory", default=".")
|
|
868
|
+
_add_json(execute)
|
|
869
|
+
execute.add_argument("run", nargs=argparse.REMAINDER)
|
|
870
|
+
execute.set_defaults(handler=command_exec)
|
|
871
|
+
|
|
872
|
+
verify = commands.add_parser("verify", help="Run a project-owned verification profile")
|
|
873
|
+
_add_project_root(verify)
|
|
874
|
+
_add_process_root(verify)
|
|
875
|
+
verify.add_argument("--profile", required=True)
|
|
876
|
+
verify.add_argument("--output", type=Path)
|
|
877
|
+
_add_json(verify)
|
|
878
|
+
verify.set_defaults(handler=command_verify)
|
|
879
|
+
|
|
880
|
+
publication = commands.add_parser(
|
|
881
|
+
"publication", help="Validate portable publication metadata"
|
|
882
|
+
)
|
|
883
|
+
publication_commands = publication.add_subparsers(
|
|
884
|
+
dest="publication_command", required=True
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
publication_branch = publication_commands.add_parser(
|
|
888
|
+
"validate-branch", help="Validate a publication branch name"
|
|
889
|
+
)
|
|
890
|
+
publication_branch.add_argument("--branch", required=True)
|
|
891
|
+
_add_json(publication_branch)
|
|
892
|
+
publication_branch.set_defaults(handler=command_publication_validate_branch)
|
|
893
|
+
|
|
894
|
+
publication_commit = publication_commands.add_parser(
|
|
895
|
+
"validate-commit", help="Validate a commit subject"
|
|
896
|
+
)
|
|
897
|
+
publication_commit.add_argument("--subject", required=True)
|
|
898
|
+
_add_json(publication_commit)
|
|
899
|
+
publication_commit.set_defaults(handler=command_publication_validate_commit)
|
|
900
|
+
|
|
901
|
+
publication_range = publication_commands.add_parser(
|
|
902
|
+
"validate-range", help="Validate a branch and every commit subject in a range"
|
|
903
|
+
)
|
|
904
|
+
_add_project_root(publication_range)
|
|
905
|
+
publication_range.add_argument("--branch", required=True)
|
|
906
|
+
publication_range.add_argument("--range", dest="range_spec", required=True)
|
|
907
|
+
_add_json(publication_range)
|
|
908
|
+
publication_range.set_defaults(handler=command_publication_validate_range)
|
|
909
|
+
|
|
910
|
+
publication_pr = publication_commands.add_parser(
|
|
911
|
+
"validate-pr", help="Validate a pull-request title and description"
|
|
912
|
+
)
|
|
913
|
+
publication_pr.add_argument("--title", required=True)
|
|
914
|
+
publication_pr.add_argument("--branch", required=True)
|
|
915
|
+
publication_pr.add_argument("--state", choices=("draft", "ready"), required=True)
|
|
916
|
+
publication_pr.add_argument("--body-file", type=Path)
|
|
917
|
+
_add_json(publication_pr)
|
|
918
|
+
publication_pr.set_defaults(handler=command_publication_validate_pr)
|
|
919
|
+
|
|
920
|
+
change = commands.add_parser("change", help="Run the canonical change lifecycle")
|
|
921
|
+
change_commands = change.add_subparsers(dest="change_command", required=True)
|
|
922
|
+
|
|
923
|
+
change_start = change_commands.add_parser("start", help="Register a change specification")
|
|
924
|
+
_add_lifecycle_common(change_start)
|
|
925
|
+
_add_actor(change_start)
|
|
926
|
+
change_start.add_argument("--contract", type=Path, required=True)
|
|
927
|
+
change_start.set_defaults(handler=command_change_start)
|
|
928
|
+
|
|
929
|
+
change_plan = change_commands.add_parser("plan", help="Register an implementation plan")
|
|
930
|
+
_add_lifecycle_common(change_plan)
|
|
931
|
+
_add_actor(change_plan)
|
|
932
|
+
change_plan.add_argument("--change-id", required=True)
|
|
933
|
+
change_plan.add_argument("--plan", type=Path, required=True)
|
|
934
|
+
change_plan.set_defaults(handler=command_change_plan)
|
|
935
|
+
|
|
936
|
+
change_implement = change_commands.add_parser(
|
|
937
|
+
"implement", help="Register an implementation actor and begin a cycle"
|
|
938
|
+
)
|
|
939
|
+
_add_lifecycle_common(change_implement)
|
|
940
|
+
_add_actor(change_implement)
|
|
941
|
+
change_implement.add_argument("--change-id", required=True)
|
|
942
|
+
change_implement.set_defaults(handler=command_change_implement)
|
|
943
|
+
|
|
944
|
+
change_verify = change_commands.add_parser(
|
|
945
|
+
"verify", help="Run and bind a required verification profile"
|
|
946
|
+
)
|
|
947
|
+
_add_lifecycle_common(change_verify)
|
|
948
|
+
_add_actor(change_verify)
|
|
949
|
+
change_verify.add_argument("--change-id", required=True)
|
|
950
|
+
change_verify.add_argument("--profile", required=True)
|
|
951
|
+
change_verify.set_defaults(handler=command_change_verify)
|
|
952
|
+
|
|
953
|
+
change_review = change_commands.add_parser(
|
|
954
|
+
"review", help="Run the independent-review gate"
|
|
955
|
+
)
|
|
956
|
+
review_commands = change_review.add_subparsers(
|
|
957
|
+
dest="change_review_command", required=True
|
|
958
|
+
)
|
|
959
|
+
review_start = review_commands.add_parser(
|
|
960
|
+
"start", help="Register an independent reviewer and immutable assignment"
|
|
961
|
+
)
|
|
962
|
+
_add_lifecycle_common(review_start)
|
|
963
|
+
_add_actor(review_start)
|
|
964
|
+
review_start.add_argument("--change-id", required=True)
|
|
965
|
+
review_start.add_argument(
|
|
966
|
+
"--method", choices=("isolated-context", "separate-person"), required=True
|
|
967
|
+
)
|
|
968
|
+
review_start.add_argument("--attested-by", required=True)
|
|
969
|
+
review_start.add_argument("--attestation-evidence", required=True)
|
|
970
|
+
review_start.set_defaults(handler=command_change_review_start)
|
|
971
|
+
|
|
972
|
+
review_submit = review_commands.add_parser(
|
|
973
|
+
"submit", help="Validate and register the independent review report"
|
|
974
|
+
)
|
|
975
|
+
_add_lifecycle_common(review_submit)
|
|
976
|
+
review_submit.add_argument("--change-id", required=True)
|
|
977
|
+
review_submit.add_argument("--report", type=Path, required=True)
|
|
978
|
+
review_submit.set_defaults(handler=command_change_review_submit)
|
|
979
|
+
|
|
980
|
+
change_finish = change_commands.add_parser(
|
|
981
|
+
"finish", help="Complete an approved current change"
|
|
982
|
+
)
|
|
983
|
+
_add_lifecycle_common(change_finish)
|
|
984
|
+
_add_actor(change_finish)
|
|
985
|
+
change_finish.add_argument("--change-id", required=True)
|
|
986
|
+
change_finish.set_defaults(handler=command_change_finish)
|
|
987
|
+
|
|
988
|
+
change_status = change_commands.add_parser("status", help="Inspect lifecycle state")
|
|
989
|
+
_add_lifecycle_common(change_status)
|
|
990
|
+
change_status.add_argument("--change-id", required=True)
|
|
991
|
+
change_status.set_defaults(handler=command_change_status)
|
|
992
|
+
|
|
993
|
+
return parser
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def main(argv: list[str] | None = None) -> int:
|
|
997
|
+
parser = build_parser()
|
|
998
|
+
args = parser.parse_args(argv)
|
|
999
|
+
try:
|
|
1000
|
+
return args.handler(args)
|
|
1001
|
+
except ContractError as error:
|
|
1002
|
+
value = _result(args.command, status="failed", errors=str(error).splitlines())
|
|
1003
|
+
if getattr(args, "json", False):
|
|
1004
|
+
_write_json(value)
|
|
1005
|
+
else:
|
|
1006
|
+
print(f"{args.command}: FAILED", file=sys.stderr)
|
|
1007
|
+
for line in str(error).splitlines():
|
|
1008
|
+
print(f" {line}", file=sys.stderr)
|
|
1009
|
+
return 2
|
|
1010
|
+
except KeyboardInterrupt:
|
|
1011
|
+
print(f"{args.command}: INTERRUPTED", file=sys.stderr)
|
|
1012
|
+
return 130
|