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,3 @@
1
+ """Agent-neutral engineering process."""
2
+
3
+ VERSION = "0.1.0"
@@ -0,0 +1,73 @@
1
+ """Private force-terminable HTTPS download worker."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ import sys
8
+ import time
9
+
10
+ from .contracts import ContractError, ManagedToolArtifact
11
+ from .tooling import _download_artifact_direct, _validated_https_target
12
+
13
+
14
+ _INPUT_LIMIT = 16_384
15
+
16
+
17
+ def _payload() -> tuple[ManagedToolArtifact, Path, float]:
18
+ content = sys.stdin.buffer.read(_INPUT_LIMIT + 1)
19
+ if len(content) > _INPUT_LIMIT:
20
+ raise ContractError("download worker input exceeds its limit")
21
+ try:
22
+ value = json.loads(content.decode("ascii"))
23
+ except (UnicodeDecodeError, json.JSONDecodeError) as error:
24
+ raise ContractError(f"download worker input is invalid: {error}") from error
25
+ if not isinstance(value, dict) or set(value) != {
26
+ "destination",
27
+ "maxDownloadBytes",
28
+ "timeoutSeconds",
29
+ "url",
30
+ }:
31
+ raise ContractError("download worker input has invalid fields")
32
+ url = value["url"]
33
+ maximum = value["maxDownloadBytes"]
34
+ timeout = value["timeoutSeconds"]
35
+ destination_value = value["destination"]
36
+ if not isinstance(url, str):
37
+ raise ContractError("download worker URL must be a string")
38
+ _validated_https_target(url, url)
39
+ if isinstance(maximum, bool) or not isinstance(maximum, int) or maximum < 1:
40
+ raise ContractError("download worker byte limit must be positive")
41
+ if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or timeout <= 0:
42
+ raise ContractError("download worker timeout must be positive")
43
+ if not isinstance(destination_value, str) or "\x00" in destination_value:
44
+ raise ContractError("download worker destination is invalid")
45
+ destination = Path(destination_value)
46
+ if not destination.is_absolute():
47
+ raise ContractError("download worker destination must be absolute")
48
+ artifact = ManagedToolArtifact(
49
+ platform="worker",
50
+ url=url,
51
+ checksum="sha256:" + "0" * 64,
52
+ archive_format="file",
53
+ strip_components=0,
54
+ max_download_bytes=maximum,
55
+ max_extracted_bytes=maximum,
56
+ max_files=1,
57
+ commands={},
58
+ )
59
+ return artifact, destination, time.monotonic() + float(timeout)
60
+
61
+
62
+ def main() -> int:
63
+ try:
64
+ artifact, destination, deadline = _payload()
65
+ _download_artifact_direct(artifact, destination, deadline=deadline)
66
+ return 0
67
+ except Exception as error:
68
+ print(str(error), file=sys.stderr)
69
+ return 125
70
+
71
+
72
+ if __name__ == "__main__":
73
+ raise SystemExit(main())
@@ -0,0 +1,162 @@
1
+ """POSIX process-group backend for foreground-task supervision."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ import signal
8
+ import subprocess
9
+ import time
10
+ from typing import Mapping
11
+
12
+ from .supervision import CleanupOutcome
13
+
14
+
15
+ def _process_group_exists(process_group: int) -> bool:
16
+ try:
17
+ os.killpg(process_group, 0)
18
+ except ProcessLookupError:
19
+ return False
20
+ except PermissionError:
21
+ return True
22
+ return True
23
+
24
+
25
+ def _wait_for_process_group(process_group: int, timeout: float) -> bool:
26
+ deadline = time.monotonic() + timeout
27
+ while time.monotonic() < deadline:
28
+ if not _process_group_exists(process_group):
29
+ return True
30
+ time.sleep(0.02)
31
+ return not _process_group_exists(process_group)
32
+
33
+
34
+ def _terminate_group(process_group: int, grace_seconds: float) -> CleanupOutcome:
35
+ if not _process_group_exists(process_group):
36
+ return CleanupOutcome(bounded=True)
37
+ try:
38
+ os.killpg(process_group, signal.SIGTERM)
39
+ except ProcessLookupError:
40
+ return CleanupOutcome(bounded=True, descendants_found=True)
41
+ except OSError:
42
+ pass
43
+ if _wait_for_process_group(process_group, grace_seconds):
44
+ return CleanupOutcome(bounded=True, descendants_found=True)
45
+ try:
46
+ os.killpg(process_group, signal.SIGKILL)
47
+ except ProcessLookupError:
48
+ return CleanupOutcome(bounded=True, descendants_found=True)
49
+ except OSError:
50
+ pass
51
+ bounded = _wait_for_process_group(process_group, grace_seconds)
52
+ return CleanupOutcome(
53
+ bounded=bounded,
54
+ descendants_found=True,
55
+ error=(
56
+ None
57
+ if bounded
58
+ else "command process group could not be terminated within the bounded grace period"
59
+ ),
60
+ )
61
+
62
+
63
+ class PosixProcessSupervisor:
64
+ def resolve_application(
65
+ self,
66
+ command: str,
67
+ *,
68
+ working_directory: Path,
69
+ environment: Mapping[str, str],
70
+ ) -> Path:
71
+ if not command or "\x00" in command:
72
+ raise OSError("command executable is invalid")
73
+ supplied = Path(command)
74
+ if supplied.is_absolute() or supplied.parent != Path("."):
75
+ candidate = supplied if supplied.is_absolute() else working_directory / supplied
76
+ try:
77
+ resolved = candidate.resolve(strict=True)
78
+ except OSError as error:
79
+ raise OSError(f"executable is unavailable: {command}") from error
80
+ if not resolved.is_file() or not os.access(resolved, os.X_OK):
81
+ raise OSError(f"executable is unavailable: {command}")
82
+ return resolved
83
+ for raw_directory in environment.get("PATH", "").split(os.pathsep):
84
+ directory = Path(raw_directory or ".")
85
+ if not directory.is_absolute():
86
+ directory = working_directory / directory
87
+ candidate = directory / command
88
+ try:
89
+ resolved = candidate.resolve(strict=True)
90
+ except OSError:
91
+ continue
92
+ if resolved.is_file() and os.access(resolved, os.X_OK):
93
+ return resolved
94
+ raise OSError(f"executable is unavailable: {command}")
95
+
96
+ def spawn(
97
+ self,
98
+ command: tuple[str, ...],
99
+ *,
100
+ working_directory: Path,
101
+ environment: Mapping[str, str],
102
+ ) -> subprocess.Popen[bytes]:
103
+ application = self.resolve_application(
104
+ command[0],
105
+ working_directory=working_directory,
106
+ environment=environment,
107
+ )
108
+ return subprocess.Popen(
109
+ command,
110
+ executable=application,
111
+ cwd=working_directory,
112
+ stdin=subprocess.DEVNULL,
113
+ stdout=subprocess.PIPE,
114
+ stderr=subprocess.PIPE,
115
+ shell=False,
116
+ env=dict(environment),
117
+ start_new_session=True,
118
+ )
119
+
120
+ def terminate(
121
+ self,
122
+ process: subprocess.Popen[bytes],
123
+ *,
124
+ grace_seconds: float,
125
+ ) -> CleanupOutcome:
126
+ try:
127
+ os.killpg(process.pid, signal.SIGTERM)
128
+ except ProcessLookupError:
129
+ pass
130
+ except OSError:
131
+ pass
132
+ try:
133
+ process.wait(timeout=grace_seconds)
134
+ except subprocess.TimeoutExpired:
135
+ try:
136
+ os.killpg(process.pid, signal.SIGKILL)
137
+ except ProcessLookupError:
138
+ pass
139
+ except OSError:
140
+ pass
141
+ try:
142
+ process.wait(timeout=grace_seconds)
143
+ except subprocess.TimeoutExpired:
144
+ return CleanupOutcome(
145
+ bounded=False,
146
+ error="command root process survived bounded process-group termination",
147
+ )
148
+ descendants = _terminate_group(process.pid, grace_seconds)
149
+ return CleanupOutcome(bounded=descendants.bounded, error=descendants.error)
150
+
151
+ def finalize(
152
+ self,
153
+ process: subprocess.Popen[bytes],
154
+ *,
155
+ grace_seconds: float,
156
+ ) -> CleanupOutcome:
157
+ # The group id remains valid while any descendant from the owned session is
158
+ # alive, even after the original process has exited.
159
+ return _terminate_group(process.pid, grace_seconds)
160
+
161
+
162
+ POSIX_SUPERVISOR = PosixProcessSupervisor()
@@ -0,0 +1,148 @@
1
+ """Windows Job Object backend for foreground-task supervision."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ import subprocess
8
+ from typing import Mapping
9
+
10
+ from .helper_launch import isolated_helper_command
11
+ from .supervision import CleanupOutcome
12
+
13
+
14
+ _UNSUPPORTED_SHELL_SUFFIXES = {".bat", ".cmd"}
15
+
16
+
17
+ def resolve_windows_application(
18
+ command: str,
19
+ *,
20
+ working_directory: Path,
21
+ environment: Mapping[str, str],
22
+ path_separator: str | None = None,
23
+ ) -> Path:
24
+ """Resolve a native executable without CreateProcessW's CWD-first search.
25
+
26
+ Relative PATH entries are ignored. A project executable is therefore selected
27
+ only when the command explicitly contains a path, never merely because a
28
+ same-named file exists in the checkout.
29
+ """
30
+
31
+ if not command or "\x00" in command:
32
+ raise OSError("command executable is invalid")
33
+ separator = os.pathsep if path_separator is None else path_separator
34
+ supplied = Path(command)
35
+ explicit_path = supplied.is_absolute() or supplied.parent != Path(".")
36
+ suffix = supplied.suffix.casefold()
37
+ if suffix in _UNSUPPORTED_SHELL_SUFFIXES:
38
+ raise OSError(
39
+ f"Windows batch command {command} requires a shell; only native .exe commands are supported"
40
+ )
41
+
42
+ candidates: list[Path] = []
43
+ names = (command,) if suffix else (f"{command}.exe",)
44
+ if explicit_path:
45
+ base = supplied if supplied.is_absolute() else working_directory / supplied
46
+ candidates.append(base if suffix else Path(f"{base}.exe"))
47
+ else:
48
+ for raw_directory in environment.get("PATH", "").split(separator):
49
+ if not raw_directory:
50
+ continue
51
+ directory = Path(raw_directory)
52
+ if not directory.is_absolute():
53
+ continue
54
+ candidates.extend(directory / name for name in names)
55
+
56
+ for candidate in candidates:
57
+ try:
58
+ resolved = candidate.resolve(strict=True)
59
+ except OSError:
60
+ continue
61
+ if resolved.is_file() and resolved.suffix.casefold() == ".exe":
62
+ return resolved
63
+ raise OSError(f"native Windows executable is unavailable: {command}")
64
+
65
+
66
+ class WindowsProcessSupervisor:
67
+ def resolve_application(
68
+ self,
69
+ command: str,
70
+ *,
71
+ working_directory: Path,
72
+ environment: Mapping[str, str],
73
+ ) -> Path:
74
+ return resolve_windows_application(
75
+ command,
76
+ working_directory=working_directory,
77
+ environment=environment,
78
+ )
79
+
80
+ def spawn(
81
+ self,
82
+ command: tuple[str, ...],
83
+ *,
84
+ working_directory: Path,
85
+ environment: Mapping[str, str],
86
+ ) -> subprocess.Popen[bytes]:
87
+ application = self.resolve_application(
88
+ command[0],
89
+ working_directory=working_directory,
90
+ environment=environment,
91
+ )
92
+ wrapper = isolated_helper_command(
93
+ "engineering_process._windows_job",
94
+ "--application",
95
+ str(application),
96
+ "--",
97
+ *command,
98
+ )
99
+ return subprocess.Popen(
100
+ wrapper,
101
+ cwd=working_directory,
102
+ stdin=subprocess.DEVNULL,
103
+ stdout=subprocess.PIPE,
104
+ stderr=subprocess.PIPE,
105
+ shell=False,
106
+ env=dict(environment),
107
+ creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
108
+ )
109
+
110
+ def terminate(
111
+ self,
112
+ process: subprocess.Popen[bytes],
113
+ *,
114
+ grace_seconds: float,
115
+ ) -> CleanupOutcome:
116
+ if process.poll() is not None:
117
+ return CleanupOutcome(bounded=True)
118
+ try:
119
+ process.terminate()
120
+ process.wait(timeout=grace_seconds)
121
+ return CleanupOutcome(bounded=True)
122
+ except (OSError, subprocess.TimeoutExpired):
123
+ try:
124
+ process.kill()
125
+ except OSError:
126
+ pass
127
+ try:
128
+ process.wait(timeout=grace_seconds)
129
+ except subprocess.TimeoutExpired:
130
+ return CleanupOutcome(
131
+ bounded=False,
132
+ error="Windows Job Object wrapper survived bounded termination",
133
+ )
134
+ return CleanupOutcome(bounded=True)
135
+
136
+ def finalize(
137
+ self,
138
+ process: subprocess.Popen[bytes],
139
+ *,
140
+ grace_seconds: float,
141
+ ) -> CleanupOutcome:
142
+ del process, grace_seconds
143
+ # The helper does not return until the target exits and its Job Object has
144
+ # no live descendants. Cleanup failures are represented by its exit code.
145
+ return CleanupOutcome(bounded=True)
146
+
147
+
148
+ WINDOWS_SUPERVISOR = WindowsProcessSupervisor()