engineering-process 0.2.1__tar.gz → 0.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 (133) hide show
  1. {engineering_process-0.2.1/engineering_process.egg-info → engineering_process-0.3.0}/PKG-INFO +33 -1
  2. {engineering_process-0.2.1 → engineering_process-0.3.0}/README.md +32 -0
  3. {engineering_process-0.2.1 → engineering_process-0.3.0}/VERSIONING.md +14 -0
  4. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/__init__.py +1 -1
  5. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/lifecycle.py +36 -1
  6. {engineering_process-0.2.1 → engineering_process-0.3.0/engineering_process.egg-info}/PKG-INFO +33 -1
  7. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process.egg-info/SOURCES.txt +1 -0
  8. {engineering_process-0.2.1 → engineering_process-0.3.0}/pyproject.toml +1 -1
  9. engineering_process-0.3.0/release.json +54 -0
  10. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_lifecycle.py +62 -0
  11. engineering_process-0.3.0/tests/test_process_install.py +235 -0
  12. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_self_hosting.py +36 -1
  13. engineering_process-0.2.1/release.json +0 -106
  14. {engineering_process-0.2.1 → engineering_process-0.3.0}/LICENSE +0 -0
  15. {engineering_process-0.2.1 → engineering_process-0.3.0}/MANIFEST.in +0 -0
  16. {engineering_process-0.2.1 → engineering_process-0.3.0}/PRODUCTION_STANDARD.md +0 -0
  17. {engineering_process-0.2.1 → engineering_process-0.3.0}/bundles.json +0 -0
  18. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/__main__.py +0 -0
  19. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/_download_worker.py +0 -0
  20. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/_supervisor_posix.py +0 -0
  21. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/_supervisor_windows.py +0 -0
  22. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/_windows_job.py +0 -0
  23. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/adoption.py +0 -0
  24. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/artifact_attestation.py +0 -0
  25. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/bootstrap.py +0 -0
  26. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/bundles.py +0 -0
  27. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/cli.py +0 -0
  28. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/contracts.py +0 -0
  29. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/distribution.py +0 -0
  30. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/distribution_verify.py +0 -0
  31. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/environment.py +0 -0
  32. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/evidence.py +0 -0
  33. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/git.py +0 -0
  34. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/git_attributes.py +0 -0
  35. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/helper_launch.py +0 -0
  36. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/impact.py +0 -0
  37. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/managed.py +0 -0
  38. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/markdown.py +0 -0
  39. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/publication.py +0 -0
  40. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/release.py +0 -0
  41. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/release_candidate.py +0 -0
  42. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/requirements-build.txt +0 -0
  43. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/requirements-dev.txt +0 -0
  44. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/requirements-release.txt +0 -0
  45. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/requirements-runtime.txt +0 -0
  46. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/runner.py +0 -0
  47. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/runtime.py +0 -0
  48. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/skills.py +0 -0
  49. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/supervision.py +0 -0
  50. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/supplemental.py +0 -0
  51. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/syncing.py +0 -0
  52. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process/tooling.py +0 -0
  53. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process.egg-info/dependency_links.txt +0 -0
  54. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process.egg-info/entry_points.txt +0 -0
  55. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process.egg-info/requires.txt +0 -0
  56. {engineering_process-0.2.1 → engineering_process-0.3.0}/engineering_process.egg-info/top_level.txt +0 -0
  57. {engineering_process-0.2.1 → engineering_process-0.3.0}/examples/adoption-migration.json +0 -0
  58. {engineering_process-0.2.1 → engineering_process-0.3.0}/examples/change.json +0 -0
  59. {engineering_process-0.2.1 → engineering_process-0.3.0}/examples/plan.json +0 -0
  60. {engineering_process-0.2.1 → engineering_process-0.3.0}/examples/project.json +0 -0
  61. {engineering_process-0.2.1 → engineering_process-0.3.0}/examples/release-change.json +0 -0
  62. {engineering_process-0.2.1 → engineering_process-0.3.0}/examples/release.json +0 -0
  63. {engineering_process-0.2.1 → engineering_process-0.3.0}/examples/review.json +0 -0
  64. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/assess-design/SKILL.md +0 -0
  65. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/build-frontend/SKILL.md +0 -0
  66. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/build-frontend-foundation/SKILL.md +0 -0
  67. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/change-api/SKILL.md +0 -0
  68. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/cross-repo-change/SKILL.md +0 -0
  69. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/define-change-contract/SKILL.md +0 -0
  70. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/design-module/SKILL.md +0 -0
  71. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/evolve-process/SKILL.md +0 -0
  72. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/finish-change/SKILL.md +0 -0
  73. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/govern-ui/SKILL.md +0 -0
  74. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/implement-change/SKILL.md +0 -0
  75. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/implement-module/SKILL.md +0 -0
  76. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/implement-use-case/SKILL.md +0 -0
  77. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/integrate-mcp/SKILL.md +0 -0
  78. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/maintain-docs/SKILL.md +0 -0
  79. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/plan-change/SKILL.md +0 -0
  80. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/publish-change/SKILL.md +0 -0
  81. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/review-change/SKILL.md +0 -0
  82. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/run-change/SKILL.md +0 -0
  83. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/run-change/references/execution.md +0 -0
  84. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/run-project-command/SKILL.md +0 -0
  85. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/specify-use-case/SKILL.md +0 -0
  86. {engineering_process-0.2.1 → engineering_process-0.3.0}/process_assets/skills/verify-change/SKILL.md +0 -0
  87. {engineering_process-0.2.1 → engineering_process-0.3.0}/schemas/adoption-migration.schema.json +0 -0
  88. {engineering_process-0.2.1 → engineering_process-0.3.0}/schemas/change.schema.json +0 -0
  89. {engineering_process-0.2.1 → engineering_process-0.3.0}/schemas/evidence-receipt.schema.json +0 -0
  90. {engineering_process-0.2.1 → engineering_process-0.3.0}/schemas/lifecycle.schema.json +0 -0
  91. {engineering_process-0.2.1 → engineering_process-0.3.0}/schemas/plan.schema.json +0 -0
  92. {engineering_process-0.2.1 → engineering_process-0.3.0}/schemas/process-lock.schema.json +0 -0
  93. {engineering_process-0.2.1 → engineering_process-0.3.0}/schemas/project.schema.json +0 -0
  94. {engineering_process-0.2.1 → engineering_process-0.3.0}/schemas/release-change.schema.json +0 -0
  95. {engineering_process-0.2.1 → engineering_process-0.3.0}/schemas/release.schema.json +0 -0
  96. {engineering_process-0.2.1 → engineering_process-0.3.0}/schemas/review.schema.json +0 -0
  97. {engineering_process-0.2.1 → engineering_process-0.3.0}/schemas/reviewer-attestation.schema.json +0 -0
  98. {engineering_process-0.2.1 → engineering_process-0.3.0}/schemas/supplemental-verification.schema.json +0 -0
  99. {engineering_process-0.2.1 → engineering_process-0.3.0}/schemas/verification.schema.json +0 -0
  100. {engineering_process-0.2.1 → engineering_process-0.3.0}/setup.cfg +0 -0
  101. {engineering_process-0.2.1 → engineering_process-0.3.0}/templates/AGENTS.process.md +0 -0
  102. {engineering_process-0.2.1 → engineering_process-0.3.0}/templates/PULL_REQUEST_TEMPLATE.md +0 -0
  103. {engineering_process-0.2.1 → engineering_process-0.3.0}/templates/adopt-process-windows-job.py +0 -0
  104. {engineering_process-0.2.1 → engineering_process-0.3.0}/templates/adopt-process.py +0 -0
  105. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_adoption.py +0 -0
  106. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_adoption_runner.py +0 -0
  107. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_artifact_attestation.py +0 -0
  108. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_bootstrap.py +0 -0
  109. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_bundles.py +0 -0
  110. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_cli.py +0 -0
  111. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_contracts.py +0 -0
  112. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_distribution.py +0 -0
  113. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_distribution_verify.py +0 -0
  114. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_environment.py +0 -0
  115. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_evals.py +0 -0
  116. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_impact.py +0 -0
  117. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_publication.py +0 -0
  118. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_publish_event.py +0 -0
  119. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_pypi_publication.py +0 -0
  120. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_release.py +0 -0
  121. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_release_candidate.py +0 -0
  122. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_release_preparation.py +0 -0
  123. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_release_qualification.py +0 -0
  124. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_runner.py +0 -0
  125. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_runtime.py +0 -0
  126. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_schemas.py +0 -0
  127. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_skills.py +0 -0
  128. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_source_checkout.py +0 -0
  129. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_supervision.py +0 -0
  130. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_supplemental.py +0 -0
  131. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_syncing.py +0 -0
  132. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_tooling.py +0 -0
  133. {engineering_process-0.2.1 → engineering_process-0.3.0}/tests/test_windows_job.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engineering-process
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Agent-neutral end-to-end engineering lifecycle and deterministic process CLI
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/phuongnse/engineering-process
@@ -136,6 +136,26 @@ attribute files are rejected by existing managed-tree ownership and content chec
136
136
  External Git overrides that alter a checkout still fail byte-exact distribution
