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.
Files changed (69) hide show
  1. engineering_process/__init__.py +3 -0
  2. engineering_process/_download_worker.py +73 -0
  3. engineering_process/_supervisor_posix.py +162 -0
  4. engineering_process/_supervisor_windows.py +148 -0
  5. engineering_process/_windows_job.py +419 -0
  6. engineering_process/bootstrap.py +217 -0
  7. engineering_process/bundles.py +81 -0
  8. engineering_process/cli.py +1012 -0
  9. engineering_process/contracts.py +1274 -0
  10. engineering_process/distribution.py +90 -0
  11. engineering_process/environment.py +753 -0
  12. engineering_process/helper_launch.py +37 -0
  13. engineering_process/lifecycle.py +1024 -0
  14. engineering_process/managed.py +89 -0
  15. engineering_process/markdown.py +78 -0
  16. engineering_process/publication.py +463 -0
  17. engineering_process/requirements-build.txt +1 -0
  18. engineering_process/requirements-dev.txt +10 -0
  19. engineering_process/requirements-runtime.txt +3 -0
  20. engineering_process/runner.py +191 -0
  21. engineering_process/runtime.py +55 -0
  22. engineering_process/skills.py +118 -0
  23. engineering_process/supervision.py +72 -0
  24. engineering_process/syncing.py +398 -0
  25. engineering_process/tooling.py +891 -0
  26. engineering_process-0.1.0.data/data/share/engineering-process/bundles.json +47 -0
  27. engineering_process-0.1.0.data/data/share/engineering-process/examples/change.json +26 -0
  28. engineering_process-0.1.0.data/data/share/engineering-process/examples/plan.json +28 -0
  29. engineering_process-0.1.0.data/data/share/engineering-process/examples/project.json +54 -0
  30. engineering_process-0.1.0.data/data/share/engineering-process/examples/review.json +31 -0
  31. engineering_process-0.1.0.data/data/share/engineering-process/schemas/change.schema.json +88 -0
  32. engineering_process-0.1.0.data/data/share/engineering-process/schemas/lifecycle.schema.json +122 -0
  33. engineering_process-0.1.0.data/data/share/engineering-process/schemas/plan.schema.json +105 -0
  34. engineering_process-0.1.0.data/data/share/engineering-process/schemas/process-lock.schema.json +39 -0
  35. engineering_process-0.1.0.data/data/share/engineering-process/schemas/project.schema.json +515 -0
  36. engineering_process-0.1.0.data/data/share/engineering-process/schemas/review.schema.json +140 -0
  37. engineering_process-0.1.0.data/data/share/engineering-process/schemas/reviewer-attestation.schema.json +30 -0
  38. engineering_process-0.1.0.data/data/share/engineering-process/schemas/verification.schema.json +120 -0
  39. engineering_process-0.1.0.data/data/share/engineering-process/skills/assess-design/SKILL.md +39 -0
  40. engineering_process-0.1.0.data/data/share/engineering-process/skills/build-frontend/SKILL.md +38 -0
  41. engineering_process-0.1.0.data/data/share/engineering-process/skills/build-frontend-foundation/SKILL.md +36 -0
  42. engineering_process-0.1.0.data/data/share/engineering-process/skills/change-api/SKILL.md +36 -0
  43. engineering_process-0.1.0.data/data/share/engineering-process/skills/cross-repo-change/SKILL.md +37 -0
  44. engineering_process-0.1.0.data/data/share/engineering-process/skills/define-change-contract/SKILL.md +40 -0
  45. engineering_process-0.1.0.data/data/share/engineering-process/skills/design-module/SKILL.md +37 -0
  46. engineering_process-0.1.0.data/data/share/engineering-process/skills/evolve-process/SKILL.md +37 -0
  47. engineering_process-0.1.0.data/data/share/engineering-process/skills/finish-change/SKILL.md +34 -0
  48. engineering_process-0.1.0.data/data/share/engineering-process/skills/govern-ui/SKILL.md +40 -0
  49. engineering_process-0.1.0.data/data/share/engineering-process/skills/implement-change/SKILL.md +41 -0
  50. engineering_process-0.1.0.data/data/share/engineering-process/skills/implement-module/SKILL.md +36 -0
  51. engineering_process-0.1.0.data/data/share/engineering-process/skills/implement-use-case/SKILL.md +36 -0
  52. engineering_process-0.1.0.data/data/share/engineering-process/skills/integrate-mcp/SKILL.md +37 -0
  53. engineering_process-0.1.0.data/data/share/engineering-process/skills/maintain-docs/SKILL.md +35 -0
  54. engineering_process-0.1.0.data/data/share/engineering-process/skills/plan-change/SKILL.md +37 -0
  55. engineering_process-0.1.0.data/data/share/engineering-process/skills/publish-change/SKILL.md +38 -0
  56. engineering_process-0.1.0.data/data/share/engineering-process/skills/review-change/SKILL.md +49 -0
  57. engineering_process-0.1.0.data/data/share/engineering-process/skills/run-change/SKILL.md +51 -0
  58. engineering_process-0.1.0.data/data/share/engineering-process/skills/run-change/references/execution.md +88 -0
  59. engineering_process-0.1.0.data/data/share/engineering-process/skills/run-project-command/SKILL.md +55 -0
  60. engineering_process-0.1.0.data/data/share/engineering-process/skills/specify-use-case/SKILL.md +37 -0
  61. engineering_process-0.1.0.data/data/share/engineering-process/skills/verify-change/SKILL.md +38 -0
  62. engineering_process-0.1.0.data/data/share/engineering-process/templates/AGENTS.process.md +17 -0
  63. engineering_process-0.1.0.data/data/share/engineering-process/templates/PULL_REQUEST_TEMPLATE.md +27 -0
  64. engineering_process-0.1.0.dist-info/METADATA +387 -0
  65. engineering_process-0.1.0.dist-info/RECORD +69 -0
  66. engineering_process-0.1.0.dist-info/WHEEL +5 -0
  67. engineering_process-0.1.0.dist-info/entry_points.txt +2 -0
  68. engineering_process-0.1.0.dist-info/licenses/LICENSE +21 -0
  69. 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
+ }