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,419 @@
|
|
|
1
|
+
"""Run one command in a Windows kill-on-close Job Object.
|
|
2
|
+
|
|
3
|
+
This private helper is launched by the environment executor. The Job Object is
|
|
4
|
+
attached through the process creation attribute list so the target is contained
|
|
5
|
+
before its first instruction can run, including if this helper is terminated while
|
|
6
|
+
CreateProcessW is in progress.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ctypes
|
|
12
|
+
from ctypes import wintypes
|
|
13
|
+
import ntpath
|
|
14
|
+
import os
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
CREATE_NEW_PROCESS_GROUP = 0x00000200
|
|
22
|
+
EXTENDED_STARTUPINFO_PRESENT = 0x00080000
|
|
23
|
+
INFINITE = 0xFFFFFFFF
|
|
24
|
+
JOB_OBJECT_BASIC_ACCOUNTING_INFORMATION_CLASS = 1
|
|
25
|
+
JOB_OBJECT_EXTENDED_LIMIT_INFORMATION_CLASS = 9
|
|
26
|
+
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
|
|
27
|
+
PROC_THREAD_ATTRIBUTE_JOB_LIST = 0x0002000D
|
|
28
|
+
STARTF_USESTDHANDLES = 0x00000100
|
|
29
|
+
STD_INPUT_HANDLE = wintypes.DWORD(-10).value
|
|
30
|
+
STD_OUTPUT_HANDLE = wintypes.DWORD(-11).value
|
|
31
|
+
STD_ERROR_HANDLE = wintypes.DWORD(-12).value
|
|
32
|
+
WAIT_FAILED = 0xFFFFFFFF
|
|
33
|
+
WAIT_OBJECT_0 = 0x00000000
|
|
34
|
+
WAIT_TIMEOUT = 0x00000102
|
|
35
|
+
CLEANUP_GRACE_MILLISECONDS = 5_000
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class IO_COUNTERS(ctypes.Structure):
|
|
39
|
+
_fields_ = [
|
|
40
|
+
("ReadOperationCount", ctypes.c_ulonglong),
|
|
41
|
+
("WriteOperationCount", ctypes.c_ulonglong),
|
|
42
|
+
("OtherOperationCount", ctypes.c_ulonglong),
|
|
43
|
+
("ReadTransferCount", ctypes.c_ulonglong),
|
|
44
|
+
("WriteTransferCount", ctypes.c_ulonglong),
|
|
45
|
+
("OtherTransferCount", ctypes.c_ulonglong),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class JOBOBJECT_BASIC_LIMIT_INFORMATION(ctypes.Structure):
|
|
50
|
+
_fields_ = [
|
|
51
|
+
("PerProcessUserTimeLimit", ctypes.c_longlong),
|
|
52
|
+
("PerJobUserTimeLimit", ctypes.c_longlong),
|
|
53
|
+
("LimitFlags", wintypes.DWORD),
|
|
54
|
+
("MinimumWorkingSetSize", ctypes.c_size_t),
|
|
55
|
+
("MaximumWorkingSetSize", ctypes.c_size_t),
|
|
56
|
+
("ActiveProcessLimit", wintypes.DWORD),
|
|
57
|
+
("Affinity", ctypes.c_size_t),
|
|
58
|
+
("PriorityClass", wintypes.DWORD),
|
|
59
|
+
("SchedulingClass", wintypes.DWORD),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class JOBOBJECT_EXTENDED_LIMIT_INFORMATION(ctypes.Structure):
|
|
64
|
+
_fields_ = [
|
|
65
|
+
("BasicLimitInformation", JOBOBJECT_BASIC_LIMIT_INFORMATION),
|
|
66
|
+
("IoInfo", IO_COUNTERS),
|
|
67
|
+
("ProcessMemoryLimit", ctypes.c_size_t),
|
|
68
|
+
("JobMemoryLimit", ctypes.c_size_t),
|
|
69
|
+
("PeakProcessMemoryUsed", ctypes.c_size_t),
|
|
70
|
+
("PeakJobMemoryUsed", ctypes.c_size_t),
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class JOBOBJECT_BASIC_ACCOUNTING_INFORMATION(ctypes.Structure):
|
|
75
|
+
_fields_ = [
|
|
76
|
+
("TotalUserTime", ctypes.c_longlong),
|
|
77
|
+
("TotalKernelTime", ctypes.c_longlong),
|
|
78
|
+
("ThisPeriodTotalUserTime", ctypes.c_longlong),
|
|
79
|
+
("ThisPeriodTotalKernelTime", ctypes.c_longlong),
|
|
80
|
+
("TotalPageFaultCount", wintypes.DWORD),
|
|
81
|
+
("TotalProcesses", wintypes.DWORD),
|
|
82
|
+
("ActiveProcesses", wintypes.DWORD),
|
|
83
|
+
("TotalTerminatedProcesses", wintypes.DWORD),
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class STARTUPINFOW(ctypes.Structure):
|
|
88
|
+
_fields_ = [
|
|
89
|
+
("cb", wintypes.DWORD),
|
|
90
|
+
("lpReserved", wintypes.LPWSTR),
|
|
91
|
+
("lpDesktop", wintypes.LPWSTR),
|
|
92
|
+
("lpTitle", wintypes.LPWSTR),
|
|
93
|
+
("dwX", wintypes.DWORD),
|
|
94
|
+
("dwY", wintypes.DWORD),
|
|
95
|
+
("dwXSize", wintypes.DWORD),
|
|
96
|
+
("dwYSize", wintypes.DWORD),
|
|
97
|
+
("dwXCountChars", wintypes.DWORD),
|
|
98
|
+
("dwYCountChars", wintypes.DWORD),
|
|
99
|
+
("dwFillAttribute", wintypes.DWORD),
|
|
100
|
+
("dwFlags", wintypes.DWORD),
|
|
101
|
+
("wShowWindow", wintypes.WORD),
|
|
102
|
+
("cbReserved2", wintypes.WORD),
|
|
103
|
+
("lpReserved2", ctypes.POINTER(ctypes.c_byte)),
|
|
104
|
+
("hStdInput", wintypes.HANDLE),
|
|
105
|
+
("hStdOutput", wintypes.HANDLE),
|
|
106
|
+
("hStdError", wintypes.HANDLE),
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class STARTUPINFOEXW(ctypes.Structure):
|
|
111
|
+
_fields_ = [
|
|
112
|
+
("StartupInfo", STARTUPINFOW),
|
|
113
|
+
("lpAttributeList", ctypes.c_void_p),
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class PROCESS_INFORMATION(ctypes.Structure):
|
|
118
|
+
_fields_ = [
|
|
119
|
+
("hProcess", wintypes.HANDLE),
|
|
120
|
+
("hThread", wintypes.HANDLE),
|
|
121
|
+
("dwProcessId", wintypes.DWORD),
|
|
122
|
+
("dwThreadId", wintypes.DWORD),
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _last_error(label: str) -> OSError:
|
|
127
|
+
get_last_error = getattr(ctypes, "get_last_error", lambda: 0)
|
|
128
|
+
error_code = get_last_error()
|
|
129
|
+
win_error = getattr(ctypes, "WinError", None)
|
|
130
|
+
if win_error is not None:
|
|
131
|
+
return win_error(error_code, label)
|
|
132
|
+
return OSError(error_code, f"{label} failed")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _configure_kernel32(kernel32: Any) -> None:
|
|
136
|
+
kernel32.CreateJobObjectW.restype = wintypes.HANDLE
|
|
137
|
+
kernel32.SetInformationJobObject.argtypes = [
|
|
138
|
+
wintypes.HANDLE,
|
|
139
|
+
ctypes.c_int,
|
|
140
|
+
ctypes.c_void_p,
|
|
141
|
+
wintypes.DWORD,
|
|
142
|
+
]
|
|
143
|
+
kernel32.SetInformationJobObject.restype = wintypes.BOOL
|
|
144
|
+
kernel32.InitializeProcThreadAttributeList.argtypes = [
|
|
145
|
+
ctypes.c_void_p,
|
|
146
|
+
wintypes.DWORD,
|
|
147
|
+
wintypes.DWORD,
|
|
148
|
+
ctypes.POINTER(ctypes.c_size_t),
|
|
149
|
+
]
|
|
150
|
+
kernel32.InitializeProcThreadAttributeList.restype = wintypes.BOOL
|
|
151
|
+
kernel32.UpdateProcThreadAttribute.argtypes = [
|
|
152
|
+
ctypes.c_void_p,
|
|
153
|
+
wintypes.DWORD,
|
|
154
|
+
ctypes.c_size_t,
|
|
155
|
+
ctypes.c_void_p,
|
|
156
|
+
ctypes.c_size_t,
|
|
157
|
+
ctypes.c_void_p,
|
|
158
|
+
ctypes.c_void_p,
|
|
159
|
+
]
|
|
160
|
+
kernel32.UpdateProcThreadAttribute.restype = wintypes.BOOL
|
|
161
|
+
kernel32.DeleteProcThreadAttributeList.argtypes = [ctypes.c_void_p]
|
|
162
|
+
kernel32.DeleteProcThreadAttributeList.restype = None
|
|
163
|
+
kernel32.CreateProcessW.argtypes = [
|
|
164
|
+
wintypes.LPCWSTR,
|
|
165
|
+
wintypes.LPWSTR,
|
|
166
|
+
ctypes.c_void_p,
|
|
167
|
+
ctypes.c_void_p,
|
|
168
|
+
wintypes.BOOL,
|
|
169
|
+
wintypes.DWORD,
|
|
170
|
+
ctypes.c_void_p,
|
|
171
|
+
wintypes.LPCWSTR,
|
|
172
|
+
ctypes.POINTER(STARTUPINFOW),
|
|
173
|
+
ctypes.POINTER(PROCESS_INFORMATION),
|
|
174
|
+
]
|
|
175
|
+
kernel32.CreateProcessW.restype = wintypes.BOOL
|
|
176
|
+
kernel32.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD]
|
|
177
|
+
kernel32.WaitForSingleObject.restype = wintypes.DWORD
|
|
178
|
+
kernel32.GetExitCodeProcess.argtypes = [
|
|
179
|
+
wintypes.HANDLE,
|
|
180
|
+
ctypes.POINTER(wintypes.DWORD),
|
|
181
|
+
]
|
|
182
|
+
kernel32.GetExitCodeProcess.restype = wintypes.BOOL
|
|
183
|
+
kernel32.QueryInformationJobObject.argtypes = [
|
|
184
|
+
wintypes.HANDLE,
|
|
185
|
+
ctypes.c_int,
|
|
186
|
+
ctypes.c_void_p,
|
|
187
|
+
wintypes.DWORD,
|
|
188
|
+
ctypes.POINTER(wintypes.DWORD),
|
|
189
|
+
]
|
|
190
|
+
kernel32.QueryInformationJobObject.restype = wintypes.BOOL
|
|
191
|
+
kernel32.TerminateJobObject.argtypes = [wintypes.HANDLE, wintypes.UINT]
|
|
192
|
+
kernel32.TerminateJobObject.restype = wintypes.BOOL
|
|
193
|
+
kernel32.GetStdHandle.argtypes = [wintypes.DWORD]
|
|
194
|
+
kernel32.GetStdHandle.restype = wintypes.HANDLE
|
|
195
|
+
kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
|
|
196
|
+
kernel32.CloseHandle.restype = wintypes.BOOL
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _active_processes(kernel32: Any, job: int) -> int:
|
|
200
|
+
accounting = JOBOBJECT_BASIC_ACCOUNTING_INFORMATION()
|
|
201
|
+
returned_length = wintypes.DWORD()
|
|
202
|
+
if not kernel32.QueryInformationJobObject(
|
|
203
|
+
job,
|
|
204
|
+
JOB_OBJECT_BASIC_ACCOUNTING_INFORMATION_CLASS,
|
|
205
|
+
ctypes.byref(accounting),
|
|
206
|
+
ctypes.sizeof(accounting),
|
|
207
|
+
ctypes.byref(returned_length),
|
|
208
|
+
):
|
|
209
|
+
raise _last_error("QueryInformationJobObject")
|
|
210
|
+
return int(accounting.ActiveProcesses)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _cleanup_process_and_job(
|
|
214
|
+
kernel32: Any,
|
|
215
|
+
job: int,
|
|
216
|
+
process_info: PROCESS_INFORMATION,
|
|
217
|
+
) -> tuple[list[OSError], int | None]:
|
|
218
|
+
errors: list[OSError] = []
|
|
219
|
+
if process_info.hProcess:
|
|
220
|
+
try:
|
|
221
|
+
active_processes = _active_processes(kernel32, job)
|
|
222
|
+
except OSError as error:
|
|
223
|
+
errors.append(error)
|
|
224
|
+
active_processes = None
|
|
225
|
+
active_processes_before_cleanup = active_processes
|
|
226
|
+
|
|
227
|
+
if active_processes is None or active_processes > 0:
|
|
228
|
+
if not kernel32.TerminateJobObject(job, 125):
|
|
229
|
+
errors.append(_last_error("TerminateJobObject"))
|
|
230
|
+
deadline = time.monotonic() + CLEANUP_GRACE_MILLISECONDS / 1000
|
|
231
|
+
while True:
|
|
232
|
+
try:
|
|
233
|
+
active_processes = _active_processes(kernel32, job)
|
|
234
|
+
except OSError as error:
|
|
235
|
+
errors.append(error)
|
|
236
|
+
break
|
|
237
|
+
if active_processes == 0:
|
|
238
|
+
break
|
|
239
|
+
if time.monotonic() >= deadline:
|
|
240
|
+
errors.append(
|
|
241
|
+
OSError(
|
|
242
|
+
"Job Object retained active processes after bounded termination"
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
break
|
|
246
|
+
time.sleep(0.01)
|
|
247
|
+
|
|
248
|
+
wait_result = kernel32.WaitForSingleObject(
|
|
249
|
+
process_info.hProcess, CLEANUP_GRACE_MILLISECONDS
|
|
250
|
+
)
|
|
251
|
+
if wait_result == WAIT_FAILED:
|
|
252
|
+
errors.append(_last_error("WaitForSingleObject during cleanup"))
|
|
253
|
+
elif wait_result == WAIT_TIMEOUT:
|
|
254
|
+
errors.append(OSError("target process survived bounded Job Object cleanup"))
|
|
255
|
+
elif wait_result != WAIT_OBJECT_0:
|
|
256
|
+
errors.append(
|
|
257
|
+
OSError(f"unexpected target cleanup wait result: {wait_result}")
|
|
258
|
+
)
|
|
259
|
+
return errors, (
|
|
260
|
+
active_processes_before_cleanup if process_info.hProcess else None
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _close_handle(kernel32: Any, handle: int, label: str) -> OSError | None:
|
|
265
|
+
if handle and not kernel32.CloseHandle(handle):
|
|
266
|
+
return _last_error(f"CloseHandle({label})")
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _run(
|
|
271
|
+
application: str,
|
|
272
|
+
command: list[str],
|
|
273
|
+
*,
|
|
274
|
+
kernel32: Any | None = None,
|
|
275
|
+
) -> int:
|
|
276
|
+
if not ntpath.isabs(application) or ntpath.splitext(application)[1].casefold() != ".exe":
|
|
277
|
+
raise OSError("Windows Job Object application must be an absolute .exe path")
|
|
278
|
+
if kernel32 is None:
|
|
279
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
280
|
+
_configure_kernel32(kernel32)
|
|
281
|
+
|
|
282
|
+
job = kernel32.CreateJobObjectW(None, None)
|
|
283
|
+
if not job:
|
|
284
|
+
raise _last_error("CreateJobObjectW")
|
|
285
|
+
process_info = PROCESS_INFORMATION()
|
|
286
|
+
attribute_list: ctypes.c_void_p | None = None
|
|
287
|
+
attribute_list_initialized = False
|
|
288
|
+
caught_error: BaseException | None = None
|
|
289
|
+
exit_code: int | None = None
|
|
290
|
+
try:
|
|
291
|
+
limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION()
|
|
292
|
+
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
|
293
|
+
if not kernel32.SetInformationJobObject(
|
|
294
|
+
job,
|
|
295
|
+
JOB_OBJECT_EXTENDED_LIMIT_INFORMATION_CLASS,
|
|
296
|
+
ctypes.byref(limits),
|
|
297
|
+
ctypes.sizeof(limits),
|
|
298
|
+
):
|
|
299
|
+
raise _last_error("SetInformationJobObject")
|
|
300
|
+
|
|
301
|
+
attribute_list_size = ctypes.c_size_t()
|
|
302
|
+
kernel32.InitializeProcThreadAttributeList(
|
|
303
|
+
None, 1, 0, ctypes.byref(attribute_list_size)
|
|
304
|
+
)
|
|
305
|
+
if attribute_list_size.value == 0:
|
|
306
|
+
raise _last_error("InitializeProcThreadAttributeList(size)")
|
|
307
|
+
attribute_list_buffer = ctypes.create_string_buffer(attribute_list_size.value)
|
|
308
|
+
attribute_list = ctypes.cast(attribute_list_buffer, ctypes.c_void_p)
|
|
309
|
+
if not kernel32.InitializeProcThreadAttributeList(
|
|
310
|
+
attribute_list, 1, 0, ctypes.byref(attribute_list_size)
|
|
311
|
+
):
|
|
312
|
+
raise _last_error("InitializeProcThreadAttributeList")
|
|
313
|
+
attribute_list_initialized = True
|
|
314
|
+
job_handles = (wintypes.HANDLE * 1)(job)
|
|
315
|
+
if not kernel32.UpdateProcThreadAttribute(
|
|
316
|
+
attribute_list,
|
|
317
|
+
0,
|
|
318
|
+
PROC_THREAD_ATTRIBUTE_JOB_LIST,
|
|
319
|
+
ctypes.cast(job_handles, ctypes.c_void_p),
|
|
320
|
+
ctypes.sizeof(job_handles),
|
|
321
|
+
None,
|
|
322
|
+
None,
|
|
323
|
+
):
|
|
324
|
+
raise _last_error("UpdateProcThreadAttribute(JOB_LIST)")
|
|
325
|
+
|
|
326
|
+
startup = STARTUPINFOEXW()
|
|
327
|
+
startup.StartupInfo.cb = ctypes.sizeof(startup)
|
|
328
|
+
startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES
|
|
329
|
+
startup.StartupInfo.hStdInput = kernel32.GetStdHandle(STD_INPUT_HANDLE)
|
|
330
|
+
startup.StartupInfo.hStdOutput = kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
|
|
331
|
+
startup.StartupInfo.hStdError = kernel32.GetStdHandle(STD_ERROR_HANDLE)
|
|
332
|
+
startup.lpAttributeList = attribute_list
|
|
333
|
+
command_line = ctypes.create_unicode_buffer(subprocess.list2cmdline(command))
|
|
334
|
+
if not kernel32.CreateProcessW(
|
|
335
|
+
application,
|
|
336
|
+
command_line,
|
|
337
|
+
None,
|
|
338
|
+
None,
|
|
339
|
+
True,
|
|
340
|
+
EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_PROCESS_GROUP,
|
|
341
|
+
None,
|
|
342
|
+
None,
|
|
343
|
+
ctypes.cast(ctypes.byref(startup), ctypes.POINTER(STARTUPINFOW)),
|
|
344
|
+
ctypes.byref(process_info),
|
|
345
|
+
):
|
|
346
|
+
raise _last_error("CreateProcessW")
|
|
347
|
+
wait_result = kernel32.WaitForSingleObject(process_info.hProcess, INFINITE)
|
|
348
|
+
if wait_result == WAIT_FAILED:
|
|
349
|
+
raise _last_error("WaitForSingleObject")
|
|
350
|
+
if wait_result != WAIT_OBJECT_0:
|
|
351
|
+
raise OSError(f"unexpected target wait result: {wait_result}")
|
|
352
|
+
native_exit_code = wintypes.DWORD()
|
|
353
|
+
if not kernel32.GetExitCodeProcess(
|
|
354
|
+
process_info.hProcess, ctypes.byref(native_exit_code)
|
|
355
|
+
):
|
|
356
|
+
raise _last_error("GetExitCodeProcess")
|
|
357
|
+
exit_code = int(native_exit_code.value)
|
|
358
|
+
except BaseException as error:
|
|
359
|
+
caught_error = error
|
|
360
|
+
finally:
|
|
361
|
+
cleanup_errors, active_processes_before_cleanup = _cleanup_process_and_job(
|
|
362
|
+
kernel32, job, process_info
|
|
363
|
+
)
|
|
364
|
+
if attribute_list_initialized and attribute_list is not None:
|
|
365
|
+
kernel32.DeleteProcThreadAttributeList(attribute_list)
|
|
366
|
+
for handle, label in (
|
|
367
|
+
(process_info.hThread, "thread"),
|
|
368
|
+
(process_info.hProcess, "process"),
|
|
369
|
+
(job, "job"),
|
|
370
|
+
):
|
|
371
|
+
if error := _close_handle(kernel32, handle, label):
|
|
372
|
+
cleanup_errors.append(error)
|
|
373
|
+
|
|
374
|
+
if caught_error is not None:
|
|
375
|
+
if cleanup_errors and isinstance(caught_error, Exception):
|
|
376
|
+
details = "; ".join(str(error) for error in cleanup_errors)
|
|
377
|
+
raise OSError(f"{caught_error}; cleanup failed: {details}") from caught_error
|
|
378
|
+
if cleanup_errors:
|
|
379
|
+
caught_error.add_note(
|
|
380
|
+
"Windows Job Object cleanup failed: "
|
|
381
|
+
+ "; ".join(str(error) for error in cleanup_errors)
|
|
382
|
+
)
|
|
383
|
+
raise caught_error
|
|
384
|
+
if cleanup_errors:
|
|
385
|
+
raise OSError(
|
|
386
|
+
"Windows Job Object cleanup failed: "
|
|
387
|
+
+ "; ".join(str(error) for error in cleanup_errors)
|
|
388
|
+
)
|
|
389
|
+
if active_processes_before_cleanup:
|
|
390
|
+
raise OSError("target command left descendant processes; they were terminated")
|
|
391
|
+
assert exit_code is not None
|
|
392
|
+
return exit_code
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def main() -> int:
|
|
396
|
+
if os.name != "nt":
|
|
397
|
+
print("Windows Job Object runner is only available on Windows", file=sys.stderr)
|
|
398
|
+
return 125
|
|
399
|
+
arguments = sys.argv[1:]
|
|
400
|
+
if len(arguments) < 4 or arguments[0] != "--application" or arguments[2] != "--":
|
|
401
|
+
print(
|
|
402
|
+
"Windows Job Object runner requires --application ABSOLUTE.exe -- COMMAND",
|
|
403
|
+
file=sys.stderr,
|
|
404
|
+
)
|
|
405
|
+
return 125
|
|
406
|
+
application = arguments[1]
|
|
407
|
+
arguments = arguments[3:]
|
|
408
|
+
if not arguments:
|
|
409
|
+
print("Windows Job Object runner requires a command", file=sys.stderr)
|
|
410
|
+
return 125
|
|
411
|
+
try:
|
|
412
|
+
return _run(application, arguments)
|
|
413
|
+
except OSError as error:
|
|
414
|
+
print(f"Windows Job Object setup failed: {error}", file=sys.stderr)
|
|
415
|
+
return 125
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
if __name__ == "__main__":
|
|
419
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from . import VERSION
|
|
9
|
+
from .bundles import load_bundles, select_bundles
|
|
10
|
+
from .contracts import ContractError, read_json, validate_project, validate_process_lock
|
|
11
|
+
from .distribution import asset_root, distribution_digest, skills_root
|
|
12
|
+
from .managed import (
|
|
13
|
+
AGENTS_END,
|
|
14
|
+
AGENTS_START,
|
|
15
|
+
managed_agents_visibility_issues,
|
|
16
|
+
merge_managed_agents,
|
|
17
|
+
)
|
|
18
|
+
from .publication import (
|
|
19
|
+
PR_DESCRIPTION_END,
|
|
20
|
+
PR_DESCRIPTION_START,
|
|
21
|
+
merge_managed_pull_request_template,
|
|
22
|
+
validate_project_extensions,
|
|
23
|
+
)
|
|
24
|
+
from .syncing import (
|
|
25
|
+
managed_parent_issues,
|
|
26
|
+
selected_skill_target_issues,
|
|
27
|
+
skill_target_ownership_issues,
|
|
28
|
+
sync_skills,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
RUNS_IGNORE = "/.process/runs/"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _serialized(document: Any) -> str:
|
|
36
|
+
return json.dumps(document, ensure_ascii=False, indent=2) + "\n"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _read_optional_text(path: Path) -> str:
|
|
40
|
+
if path.is_symlink():
|
|
41
|
+
raise ContractError(f"{path}: managed file target must not be a symlink")
|
|
42
|
+
if not os.path.lexists(path):
|
|
43
|
+
return ""
|
|
44
|
+
if not path.is_file():
|
|
45
|
+
raise ContractError(f"{path}: expected a regular file")
|
|
46
|
+
try:
|
|
47
|
+
return path.read_text(encoding="utf-8")
|
|
48
|
+
except (OSError, UnicodeError) as error:
|
|
49
|
+
raise ContractError(f"{path}: cannot read existing file: {error}") from error
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _preflight_file(path: Path, content: str, *, replace: bool) -> None:
|
|
53
|
+
if path.is_symlink():
|
|
54
|
+
raise ContractError(f"{path}: managed file target must not be a symlink")
|
|
55
|
+
if os.path.lexists(path):
|
|
56
|
+
current = _read_optional_text(path)
|
|
57
|
+
if current == content:
|
|
58
|
+
return
|
|
59
|
+
if not replace:
|
|
60
|
+
raise ContractError(f"{path}: already exists with different content; use --replace")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _preflight_parents(project_root: Path, *paths: Path) -> None:
|
|
64
|
+
for path in paths:
|
|
65
|
+
parent = path.parent
|
|
66
|
+
while parent != project_root.parent:
|
|
67
|
+
if parent.is_symlink():
|
|
68
|
+
raise ContractError(
|
|
69
|
+
f"{parent}: managed target parent must not be a symlink"
|
|
70
|
+
)
|
|
71
|
+
if os.path.lexists(parent) and not parent.is_dir():
|
|
72
|
+
raise ContractError(
|
|
73
|
+
f"{parent}: target parent must be a directory before bootstrap"
|
|
74
|
+
)
|
|
75
|
+
if parent == project_root:
|
|
76
|
+
break
|
|
77
|
+
parent = parent.parent
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _write_text(path: Path, content: str) -> None:
|
|
81
|
+
if _read_optional_text(path) == content:
|
|
82
|
+
return
|
|
83
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
path.write_text(content, encoding="utf-8")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _agents_update(project_root: Path, process_root: Path) -> tuple[Path, str]:
|
|
88
|
+
template = asset_root(process_root) / "templates" / "AGENTS.process.md"
|
|
89
|
+
if not template.is_file():
|
|
90
|
+
raise ContractError(f"{template}: missing process agent contract template")
|
|
91
|
+
try:
|
|
92
|
+
block = template.read_text(encoding="utf-8")
|
|
93
|
+
except (OSError, UnicodeError) as error:
|
|
94
|
+
raise ContractError(f"{template}: cannot read template: {error}") from error
|
|
95
|
+
path = project_root / "AGENTS.md"
|
|
96
|
+
current = _read_optional_text(path)
|
|
97
|
+
updated = merge_managed_agents(current, block)
|
|
98
|
+
issues = managed_agents_visibility_issues(updated)
|
|
99
|
+
if issues:
|
|
100
|
+
raise ContractError(f"{path}: " + "; ".join(issues))
|
|
101
|
+
return path, updated
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _pull_request_template_update(
|
|
105
|
+
project_root: Path, process_root: Path
|
|
106
|
+
) -> tuple[Path, str]:
|
|
107
|
+
source = asset_root(process_root) / "templates" / "PULL_REQUEST_TEMPLATE.md"
|
|
108
|
+
if not source.is_file():
|
|
109
|
+
raise ContractError(f"{source}: missing pull-request template")
|
|
110
|
+
try:
|
|
111
|
+
block = source.read_text(encoding="utf-8")
|
|
112
|
+
except (OSError, UnicodeError) as error:
|
|
113
|
+
raise ContractError(f"{source}: cannot read template: {error}") from error
|
|
114
|
+
path = project_root / ".github" / "PULL_REQUEST_TEMPLATE.md"
|
|
115
|
+
current = _read_optional_text(path)
|
|
116
|
+
updated = merge_managed_pull_request_template(current, block)
|
|
117
|
+
issues = validate_project_extensions(updated, allow_pending=True)
|
|
118
|
+
if issues:
|
|
119
|
+
raise ContractError(f"{path}: " + "; ".join(issues))
|
|
120
|
+
return path, updated
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _ignore_update(project_root: Path) -> tuple[Path, str]:
|
|
124
|
+
path = project_root / ".gitignore"
|
|
125
|
+
current = _read_optional_text(path)
|
|
126
|
+
lines = [RUNS_IGNORE if line == ".process/runs/" else line for line in current.splitlines()]
|
|
127
|
+
normalized: list[str] = []
|
|
128
|
+
for line in lines:
|
|
129
|
+
if line == RUNS_IGNORE and RUNS_IGNORE in normalized:
|
|
130
|
+
continue
|
|
131
|
+
normalized.append(line)
|
|
132
|
+
if RUNS_IGNORE not in normalized:
|
|
133
|
+
normalized.append(RUNS_IGNORE)
|
|
134
|
+
updated = "\n".join(normalized) + "\n"
|
|
135
|
+
return path, updated
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def initialize_project(
|
|
139
|
+
project_root: Path,
|
|
140
|
+
process_root: Path,
|
|
141
|
+
*,
|
|
142
|
+
manifest_path: Path | None,
|
|
143
|
+
requested_bundles: list[str],
|
|
144
|
+
replace: bool,
|
|
145
|
+
) -> dict[str, object]:
|
|
146
|
+
project_root = project_root.resolve()
|
|
147
|
+
process_root = process_root.resolve()
|
|
148
|
+
target_manifest = project_root / ".process" / "project.json"
|
|
149
|
+
source_manifest = target_manifest if manifest_path is None else manifest_path.resolve()
|
|
150
|
+
document = read_json(source_manifest)
|
|
151
|
+
project = validate_project(document, str(source_manifest))
|
|
152
|
+
|
|
153
|
+
available = load_bundles(process_root, skills_root(process_root))
|
|
154
|
+
selected_bundles = select_bundles(available, requested_bundles)
|
|
155
|
+
skills = tuple(
|
|
156
|
+
sorted(
|
|
157
|
+
{
|
|
158
|
+
skill
|
|
159
|
+
for bundle in selected_bundles
|
|
160
|
+
for skill in available[bundle]
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
lock_document = {
|
|
165
|
+
"schemaVersion": 1,
|
|
166
|
+
"process": {
|
|
167
|
+
"version": VERSION,
|
|
168
|
+
"digest": distribution_digest(process_root, skills),
|
|
169
|
+
},
|
|
170
|
+
"skills": list(skills),
|
|
171
|
+
}
|
|
172
|
+
validate_process_lock(lock_document, str(project_root / ".process" / "process.lock"))
|
|
173
|
+
ownership_issues = [
|
|
174
|
+
*managed_parent_issues(project_root),
|
|
175
|
+
*skill_target_ownership_issues(project_root),
|
|
176
|
+
*selected_skill_target_issues(project_root, skills),
|
|
177
|
+
]
|
|
178
|
+
if ownership_issues:
|
|
179
|
+
raise ContractError("\n".join(ownership_issues))
|
|
180
|
+
|
|
181
|
+
manifest_content = _serialized(document)
|
|
182
|
+
lock_path = project_root / ".process" / "process.lock"
|
|
183
|
+
lock_content = _serialized(lock_document)
|
|
184
|
+
agents_target = project_root / "AGENTS.md"
|
|
185
|
+
pr_target = project_root / ".github" / "PULL_REQUEST_TEMPLATE.md"
|
|
186
|
+
ignore_target = project_root / ".gitignore"
|
|
187
|
+
_preflight_parents(
|
|
188
|
+
project_root,
|
|
189
|
+
target_manifest,
|
|
190
|
+
lock_path,
|
|
191
|
+
agents_target,
|
|
192
|
+
pr_target,
|
|
193
|
+
ignore_target,
|
|
194
|
+
project_root / ".agents" / "skills" / "__process_probe__",
|
|
195
|
+
)
|
|
196
|
+
_preflight_file(target_manifest, manifest_content, replace=replace)
|
|
197
|
+
_preflight_file(lock_path, lock_content, replace=replace)
|
|
198
|
+
agents_path, agents_content = _agents_update(project_root, process_root)
|
|
199
|
+
pr_path, pr_content = _pull_request_template_update(project_root, process_root)
|
|
200
|
+
ignore_path, ignore_content = _ignore_update(project_root)
|
|
201
|
+
|
|
202
|
+
_write_text(target_manifest, manifest_content)
|
|
203
|
+
_write_text(lock_path, lock_content)
|
|
204
|
+
_write_text(agents_path, agents_content)
|
|
205
|
+
_write_text(pr_path, pr_content)
|
|
206
|
+
_write_text(ignore_path, ignore_content)
|
|
207
|
+
issues = sync_skills(project_root, process_root, check=False)
|
|
208
|
+
if issues:
|
|
209
|
+
raise ContractError("\n".join(issues))
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
"project": project.identifier,
|
|
213
|
+
"version": VERSION,
|
|
214
|
+
"digest": lock_document["process"]["digest"],
|
|
215
|
+
"bundles": list(selected_bundles),
|
|
216
|
+
"skills": list(skills),
|
|
217
|
+
}
|