engineering-process 1.1.1__tar.gz → 1.1.2__tar.gz

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 (68) hide show
  1. {engineering_process-1.1.1/engineering_process.egg-info → engineering_process-1.1.2}/PKG-INFO +1 -1
  2. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process/__init__.py +1 -1
  3. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process/commands.py +1 -1
  4. {engineering_process-1.1.1 → engineering_process-1.1.2/engineering_process.egg-info}/PKG-INFO +1 -1
  5. {engineering_process-1.1.1 → engineering_process-1.1.2}/process_assets/skills/verify-change/SKILL.md +4 -3
  6. {engineering_process-1.1.1 → engineering_process-1.1.2}/pyproject.toml +1 -1
  7. {engineering_process-1.1.1 → engineering_process-1.1.2}/tests/test_commands.py +61 -6
  8. {engineering_process-1.1.1 → engineering_process-1.1.2}/LICENSE +0 -0
  9. {engineering_process-1.1.1 → engineering_process-1.1.2}/MANIFEST.in +0 -0
  10. {engineering_process-1.1.1 → engineering_process-1.1.2}/README.md +0 -0
  11. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process/__main__.py +0 -0
  12. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process/_supervisor_posix.py +0 -0
  13. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process/_supervisor_windows.py +0 -0
  14. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process/_windows_job.py +0 -0
  15. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process/adoption.py +0 -0
  16. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process/cli.py +0 -0
  17. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process/contracts.py +0 -0
  18. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process/distribution.py +0 -0
  19. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process/helper_launch.py +0 -0
  20. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process/lifecycle.py +0 -0
  21. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process/project.py +0 -0
  22. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process/publication_compat.py +0 -0
  23. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process/release.py +0 -0
  24. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process/repository.py +0 -0
  25. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process/requirements-dev.txt +0 -0
  26. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process/requirements-runtime.txt +0 -0
  27. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process/skills.py +0 -0
  28. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process/supervision.py +0 -0
  29. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process.egg-info/SOURCES.txt +0 -0
  30. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process.egg-info/dependency_links.txt +0 -0
  31. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process.egg-info/entry_points.txt +0 -0
  32. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process.egg-info/requires.txt +0 -0
  33. {engineering_process-1.1.1 → engineering_process-1.1.2}/engineering_process.egg-info/top_level.txt +0 -0
  34. {engineering_process-1.1.1 → engineering_process-1.1.2}/process-graph.json +0 -0
  35. {engineering_process-1.1.1 → engineering_process-1.1.2}/process_assets/skills/finish-change/SKILL.md +0 -0
  36. {engineering_process-1.1.1 → engineering_process-1.1.2}/process_assets/skills/implement-change/SKILL.md +0 -0
  37. {engineering_process-1.1.1 → engineering_process-1.1.2}/process_assets/skills/improve-process/SKILL.md +0 -0
  38. {engineering_process-1.1.1 → engineering_process-1.1.2}/process_assets/skills/plan-change/SKILL.md +0 -0
  39. {engineering_process-1.1.1 → engineering_process-1.1.2}/process_assets/skills/review-change/SKILL.md +0 -0
  40. {engineering_process-1.1.1 → engineering_process-1.1.2}/process_assets/skills/run-change/SKILL.md +0 -0
  41. {engineering_process-1.1.1 → engineering_process-1.1.2}/process_assets/skills/start-change/SKILL.md +0 -0
  42. {engineering_process-1.1.1 → engineering_process-1.1.2}/schemas/change.schema.json +0 -0
  43. {engineering_process-1.1.1 → engineering_process-1.1.2}/schemas/plan.schema.json +0 -0
  44. {engineering_process-1.1.1 → engineering_process-1.1.2}/schemas/process-graph.schema.json +0 -0
  45. {engineering_process-1.1.1 → engineering_process-1.1.2}/schemas/process-lock.schema.json +0 -0
  46. {engineering_process-1.1.1 → engineering_process-1.1.2}/schemas/project-legacy.schema.json +0 -0
  47. {engineering_process-1.1.1 → engineering_process-1.1.2}/schemas/project.schema.json +0 -0
  48. {engineering_process-1.1.1 → engineering_process-1.1.2}/schemas/receipt.schema.json +0 -0
  49. {engineering_process-1.1.1 → engineering_process-1.1.2}/schemas/release-change.schema.json +0 -0
  50. {engineering_process-1.1.1 → engineering_process-1.1.2}/schemas/release.schema.json +0 -0
  51. {engineering_process-1.1.1 → engineering_process-1.1.2}/schemas/review.schema.json +0 -0
  52. {engineering_process-1.1.1 → engineering_process-1.1.2}/schemas/run.schema.json +0 -0
  53. {engineering_process-1.1.1 → engineering_process-1.1.2}/setup.cfg +0 -0
  54. {engineering_process-1.1.1 → engineering_process-1.1.2}/templates/AGENTS.process.md +0 -0
  55. {engineering_process-1.1.1 → engineering_process-1.1.2}/templates/PULL_REQUEST_TEMPLATE.md +0 -0
  56. {engineering_process-1.1.1 → engineering_process-1.1.2}/templates/adopt-process-windows-job.py +0 -0
  57. {engineering_process-1.1.1 → engineering_process-1.1.2}/templates/adopt-process.py +0 -0
  58. {engineering_process-1.1.1 → engineering_process-1.1.2}/tests/test_adoption.py +0 -0
  59. {engineering_process-1.1.1 → engineering_process-1.1.2}/tests/test_architecture.py +0 -0
  60. {engineering_process-1.1.1 → engineering_process-1.1.2}/tests/test_automation.py +0 -0
  61. {engineering_process-1.1.1 → engineering_process-1.1.2}/tests/test_cli.py +0 -0
  62. {engineering_process-1.1.1 → engineering_process-1.1.2}/tests/test_contracts.py +0 -0
  63. {engineering_process-1.1.1 → engineering_process-1.1.2}/tests/test_lifecycle.py +0 -0
  64. {engineering_process-1.1.1 → engineering_process-1.1.2}/tests/test_publication_compat.py +0 -0
  65. {engineering_process-1.1.1 → engineering_process-1.1.2}/tests/test_release.py +0 -0
  66. {engineering_process-1.1.1 → engineering_process-1.1.2}/tests/test_repository.py +0 -0
  67. {engineering_process-1.1.1 → engineering_process-1.1.2}/tests/test_skills.py +0 -0
  68. {engineering_process-1.1.1 → engineering_process-1.1.2}/tests/test_supervisor_posix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engineering-process