137
137
  attestation. A consumer never authors or maintains process skills locally.
138
138
 
139
+ CI installs the pinned authority through the repository-root
140
+ `phuongnse/engineering-process` action from the same governed release. Consumers pin
141
+ the action with the release commit's full object id and retain the human-readable
142
+ `v<SemVer>` annotation; floating tags and copied installer implementations are not
143
+ supported. The action reads the consumer-owned `requirements/process.txt` and changes
144
+ no version or source decision: it preserves the complete hash lock, public PyPI,
145
+ binary-only policy, sanitized pip environment, exact-version-only propagation retry,
146
+ bounded output and time, and cross-platform descendant cleanup. The action source is
147
+ resolved only from `github.action_path`, so an untrusted consumer checkout cannot
148
+ replace the installer.
149
+
150
+ Project-owned CI workflows remain local because they select the project's commands
151
+ and evidence. Reusable installation and publication grammar belong to this
152
+ distribution; product, architecture, dependency, documentation, and acceptance
153
+ checks remain with the consumer. The managed `.process/adopt-process.py` and Windows
154
+ Job Object sidecar are intentional bootstrap snapshots, not consumer implementations:
155
+ Renovate must install and verify a target authority before that target exists in the
156
+ checkout, and `processctl sync --check` compares those bytes with the pinned
157
+ distribution.
158
+
139
159
  For an existing consumer, a published version is adopted through one Renovate draft.
