# Task System

**Location**: `src/yc_bench/cli/task_commands.py`, `src/yc_bench/core/eta.py`, `src/yc_bench/core/progress.py`

## Task Lifecycle

```
market ──accept──> planned ──dispatch──> active ──complete──> completed_success
                      │                    │                  completed_fail
                      │                    │
                      └──cancel──> cancelled <──cancel──┘
```

### States

| Status | Meaning |
|--------|---------|
| `market` | Available for browsing, not yet accepted |
| `planned` | Accepted by company, employees can be assigned |
| `active` | Dispatched, work is progressing |
| `completed_success` | Finished on time |
| `completed_fail` | Finished late (past deadline) |
| `cancelled` | Abandoned by agent |

## Design Choices

### Two-Phase Activation (Accept → Dispatch)

Tasks go through `planned` before `active`. This separation:

1. **Allows pre-assignment**: Agent can assign employees before starting the clock
2. **Deadline starts at accept**: Creates urgency -- planning time counts against the deadline
3. **Forces commitment**: Accepting a task reserves it but the agent must still dispatch

### Deadline Calculation

```
deadline = accepted_at + max(required_qty[d] for all domains d) / deadline_qty_per_day
```

**Design choice**: Deadline is proportional to the largest single-domain requirement, not the sum. This means multi-domain tasks don't get proportionally more time -- they require parallel work.

### Prestige Gating at Accept Time

```python
def task_accept(task_id):
    for domain in task.requirements:
        if company_prestige[domain] < task.required_prestige:
            reject("Insufficient prestige in {domain}")
```

**Design choice**: Prestige check is per-domain. A task requiring prestige 3.0 with requirements in `research` and `inference` needs prestige >= 3.0 in BOTH domains. This prevents gaming by maxing one domain.

### Trust Gating at Accept Time

~20% of tasks have a `required_trust` field. At acceptance, the agent's trust with the task's client must meet the threshold:

```python
if task.required_trust > 0 and task.client_id:
    client_trust = get_trust(company_id, task.client_id)
    if client_trust < task.required_trust:
        reject("Insufficient trust with client")
```

**Design choice**: Trust gating is per-client, not global. High-trust tasks are the most valuable opportunities, gated behind relationship-building with specific clients. See [11_client_trust.md](11_client_trust.md) for full trust mechanics.

### Client Assignment and Reward Scaling

Each task belongs to a specific client. At acceptance:

1. **Reward scaling**: `actual_reward = listed_reward × trust_multiplier` (50% at trust 0, scaling up with trust and client tier)
2. **Work reduction**: `required_qty *= (1 - trust_work_reduction_max × trust/trust_max)` (up to 40% less work at max trust)
3. **Replacement generation**: A new market task replaces the accepted one, biased toward the same client's specialty domains

### Cancel Penalties

Cancelling an active task incurs:
- Prestige penalty: `reward_prestige_delta × cancel_multiplier` (configurable per difficulty)
- No financial penalty (just lost opportunity)

**Design choice**: Cancel penalties prevent the strategy of accepting everything and dropping what's inconvenient. Higher difficulties increase the cancel multiplier.

## Employee Assignment

### Assignment Rules

- Employees can only be assigned to `planned` or `active` tasks
- An employee can work on multiple tasks simultaneously (throughput splits)
- Multiple employees can work on the same task (parallel progress)

### Throughput Splitting

```
effective_rate = base_rate_per_hour / num_active_tasks
```

**Design choice**: Linear throughput splitting creates a fundamental trade-off:
- **Focus**: 1 employee on 1 task = full speed
- **Parallel**: 1 employee on 3 tasks = 1/3 speed each
- The agent must decide between fast completion of few tasks vs. slow progress on many

## Progress Tracking (`progress.py`)

### How Work Gets Done

Progress is calculated lazily during `advance_time()`:

```python
for each active task:
    for each assigned employee:
        for each domain in task requirements:
            work = employee.skill_rate[domain] / num_active_tasks × business_hours
            requirement.completed_qty += work
            requirement.completed_qty = min(completed_qty, required_qty)
```

### Multi-Domain Completion

A task is complete when ALL domain requirements reach `completed_qty >= required_qty`. The slowest domain is the bottleneck.

**Design choice**: This creates interesting optimization puzzles. If a task needs 100 units of research and 50 units of training, the agent should allocate more research-skilled employees to balance completion times.

## ETA Solver (`eta.py`)

### Completion Time Calculation

```python
def solve_task_completion_time(task, assignments, sim_time):
    for each domain d:
        remaining = required_qty[d] - completed_qty[d]
        rate = sum(effective_rate[emp][d] for emp in assignments)
        if rate == 0:
            return infinity  # no one can work on this domain
        hours_needed[d] = remaining / rate

    max_hours = max(hours_needed.values())
    return sim_time + max_hours (in business hours)
```

### Halfway Time Calculation

Used for milestone events. Finds the time when weighted average across domains reaches 50%.

### When ETAs Are Recalculated

- Task dispatched (new active task)
- Employee assigned/unassigned
- Task completed (frees employee throughput for other tasks)
- Task cancelled (same)

**Design choice**: Dynamic ETA recalculation ensures events are always accurate. When an employee is reassigned, all affected tasks get new completion projections.

## Market Task Generation

See [09_configuration.md](09_configuration.md) for details on how market tasks are generated with stratified prestige distribution and randomized requirements.

### Browsing and Filtering

The `market browse` command supports:
- Domain filter
- Prestige range filter
- Reward range filter
- Pagination (offset/limit)

All output is JSON for agent consumption.

### Sim Resume Blocking

`yc-bench sim resume` is **blocked** when there are zero active tasks, returning `{"ok": false}` instead of advancing time. This prevents catastrophic payroll drain when the agent has no work in progress. The agent loop filters blocked responses and treats them as no-ops.

The auto-advance mechanism (which forces `sim resume` after N consecutive turns without one) also checks for active tasks before advancing.