3
- Version: 1.1.1
3
+ Version: 1.1.2
4
4
  Summary: A small agent-neutral engineering lifecycle with managed adoption
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/phuongnse/engineering-process
@@ -1,3 +1,3 @@
1
1
  """Agent-neutral engineering process."""
2
2
 
3
- VERSION = "1.1.1"
3
+ VERSION = "1.1.2"
@@ -155,6 +155,7 @@ def _run_check(project_root: Path, check: dict[str, Any]) -> dict[str, Any]:
155
155
  )
156
156
  descendants = descendants or cleanup.descendants_found
157
157
  cleanup_error = cleanup_error or cleanup.error
158
+ cleanup_error = cleanup_error or (None if cleanup.bounded else "unbounded cleanup")
158
159
  exit_code = process.returncode if process.returncode is not None else -1
159
160
 
160
161
  for reader in readers:
@@ -173,7 +174,6 @@ def _run_check(project_root: Path, check: dict[str, Any]) -> dict[str, Any]:
173
174
  exit_code == 0
174
175
  and not timed_out
175
176
  and not output_exceeded
176
- and not descendants
177
177
  and not stream_failed
178
178
  )
179
179
  return {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engineering-process
3
- Version: 1.1.1
3
+ Version: 1.1.2
4
4
  Summary: A small agent-neutral engineering lifecycle with managed adoption
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/phuongnse/engineering-process
@@ -11,9 +11,10 @@ profile through the lifecycle:
11
11
  processctl change verify --change-id ID --profile PROFILE
12
12
 
13
13
  Commands are exact argument arrays with timeouts. Do not substitute a different tool
14
- or narrower check when a required command fails. A command failure, timeout,
15
- surviving descendant, or tracked repository mutation is a failure and leaves the
16
- change in implementing.
14
+ or narrower check when a required command fails. A command failure, timeout, output
15
+ or stream failure, failed descendant cleanup, or tracked repository mutation is a
16
+ failure and leaves the change in implementing. Successfully cleaned post-exit
17
+ descendants remain recorded without replacing the foreground command result.
17
18
 
18
19
  The contract must already include conditional profiles required by affected enforced
19
20
  capabilities. Run those profiles exactly; do not run every planned production gate for
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "engineering-process"
7
- version = "1.1.1"
7
+ version = "1.1.2"
8
8
  description = "A small agent-neutral engineering lifecycle with managed adoption"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import ctypes
4
+ import io
3
5
  import os
4
6
  from pathlib import Path
5
7
  import subprocess
@@ -7,8 +9,38 @@ import sys
7
9
  import tempfile
8
10
  import time
9
11
  import unittest
12
+ from unittest.mock import Mock, patch
10
13
 
11
14
  from engineering_process.commands import run_check, run_profile
15
+ from engineering_process.supervision import CleanupOutcome
16
+
17
+
18
+ def windows_process_is_running(process_id: int) -> bool:
19
+ from ctypes import wintypes
20
+
21
+ synchronize = 0x00100000
22
+ wait_failed = 0xFFFFFFFF
23
+ wait_timeout = 0x00000102
24
+ kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
25
+ kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
26
+ kernel32.OpenProcess.restype = wintypes.HANDLE
27
+ kernel32.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD]
28
+ kernel32.WaitForSingleObject.restype = wintypes.DWORD
29
+ kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
30
+ kernel32.CloseHandle.restype = wintypes.BOOL
31
+ handle = kernel32.OpenProcess(synchronize, False, process_id)
32
+ if not handle:
33
+ error = ctypes.get_last_error()
34
+ if error == 87: # ERROR_INVALID_PARAMETER: the PID no longer exists.
35
+ return False
36
+ raise ctypes.WinError(error)
37
+ try:
38
+ result = kernel32.WaitForSingleObject(handle, 0)
39
+ if result == wait_failed:
40
+ raise ctypes.WinError(ctypes.get_last_error())
41
+ return result == wait_timeout
42
+ finally:
43
+ kernel32.CloseHandle(handle)
12
44
 