140
160
  The managed runner installs the target authority from the complete hash lock outside
141
161
  the checkout and atomically updates the process lock and managed assets. If the
@@ -386,6 +406,13 @@ processctl change verify --change-id issue-123 --profile review \
386
406
  --actor worker --context worker-session --actor-kind agent
387
407
  ~~~
388
408
 
409
+ If later remote evidence or a source correction invalidates that verified checkpoint
410
+ before review, commit the correction and run `change implement` again. The CLI
411
+ preserves the earlier evidence, records `verification-invalidated`, increments the
412
+ cycle, and requires every profile again. It accepts this transition only when the
413
+ recorded verification is actually stale; a current verified checkpoint cannot use it
414
+ to bypass independent review.
415
+
389
416
  After the phase becomes `verified`, a separate reviewer context registers its
390
417
  assignment:
391
418
 
@@ -504,6 +531,11 @@ authenticates who produced it.
504
531
  draft contains the new lock, managed contracts, skill snapshots, and any
505
532
  target-version consumer-owned project migration; after CI and fresh-context
506
533
  independent review, merge is the end of adoption.
534
+ - The repository-root GitHub Action is the shared CI bootstrap surface. Consumers pin
535
+ its full governed release commit, while the exact Python authority remains selected
536
+ exclusively by their hash-locked `requirements/process.txt`. The action invokes the
537
+ producer-owned installer from its immutable action checkout and never downloads or
538
+ executes helper source from the consumer branch.
507
539
  - Versioned JSON schemas define change, plan, verification, review, lifecycle,
508
540
  completion-related artifacts, release-change fragments, and the release
509
541
  classification contract. The generated Release PR gate binds that contract to the
@@ -101,6 +101,26 @@ attribute files are rejected by existing managed-tree ownership and content chec
101
101
  External Git overrides that alter a checkout still fail byte-exact distribution
102
102
  attestation. A consumer never authors or maintains process skills locally.
103
103
 
104
+ CI installs the pinned authority through the repository-root
105
+ `phuongnse/engineering-process` action from the same governed release. Consumers pin
106
+ the action with the release commit's full object id and retain the human-readable
107
+ `v<SemVer>` annotation; floating tags and copied installer implementations are not
108
+ supported. The action reads the consumer-owned `requirements/process.txt` and changes
109
+ no version or source decision: it preserves the complete hash lock, public PyPI,
110
+ binary-only policy, sanitized pip environment, exact-version-only propagation retry,
111
+ bounded output and time, and cross-platform descendant cleanup. The action source is
112
+ resolved only from `github.action_path`, so an untrusted consumer checkout cannot
113
+ replace the installer.
114
+
115
+ Project-owned CI workflows remain local because they select the project's commands
116
+ and evidence. Reusable installation and publication grammar belong to this
117
+ distribution; product, architecture, dependency, documentation, and acceptance
118
+ checks remain with the consumer. The managed `.process/adopt-process.py` and Windows
119
+ Job Object sidecar are intentional bootstrap snapshots, not consumer implementations:
120
+ Renovate must install and verify a target authority before that target exists in the
121
+ checkout, and `processctl sync --check` compares those bytes with the pinned
122
+ distribution.
123
+
104
124
  For an existing consumer, a published version is adopted through one Renovate draft.
105
125
  The managed runner installs the target authority from the complete hash lock outside
106
126
  the checkout and atomically updates the process lock and managed assets. If the
