engineering-process 2.2.0__tar.gz → 2.3.0__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 (98) hide show
  1. {engineering_process-2.2.0/engineering_process.egg-info → engineering_process-2.3.0}/PKG-INFO +39 -2
  2. {engineering_process-2.2.0 → engineering_process-2.3.0}/README.md +38 -1
  3. engineering_process-2.3.0/RELEASE_NOTES.md +23 -0
  4. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/__init__.py +1 -1
  5. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/_supervisor_posix.py +10 -4
  6. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/_supervisor_windows.py +2 -1
  7. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/artifact_standards.py +8 -0
  8. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/cli.py +17 -4
  9. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/commands.py +6 -0
  10. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/contracts.py +1 -0
  11. engineering_process-2.3.0/engineering_process/issue.py +112 -0
  12. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/lifecycle.py +36 -0
  13. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/project.py +5 -0
  14. engineering_process-2.3.0/engineering_process/publication_compat.py +58 -0
  15. engineering_process-2.2.0/engineering_process/publication_compat.py → engineering_process-2.3.0/engineering_process/source_publication.py +55 -41
  16. {engineering_process-2.2.0 → engineering_process-2.3.0/engineering_process.egg-info}/PKG-INFO +39 -2
  17. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process.egg-info/SOURCES.txt +4 -0
  18. {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/skills/change-complete/SKILL.md +32 -0
  19. {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/skills/change-review/SKILL.md +9 -2
  20. {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/skills/change-start/SKILL.md +7 -1
  21. {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/skills/change-verify/SKILL.md +10 -0
  22. {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/skills/deliver-change/SKILL.md +26 -0
  23. engineering_process-2.3.0/process_assets/standards/issue.v1.json +78 -0
  24. {engineering_process-2.2.0 → engineering_process-2.3.0}/pyproject.toml +3 -1
  25. {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/artifact-standard.schema.json +86 -1
  26. engineering_process-2.3.0/schemas/issue-data.schema.json +44 -0
  27. {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/project.schema.json +8 -0
  28. {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/receipt.schema.json +20 -2
  29. {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_adoption.py +11 -2
  30. {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_architecture.py +2 -0
  31. {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_artifact_standards.py +155 -1
  32. {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_commands.py +100 -2
  33. {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_contracts.py +70 -0
  34. {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_lifecycle.py +153 -1
  35. {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_publication_compat.py +101 -0
  36. {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_supervisor_posix.py +35 -0
  37. engineering_process-2.2.0/RELEASE_NOTES.md +0 -18
  38. {engineering_process-2.2.0 → engineering_process-2.3.0}/LICENSE +0 -0
  39. {engineering_process-2.2.0 → engineering_process-2.3.0}/MANIFEST.in +0 -0
  40. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/__main__.py +0 -0
  41. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/_supervisor_contract.py +0 -0
  42. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/_windows_job.py +0 -0
  43. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/adoption.py +0 -0
  44. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/automation_name.py +0 -0
  45. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/distribution.py +0 -0
  46. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/helper_launch.py +0 -0
  47. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/pr_description.py +0 -0
  48. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/production_engineering.py +0 -0
  49. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/release.py +0 -0
  50. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/release_notes.py +0 -0
  51. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/repository.py +0 -0
  52. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/requirements-dev.txt +0 -0
  53. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/requirements-runtime.txt +0 -0
  54. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/skills.py +0 -0
  55. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process/supervision.py +0 -0
  56. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process.egg-info/dependency_links.txt +0 -0
  57. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process.egg-info/entry_points.txt +0 -0
  58. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process.egg-info/requires.txt +0 -0
  59. {engineering_process-2.2.0 → engineering_process-2.3.0}/engineering_process.egg-info/top_level.txt +0 -0
  60. {engineering_process-2.2.0 → engineering_process-2.3.0}/process-graph.json +0 -0
  61. {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/skills/change-implement/SKILL.md +0 -0
  62. {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/skills/change-plan/SKILL.md +0 -0
  63. {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/skills/process-improve/SKILL.md +0 -0
  64. {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/skills/production-engineering/SKILL.md +0 -0
  65. {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/skills/production-engineering/invariants.json +0 -0
  66. {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/standards/automation-name.v1.json +0 -0
  67. {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/standards/pull-request.v1.json +0 -0
  68. {engineering_process-2.2.0 → engineering_process-2.3.0}/process_assets/standards/release-notes.v1.json +0 -0
  69. {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/artifact-selection.schema.json +0 -0
  70. {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/automation-name-data.schema.json +0 -0
  71. {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/change.schema.json +0 -0
  72. {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/plan.schema.json +0 -0
  73. {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/pr-description-data.schema.json +0 -0
  74. {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/process-graph.schema.json +0 -0
  75. {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/process-lock.schema.json +0 -0
  76. {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/production-engineering.schema.json +0 -0
  77. {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/project-legacy.schema.json +0 -0
  78. {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/release-change.schema.json +0 -0
  79. {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/release-notes-data.schema.json +0 -0
  80. {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/release.schema.json +0 -0
  81. {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/renovate-preset.schema.json +0 -0
  82. {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/review.schema.json +0 -0
  83. {engineering_process-2.2.0 → engineering_process-2.3.0}/schemas/run.schema.json +0 -0
  84. {engineering_process-2.2.0 → engineering_process-2.3.0}/setup.cfg +0 -0
  85. {engineering_process-2.2.0 → engineering_process-2.3.0}/templates/AGENTS.process.md +0 -0
  86. {engineering_process-2.2.0 → engineering_process-2.3.0}/templates/PULL_REQUEST_TEMPLATE.md +0 -0
  87. {engineering_process-2.2.0 → engineering_process-2.3.0}/templates/adopt-process-windows-job.py +0 -0
  88. {engineering_process-2.2.0 → engineering_process-2.3.0}/templates/adopt-process.py +0 -0
  89. {engineering_process-2.2.0 → engineering_process-2.3.0}/templates/renovate.json +0 -0
  90. {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_automation.py +0 -0
  91. {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_cli.py +0 -0
  92. {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_distribution.py +0 -0
  93. {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_production_engineering.py +0 -0
  94. {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_publication_workflow.py +0 -0
  95. {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_pypi_cache_horizon.py +0 -0
  96. {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_release.py +0 -0
  97. {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_repository.py +0 -0
  98. {engineering_process-2.2.0 → engineering_process-2.3.0}/tests/test_skills.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engineering-process
3
- Version: 2.2.0
3
+ Version: 2.3.0
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
@@ -73,10 +73,31 @@ The coordinator hands review to an actual reviewer. A new agent review starts in
73
73
  fresh context with the accepted source artifacts and no suggested verdict; the same
74
74
  reviewer continues corrections. The reviewer inspects the change and authors its
75
75
  own report. The runner's existing task/session interaction and returned result make
76
- that work inspectable. Sharing a model, provider, or account is allowed; this is a
76
+ that work inspectable. Sharing a provider or account is allowed; this is a
77
77
  workflow for independent judgment, not authenticated identity or merge enforcement.
78
78
  See [change-review](process_assets/skills/change-review/SKILL.md) for the handoff.
79
79
 
80
+ Every delegated agent and reviewer follows the active user-selected model and
81
+ reasoning effort under [deliver-change's settings rule](process_assets/skills/deliver-change/SKILL.md#preserve-agent-execution-settings).
82
+ It covers fresh agents and resumed sessions, requires native runtime confirmation,
83
+ and permits no autonomous upgrade or downgrade. The portable guidance does not make
84
+ processctl a provider runtime or a model-quality evaluator.
85
+
86
+ Consumers using the packaged publication compatibility policy can enable
87
+ `lifecycle.publication.required: true` in `.process/project.json` after adopting a
88
+ version that supports it. Start then rejects an invalid branch before creating a
89
+ run. Finish checks the current branch, head subject, and pinned comparison-base-to-head
90
+ range, and writes publication metadata in a version 2 receipt. Version 1 receipts and
91
+ consumers without the opt-in remain supported. Missing pinned bases and mutations
92
+ during the check fail completion.
93
+
94
+ PR readiness additionally requires the selected ready-state body/title and exact
95
+ head/range checks. Follow the [completion preflight](process_assets/skills/change-complete/SKILL.md)
96
+ and repeat those checks in required CI. This repository's existing
97
+ `verification/verify_publication.py --pull-request` uses the actual PR metadata and
98
+ selected consumer standard; local lifecycle completion alone does not establish
99
+ external PR readiness. Integration-branch pushes remain consumer-owned operations.
100
+
80
101
  Runtime architecture is enforced by semantic fitness functions, not module or source-
81
102
  line quotas. Every module has an explicit dependency layer, imports point toward lower
82
103
  layers, the internal graph remains acyclic, and lifecycle.py alone owns state
@@ -122,6 +143,20 @@ Commands are argument arrays, never shell strings. Each command has a finite tim
122
143
  Output has a hard aggregate budget; evidence stores byte counts and hashes, never raw
123
144
  stdout or stderr that could contain secrets.
124
145
 
146
+ For a fresh checkout of this repository, create a virtual environment with a supported
147
+ Python and install `engineering_process/requirements-runtime.txt`,
148
+ `engineering_process/requirements-dev.txt`, and
149
+ `engineering_process/requirements-build.txt` through that interpreter. Use
150
+ `.venv/Scripts/python.exe` on Windows or `.venv/bin/python` on POSIX to invoke the
151
+ source CLI and verification scripts. Other consumers own their runtime paths and
152
+ setup commands; installed consumers can use their environment's `processctl` entry
153
+ point without a source checkout.
154
+
155
+ The bounded runner prepends the invoking Python executable's directory to child
156
+ `PATH`. Bare commands therefore search that environment before the inherited path,
157
+ while declared argument arrays, explicit executable paths, secret filtering and
158
+ remaining path entries are preserved. This alignment does not install dependencies.
159
+
125
160
  A failed profile also reports a safe `diagnostic` descriptor containing only its
126
161
  validated profile identifier, check identifier, canonical one-based profile position,
127
162
  and a fixed `processctl` argument array. The position remains unambiguous even when a
@@ -273,6 +308,8 @@ consumer-owned skills and instructions. Applying the same version twice is a no-
273
308
  Consumer-owned `.process/standards.json` selections and override definitions are also
274
309
  preserved; the managed PR template follows the effective supported standard. See
275
310
  [consumer document standards](ARTIFACT_STANDARDS.md).
311
+ [Issue records](ARTIFACT_STANDARDS.md#issues) use the same default-and-override
312
+ mechanism for open requests and closed evidence while tracker actions remain consumer-owned.
276
313
  [Automation naming](ARTIFACT_STANDARDS.md#automation-names) uses the same versioned
277
314
  selection and override mechanism; consumers apply it in their bootstrap and provider checks.
278
315
  The legacy managed runner can enter 1.0 directly, so consumers do not need a chain of
@@ -49,10 +49,31 @@ The coordinator hands review to an actual reviewer. A new agent review starts in
49
49
  fresh context with the accepted source artifacts and no suggested verdict; the same
50
50
  reviewer continues corrections. The reviewer inspects the change and authors its
51
51
  own report. The runner's existing task/session interaction and returned result make
52
- that work inspectable. Sharing a model, provider, or account is allowed; this is a
52
+ that work inspectable. Sharing a provider or account is allowed; this is a
53
53
  workflow for independent judgment, not authenticated identity or merge enforcement.
54
54
  See [change-review](process_assets/skills/change-review/SKILL.md) for the handoff.
55
55
 
56
+ Every delegated agent and reviewer follows the active user-selected model and
57
+ reasoning effort under [deliver-change's settings rule](process_assets/skills/deliver-change/SKILL.md#preserve-agent-execution-settings).
58
+ It covers fresh agents and resumed sessions, requires native runtime confirmation,
59
+ and permits no autonomous upgrade or downgrade. The portable guidance does not make
60
+ processctl a provider runtime or a model-quality evaluator.
61
+
62
+ Consumers using the packaged publication compatibility policy can enable
63
+ `lifecycle.publication.required: true` in `.process/project.json` after adopting a
64
+ version that supports it. Start then rejects an invalid branch before creating a
65
+ run. Finish checks the current branch, head subject, and pinned comparison-base-to-head
66
+ range, and writes publication metadata in a version 2 receipt. Version 1 receipts and
67
+ consumers without the opt-in remain supported. Missing pinned bases and mutations
68
+ during the check fail completion.
69
+
70
+ PR readiness additionally requires the selected ready-state body/title and exact
71
+ head/range checks. Follow the [completion preflight](process_assets/skills/change-complete/SKILL.md)
72
+ and repeat those checks in required CI. This repository's existing
73
+ `verification/verify_publication.py --pull-request` uses the actual PR metadata and
74
+ selected consumer standard; local lifecycle completion alone does not establish
75
+ external PR readiness. Integration-branch pushes remain consumer-owned operations.
76
+
56
77
  Runtime architecture is enforced by semantic fitness functions, not module or source-
57
78
  line quotas. Every module has an explicit dependency layer, imports point toward lower
58
79
  layers, the internal graph remains acyclic, and lifecycle.py alone owns state
@@ -98,6 +119,20 @@ Commands are argument arrays, never shell strings. Each command has a finite tim
98
119
  Output has a hard aggregate budget; evidence stores byte counts and hashes, never raw
99
120
  stdout or stderr that could contain secrets.
100
121
 
122
+ For a fresh checkout of this repository, create a virtual environment with a supported
123
+ Python and install `engineering_process/requirements-runtime.txt`,
124
+ `engineering_process/requirements-dev.txt`, and
125
+ `engineering_process/requirements-build.txt` through that interpreter. Use
126
+ `.venv/Scripts/python.exe` on Windows or `.venv/bin/python` on POSIX to invoke the
127
+ source CLI and verification scripts. Other consumers own their runtime paths and
128
+ setup commands; installed consumers can use their environment's `processctl` entry
129
+ point without a source checkout.
130
+
131
+ The bounded runner prepends the invoking Python executable's directory to child
132
+ `PATH`. Bare commands therefore search that environment before the inherited path,
133
+ while declared argument arrays, explicit executable paths, secret filtering and
134
+ remaining path entries are preserved. This alignment does not install dependencies.
135
+
101
136
  A failed profile also reports a safe `diagnostic` descriptor containing only its
102
137
  validated profile identifier, check identifier, canonical one-based profile position,
103
138
  and a fixed `processctl` argument array. The position remains unambiguous even when a
@@ -249,6 +284,8 @@ consumer-owned skills and instructions. Applying the same version twice is a no-
249
284
  Consumer-owned `.process/standards.json` selections and override definitions are also
250
285
  preserved; the managed PR template follows the effective supported standard. See
251
286
  [consumer document standards](ARTIFACT_STANDARDS.md).
287
+ [Issue records](ARTIFACT_STANDARDS.md#issues) use the same default-and-override
288
+ mechanism for open requests and closed evidence while tracker actions remain consumer-owned.
252
289
  [Automation naming](ARTIFACT_STANDARDS.md#automation-names) uses the same versioned
253
290
  selection and override mechanism; consumers apply it in their bootstrap and provider checks.
254
291
  The legacy managed runner can enter 1.0 directly, so consumers do not need a chain of
@@ -0,0 +1,23 @@
1
+ # Engineering Process v2.3.0
2
+
3
+ Changes since v2.2.0.
4
+
5
+ ## Features
6
+
7
+ - Generate and verify open and closed issue records from a versioned default or consumer\-owned override while keeping tracker actions and product workflow consumer\-owned\. ([#182](https://github.com/phuongnse/engineering-process/issues/182))
8
+ - Opt into publication checks at lifecycle start and completion\, with pinned source metadata in version 2 receipts and exact PR head\/body checks before readiness\; existing consumers and version 1 receipts remain supported\. ([#180](https://github.com/phuongnse/engineering-process/issues/180))
9
+
10
+ ## Fixes
11
+
12
+ - Require delegated agents and reviewers to preserve the active user\-selected model and reasoning effort\, with native runtime confirmation on spawn and resume and no autonomous model changes\. ([#182](https://github.com/phuongnse/engineering-process/issues/182))
13
+ - Resolve bare child commands through the invoking Python environment in fresh sessions\, preserving installed dependencies\, declared argument arrays\, explicit executables and environment filtering\. ([#181](https://github.com/phuongnse/engineering-process/issues/181))
14
+
15
+ ## Upgrade and compatibility
16
+
17
+ Merge the complete hash-locked package/adoption PR, update the local and CI environments to the selected version, and start a fresh agent session.
18
+
19
+ Consumer CI, naming conventions and branch-protection settings remain consumer-owned; adoption does not configure them automatically.
20
+
21
+ See [versioning and compatibility](https://github.com/phuongnse/engineering-process/blob/v2.3.0/VERSIONING.md) and [adoption guidance](https://github.com/phuongnse/engineering-process/blob/v2.3.0/SELF_HOSTING.md).
22
+
23
+ [Full change comparison](https://github.com/phuongnse/engineering-process/compare/v2.2.0...v2.3.0)
@@ -1,3 +1,3 @@
1
1
  """Agent-neutral engineering process."""
2
2
 
3
- VERSION = "2.2.0"
3
+ VERSION = "2.3.0"
@@ -7,6 +7,7 @@ import ctypes
7
7
  import os
8
8
  from pathlib import Path
9
9
  import secrets
10
+ import shutil
10
11
  import signal
11
12
  import subprocess
12
13
  import sys
@@ -35,9 +36,13 @@ def _enable_subreaper() -> None:
35
36
 
36
37
 
37
38
  def _ps_process_table() -> tuple[dict[int, int], str | None]:
39
+ # Supervision must not depend on the consumer's PATH.
40
+ application = shutil.which("ps", path=os.defpath)
41
+ if application is None:
42
+ return {}, "process table snapshot could not start"
38
43
  try:
39
44
  result = subprocess.run(
40
- ["ps", "-axo", "pid=,ppid="],
45
+ [application, "-axo", "pid=,ppid="],
41
46
  stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
42
47
  timeout=PROCESS_TABLE_TIMEOUT_SECONDS, check=False, text=True,
43
48
  )
@@ -190,7 +195,7 @@ class PosixProcessSupervisor:
190
195
  if not command or "\x00" in command:
191
196
  raise OSError("command executable is invalid")
192
197
  supplied = Path(command)
193
- if supplied.is_absolute() or supplied.parent != Path("."):
198
+ if supplied.is_absolute() or os.path.dirname(command):
194
199
  candidate = supplied if supplied.is_absolute() else working_directory / supplied
195
200
  try:
196
201
  resolved = candidate.resolve(strict=True)
@@ -198,7 +203,8 @@ class PosixProcessSupervisor:
198
203
  raise OSError(f"executable is unavailable: {command}") from error
199
204
  if not resolved.is_file() or not os.access(resolved, os.X_OK):
200
205
  raise OSError(f"executable is unavailable: {command}")
201
- return resolved
206
+ # The symlink path may select a virtual environment.
207
+ return candidate.absolute()
202
208
  for raw_directory in environment.get("PATH", "").split(os.pathsep):
203
209
  directory = Path(raw_directory or ".")
204
210
  if not directory.is_absolute():
@@ -209,7 +215,7 @@ class PosixProcessSupervisor:
209
215
  except OSError:
210
216
  continue
211
217
  if resolved.is_file() and os.access(resolved, os.X_OK):
212
- return resolved
218
+ return candidate.absolute()
213
219
  raise OSError(f"executable is unavailable: {command}")
214
220
 
215
221
  def spawn(
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import ntpath
6
7
  import os
7
8
  from pathlib import Path
8
9
  import subprocess
@@ -99,7 +100,7 @@ def resolve_windows_application(
99
100
  raise OSError("command executable is invalid")
100
101
  separator = os.pathsep if path_separator is None else path_separator
101
102
  supplied = Path(command)
102
- explicit_path = supplied.is_absolute() or supplied.parent != Path(".")
103
+ explicit_path = supplied.is_absolute() or bool(ntpath.dirname(command))
103
104
  suffix = supplied.suffix.casefold()
104
105
  if suffix in _UNSUPPORTED_SHELL_SUFFIXES:
105
106
  raise OSError(
@@ -84,6 +84,14 @@ def _validate_relations(document: dict[str, Any]) -> None:
84
84
  _unique([group["type"] for group in rules["groups"]], "change types")
85
85
  _unique([section["id"] for section in rules["sections"]], "release section ids")
86
86
  _unique([item["heading"] for item in rules["groups"] + rules["sections"]], "release headings")
87
+ elif document["adapter"] == "issue":
88
+ for state, definition in rules["states"].items():
89
+ sections = definition["sections"]
90
+ _unique([section["heading"] for section in sections], f"{state} issue section headings")
91
+ for kind in ("fields", "checks"):
92
+ entries = [entry for section in sections for entry in section[kind]]
93
+ _unique([entry["id"] for entry in entries], f"{state} issue {kind} ids")
94
+ _unique([entry["label"] for entry in entries], f"{state} issue {kind} labels")
87
95
 
88
96
 
89
97
  def read_document(path: Path) -> bytes:
@@ -40,6 +40,7 @@ from .lifecycle import (
40
40
  submit_review,
41
41
  verify_change,
42
42
  )
43
+ from .issue import render_issue
43
44
  from .project import load_project, readiness_summary
44
45
  from .production_engineering import (
45
46
  validate_plan_assessments,
@@ -420,17 +421,29 @@ def command_artifact(args: argparse.Namespace) -> Result:
420
421
  elif operation == "renovate-preset":
421
422
  payload = formatted_json_bytes(render_renovate_preset(standard, process_root=process_root))
422
423
  else:
424
+ state = args.state or ("open" if standard.document["adapter"] == "issue" else "ready")
425
+ allowed_states = {
426
+ "pr-description": {"draft", "ready"},
427
+ "release-notes": {"draft", "ready"},
428
+ "automation-name": {"draft", "ready"},
429
+ "issue": {"open", "closed"},
430
+ }[standard.document["adapter"]]
431
+ if state not in allowed_states:
432
+ raise ProcessError(
433
+ f"{standard.document['adapter']} state must be "
434
+ + " or ".join(sorted(allowed_states))
435
+ )
423
436
  data = load_and_validate(args.data_file, standard.document["adapter"] + "-data", schema_root=schemas_root(process_root)) if args.data_file is not None else None
424
437
  if data is not None:
425
- renderer = {"pr-description": render_description, "release-notes": render_notes, "automation-name": render_name}[standard.document["adapter"]]
426
- payload = renderer(standard, data, state=args.state, process_root=process_root).encode("utf-8")
438
+ renderer = {"pr-description": render_description, "release-notes": render_notes, "automation-name": render_name, "issue": render_issue}[standard.document["adapter"]]
439
+ payload = renderer(standard, data, state=state, process_root=process_root).encode("utf-8")
427
440
  details["dataDigest"] = digest_json(data)
428
441
  elif operation == "render" or standard.document["adapter"] != "pr-description":
429
442
  raise ProcessError("this artifact operation requires --data-file")
430
443
  if operation == "validate":
431
444
  actual = read_document(args.body_file)
432
445
  if data is None:
433
- issues = body_issues(actual.decode("utf-8"), args.state, standard)
446
+ issues = body_issues(actual.decode("utf-8"), state, standard)
434
447
  elif actual != payload:
435
448
  issues.append("artifact bytes differ from the selected standard and input data")
436
449
  payload = actual
@@ -551,7 +564,7 @@ def build_parser() -> argparse.ArgumentParser:
551
564
  leaf = _leaf(artifact_commands, name, command_artifact)
552
565
  leaf.add_argument("--artifact", required=True)
553
566
  if name in {"render", "validate"}:
554
- leaf.add_argument("--state", choices=("draft", "ready"), default="ready")
567
+ leaf.add_argument("--state", choices=("draft", "ready", "open", "closed"))
555
568
  leaf.add_argument("--data-file", type=Path, required=name == "render")
556
569
  if name == "validate":
557
570
  leaf.add_argument("--body-file", type=Path, required=True)
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import hashlib
6
6
  import os
7
7
  from pathlib import Path
8
+ import sys
8
9
  import threading
9
10
  import time
10
11
  from typing import Any, BinaryIO
@@ -28,6 +29,11 @@ def _child_environment() -> dict[str, str]:
28
29
  if any(marker in upper for marker in SECRET_MARKERS):
29
30
  continue
30
31
  environment[name] = value
32
+ runtime_directory = str(Path(sys.executable).absolute().parent)
33
+ inherited_path = environment.get("PATH", "")
34
+ environment["PATH"] = os.pathsep.join(
35
+ entry for entry in (runtime_directory, inherited_path) if entry
36
+ )
31
37
  environment["PYTHONUNBUFFERED"] = "1"
32
38
  return environment
33
39
 
@@ -20,6 +20,7 @@ CONTRACT_KINDS = (
20
20
  "artifact-selection",
21
21
  "artifact-standard",
22
22
  "automation-name-data",
23
+ "issue-data",
23
24
  "change",
24
25
  "plan",
25
26
  "process-graph",
@@ -0,0 +1,112 @@
1
+ """Issue record generation from one consumer-selected standard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ import re
7
+ from typing import Any
8
+ from urllib.parse import urlsplit
9
+
10
+ from .artifact_standards import ArtifactStandard, MAX_DOCUMENT_BYTES
11
+ from .contracts import ProcessError, validate_document
12
+ from .distribution import distribution_root, schemas_root
13
+
14
+
15
+ _HTTPS_AUTHORITY = re.compile(
16
+ r"(?:\[[^\[\]]+\]|(?:[A-Za-z0-9._~!$&'()*+,;=-]|%[0-9A-Fa-f]{2})+)"
17
+ r"(?::[0-9]*)?"
18
+ )
19
+
20
+
21
+ def _record_url(value: str) -> bool:
22
+ if any(character.isspace() or ord(character) < 32 or 127 <= ord(character) <= 159
23
+ for character in value):
24
+ return False
25
+ try:
26
+ parsed = urlsplit(value)
27
+ # urlsplit defers port validation until this attribute is read.
28
+ parsed.port
29
+ return (
30
+ parsed.scheme == "https"
31
+ and parsed.hostname is not None
32
+ and parsed.username is None
33
+ and parsed.password is None
34
+ and _HTTPS_AUTHORITY.fullmatch(parsed.netloc) is not None
35
+ )
36
+ except ValueError:
37
+ return False
38
+
39
+
40
+ def _validate_field(field: dict[str, Any], value: str) -> None:
41
+ format_name = field.get("format", "text")
42
+ if format_name == "record-url" and not _record_url(value):
43
+ raise ProcessError(f"issue field {field['label']} requires one durable HTTPS URL")
44
+ if format_name == "record-references":
45
+ references = value.split(", ")
46
+ if value.casefold() != "none" and (
47
+ not references or any(not _record_url(reference) for reference in references)
48
+ ):
49
+ raise ProcessError(
50
+ f"issue field {field['label']} requires none or comma-separated durable HTTPS URLs"
51
+ )
52
+
53
+
54
+ def render_issue(
55
+ standard: ArtifactStandard,
56
+ data: dict[str, Any],
57
+ *,
58
+ state: str = "open",
59
+ process_root: Path | None = None,
60
+ ) -> str:
61
+ if standard.document["adapter"] != "issue":
62
+ raise ProcessError("selected standard requires a different issue adapter")
63
+ if state not in {"open", "closed"}:
64
+ raise ProcessError("issue state must be open or closed")
65
+ validate_document(
66
+ data,
67
+ "issue-data",
68
+ schema_root=schemas_root(distribution_root(process_root)),
69
+ )
70
+ title_rules = standard.rules["title"]
71
+ title = data["title"]
72
+ if len(title) > title_rules["maxLength"]:
73
+ raise ProcessError("issue title exceeds the selected length limit")
74
+ if prefix := title_rules.get("prefix"):
75
+ if not title.startswith(prefix):
76
+ raise ProcessError(f"issue title must start with {prefix}")
77
+ title_value = title[len(prefix):] if prefix else title
78
+ if not title_value.strip() or standard.unresolved(title_value):
79
+ raise ProcessError(f"{state} issue has unresolved title")
80
+
81
+ sections = standard.rules["states"][state]["sections"]
82
+ fields = [field for section in sections for field in section["fields"]]
83
+ checks = [check for section in sections for check in section["checks"]]
84
+ expected_fields = {field["id"] for field in fields}
85
+ expected_checks = {check["id"] for check in checks}
86
+ unknown_fields = sorted(data["fields"].keys() - expected_fields)
87
+ unknown_checks = sorted(data["checks"].keys() - expected_checks)
88
+ if unknown_fields:
89
+ raise ProcessError("issue data contains unknown fields: " + ", ".join(unknown_fields))
90
+ if unknown_checks:
91
+ raise ProcessError("issue data contains unknown checks: " + ", ".join(unknown_checks))
92
+
93
+ lines = [f"# {title}", ""]
94
+ for section in sections:
95
+ lines.extend([section["heading"], ""])
96
+ for field in section["fields"]:
97
+ value = data["fields"].get(field["id"], standard.document["pendingValues"][0])
98
+ if standard.unresolved(value):
99
+ raise ProcessError(f"{state} issue has unresolved value for {field['label']}")
100
+ _validate_field(field, value)
101
+ lines.extend([f"### {field['label']}", "", value, ""])
102
+ for check in section["checks"]:
103
+ completed = data["checks"].get(check["id"], False)
104
+ if state == "closed" and not completed:
105
+ raise ProcessError(f"closed issue has unchecked item: {check['label']}")
106
+ lines.append(f"- [{'x' if completed else ' '}] {check['label']}")
107
+ if section["checks"]:
108
+ lines.append("")
109
+ body = "\n".join(lines)
110
+ if len(body.encode("utf-8")) > MAX_DOCUMENT_BYTES:
111
+ raise ProcessError("issue record exceeds its size limit")
112
+ return body
@@ -24,6 +24,8 @@ from .contracts import (
24
24
  from .distribution import schemas_root
25
25
  from .project import (
26
26
  accepted_issue_url_prefix,
27
+ load_project,
28
+ publication_required,
27
29
  require_consumer_evidence,
28
30
  required_profiles,
29
31
  )
@@ -33,6 +35,7 @@ from .production_engineering import (
33
35
  validate_plan_assessments,
34
36
  validate_review_assessments,
35
37
  )
38
+ from .source_publication import branch_issues, current_branch, validate_current_source
36
39
  from .repository import repository_snapshot, resolve_commit, same_checkpoint
37
40
 
38
41
 
@@ -180,6 +183,12 @@ def start_change(
180
183
  )
181
184
  if project["project"] not in contract["affectedProjects"]:
182
185
  raise ProcessError("change affectedProjects must include the current project")
186
+ if publication_required(project):
187
+ issues = branch_issues(current_branch(project_root))
188
+ if issues:
189
+ raise ProcessError(
190
+ "publication branch validation failed: " + "; ".join(issues)
191
+ )
183
192
 
184
193
  actor = _actor(actor_id, context_id, kind)
185
194
  comparison_base = resolve_commit(project_root, contract["comparisonBase"])
@@ -538,7 +547,27 @@ def finish_change(
538
547
  state = _load_state(project_root, process_root, change_id)
539
548
  _require_phase(state, "approved")
540
549
  actor = _actor(actor_id, context_id, kind)
550
+ project = load_project(project_root, process_root)
541
551
  checkpoint = repository_snapshot(project_root)
552
+ publication = None
553
+ if publication_required(project):
554
+ if not (comparison_base := state.get("comparisonBaseCommit")):
555
+ raise ProcessError("publication completion requires a pinned comparison base")
556
+ publication = validate_current_source(
557
+ project_root, comparison_base
558
+ )
559
+ if publication["range"] != f"{comparison_base}..{checkpoint['head']}":
560
+ raise ProcessError("publication validation does not match the reviewed HEAD")
561
+ after_publication = repository_snapshot(project_root)
562
+ if (not same_checkpoint(checkpoint, after_publication)
563
+ or current_branch(project_root) != publication["branch"]):
564
+ raise ProcessError("repository changed while publication validation was running")
565
+ if publication["issues"]:
566
+ raise ProcessError(
567
+ "publication compatibility checks failed: "
568
+ + "; ".join(publication["issues"])
569
+ )
570
+ checkpoint = after_publication
542
571
  if not same_checkpoint(checkpoint, state["reviewAssignment"]["checkpoint"]):
543
572
  raise ProcessError("repository changed after approval")
544
573
 
@@ -573,6 +602,13 @@ def finish_change(
573
602
  "verdict": state["review"]["document"]["verdict"],
574
603
  },
575
604
  }
605
+ if publication is not None:
606
+ receipt["schemaVersion"] = 2
607
+ receipt["publication"] = {
608
+ "branch": publication["branch"],
609
+ "subject": publication["subject"],
610
+ "range": publication["range"],
611
+ }
576
612
  validate_document(
577
613
  receipt,
578
614
  "receipt",
@@ -172,6 +172,11 @@ def require_consumer_evidence(project: dict[str, Any]) -> bool:
172
172
  return bool(policy.get("requireConsumerEvidence", False))
173
173
 
174
174
 
175
+ def publication_required(project: dict[str, Any]) -> bool:
176
+ policy = project["lifecycle"].get("publication", {})
177
+ return bool(policy.get("required", False))
178
+
179
+
175
180
  def accepted_issue_url_prefix(project: dict[str, Any]) -> str | None:
176
181
  return project["lifecycle"].get("processChanges", {}).get(
177
182
  "acceptedIssueUrlPrefix"
@@ -0,0 +1,58 @@
1
+ """Small 1.x compatibility checks for pre-1.0 consumer workflows.
2
+
3
+ Publication policy is consumer-owned in the new process. These four read-only checks
4
+ remain temporarily so an existing consumer can adopt 1.0 before moving the equivalent
5
+ rules into its own repository.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from .contracts import ProcessError
14
+ from .artifact_standards import resolve_standard
15
+ from .distribution import distribution_root
16
+ from .pr_description import _without_html_comments, body_issues
17
+ from .source_publication import (
18
+ BRANCH, SUBJECT, MAX_COMMITS, branch_issues, commit_issues,
19
+ current_branch, current_commit_subject, validate_current_source, validate_range,
20
+ )
21
+
22
+
23
+ MAX_BODY_BYTES = 1_000_000
24
+
25
+
26
+ def _pull_request_body_issues(body: str, state: str) -> list[str]:
27
+ standard = resolve_standard(Path.cwd(), distribution_root(), "pull-request")
28
+ return body_issues(body, state, standard)
29
+
30
+
31
+ def validate_pull_request(
32
+ *,
33
+ title: str,
34
+ branch: str,
35
+ state: str,
36
+ body_path: Path | None,
37
+ project_root: Path | None = None,
38
+ process_root: Path | None = None,
39
+ ) -> dict[str, Any]:
40
+ issues = branch_issues(branch) + commit_issues(title)
41
+ if state not in {"draft", "ready"}:
42
+ issues.append("pull request state must be draft or ready")
43
+ body = ""
44
+ if body_path is not None:
45
+ try:
46
+ data = body_path.read_bytes()
47
+ except OSError as error:
48
+ raise ProcessError(f"cannot read pull request body: {error}") from error
49
+ if len(data) > MAX_BODY_BYTES:
50
+ issues.append("pull request body exceeds its size limit")
51
+ else:
52
+ try:
53
+ body = data.decode("utf-8")
54
+ except UnicodeError:
55
+ issues.append("pull request body must be UTF-8")
56
+ standard = resolve_standard(project_root or Path.cwd(), distribution_root(process_root), "pull-request")
57
+ issues.extend(body_issues(body, state, standard))
58
+ return {"issues": issues, "standard": standard.metadata}