feat(integration): prepaid billing, engine UI, credits console, direct tasks, v2 final

- Prepaid credit system: org wallet, debit on usage, chat gate, admin credit console
- Engine UI: /manager, /tasks, /tasks/[id], /workforce, /dashboard, /traces pages
- Direct task creation (HTTP POST /tasks, MCP tasks_create) at pending_manager_review
- Pause/resume/cancel/answer actions in engine API and UI
- Employee cap (7 per org default), review pipeline guard (active reviewer required)
- Scheduler: wide window prevents org starvation; archived employees excluded from cap
- Tenant isolation: project-id cross-org 404, engine never uses another tenant's sandbox
- Order route fix: manager built with pipeline states + employee profiles
- Contract extension: ModelGrantPort.get_org_policy with DefaultOrgPolicy
- Engine event types in @agy/contracts (task.step, task.gate, run.log, run.tool, team.changed)
- api-engine.ts typed client; api-credit.ts client
- Alembic migration c4d5e6f7a8b9: prepaid credit + engine UI tables
- 859 tests passed, 1 skipped; ruff/mypy/tsc/eslint/vitest all green

Refs: PLAN-v2.md §9 (T1–T43), todo-v2-final.md
This commit is contained in:
mjalmoudy94
2026-10-14 23:19:05 +03:30
parent fef5a9deab
commit d532929189
117 changed files with 13119 additions and 640 deletions
+67 -10
View File
@@ -14,7 +14,8 @@ git (in-memory ``FakeRepoWorkspace``) are fakes. What it defends:
* red tests and a forbidden diff send the work *back* with cycle + 1 instead of
merging it
* ``analysis_only`` never creates a worktree
* an overdrawn wallet pauses the task before a single turn is spent
* an empty, overdrawn or missing wallet pauses the task before a single turn is
spent (prepaid billing, decision D-B)
* a transient provider failure is retried, then handed to the manager
"""
@@ -92,13 +93,19 @@ class _OverdrawnWallet:
class _ZeroWallet:
"""A wallet at exactly zero: the normal starting state of a fresh, post-paid org."""
"""A wallet at exactly zero: prepaid, so no credit is left."""
balance = 0.0
class _FundedWallet:
"""A wallet with credit left: the org may spend a turn."""
balance = 5.0
class _Wallets:
def __init__(self, wallet: _OverdrawnWallet | _ZeroWallet | None) -> None:
def __init__(self, wallet: _OverdrawnWallet | _ZeroWallet | _FundedWallet | None) -> None:
self._wallet = wallet
async def get_for_org(self, organization_id: str):
@@ -567,14 +574,12 @@ async def test_an_overdrawn_wallet_pauses_the_task_before_a_turn_is_spent(
assert BUDGET_PAUSED_MESSAGE
async def test_a_zero_wallet_does_not_pause_a_fresh_org(world, session_factory):
"""A 0.0 balance is every org's starting state, so it must not pause the task.
async def test_a_zero_wallet_pauses_the_task_before_a_turn_is_spent(world, session_factory):
"""A zero balance is no credit left: the task pauses before a turn is spent.
This is an intentional behaviour change. The scheduler used to pause on
``balance <= 0``, which froze every new org before its first turn: usage is
post-paid and nothing debits a wallet. Only an overdrawn wallet (below zero)
pauses now. The old test that encoded the previous rule was renamed and
updated, as PLAN-v2 §7.4 requires for a deliberate behaviour change.
Prepaid billing (decision D-B) reverses the post-paid rule of the earlier plan
(its T3), under which a zero balance was every new org's normal state and did
not pause. Now the platform owner credits an org before any work runs.
"""
scripted = ScriptedModel([ScriptedReply(content="[[ASSIGN/]]")])
workspace = FakeRepoWorkspace()
@@ -594,6 +599,58 @@ async def test_a_zero_wallet_does_not_pause_a_fresh_org(world, session_factory):
)
await _run(scheduler, session, 1)
state = await PipelineStateRepository(session).get(row.id)
task = await TaskRepository(session).get(row.id)
assert state is not None and state.stage is Stage.PAUSED
assert task is not None and task.status is TaskStatus.WAITING_APPROVAL
assert scripted.calls == 0
async def test_a_missing_wallet_pauses_the_task(world, session_factory):
"""No wallet row at all counts as zero credit (prepaid): the task pauses."""
scripted = ScriptedModel([ScriptedReply(content="[[ASSIGN/]]")])
workspace = FakeRepoWorkspace()
async with session_factory() as session:
await _team(session, world)
row = await _task(session, world, stage=Stage.PENDING_MANAGER_REVIEW)
await session.commit()
scheduler = _scheduler(
session,
world,
scripted=scripted,
sandbox=FakeSandbox(),
workspace=workspace,
memory=RecordingMemory(),
delivery=NullDeliveryHooks(),
wallets=_Wallets(None),
)
await _run(scheduler, session, 1)
state = await PipelineStateRepository(session).get(row.id)
assert state is not None and state.stage is Stage.PAUSED
assert scripted.calls == 0
async def test_a_funded_wallet_does_not_pause(world, session_factory):
"""Credit left means the org may spend a turn: the task is not paused."""
scripted = ScriptedModel([ScriptedReply(content="[[ASSIGN/]]")])
workspace = FakeRepoWorkspace()
async with session_factory() as session:
await _team(session, world)
row = await _task(session, world, stage=Stage.PENDING_MANAGER_REVIEW)
await session.commit()
scheduler = _scheduler(
session,
world,
scripted=scripted,
sandbox=FakeSandbox(),
workspace=workspace,
memory=RecordingMemory(),
delivery=NullDeliveryHooks(),
wallets=_Wallets(_FundedWallet()),
)
await _run(scheduler, session, 1)
state = await PipelineStateRepository(session).get(row.id)
assert state is not None and state.stage is not Stage.PAUSED
assert scripted.calls == 1