@@ -351,6 +371,13 @@ processctl change verify --change-id issue-123 --profile review \
351
371
  --actor worker --context worker-session --actor-kind agent
352
372
  ~~~
353
373
 
374
+ If later remote evidence or a source correction invalidates that verified checkpoint
375
+ before review, commit the correction and run `change implement` again. The CLI
376
+ preserves the earlier evidence, records `verification-invalidated`, increments the
377
+ cycle, and requires every profile again. It accepts this transition only when the
378
+ recorded verification is actually stale; a current verified checkpoint cannot use it
379
+ to bypass independent review.
380
+
354
381
  After the phase becomes `verified`, a separate reviewer context registers its
355
382
  assignment:
356
383
 
@@ -469,6 +496,11 @@ authenticates who produced it.
469
496
  draft contains the new lock, managed contracts, skill snapshots, and any
470
497
  target-version consumer-owned project migration; after CI and fresh-context
471
498
  independent review, merge is the end of adoption.
499
+ - The repository-root GitHub Action is the shared CI bootstrap surface. Consumers pin
500
+ its full governed release commit, while the exact Python authority remains selected
501
+ exclusively by their hash-locked `requirements/process.txt`. The action invokes the
502
+ producer-owned installer from its immutable action checkout and never downloads or
503
+ executes helper source from the consumer branch.
472
504
  - Versioned JSON schemas define change, plan, verification, review, lifecycle,
473
505
  completion-related artifacts, release-change fragments, and the release
474
506
  classification contract. The generated Release PR gate binds that contract to the
@@ -98,6 +98,20 @@ Release, self-adoption, and consumer adoption are separate changes:
98
98
  checkpoint. Merge completes adoption; there is no post-merge synchronization.
99
99
  5. N+1 governs only changes that begin after the adoption checkpoint.
100
100
 
101
+ The repository-root GitHub Action and Python package are two surfaces of the same
102
+ governed release checkpoint. A consumer invocation pins the action by the release
103
+ commit's full object id and keeps the `v<SemVer>` annotation; Renovate groups that
104
+ GitHub Action identity with the direct Python authority update. The action does not
105
+ select the package version. It installs only the consumer's complete hash lock, so
106
+ the process lock, Python artifacts, managed assets, action source, and reviewed tag
107
+ remain independently checkable parts of one release identity.
108
+
109
+ Consumer repositories do not own copies of reusable installation or publication
110
+ algorithms. They retain declarative process/project locks, exact project commands,
111
+ and managed bootstrap snapshots required before a target version is installed. A
112
+ clean-cutover PR may delete an obsolete local helper only after it pins an immutable
113
+ public action checkpoint; no compatibility shim or dual execution path is retained.
114
+
101
115
  Renovate PRs are generated adoption candidates, not trusted adoption evidence.
102
116
  Automerge is forbidden for process-authority updates. A PR that changes only a
103
117
  requirement pin, omits generated hashes or managed assets, or requires a post-merge
@@ -1,3 +1,3 @@
1
1
  """Agent-neutral engineering process."""
2
2
 
3
- VERSION = "0.2.1"
3
+ VERSION = "0.3.0"
@@ -624,10 +624,45 @@ def _begin_implementation_unlocked(
624
624
  kind: str,
625
625
  ) -> dict[str, Any]:
626
626
  state = load_state(project_root, change_id)
627
- _require_phase(state, "planned", "implementing", "changes-requested")
628
627
  _contract(project_root, state)
629
628
  _plan(project_root, state)
630
629
  actor = _actor(actor_id, context_id, kind)
630
+ if state["phase"] == "verified":
631
+ source = source_state(project_root)
632
+ checkpoints = {item["checkpoint"] for item in state["verification"]}
633
+ fingerprints = {
634
+ item["workspaceFingerprint"] for item in state["verification"]
635
+ }
636
+ current = (
637
+ source["dirty"] is False
638
+ and source["checkpoint"] is not None
639
+ and source["fingerprint"] is not None
640
+ and checkpoints == {source["checkpoint"]}
641
+ and fingerprints == {source["fingerprint"]}
642
+ )
643
+ if current:
644
+ raise ContractError(
645
+ "current verified change must enter independent review before "
646
+ "another implementation cycle"
647
+ )
648
+ previous_cycle = state["cycle"]
649
+ previous_verification = list(state["verification"])
650
+ state["cycle"] += 1
651
+ state["implementationActors"] = []
652
+ state["verification"] = []
653
+ state["reviewAssignment"] = None
654
+ state["review"] = None
655
+ _event(
656
+ state,
657
+ "verification-invalidated",
658
+ actor,
659
+ cycle=state["cycle"],
660
+ previousCycle=previous_cycle,
661
+ previousVerification=previous_verification,
662
+ reason="source-changed-after-verification",
663
+ )
664
+ else:
665
+ _require_phase(state, "planned", "implementing", "changes-requested")
631
666
  if state["phase"] == "changes-requested":
