engineering-process 2.2.0__tar.gz → 2.3.1__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 (101) hide show
  1. {engineering_process-2.2.0/engineering_process.egg-info → engineering_process-2.3.1}/PKG-INFO +70 -7
  2. {engineering_process-2.2.0 → engineering_process-2.3.1}/README.md +69 -6
  3. engineering_process-2.3.1/RELEASE_NOTES.md +18 -0
  4. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/__init__.py +1 -1
  5. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/_supervisor_posix.py +10 -4
  6. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/_supervisor_windows.py +2 -1
  7. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/artifact_standards.py +8 -0
  8. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/cli.py +22 -5
  9. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/commands.py +6 -0
  10. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/contracts.py +15 -3
  11. engineering_process-2.3.1/engineering_process/issue.py +112 -0
  12. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/lifecycle.py +109 -3
  13. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/project.py +5 -0
  14. engineering_process-2.3.1/engineering_process/publication_compat.py +58 -0
  15. engineering_process-2.3.1/engineering_process/review_contexts.py +291 -0
  16. engineering_process-2.2.0/engineering_process/publication_compat.py → engineering_process-2.3.1/engineering_process/source_publication.py +55 -41
  17. {engineering_process-2.2.0 → engineering_process-2.3.1/engineering_process.egg-info}/PKG-INFO +70 -7
  18. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process.egg-info/SOURCES.txt +7 -1
  19. {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/skills/change-complete/SKILL.md +32 -0
  20. {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/skills/change-review/SKILL.md +40 -4
  21. {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/skills/change-start/SKILL.md +7 -1
  22. {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/skills/change-verify/SKILL.md +10 -0
  23. {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/skills/deliver-change/SKILL.md +29 -0
  24. engineering_process-2.3.1/process_assets/standards/issue.v1.json +78 -0
  25. {engineering_process-2.2.0 → engineering_process-2.3.1}/pyproject.toml +3 -1
  26. {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/artifact-standard.schema.json +86 -1
  27. engineering_process-2.3.1/schemas/issue-data.schema.json +44 -0
  28. {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/project.schema.json +8 -0
  29. {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/receipt.schema.json +20 -2
  30. {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/run.schema.json +28 -1
  31. {engineering_process-2.2.0 → engineering_process-2.3.1}/templates/adopt-process.py +10 -0
  32. {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_adoption.py +11 -2
  33. {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_architecture.py +3 -0
  34. {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_artifact_standards.py +155 -1
  35. {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_automation.py +5 -15
  36. {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_cli.py +1 -0
  37. {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_commands.py +100 -2
  38. {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_contracts.py +70 -0
  39. {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_lifecycle.py +153 -1
  40. {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_publication_compat.py +101 -0
  41. engineering_process-2.3.1/tests/test_pypi_cache.py +184 -0
  42. engineering_process-2.3.1/tests/test_review_contexts.py +507 -0
  43. {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_supervisor_posix.py +35 -0
  44. engineering_process-2.2.0/RELEASE_NOTES.md +0 -18
  45. engineering_process-2.2.0/tests/test_pypi_cache_horizon.py +0 -81
  46. {engineering_process-2.2.0 → engineering_process-2.3.1}/LICENSE +0 -0
  47. {engineering_process-2.2.0 → engineering_process-2.3.1}/MANIFEST.in +0 -0
  48. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/__main__.py +0 -0
  49. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/_supervisor_contract.py +0 -0
  50. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/_windows_job.py +0 -0
  51. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/adoption.py +0 -0
  52. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/automation_name.py +0 -0
  53. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/distribution.py +0 -0
  54. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/helper_launch.py +0 -0
  55. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/pr_description.py +0 -0
  56. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/production_engineering.py +0 -0
  57. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/release.py +0 -0
  58. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/release_notes.py +0 -0
  59. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/repository.py +0 -0
  60. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/requirements-dev.txt +0 -0
  61. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/requirements-runtime.txt +0 -0
  62. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/skills.py +0 -0
  63. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/supervision.py +0 -0
  64. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process.egg-info/dependency_links.txt +0 -0
  65. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process.egg-info/entry_points.txt +0 -0
  66. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process.egg-info/requires.txt +0 -0
  67. {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process.egg-info/top_level.txt +0 -0
  68. {engineering_process-2.2.0 → engineering_process-2.3.1}/process-graph.json +0 -0
  69. {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/skills/change-implement/SKILL.md +0 -0
  70. {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/skills/change-plan/SKILL.md +0 -0
  71. {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/skills/process-improve/SKILL.md +0 -0
  72. {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/skills/production-engineering/SKILL.md +0 -0
  73. {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/skills/production-engineering/invariants.json +0 -0
  74. {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/standards/automation-name.v1.json +0 -0
  75. {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/standards/pull-request.v1.json +0 -0
  76. {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/standards/release-notes.v1.json +0 -0
  77. {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/artifact-selection.schema.json +0 -0
  78. {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/automation-name-data.schema.json +0 -0
  79. {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/change.schema.json +0 -0
  80. {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/plan.schema.json +0 -0
  81. {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/pr-description-data.schema.json +0 -0
  82. {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/process-graph.schema.json +0 -0
  83. {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/process-lock.schema.json +0 -0
  84. {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/production-engineering.schema.json +0 -0
  85. {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/project-legacy.schema.json +0 -0
  86. {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/release-change.schema.json +0 -0
  87. {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/release-notes-data.schema.json +0 -0
  88. {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/release.schema.json +0 -0
  89. {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/renovate-preset.schema.json +0 -0
  90. {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/review.schema.json +0 -0
  91. {engineering_process-2.2.0 → engineering_process-2.3.1}/setup.cfg +0 -0
  92. {engineering_process-2.2.0 → engineering_process-2.3.1}/templates/AGENTS.process.md +0 -0
  93. {engineering_process-2.2.0 → engineering_process-2.3.1}/templates/PULL_REQUEST_TEMPLATE.md +0 -0
  94. {engineering_process-2.2.0 → engineering_process-2.3.1}/templates/adopt-process-windows-job.py +0 -0
  95. {engineering_process-2.2.0 → engineering_process-2.3.1}/templates/renovate.json +0 -0
  96. {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_distribution.py +0 -0
  97. {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_production_engineering.py +0 -0
  98. {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_publication_workflow.py +0 -0
  99. {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_release.py +0 -0
  100. {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_repository.py +0 -0
  101. {engineering_process-2.2.0 → engineering_process-2.3.1}/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.1
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
@@ -64,19 +64,56 @@ under .process/receipts. Both paths are ignored by Git. Verification evidence is
64
64
  to HEAD plus a fingerprint of tracked and non-ignored untracked files. Any relevant
65
65
  mutation invalidates it.
66
66
 
67
- Independent review is one direct rule: neither the reviewer actor nor reviewer context
68
- may have implemented the current cycle. There is no attestation hierarchy,
67
+ Neither the reviewer actor nor reviewer context may have implemented the current
68
+ cycle. An agent reviewer context also cannot be recorded in another accepted change
69
+ in this repository or its registered worktrees. There is no attestation hierarchy,
69
70
  recommendation chain, authority-transition protocol, remote-evidence federation, or
70
71
  second handwritten validator.
71
72
 
72
- The coordinator hands review to an actual reviewer. A new agent review starts in a
73
- fresh context with the accepted source artifacts and no suggested verdict; the same
74
- reviewer continues corrections. The reviewer inspects the change and authors its
73
+ The coordinator hands each new change to a newly spawned reviewer, with the accepted
74
+ source artifacts and no inherited implementation or other-change review conversation.
75
+ The same reviewer continues only that change's corrections. The reviewer inspects the change and authors its
75
76
  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
77
+ that work inspectable. Sharing a provider or account is allowed; this is a
77
78
  workflow for independent judgment, not authenticated identity or merge enforcement.
78
79
  See [change-review](process_assets/skills/change-review/SKILL.md) for the handoff.
79
80
 
81
+ Start, submission and finish check schema-valid canonical run history with bounded
82
+ worktree/file reads. A short OS lock in common Git metadata serializes canonical
83
+ history writes and review checks; profiles run outside it and reload current state
84
+ before recording results. The lock stores no identity registry. The check rejects known context reuse,
85
+ but cannot establish native conversation freshness or inspect removed history,
86
+ other clones or other repositories. Keep native creation, non-inherited dispatch
87
+ and effective settings evidence in the existing handoff.
88
+
89
+ `processctl change review replace-reused` repairs only an initial pending assignment
90
+ proven to reuse another change's agent context, before a submitted review or normal
91
+ report file exists. It validates a fresh replacement and current verification,
92
+ preserves the prior assignment and conflict in canonical history, and leaves cycle
93
+ and evidence unchanged. Valid assignments and reviewed correction cycles cannot use
94
+ this operation. It does not provide an unrestricted reviewer reset.
95
+
96
+ Every delegated agent and reviewer follows the active user-selected model and
97
+ reasoning effort under [deliver-change's settings rule](process_assets/skills/deliver-change/SKILL.md#preserve-agent-execution-settings).
98
+ It covers fresh agents and resumed sessions, requires native runtime confirmation,
99
+ and permits no autonomous upgrade or downgrade. The portable guidance does not make
100
+ processctl a provider runtime or a model-quality evaluator.
101
+
102
+ Consumers using the packaged publication compatibility policy can enable
103
+ `lifecycle.publication.required: true` in `.process/project.json` after adopting a
104
+ version that supports it. Start then rejects an invalid branch before creating a
105
+ run. Finish checks the current branch, head subject, and pinned comparison-base-to-head
106
+ range, and writes publication metadata in a version 2 receipt. Version 1 receipts and
107
+ consumers without the opt-in remain supported. Missing pinned bases and mutations
108
+ during the check fail completion.
109
+
110
+ PR readiness additionally requires the selected ready-state body/title and exact
111
+ head/range checks. Follow the [completion preflight](process_assets/skills/change-complete/SKILL.md)
112
+ and repeat those checks in required CI. This repository's existing
113
+ `verification/verify_publication.py --pull-request` uses the actual PR metadata and
114
+ selected consumer standard; local lifecycle completion alone does not establish
115
+ external PR readiness. Integration-branch pushes remain consumer-owned operations.
116
+
80
117
  Runtime architecture is enforced by semantic fitness functions, not module or source-
81
118
  line quotas. Every module has an explicit dependency layer, imports point toward lower
82
119
  layers, the internal graph remains acyclic, and lifecycle.py alone owns state
@@ -122,6 +159,20 @@ Commands are argument arrays, never shell strings. Each command has a finite tim
122
159
  Output has a hard aggregate budget; evidence stores byte counts and hashes, never raw
123
160
  stdout or stderr that could contain secrets.
124
161
 
162
+ For a fresh checkout of this repository, create a virtual environment with a supported
163
+ Python and install `engineering_process/requirements-runtime.txt`,
164
+ `engineering_process/requirements-dev.txt`, and
165
+ `engineering_process/requirements-build.txt` through that interpreter. Use
166
+ `.venv/Scripts/python.exe` on Windows or `.venv/bin/python` on POSIX to invoke the
167
+ source CLI and verification scripts. Other consumers own their runtime paths and
168
+ setup commands; installed consumers can use their environment's `processctl` entry
169
+ point without a source checkout.
170
+
171
+ The bounded runner prepends the invoking Python executable's directory to child
172
+ `PATH`. Bare commands therefore search that environment before the inherited path,
173
+ while declared argument arrays, explicit executable paths, secret filtering and
174
+ remaining path entries are preserved. This alignment does not install dependencies.
175
+
125
176
  A failed profile also reports a safe `diagnostic` descriptor containing only its
126
177
  validated profile identifier, check identifier, canonical one-based profile position,
127
178
  and a fixed `processctl` argument array. The position remains unambiguous even when a
@@ -273,6 +324,8 @@ consumer-owned skills and instructions. Applying the same version twice is a no-
273
324
  Consumer-owned `.process/standards.json` selections and override definitions are also
274
325
  preserved; the managed PR template follows the effective supported standard. See
275
326
  [consumer document standards](ARTIFACT_STANDARDS.md).
327
+ [Issue records](ARTIFACT_STANDARDS.md#issues) use the same default-and-override
328
+ mechanism for open requests and closed evidence while tracker actions remain consumer-owned.
276
329
  [Automation naming](ARTIFACT_STANDARDS.md#automation-names) uses the same versioned
277
330
  selection and override mechanism; consumers apply it in their bootstrap and provider checks.
278
331
  The legacy managed runner can enter 1.0 directly, so consumers do not need a chain of
@@ -548,6 +601,16 @@ package rule keeps the adoption pull request in draft and runs exactly:
548
601
 
549
602
  python .process/adopt-process.py --project-root . --requirements-lock requirements/process.txt
550
603
 
604
+ The bootstrap refreshes `engineering-process` index metadata using pip's
605
+ `--refresh-package` option, with `--no-cache-dir` for older pip. Exact pins, hashes,
606
+ isolated installation and bounded failures still apply. Consumer CI that installs
607
+ the lock directly must also request fresh metadata: use
608
+ `--refresh-package engineering-process` with pip 26.2 or newer, or the portable
609
+ `--no-cache-dir` option. Keep this setting on the process installation command;
610
+ other dependency caches do not need to be disabled. For non-isolated pip-compile,
611
+ the equivalent `PIP_REFRESH_PACKAGE=engineering-process` environment setting keeps
612
+ fresh release lookup compatible with older pip, which revalidated by default.
613
+
551
614
  postUpgradeTasks.fileFilters includes the managed paths, so Renovate commits the new
552
615
  hash lock and the fully materialized process in the same pull request. A self-hosted
553
616
  Renovate administrator must allow only this anchored command and must keep shell
@@ -40,19 +40,56 @@ under .process/receipts. Both paths are ignored by Git. Verification evidence is
40
40
  to HEAD plus a fingerprint of tracked and non-ignored untracked files. Any relevant
41
41
  mutation invalidates it.
42
42
 
43
- Independent review is one direct rule: neither the reviewer actor nor reviewer context
44
- may have implemented the current cycle. There is no attestation hierarchy,
43
+ Neither the reviewer actor nor reviewer context may have implemented the current
44
+ cycle. An agent reviewer context also cannot be recorded in another accepted change
45
+ in this repository or its registered worktrees. There is no attestation hierarchy,
45
46
  recommendation chain, authority-transition protocol, remote-evidence federation, or
46
47
  second handwritten validator.
47
48
 
48
- The coordinator hands review to an actual reviewer. A new agent review starts in a
49
- fresh context with the accepted source artifacts and no suggested verdict; the same
50
- reviewer continues corrections. The reviewer inspects the change and authors its
49
+ The coordinator hands each new change to a newly spawned reviewer, with the accepted
50
+ source artifacts and no inherited implementation or other-change review conversation.
51
+ The same reviewer continues only that change's corrections. The reviewer inspects the change and authors its
51
52
  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
53
+ that work inspectable. Sharing a provider or account is allowed; this is a
53
54
  workflow for independent judgment, not authenticated identity or merge enforcement.
54
55
  See [change-review](process_assets/skills/change-review/SKILL.md) for the handoff.
55
56
 
57
+ Start, submission and finish check schema-valid canonical run history with bounded
58
+ worktree/file reads. A short OS lock in common Git metadata serializes canonical
59
+ history writes and review checks; profiles run outside it and reload current state
60
+ before recording results. The lock stores no identity registry. The check rejects known context reuse,
61
+ but cannot establish native conversation freshness or inspect removed history,
62
+ other clones or other repositories. Keep native creation, non-inherited dispatch
63
+ and effective settings evidence in the existing handoff.
64
+
65
+ `processctl change review replace-reused` repairs only an initial pending assignment
66
+ proven to reuse another change's agent context, before a submitted review or normal
67
+ report file exists. It validates a fresh replacement and current verification,
68
+ preserves the prior assignment and conflict in canonical history, and leaves cycle
69
+ and evidence unchanged. Valid assignments and reviewed correction cycles cannot use
70
+ this operation. It does not provide an unrestricted reviewer reset.
71
+
72
+ Every delegated agent and reviewer follows the active user-selected model and
73
+ reasoning effort under [deliver-change's settings rule](process_assets/skills/deliver-change/SKILL.md#preserve-agent-execution-settings).
74
+ It covers fresh agents and resumed sessions, requires native runtime confirmation,
75
+ and permits no autonomous upgrade or downgrade. The portable guidance does not make
76
+ processctl a provider runtime or a model-quality evaluator.
77
+
78
+ Consumers using the packaged publication compatibility policy can enable
79
+ `lifecycle.publication.required: true` in `.process/project.json` after adopting a
80
+ version that supports it. Start then rejects an invalid branch before creating a
81
+ run. Finish checks the current branch, head subject, and pinned comparison-base-to-head
82
+ range, and writes publication metadata in a version 2 receipt. Version 1 receipts and
83
+ consumers without the opt-in remain supported. Missing pinned bases and mutations
84
+ during the check fail completion.
85
+
86
+ PR readiness additionally requires the selected ready-state body/title and exact
87
+ head/range checks. Follow the [completion preflight](process_assets/skills/change-complete/SKILL.md)
88
+ and repeat those checks in required CI. This repository's existing
89
+ `verification/verify_publication.py --pull-request` uses the actual PR metadata and
90
+ selected consumer standard; local lifecycle completion alone does not establish
91
+ external PR readiness. Integration-branch pushes remain consumer-owned operations.
92
+
56
93
  Runtime architecture is enforced by semantic fitness functions, not module or source-
57
94
  line quotas. Every module has an explicit dependency layer, imports point toward lower
58
95
  layers, the internal graph remains acyclic, and lifecycle.py alone owns state
@@ -98,6 +135,20 @@ Commands are argument arrays, never shell strings. Each command has a finite tim
98
135
  Output has a hard aggregate budget; evidence stores byte counts and hashes, never raw
99
136
  stdout or stderr that could contain secrets.
100
137
 
138
+ For a fresh checkout of this repository, create a virtual environment with a supported
139
+ Python and install `engineering_process/requirements-runtime.txt`,
140
+ `engineering_process/requirements-dev.txt`, and
141
+ `engineering_process/requirements-build.txt` through that interpreter. Use
142
+ `.venv/Scripts/python.exe` on Windows or `.venv/bin/python` on POSIX to invoke the
143
+ source CLI and verification scripts. Other consumers own their runtime paths and
144
+ setup commands; installed consumers can use their environment's `processctl` entry
145
+ point without a source checkout.
146
+
147
+ The bounded runner prepends the invoking Python executable's directory to child
148
+ `PATH`. Bare commands therefore search that environment before the inherited path,
149
+ while declared argument arrays, explicit executable paths, secret filtering and
150
+ remaining path entries are preserved. This alignment does not install dependencies.
151
+
101
152
  A failed profile also reports a safe `diagnostic` descriptor containing only its
102
153
  validated profile identifier, check identifier, canonical one-based profile position,
103
154
  and a fixed `processctl` argument array. The position remains unambiguous even when a
@@ -249,6 +300,8 @@ consumer-owned skills and instructions. Applying the same version twice is a no-
249
300
  Consumer-owned `.process/standards.json` selections and override definitions are also
250
301
  preserved; the managed PR template follows the effective supported standard. See
251
302
  [consumer document standards](ARTIFACT_STANDARDS.md).
303
+ [Issue records](ARTIFACT_STANDARDS.md#issues) use the same default-and-override
304
+ mechanism for open requests and closed evidence while tracker actions remain consumer-owned.
252
305
  [Automation naming](ARTIFACT_STANDARDS.md#automation-names) uses the same versioned
253
306
  selection and override mechanism; consumers apply it in their bootstrap and provider checks.
254
307
  The legacy managed runner can enter 1.0 directly, so consumers do not need a chain of
@@ -524,6 +577,16 @@ package rule keeps the adoption pull request in draft and runs exactly:
524
577
 
525
578
  python .process/adopt-process.py --project-root . --requirements-lock requirements/process.txt
526
579
 
580
+ The bootstrap refreshes `engineering-process` index metadata using pip's
581
+ `--refresh-package` option, with `--no-cache-dir` for older pip. Exact pins, hashes,
582
+ isolated installation and bounded failures still apply. Consumer CI that installs
583
+ the lock directly must also request fresh metadata: use
584
+ `--refresh-package engineering-process` with pip 26.2 or newer, or the portable
585
+ `--no-cache-dir` option. Keep this setting on the process installation command;
586
+ other dependency caches do not need to be disabled. For non-isolated pip-compile,
587
+ the equivalent `PIP_REFRESH_PACKAGE=engineering-process` environment setting keeps
588
+ fresh release lookup compatible with older pip, which revalidated by default.
589
+
527
590
  postUpgradeTasks.fileFilters includes the managed paths, so Renovate commits the new
528
591
  hash lock and the fully materialized process in the same pull request. A self-hosted
529
592
  Renovate administrator must allow only this anchored command and must keep shell
@@ -0,0 +1,18 @@
1
+ # Engineering Process v2.3.1
2
+
3
+ Changes since v2.3.0.
4
+
5
+ ## Fixes
6
+
7
+ - Refresh package metadata when installing the hash\-locked process so newly published releases remain visible through warm pip caches\; remove the publication\-time cache expiry wait\. Consumer CI using pip 26\.2 or newer should pass \-\-refresh\-package engineering\-process\, or use \-\-no\-cache\-dir with older pip\. ([#134](https://github.com/phuongnse/engineering-process/issues/134))
8
+ - Reject recorded agent reviewer\-context reuse across changes in the same repository and its worktrees\. Preserve same\-change correction reviews\, and provide an audited replacement only for a reused initial assignment with no submitted review\. Native fresh\-session and unchanged model\/effort evidence remain required from the host\. ([#182](https://github.com/phuongnse/engineering-process/issues/182))
9
+
10
+ ## Upgrade and compatibility
11
+
12
+ Merge the complete hash-locked package/adoption PR, update the local and CI environments to the selected version, and start a fresh agent session.
13
+
14
+ Consumer CI, naming conventions and branch-protection settings remain consumer-owned; adoption does not configure them automatically.
15
+
16
+ See [versioning and compatibility](https://github.com/phuongnse/engineering-process/blob/v2.3.1/VERSIONING.md) and [adoption guidance](https://github.com/phuongnse/engineering-process/blob/v2.3.1/SELF_HOSTING.md).
17
+
18
+ [Full change comparison](https://github.com/phuongnse/engineering-process/compare/v2.3.0...v2.3.1)
@@ -1,3 +1,3 @@
1
1
  """Agent-neutral engineering process."""
2
2
 
3
- VERSION = "2.2.0"
3
+ VERSION = "2.3.1"
@@ -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,
@@ -329,9 +330,10 @@ def command_change_review_start(args: argparse.Namespace) -> Result:
329
330
  actor_id=args.actor,
330
331
  context_id=args.context,
331
332
  kind=args.actor_kind,
333
+ replace_reused=args.review_command == "replace-reused",
332
334
  )
333
335
  return _state_result(
334
- "change review start",
336
+ f"change review {args.review_command}",
335
337
  state,
336
338
  assignment=state["reviewAssignment"],
337
339
  processSignals=process_improvement_signals(state),
@@ -420,17 +422,29 @@ def command_artifact(args: argparse.Namespace) -> Result:
420
422
  elif operation == "renovate-preset":
421
423
  payload = formatted_json_bytes(render_renovate_preset(standard, process_root=process_root))
422
424
  else:
425
+ state = args.state or ("open" if standard.document["adapter"] == "issue" else "ready")
426
+ allowed_states = {
427
+ "pr-description": {"draft", "ready"},
428
+ "release-notes": {"draft", "ready"},
429
+ "automation-name": {"draft", "ready"},
430
+ "issue": {"open", "closed"},
431
+ }[standard.document["adapter"]]
432
+ if state not in allowed_states:
433
+ raise ProcessError(
434
+ f"{standard.document['adapter']} state must be "
435
+ + " or ".join(sorted(allowed_states))
436
+ )
423
437
  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
438
  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")
439
+ renderer = {"pr-description": render_description, "release-notes": render_notes, "automation-name": render_name, "issue": render_issue}[standard.document["adapter"]]
440
+ payload = renderer(standard, data, state=state, process_root=process_root).encode("utf-8")
427
441
  details["dataDigest"] = digest_json(data)
428
442
  elif operation == "render" or standard.document["adapter"] != "pr-description":
429
443
  raise ProcessError("this artifact operation requires --data-file")
430
444
  if operation == "validate":
431
445
  actual = read_document(args.body_file)
432
446
  if data is None:
433
- issues = body_issues(actual.decode("utf-8"), args.state, standard)
447
+ issues = body_issues(actual.decode("utf-8"), state, standard)
434
448
  elif actual != payload:
435
449
  issues.append("artifact bytes differ from the selected standard and input data")
436
450
  payload = actual
@@ -529,6 +543,9 @@ def build_parser() -> argparse.ArgumentParser:
529
543
  review_start = _leaf(review_commands, "start", command_change_review_start)
530
544
  _add_actor(review_start)
531
545
  review_start.add_argument("--change-id", required=True)
546
+ review_replace = _leaf(review_commands, "replace-reused", command_change_review_start)
547
+ _add_actor(review_replace)
548
+ review_replace.add_argument("--change-id", required=True)
532
549
  review_submit = _leaf(review_commands, "submit", command_change_review_submit)
533
550
  review_submit.add_argument("--change-id", required=True)
534
551
  review_submit.add_argument("--review", type=Path, required=True)
@@ -551,7 +568,7 @@ def build_parser() -> argparse.ArgumentParser:
551
568
  leaf = _leaf(artifact_commands, name, command_artifact)
552
569
  leaf.add_argument("--artifact", required=True)
553
570
  if name in {"render", "validate"}:
554
- leaf.add_argument("--state", choices=("draft", "ready"), default="ready")
571
+ leaf.add_argument("--state", choices=("draft", "ready", "open", "closed"))
555
572
  leaf.add_argument("--data-file", type=Path, required=name == "render")
556
573
  if name == "validate":
557
574
  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",
@@ -50,9 +51,20 @@ def read_json(path: Path, *, maximum_bytes: int = MAX_JSON_BYTES) -> Any:
50
51
  if size > maximum_bytes:
51
52
  raise ProcessError(f"{path} exceeds {maximum_bytes} bytes")
52
53
  try:
53
- return json.loads(path.read_text(encoding="utf-8"))
54
- except (OSError, UnicodeError, json.JSONDecodeError) as error:
55
- raise ProcessError(f"{path} is not valid UTF-8 JSON: {error}") from error
54
+ with path.open("rb") as stream:
55
+ content = stream.read(maximum_bytes + 1)
56
+ except OSError as error:
57
+ raise ProcessError(f"cannot read {path}: {error}") from error
58
+ if len(content) > maximum_bytes:
59
+ raise ProcessError(f"{path} exceeds {maximum_bytes} bytes")
60
+ return parse_json_bytes(content, source=str(path))
61
+
62
+
63
+ def parse_json_bytes(content: bytes, *, source: str) -> Any:
64
+ try:
65
+ return json.loads(content.decode("utf-8"))
66
+ except (UnicodeError, json.JSONDecodeError) as error:
67
+ raise ProcessError(f"{source} is not valid UTF-8 JSON: {error}") from error
56
68
 
57
69
 
58
70
  def canonical_bytes(value: Any) -> bytes:
@@ -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