feat(engine): Phase 4 — scheduler tick, gate chain, platform git, budget
The AI workforce engine's driver. One tick moves every ready task one step:
a turn, a gate, or a platform merge. The AI never runs git.
- scheduler.py: tick() over TURN_STAGES/MERGE_STAGES; selection caps
(per-org 3, global 8); optimistic advance so two workers cannot both win;
worktree created for code pipelines, released on completion/cancel/failure
(kept during error_review); budget pause before any turn is spent; merge
conflict back-off; transient-failure retries then the manager's error turn.
- gates.py: diff guard (empty/.git/CI/symlink/oversize), no-progress hash,
tests in the sandbox, green gate that only trusts a green tests row or an
explicit waiver. Every verdict is a gate_results row + a task.gate event.
Fixed forbidden_paths: lstrip("./") stripped all leading dots, hiding
.git and .github from the guard.
- container_engine.build_scheduler + worker drives the engine tick.
- ManagerOutcome.error_kind; TaskSpendRepository.spend_for_task.
- New settings engine_max_concurrent_turns, engine_max_turns_per_org,
engine_tests_timeout_seconds, engine_max_merge_conflicts,
engine_max_task_credits (+ .env.example).
- tests: test_engine_gates.py (13), test_engine_flow.py (11).
Checks: pytest 746 passed/1 skipped, ruff clean, mypy clean (160 files).
This commit is contained in:
@@ -158,6 +158,9 @@ class ManagerOutcome:
|
||||
message: str = ""
|
||||
employee_id: str | None = None
|
||||
pipeline: PipelineKey | None = None
|
||||
#: The runner's error vocabulary, so the scheduler can tell a transient
|
||||
#: provider failure from a permanent one without re-classifying a string.
|
||||
error_kind: str = ""
|
||||
|
||||
|
||||
def _fuzzy_match(name: str, candidates: list[str]) -> str | None:
|
||||
@@ -654,7 +657,10 @@ class ManagerTurns:
|
||||
)
|
||||
if not result.ok:
|
||||
return ManagerOutcome(
|
||||
acted=False, message=result.error, stage=Stage.PENDING_MANAGER_REVIEW
|
||||
acted=False,
|
||||
message=result.error,
|
||||
stage=Stage.PENDING_MANAGER_REVIEW,
|
||||
error_kind=result.error_kind,
|
||||
)
|
||||
parsed = parse_reply(result.reply_text, role=RoleKey.MANAGER, mode=Mode.TRIAGE)
|
||||
assign = parsed.first(TagName.ASSIGN)
|
||||
@@ -769,7 +775,10 @@ class ManagerTurns:
|
||||
)
|
||||
if not result.ok:
|
||||
return ManagerOutcome(
|
||||
acted=False, message=result.error, stage=Stage.PENDING_FINAL_REVIEW
|
||||
acted=False,
|
||||
message=result.error,
|
||||
stage=Stage.PENDING_FINAL_REVIEW,
|
||||
error_kind=result.error_kind,
|
||||
)
|
||||
parsed = parse_reply(result.reply_text, role=RoleKey.MANAGER, mode=Mode.FINAL)
|
||||
if parsed.has(TagName.COMPLETE):
|
||||
@@ -822,7 +831,10 @@ class ManagerTurns:
|
||||
)
|
||||
if not result.ok:
|
||||
return ManagerOutcome(
|
||||
acted=False, message=result.error, stage=Stage.ERROR_REVIEW
|
||||
acted=False,
|
||||
message=result.error,
|
||||
stage=Stage.ERROR_REVIEW,
|
||||
error_kind=result.error_kind,
|
||||
)
|
||||
parsed = parse_reply(result.reply_text, role=RoleKey.MANAGER, mode=Mode.ERROR)
|
||||
resolve = parsed.first(TagName.RESOLVE_ERROR)
|
||||
|
||||
Reference in New Issue
Block a user