632
667
  state["cycle"] += 1
633
668
  state["implementationActors"] = []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engineering-process
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Agent-neutral end-to-end engineering lifecycle and deterministic process CLI
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/phuongnse/engineering-process
@@ -136,6 +136,26 @@ attribute files are rejected by existing managed-tree ownership and content chec
136
136
  External Git overrides that alter a checkout still fail byte-exact distribution
137
137
  attestation. A consumer never authors or maintains process skills locally.
138
138
 
139
+ CI installs the pinned authority through the repository-root
140
+ `phuongnse/engineering-process` action from the same governed release. Consumers pin
141
+ the action with the release commit's full object id and retain the human-readable
142
+ `v<SemVer>` annotation; floating tags and copied installer implementations are not
143
+ supported. The action reads the consumer-owned `requirements/process.txt` and changes
144
+ no version or source decision: it preserves the complete hash lock, public PyPI,
145
+ binary-only policy, sanitized pip environment, exact-version-only propagation retry,
146
+ bounded output and time, and cross-platform descendant cleanup. The action source is
147
+ resolved only from `github.action_path`, so an untrusted consumer checkout cannot
148
+ replace the installer.
149
+
150
+ Project-owned CI workflows remain local because they select the project's commands
151
+ and evidence. Reusable installation and publication grammar belong to this
152
+ distribution; product, architecture, dependency, documentation, and acceptance
153
+ checks remain with the consumer. The managed `.process/adopt-process.py` and Windows
154
+ Job Object sidecar are intentional bootstrap snapshots, not consumer implementations:
155
+ Renovate must install and verify a target authority before that target exists in the
156
+ checkout, and `processctl sync --check` compares those bytes with the pinned
157
+ distribution.
158
+
139
159
  For an existing consumer, a published version is adopted through one Renovate draft.
140
160
  The managed runner installs the target authority from the complete hash lock outside
141
161
  the checkout and atomically updates the process lock and managed assets. If the
@@ -386,6 +406,13 @@ processctl change verify --change-id issue-123 --profile review \
386
406
  --actor worker --context worker-session --actor-kind agent
387
407
  ~~~
388
408
 
409
+ If later remote evidence or a source correction invalidates that verified checkpoint
410
+ before review, commit the correction and run `change implement` again. The CLI
411
+ preserves the earlier evidence, records `verification-invalidated`, increments the
412
+ cycle, and requires every profile again. It accepts this transition only when the
413
+ recorded verification is actually stale; a current verified checkpoint cannot use it
414
+ to bypass independent review.
415
+
389
416
  After the phase becomes `verified`, a separate reviewer context registers its
390
417
  assignment:
391
418
 
@@ -504,6 +531,11 @@ authenticates who produced it.
504
531
  draft contains the new lock, managed contracts, skill snapshots, and any
505
532
  target-version consumer-owned project migration; after CI and fresh-context
506
533
  independent review, merge is the end of adoption.
534
+ - The repository-root GitHub Action is the shared CI bootstrap surface. Consumers pin
535
+ its full governed release commit, while the exact Python authority remains selected
536
+ exclusively by their hash-locked `requirements/process.txt`. The action invokes the
537
+ producer-owned installer from its immutable action checkout and never downloads or
538
+ executes helper source from the consumer branch.
507
539
  - Versioned JSON schemas define change, plan, verification, review, lifecycle,
508
540
  completion-related artifacts, release-change fragments, and the release
509
541
  classification contract. The generated Release PR gate binds that contract to the
@@ -109,6 +109,7 @@ tests/test_environment.py
109
109
  tests/test_evals.py
110
110
  tests/test_impact.py
111
111
  tests/test_lifecycle.py
112
+ tests/test_process_install.py
112
113
  tests/test_publication.py
113
114
  tests/test_publish_event.py
114
115
  tests/test_pypi_publication.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "engineering-process"
7
- version = "0.2.1"
7
+ version = "0.3.0"
8
8
  description = "Agent-neutral end-to-end engineering lifecycle and deterministic process CLI"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,54 @@