13
45
 
14
46
  class CommandTests(unittest.TestCase):
@@ -64,6 +96,27 @@ class CommandTests(unittest.TestCase):
64
96
  self.assertEqual("failed", report["status"])
65
97
  self.assertTrue(report["timedOut"])
66
98
 
99
+ def test_unbounded_cleanup_fails_without_an_adapter_error(self) -> None:
100
+ process = Mock(stdout=io.BytesIO(), stderr=io.BytesIO(), returncode=0)
101
+ process.poll.return_value = 0
102
+ supervisor = Mock()
103
+ supervisor.spawn.return_value = process
104
+ supervisor.finalize.return_value = CleanupOutcome(bounded=False)
105
+ with tempfile.TemporaryDirectory() as directory, patch(
106
+ "engineering_process.commands.process_supervisor",
107
+ return_value=supervisor,
108
+ ):
109
+ report = run_check(
110
+ Path(directory),
111
+ {
112
+ "id": "unbounded-cleanup",
113
+ "run": [sys.executable, "-c", "raise SystemExit(0)"],
114
+ "timeoutSeconds": 10,
115
+ },
116
+ )
117
+ self.assertEqual("failed", report["status"])
118
+ self.assertTrue(report["streamFailed"])
119
+
67
120
  @unittest.skipUnless(
68
121
  sys.platform.startswith("linux"), "Linux subreaper ownership assertion"
69
122
  )
@@ -93,7 +146,7 @@ class CommandTests(unittest.TestCase):
93
146
  unrelated.wait(timeout=3)
94
147
 
95
148
  @unittest.skipIf(os.name == "nt", "POSIX process-group assertion")
96
- def test_surviving_descendant_is_terminated_and_fails(self) -> None:
149
+ def test_surviving_descendant_is_terminated_after_success(self) -> None:
97
150
  script = (
98
151
  "import subprocess, sys; "
99
152
  "subprocess.Popen([sys.executable, '-c', 'import time; time.sleep(30)']); "
@@ -108,7 +161,7 @@ class CommandTests(unittest.TestCase):
108
161
  "timeoutSeconds": 10,
109
162
  },
110
163
  )
111
- self.assertEqual("failed", report["status"])
164
+ self.assertEqual("passed", report["status"])
112
165
  self.assertTrue(report["descendantsTerminated"])
113
166
 
114
167
  @unittest.skipUnless(
@@ -142,7 +195,7 @@ class CommandTests(unittest.TestCase):
142
195
  @unittest.skipUnless(
143
196
  sys.platform.startswith("linux"), "Linux subreaper containment assertion"
144
197
  )
145
- def test_detached_descendant_is_terminated_and_fails(self) -> None:
198
+ def test_detached_descendant_is_terminated_after_success(self) -> None:
146
199
  with tempfile.TemporaryDirectory() as directory:
147
200
  root = Path(directory)
148
201
  pid_path = root / "child.pid"
@@ -166,12 +219,12 @@ class CommandTests(unittest.TestCase):
166
219
  deadline = time.monotonic() + 3
167
220
  while Path(f"/proc/{pid}").exists() and time.monotonic() < deadline:
168
221
  time.sleep(0.02)
169
- self.assertEqual("failed", report["status"])
222
+ self.assertEqual("passed", report["status"])
170
223
  self.assertTrue(report["descendantsTerminated"])
171
224
  self.assertFalse(Path(f"/proc/{pid}").exists())
172
225
 
173
226
  @unittest.skipUnless(os.name == "nt", "Windows Job Object containment assertion")
174
- def test_windows_job_object_terminates_descendants(self) -> None:
227
+ def test_windows_job_object_terminates_descendants_after_success(self) -> None:
175
228
  with tempfile.TemporaryDirectory() as directory:
176
229
  root = Path(directory)
177
230
  pid_path = root / "child.pid"
@@ -189,8 +242,10 @@ class CommandTests(unittest.TestCase):
189
242
  "timeoutSeconds": 10,
190
243
  },
191
244
  )
192
- self.assertEqual("failed", report["status"])
245
+ pid = int(pid_path.read_text(encoding="utf-8"))
246
+ self.assertEqual("passed", report["status"])
193
247
  self.assertTrue(report["descendantsTerminated"])
248
+ self.assertFalse(windows_process_is_running(pid))
194
249
 
195
250
 
196
251
  if __name__ == "__main__":