Automatically prepare releases for completed features (#182)
Co-authored-by: dev_agent <17+dev_agent@code.cubecraftcreations.com> Co-committed-by: dev_agent <17+dev_agent@code.cubecraftcreations.com>
This commit was merged in pull request #182.
This commit is contained in:
@@ -51,6 +51,36 @@ class CyclePromptTests(unittest.TestCase):
|
|||||||
self.assertNotIn("The persistent user service runs", output)
|
self.assertNotIn("The persistent user service runs", output)
|
||||||
self.assertNotIn("---\n\n---\n\n# 27", output)
|
self.assertNotIn("---\n\n---\n\n# 27", output)
|
||||||
|
|
||||||
|
def test_task_complete_feature_triggers_one_guarded_promotion(self):
|
||||||
|
output = RENDER(TEMPLATE, PROFILE)
|
||||||
|
self.assertIn(
|
||||||
|
"feature becomes task-complete and its reviewed merge into `dev` is verified",
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
self.assertIn("Every required feature task is `done`", output)
|
||||||
|
self.assertIn("No task review, remediation, approval, or merge remains pending", output)
|
||||||
|
self.assertIn("feature acceptance criteria and integrated checks pass", output)
|
||||||
|
self.assertIn("Independent approval applies to the exact feature head", output)
|
||||||
|
self.assertIn("approver's merge of that head into `dev` is verified", output)
|
||||||
|
self.assertIn("reflects the completed feature and exact repository references", output)
|
||||||
|
self.assertIn("Task completion inside an unmerged feature branch does not trigger", output)
|
||||||
|
self.assertIn("Do not create a duplicate promotion", output)
|
||||||
|
self.assertIn("Allow only one active promotion pull request to `main`", output)
|
||||||
|
self.assertIn("if it was closed without merge, recalculate", output)
|
||||||
|
self.assertIn("sole active orchestrator for the project", output)
|
||||||
|
self.assertIn("including controller exclusivity, reconciliation", output)
|
||||||
|
self.assertIn("RELEASE_PREPARATION_POLICY = EXCLUDED", output)
|
||||||
|
self.assertIn("Active promotion branch and pull request", output)
|
||||||
|
self.assertIn("An active promotion that has not reached `RELEASE_READY`", output)
|
||||||
|
self.assertIn("RELEASE_PREPARATION_POLICY:\n", output)
|
||||||
|
self.assertIn("A ready promotion is a local checkpoint", output)
|
||||||
|
self.assertIn("continue that work while the ready promotion waits", output)
|
||||||
|
self.assertLess(
|
||||||
|
output.index("Begin an automatically triggered, requested, or scheduled release"),
|
||||||
|
output.index("Advance to the next eligible feature"),
|
||||||
|
)
|
||||||
|
self.assertIn("Only Joshua may merge into `main`", output)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import fcntl
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -47,6 +48,7 @@ class ForegroundLauncherTests(unittest.TestCase):
|
|||||||
"NQ_PROJECT_ROOT": str(project),
|
"NQ_PROJECT_ROOT": str(project),
|
||||||
"OPENCODE_BIN": str(opencode),
|
"OPENCODE_BIN": str(opencode),
|
||||||
"SYSTEMCTL_BIN": str(systemctl),
|
"SYSTEMCTL_BIN": str(systemctl),
|
||||||
|
"NQ_STATE_DIR": str(root / "state"),
|
||||||
})
|
})
|
||||||
printed = subprocess.run([str(SCRIPT), "--print-prompt"], env=env,
|
printed = subprocess.run([str(SCRIPT), "--print-prompt"], env=env,
|
||||||
text=True, capture_output=True)
|
text=True, capture_output=True)
|
||||||
@@ -67,6 +69,16 @@ class ForegroundLauncherTests(unittest.TestCase):
|
|||||||
["--check-interactive", "--project", str(project)])
|
["--check-interactive", "--project", str(project)])
|
||||||
|
|
||||||
output.unlink()
|
output.unlink()
|
||||||
|
lock_path = root / "state/controller.lock"
|
||||||
|
with lock_path.open("w") as held_lock:
|
||||||
|
fcntl.flock(held_lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
locked = subprocess.run([str(SCRIPT)], env=env, text=True,
|
||||||
|
capture_output=True)
|
||||||
|
self.assertEqual(locked.returncode, 75)
|
||||||
|
self.assertIn("Another NestQuest orchestrator or controller",
|
||||||
|
locked.stderr)
|
||||||
|
self.assertFalse(output.exists())
|
||||||
|
|
||||||
open_marker.touch()
|
open_marker.touch()
|
||||||
refused = subprocess.run([str(SCRIPT)], env=env, text=True,
|
refused = subprocess.run([str(SCRIPT)], env=env, text=True,
|
||||||
capture_output=True)
|
capture_output=True)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ control_home=${NQ_CONTROL_HOME:-/home/overseer/.local/share/nestquest-controller
|
|||||||
project=${NQ_PROJECT_ROOT:-/mnt/ai-storage/projects/NestQuest}
|
project=${NQ_PROJECT_ROOT:-/mnt/ai-storage/projects/NestQuest}
|
||||||
opencode_bin=${OPENCODE_BIN:-/home/overseer/.opencode/bin/opencode}
|
opencode_bin=${OPENCODE_BIN:-/home/overseer/.opencode/bin/opencode}
|
||||||
systemctl_bin=${SYSTEMCTL_BIN:-systemctl}
|
systemctl_bin=${SYSTEMCTL_BIN:-systemctl}
|
||||||
|
state_dir=${NQ_STATE_DIR:-/home/overseer/.local/state/nestquest-controller}
|
||||||
renderer="$control_home/scripts/render-cycle-prompt"
|
renderer="$control_home/scripts/render-cycle-prompt"
|
||||||
profile="$control_home/templates/nestquest.profile.json"
|
profile="$control_home/templates/nestquest.profile.json"
|
||||||
|
|
||||||
@@ -41,6 +42,17 @@ fi
|
|||||||
# Foreground mode owns orchestration. A dormant service must never race it.
|
# Foreground mode owns orchestration. A dormant service must never race it.
|
||||||
"$systemctl_bin" --user stop nestquest-controller.service
|
"$systemctl_bin" --user stop nestquest-controller.service
|
||||||
|
|
||||||
|
# Share the controller's lock and hold it across the final exec. This closes
|
||||||
|
# the launch-time race between two foreground sessions as well as the race with
|
||||||
|
# the background controller.
|
||||||
|
mkdir -p "$state_dir"
|
||||||
|
chmod 700 "$state_dir"
|
||||||
|
exec 9>"$state_dir/controller.lock"
|
||||||
|
if ! flock -n 9; then
|
||||||
|
echo "Another NestQuest orchestrator or controller is running." >&2
|
||||||
|
exit 75
|
||||||
|
fi
|
||||||
|
|
||||||
session_status=$("$control_home/scripts/nq-controller" --check-interactive --project "$project")
|
session_status=$("$control_home/scripts/nq-controller" --check-interactive --project "$project")
|
||||||
if [[ "$session_status" == "interactive NestQuest session open" ]]; then
|
if [[ "$session_status" == "interactive NestQuest session open" ]]; then
|
||||||
echo "Another interactive NestQuest OpenCode session is open." >&2
|
echo "Another interactive NestQuest OpenCode session is open." >&2
|
||||||
|
|||||||
@@ -150,6 +150,15 @@ Possible scopes:
|
|||||||
- `PROJECT_QUEUE`
|
- `PROJECT_QUEUE`
|
||||||
- `RELEASE_PREPARATION`
|
- `RELEASE_PREPARATION`
|
||||||
|
|
||||||
|
Also set `RELEASE_PREPARATION_POLICY` at startup:
|
||||||
|
|
||||||
|
- `AUTO` by default.
|
||||||
|
- `EXCLUDED` only when the user's launch request explicitly says not to prepare
|
||||||
|
a release or `{{INTEGRATION_BRANCH}}`-to-`{{RELEASE_BRANCH}}` promotion.
|
||||||
|
|
||||||
|
Record both `MISSION_SCOPE` and `RELEASE_PREPARATION_POLICY` in durable run
|
||||||
|
state. Do not infer exclusion merely from `SINGLE_TASK` or `SINGLE_FEATURE`.
|
||||||
|
|
||||||
If the user explicitly names one task or feature, respect that scope.
|
If the user explicitly names one task or feature, respect that scope.
|
||||||
|
|
||||||
If the user launches the orchestrator for general/autonomous {{PROJECT_NAME}} development without limiting it to one feature, use:
|
If the user launches the orchestrator for general/autonomous {{PROJECT_NAME}} development without limiting it to one feature, use:
|
||||||
@@ -171,6 +180,7 @@ Maintain a durable orchestration checkpoint using {{WORK_TRACKER}} wherever its
|
|||||||
Track at minimum:
|
Track at minimum:
|
||||||
|
|
||||||
- `MISSION_SCOPE`
|
- `MISSION_SCOPE`
|
||||||
|
- `RELEASE_PREPARATION_POLICY`
|
||||||
- Current feature
|
- Current feature
|
||||||
- Current controller phase
|
- Current controller phase
|
||||||
- Active development agents
|
- Active development agents
|
||||||
@@ -185,6 +195,9 @@ Track at minimum:
|
|||||||
- Blocked items
|
- Blocked items
|
||||||
- Tests/checks status
|
- Tests/checks status
|
||||||
- Last verified repository state
|
- Last verified repository state
|
||||||
|
- Active promotion branch and pull request
|
||||||
|
- Triggering feature and exact `{{INTEGRATION_BRANCH}}` commit for each automatic promotion
|
||||||
|
- Completed features pending the next promotion
|
||||||
- `NEXT_ACTION`
|
- `NEXT_ACTION`
|
||||||
|
|
||||||
After every material state transition:
|
After every material state transition:
|
||||||
@@ -254,8 +267,8 @@ Choose the highest-priority actionable transition in this order:
|
|||||||
8. Remediate feature-level review findings.
|
8. Remediate feature-level review findings.
|
||||||
9. Obtain feature approval.
|
9. Obtain feature approval.
|
||||||
10. Merge an approved feature into `{{INTEGRATION_BRANCH}}`.
|
10. Merge an approved feature into `{{INTEGRATION_BRANCH}}`.
|
||||||
11. Advance to the next eligible feature when mission scope permits.
|
11. Begin an automatically triggered, requested, or scheduled release when appropriate. The automatic post-feature trigger is in scope for both `SINGLE_FEATURE` and `PROJECT_QUEUE` unless `RELEASE_PREPARATION_POLICY = EXCLUDED`.
|
||||||
12. Begin a requested/scheduled release when appropriate.
|
12. Advance to the next eligible feature when mission scope permits.
|
||||||
13. Continue unrelated work around locally blocked items.
|
13. Continue unrelated work around locally blocked items.
|
||||||
14. Escalate only when the defined escalation rules require it.
|
14. Escalate only when the defined escalation rules require it.
|
||||||
|
|
||||||
@@ -588,9 +601,10 @@ Immediately transition to `FEATURE_INTEGRATION`.
|
|||||||
|
|
||||||
A merged feature is a transition.
|
A merged feature is a transition.
|
||||||
|
|
||||||
It is terminal ONLY when:
|
It is terminal only when `MISSION_SCOPE = SINGLE_FEATURE` and no authorized
|
||||||
|
post-feature release transition is pending. Preparing the automatic promotion
|
||||||
`MISSION_SCOPE = SINGLE_FEATURE`
|
for that completed feature is part of `SINGLE_FEATURE` scope when
|
||||||
|
`RELEASE_PREPARATION_POLICY = AUTO`.
|
||||||
|
|
||||||
Otherwise select the next eligible work.
|
Otherwise select the next eligible work.
|
||||||
|
|
||||||
@@ -637,13 +651,19 @@ The orchestrator itself does not perform the merge.
|
|||||||
When `MISSION_SCOPE = PROJECT_QUEUE` and a feature is successfully merged into `{{INTEGRATION_BRANCH}}`:
|
When `MISSION_SCOPE = PROJECT_QUEUE` and a feature is successfully merged into `{{INTEGRATION_BRANCH}}`:
|
||||||
|
|
||||||
1. Reconcile {{WORK_TRACKER}}.
|
1. Reconcile {{WORK_TRACKER}}.
|
||||||
2. Check for a requested/scheduled release.
|
2. Evaluate the automatic, requested, and scheduled release triggers.
|
||||||
3. Identify remaining incomplete features.
|
3. Identify remaining incomplete features.
|
||||||
4. Determine which features are eligible based on dependencies and priority.
|
4. Determine which features are eligible based on dependencies and priority.
|
||||||
5. Select the next eligible feature.
|
5. Select the next eligible feature.
|
||||||
6. Initialize it.
|
6. Initialize it.
|
||||||
7. Continue the development loop.
|
7. Continue the development loop.
|
||||||
|
|
||||||
|
If an automatic promotion becomes `RELEASE_READY` while another eligible
|
||||||
|
feature remains, keep that promotion waiting for {{RELEASE_OWNER_NAME}} and
|
||||||
|
continue the project queue. A ready promotion is a local checkpoint in this
|
||||||
|
case, not a global terminal condition. Features completed while it remains open
|
||||||
|
are recorded as pending for the next promotion under Section 18.
|
||||||
|
|
||||||
Do not return to the user simply because one feature completed.
|
Do not return to the user simply because one feature completed.
|
||||||
|
|
||||||
If several features are available, choose using:
|
If several features are available, choose using:
|
||||||
@@ -658,27 +678,52 @@ If several features are available, choose using:
|
|||||||
|
|
||||||
# 18. RELEASE RULES
|
# 18. RELEASE RULES
|
||||||
|
|
||||||
Completed features accumulate in `{{INTEGRATION_BRANCH}}`.
|
Completed features accumulate in `{{INTEGRATION_BRANCH}}`. With no active
|
||||||
|
promotion, each verified feature completion starts release preparation. While
|
||||||
|
one promotion is active, later completed features accumulate for the next
|
||||||
|
promotion.
|
||||||
|
|
||||||
Prepare a release only when:
|
Prepare a release when any of these conditions is true:
|
||||||
|
|
||||||
- {{RELEASE_OWNER_NAME}} requests it; OR
|
- {{RELEASE_OWNER_NAME}} requests it; OR
|
||||||
- {{WORK_TRACKER}} schedules it.
|
- {{WORK_TRACKER}} schedules it; OR
|
||||||
|
- `RELEASE_PREPARATION_POLICY = AUTO`, and a feature becomes task-complete and its reviewed merge into `{{INTEGRATION_BRANCH}}` is verified.
|
||||||
|
|
||||||
|
For the automatic trigger, a feature is task-complete only when:
|
||||||
|
|
||||||
|
- Every required feature task is `done`, or deliberately `canceled` with recorded evidence.
|
||||||
|
- No task review, remediation, approval, or merge remains pending.
|
||||||
|
- The feature acceptance criteria and integrated checks pass.
|
||||||
|
- Independent approval applies to the exact feature head.
|
||||||
|
- The approver's merge of that head into `{{INTEGRATION_BRANCH}}` is verified.
|
||||||
|
- {{WORK_TRACKER}} reflects the completed feature and exact repository references.
|
||||||
|
|
||||||
|
Task completion inside an unmerged feature branch does not trigger a release.
|
||||||
|
The verified feature-to-`{{INTEGRATION_BRANCH}}` merge is the triggering transition.
|
||||||
|
|
||||||
|
Before creating a release branch or pull request:
|
||||||
|
|
||||||
|
1. Verify this session is the sole active orchestrator for the project. `{{TERMINAL_LAUNCHER}}` enforces that exclusivity by holding the shared controller lock for the full foreground session. If that lock is not held by this session, do not create a release branch or pull request; escalate instead.
|
||||||
|
2. Reconcile open release records, release branches, and promotion pull requests.
|
||||||
|
3. Do not create a duplicate promotion for an already-covered `{{INTEGRATION_BRANCH}}` commit.
|
||||||
|
4. Allow only one active promotion pull request to `{{RELEASE_BRANCH}}`. If an earlier promotion is still open and does not contain the newly completed feature, record the feature as pending for the next automatic promotion. Resume it after the active promotion is merged or closed; if it was closed without merge, recalculate the next promotion from the current validated `{{INTEGRATION_BRANCH}}` head.
|
||||||
|
5. Record the triggering feature and exact `{{INTEGRATION_BRANCH}}` commit so the automatic trigger runs once for that merge.
|
||||||
|
|
||||||
Release process:
|
Release process:
|
||||||
|
|
||||||
1. Reconcile release scope.
|
1. Enforce every pre-creation requirement above, including controller exclusivity, reconciliation, duplicate and active-promotion checks, and trigger recording.
|
||||||
2. Confirm included features are complete.
|
2. Reconcile release scope.
|
||||||
3. Create release branch from exact validated `{{INTEGRATION_BRANCH}}`.
|
3. Confirm included features are complete.
|
||||||
4. Run complete release checks.
|
4. Create release branch from exact validated `{{INTEGRATION_BRANCH}}`.
|
||||||
5. Open release PR → `{{RELEASE_BRANCH}}`.
|
5. Run complete release checks.
|
||||||
6. Obtain fresh independent {{REVIEWER_VENDOR}} review.
|
6. Open release PR → `{{RELEASE_BRANCH}}`.
|
||||||
7. Remediate findings through development agents.
|
7. Obtain fresh independent {{REVIEWER_VENDOR}} review.
|
||||||
8. Incorporate every release correction back into `{{INTEGRATION_BRANCH}}`.
|
8. Remediate findings through development agents.
|
||||||
9. Revalidate.
|
9. Incorporate every release correction back into `{{INTEGRATION_BRANCH}}`.
|
||||||
10. Assign final release PR to `{{HOST_ACCOUNT}}`.
|
10. Revalidate.
|
||||||
11. Update {{WORK_TRACKER}}.
|
11. Assign final release PR to `{{HOST_ACCOUNT}}`.
|
||||||
12. Stop before {{RELEASE_BRANCH}} merge.
|
12. Update {{WORK_TRACKER}}.
|
||||||
|
13. Stop before {{RELEASE_BRANCH}} merge.
|
||||||
|
|
||||||
Only {{RELEASE_OWNER_NAME}} may merge into `{{RELEASE_BRANCH}}`.
|
Only {{RELEASE_OWNER_NAME}} may merge into `{{RELEASE_BRANCH}}`.
|
||||||
|
|
||||||
@@ -724,13 +769,23 @@ For `PROJECT_QUEUE`, this additionally requires:
|
|||||||
|
|
||||||
- No eligible incomplete feature remains.
|
- No eligible incomplete feature remains.
|
||||||
- No task/review/remediation/merge is pending.
|
- No task/review/remediation/merge is pending.
|
||||||
|
- No release preparation, release review, or release remediation is pending.
|
||||||
|
- Every in-scope automatic trigger is either incorporated into the active
|
||||||
|
`RELEASE_READY` promotion or durably recorded for the next promotion.
|
||||||
- No development agent is active.
|
- No development agent is active.
|
||||||
- No approver is active.
|
- No approver is active.
|
||||||
|
|
||||||
|
An active promotion that has not reached `RELEASE_READY` makes
|
||||||
|
`MISSION_COMPLETE` false.
|
||||||
|
|
||||||
## `RELEASE_READY`
|
## `RELEASE_READY`
|
||||||
|
|
||||||
Release PR has passed required gates and is assigned to `{{HOST_ACCOUNT}}`, awaiting {{RELEASE_OWNER_NAME}}'s {{RELEASE_BRANCH}} merge.
|
Release PR has passed required gates and is assigned to `{{HOST_ACCOUNT}}`, awaiting {{RELEASE_OWNER_NAME}}'s {{RELEASE_BRANCH}} merge.
|
||||||
|
|
||||||
|
`RELEASE_READY` is globally terminal only when no other in-scope engineering
|
||||||
|
action is executable. In `PROJECT_QUEUE`, if another eligible incomplete
|
||||||
|
feature remains, continue that work while the ready promotion waits.
|
||||||
|
|
||||||
## `GLOBAL_ESCALATION`
|
## `GLOBAL_ESCALATION`
|
||||||
|
|
||||||
All remaining in-scope work is blocked by decisions/access that require {{RELEASE_OWNER_NAME}}.
|
All remaining in-scope work is blocked by decisions/access that require {{RELEASE_OWNER_NAME}}.
|
||||||
@@ -778,9 +833,12 @@ Output a machine-readable continuation checkpoint:
|
|||||||
```text
|
```text
|
||||||
ORCHESTRATOR_STATE: CONTINUE
|
ORCHESTRATOR_STATE: CONTINUE
|
||||||
MISSION_SCOPE:
|
MISSION_SCOPE:
|
||||||
|
RELEASE_PREPARATION_POLICY:
|
||||||
CONTROLLER_PHASE:
|
CONTROLLER_PHASE:
|
||||||
CURRENT_FEATURE:
|
CURRENT_FEATURE:
|
||||||
ACTIVE_TASKS:
|
ACTIVE_TASKS:
|
||||||
|
ACTIVE_PROMOTION:
|
||||||
|
PENDING_PROMOTION_FEATURES:
|
||||||
BLOCKED_ITEMS:
|
BLOCKED_ITEMS:
|
||||||
LAST_VERIFIED_STATE:
|
LAST_VERIFIED_STATE:
|
||||||
NEXT_ACTION:
|
NEXT_ACTION:
|
||||||
|
|||||||
Reference in New Issue
Block a user