1
+ {
2
+ "schemaVersion": 3,
3
+ "previousVersion": "0.2.1",
4
+ "version": "0.3.0",
5
+ "classification": "minor",
6
+ "compatibility": "backward-compatible",
7
+ "schemaImpact": "unchanged",
8
+ "migration": null,
9
+ "identity": {
10
+ "package": "engineering-process",
11
+ "distribution": "engineering_process",
12
+ "tag": "v0.3.0",
13
+ "releaseName": "v0.3.0",
14
+ "runtimeVersion": {
15
+ "path": "engineering_process/__init__.py",
16
+ "variable": "VERSION"
17
+ },
18
+ "artifacts": [
19
+ "engineering_process-0.3.0-py3-none-any.whl",
20
+ "engineering_process-0.3.0.tar.gz"
21
+ ],
22
+ "receiptAsset": "engineering-process-v0.3.0-evidence.json",
23
+ "authorizationAsset": null
24
+ },
25
+ "provenance": {
26
+ "mode": "governed",
27
+ "statement": "The public N-1 lifecycle receipt and reviewed Release PR authorize this release.",
28
+ "lifecycleReceipt": {
29
+ "asset": "engineering-process-v0.3.0-evidence.json",
30
+ "project": "engineering-process",
31
+ "changeId": "release-0-3-0",
32
+ "cycle": 1
33
+ }
34
+ },
35
+ "changes": [
36
+ {
37
+ "id": "consumer-process-boundary-cleanup",
38
+ "type": "capability",
39
+ "surfaces": [
40
+ "ci-bootstrap",
41
+ "workflow"
42
+ ],
43
+ "rationale": "Provide one immutable producer-owned CI installation action so consumers can remove copied process algorithms without weakening exact hash locks or bounded registry propagation recovery."
44
+ },
45
+ {
46
+ "id": "reopen-stale-verification",
47
+ "type": "fix",
48
+ "surfaces": [
49
+ "lifecycle"
50
+ ],
51
+ "rationale": "Allow a verified change invalidated by later remote evidence or a source correction to begin a new implementation cycle while preserving prior evidence and still forbidding current verified work from bypassing independent review."
52
+ }
53
+ ]
54
+ }
@@ -300,6 +300,68 @@ class LifecycleTests(unittest.TestCase):
300
300
  self.assertEqual(report["impact"]["baseRef"], "HEAD")
301
301
  self.assertEqual(report["impact"]["changedPaths"], [])
302
302
 
303
+ def test_stale_verified_source_can_begin_a_new_implementation_cycle(self):
304
+ with tempfile.TemporaryDirectory() as directory:
305
+ base = Path(directory)
306
+ root = base / "project"
307
+ inputs = base / "inputs"
308
+ root.mkdir()
309
+ inputs.mkdir()
310
+ self.initialize_repository(root)
311
+ self.prepare_verified_change(root, inputs)
312
+
313
+ (root / "tracked.txt").write_text("remote CI correction\n", encoding="utf-8")
314
+ subprocess.run(["git", "add", "tracked.txt"], cwd=root, check=True)
315
+ subprocess.run(
316
+ ["git", "commit", "-qm", "fix: address remote verification"],
317
+ cwd=root,
318
+ check=True,
319
+ )
320
+
321
+ state = begin_implementation(
322
+ root,
323
+ "change-1",
324
+ actor_id="worker",
325
+ context_id="worker-context",
326
+ kind="agent",
327
+ )
328
+
329
+ self.assertEqual("implementing", state["phase"])
330
+ self.assertEqual(2, state["cycle"])
331
+ self.assertEqual([], state["verification"])
332
+ invalidated = [
333
+ event
334
+ for event in state["history"]
335
+ if event["event"] == "verification-invalidated"
336
+ ]
337
+ self.assertEqual(1, len(invalidated))
338
+ self.assertEqual(1, invalidated[0]["previousCycle"])
339
+ self.assertEqual(
340
+ "source-changed-after-verification", invalidated[0]["reason"]
341
+ )
342
+ self.assertEqual(2, len(invalidated[0]["previousVerification"]))
343
+
344
+ def test_current_verified_source_cannot_bypass_independent_review(self):
345
+ with tempfile.TemporaryDirectory() as directory:
346
+ base = Path(directory)
347
+ root = base / "project"
348
+ inputs = base / "inputs"
349
+ root.mkdir()
350
+ inputs.mkdir()
351
+ self.initialize_repository(root)
352
+ self.prepare_verified_change(root, inputs)
353
+
354
+ with self.assertRaisesRegex(
355
+ ContractError, "must enter independent review"
356
+ ):
357
+ begin_implementation(
358
+ root,
359
+ "change-1",
360
+ actor_id="worker",
361
+ context_id="worker-context",
362
+ kind="agent",
363
+ )
364
+
303
365
  def test_full_lifecycle_requires_independent_review(self):
304
366
  with tempfile.TemporaryDirectory() as directory:
305
367
  base = Path(directory)
@@ -0,0 +1,235 @@
1
+ import os
2
+ from pathlib import Path
3
+ import sys
4
+ import tempfile
5
+ import unittest
6
+ from unittest.mock import patch
7
+
8
+ from verification import install_process_runtime as installer
9
+ from verification.install_process_runtime import (
10
+ Attempt,
11
+ BACKOFF_SECONDS,
12
+ InstallError,
13
+ PUBLIC_INDEX,
14
+ install_process_runtime,
15
+ )
16
+
17
+
18
+ class ProcessRuntimeInstallTests(unittest.TestCase):
19
+ def fixture(self, root: Path) -> Path:
20
+ lock = root / "requirements" / "process.txt"
21
+ lock.parent.mkdir()
22
+ lock.write_text(
23
+ "--only-binary :all:\n\nengineering-process==0.2.1 \\\n"
24
+ " --hash=sha256:cbabb56b367f5d48e5f2d0e6dd5837eca7ea46c40b47ef232d7d04e014609153\n",
25
+ encoding="utf-8",
26
+ )
27
+ return lock
28
+
29
+ def test_installs_exact_hash_lock_from_public_index_with_sanitized_environment(self):
30
+ with tempfile.TemporaryDirectory() as directory:
31
+ root = Path(directory)
32
+ lock = self.fixture(root)
33
+ calls = []
34
+
35
+ def runner(command, cwd, environment):
36
+ calls.append((tuple(command), cwd, environment))
37
+ return Attempt(0, b"installed\n", b"")
38
+
39
+ index_name = "_".join(("PIP", "INDEX", "URL"))
40
+ extra_index_name = "_".join(("PIP", "EXTRA", "INDEX", "URL"))
41
+ with patch.dict(
42
+ os.environ,
43
+ {
44
+ index_name: "untrusted-index-sentinel",
45
+ extra_index_name: "untrusted-extra-sentinel",
46
+ "PYTHONPATH": "/untrusted",
47
+ },
48
+ clear=False,
49
+ ):
50
+ install_process_runtime(root, lock, runner=runner)
51
+
52
+ self.assertEqual(1, len(calls))
53
+ command, cwd, environment = calls[0]
54
+ self.assertEqual(root.resolve(), cwd)
55
+ self.assertIn("--require-hashes", command)
56
+ self.assertIn("--no-cache-dir", command)
57
+ self.assertEqual(PUBLIC_INDEX, command[command.index("--index-url") + 1])
58
+ self.assertEqual(str(lock.resolve()), command[-1])
59
+ self.assertNotIn(index_name, environment)
60
+ self.assertNotIn(extra_index_name, environment)
61
+ self.assertNotIn("PYTHONPATH", environment)
62
+ self.assertEqual(os.devnull, environment["PIP_CONFIG_FILE"])
63
+
64
+ def test_retries_only_exact_pinned_version_absence(self):
65
+ with tempfile.TemporaryDirectory() as directory:
66
+ root = Path(directory)
67
+ lock = self.fixture(root)
68
+ attempts = iter(
69
+ (
70
+ Attempt(
71
+ 1,
72
+ b"",
73
+ (
74
+ b"Could not find a version that satisfies the requirement "
75
+ b"engineering-process==0.2.1\n"
76
+ b"No matching distribution found for "
77
+ b"engineering-process==0.2.1\n"
78
+ ),
79
+ ),
80
+ Attempt(0, b"installed\n", b""),
81
+ )
82
+ )
83
+ sleeps = []
84
+
85
+ install_process_runtime(
86
+ root,
87
+ lock,
88
+ runner=lambda *_arguments: next(attempts),
89
+ sleeper=sleeps.append,
90
+ )
91
+
92
+ self.assertEqual([BACKOFF_SECONDS[0]], sleeps)
93
+
94
+ def test_fails_immediately_for_hash_or_other_deterministic_errors(self):
95
+ with tempfile.TemporaryDirectory() as directory:
96
+ root = Path(directory)
97
+ lock = self.fixture(root)
98
+ sleeps = []
99
+
100
+ with self.assertRaisesRegex(InstallError, "non-retryable"):
101
+ install_process_runtime(
102
+ root,
103
+ lock,
104
+ runner=lambda *_arguments: Attempt(
105
+ 1, b"", b"THESE PACKAGES DO NOT MATCH THE HASHES\n"
106
+ ),
107
+ sleeper=sleeps.append,
108
+ )
109
+
110
+ self.assertEqual([], sleeps)
111
+
112
+ def test_bounds_timeout_output_and_eventual_absence(self):
113
+ scenarios = (
114
+ (Attempt(-1, b"", b"", timed_out=True), "exceeded 300 seconds"),
115
+ (Attempt(1, b"", b"", output_exceeded=True), "output exceeded"),
116
+ )
117
+ for attempt, expected in scenarios:
118
+ with self.subTest(expected), tempfile.TemporaryDirectory() as directory:
119
+ root = Path(directory)
120
+ lock = self.fixture(root)
121
+ with self.assertRaisesRegex(InstallError, expected):
122
+ install_process_runtime(
123
+ root,
124
+ lock,
125
+ runner=lambda *_arguments, value=attempt: value,
126
+ sleeper=lambda _delay: None,
127
+ )
128
+
129
+ with tempfile.TemporaryDirectory() as directory:
130
+ root = Path(directory)
131
+ lock = self.fixture(root)
132
+ transient = Attempt(
133
+ 1,
134
+ b"",
135
+ (
136
+ b"Could not find a version that satisfies the requirement "
137
+ b"engineering-process==0.2.1\n"
138
+ b"No matching distribution found for engineering-process==0.2.1\n"
139
+ ),
140
+ )
141
+ sleeps = []
142
+ with self.assertRaisesRegex(InstallError, "did not become visible"):
143
+ install_process_runtime(
144
+ root,
145
+ lock,
146
+ runner=lambda *_arguments: transient,
147
+ sleeper=sleeps.append,
148
+ )
149
+
150
+ self.assertEqual(list(BACKOFF_SECONDS), sleeps)
151
+
152
+ def test_rejects_ambiguous_non_binary_or_symlinked_locks(self):
153
+ with tempfile.TemporaryDirectory() as directory:
154
+ root = Path(directory)
155
+ lock = self.fixture(root)
156
+ lock.write_text(
157
+ "engineering-process==0.2.1\nengineering-process==0.2.1\n",
158
+ encoding="utf-8",
159
+ )
160
+ with self.assertRaisesRegex(InstallError, "exactly one"):
161
+ install_process_runtime(
162
+ root,
163
+ lock,
164
+ runner=lambda *_arguments: Attempt(0, b"", b""),
165
+ )
166
+
167
+ if os.name != "nt":
168
+ with tempfile.TemporaryDirectory() as directory:
169
+ root = Path(directory)
170
+ target = root / "target.txt"
171
+ target.write_text(
172
+ "--only-binary :all:\nengineering-process==0.2.1\n",
173
+ encoding="utf-8",
174
+ )
175
+ link = root / "requirements" / "process.txt"
176
+ link.parent.mkdir()
177
+ link.symlink_to(target)
178
+ with self.assertRaisesRegex(InstallError, "symlinks"):
179
+ install_process_runtime(
180
+ root,
181
+ link,
182
+ runner=lambda *_arguments: Attempt(0, b"", b""),
183
+ )
184
+
185
+ def test_real_runner_bounds_output_and_terminates_on_timeout(self):
186
+ with tempfile.TemporaryDirectory() as directory:
187
+ root = Path(directory)
188
+ with patch.object(installer, "MAX_OUTPUT_BYTES", 1024):
189
+ attempt = installer._run_attempt(
190
+ [
191
+ sys.executable,
192
+ "-c",
193
+ "import sys; sys.stdout.buffer.write(b'x' * 100000); sys.stdout.flush()",
194
+ ],
195
+ root,
196
+ os.environ.copy(),
197
+ )
198
+ self.assertTrue(attempt.output_exceeded)
199
+ self.assertLessEqual(len(attempt.stdout) + len(attempt.stderr), 1024)
200
+
201
+ with (
202
+ patch.object(installer, "ATTEMPT_TIMEOUT_SECONDS", 0.1),
203
+ patch.object(installer, "TERMINATION_TIMEOUT_SECONDS", 1.0),
204
+ ):
205
+ started = installer.time.monotonic()
206
+ attempt = installer._run_attempt(
207
+ [sys.executable, "-c", "import time; time.sleep(30)"],
208
+ root,
209
+ os.environ.copy(),
210
+ )
211
+ elapsed = installer.time.monotonic() - started
212
+ self.assertTrue(attempt.timed_out)
213
+ self.assertLess(elapsed, 3.0)
214
+
215
+ @unittest.skipIf(os.name == "nt", "Windows descendants are covered by Job Object tests")
216
+ def test_real_runner_terminates_descendants_left_by_successful_root(self):
217
+ with tempfile.TemporaryDirectory() as directory:
218
+ attempt = installer._run_attempt(
219
+ [
220
+ sys.executable,
221
+ "-c",
222
+ (
223
+ "import subprocess, sys; "
224
+ "subprocess.Popen([sys.executable, '-c', "
225
+ "'import time; time.sleep(30)'])"
226
+ ),
227
+ ],
228
+ Path(directory),
229
+ os.environ.copy(),
230
+ )
231
+ self.assertTrue(attempt.descendants_terminated)
232
+
233
+
234
+ if __name__ == "__main__":
235
+ unittest.main()