# Portfolio Autopilot

- Repo: n/a — lives in Nano's scheduled tasks + config
- Goal: Quota-aware autonomous work loop that advances portfolio projects using excess LLM capacity, while preserving a hard reserve for James's interactive use. Self-adjusting over time.
- Stage: spec
- Guard: autonomous-ok
- Metric: portfolio stages advanced per week at ≤ 15% of James's interactive quota consumed
- Blocked on James: confirm priority tiers (P1/P2/P3) for each project
- Next action: build routing logic + config file; wire to CodexBar endpoint at host.docker.internal:7842

## Design

### Three levers (all configurable)

| Lever | Default | Purpose |
|---|---|---|
| Base reserve (5h window) | 25% | Interactive buffer — scales down as reset approaches |
| Base reserve (7d window) | 15% | Interactive buffer — scales down as reset approaches |

**No per-session cap.** If there's burnable quota and work to do, use it.

### Adaptive reserve formula

```
adaptive_reserve_pct = base_reserve_pct × (resets_in_sec / window_duration_sec)
burnable_pct         = remaining_pct − adaptive_reserve_pct
```

This means:
- Start of window: full reserve held
- Mid-window: reserve shrinks proportionally
- Near reset: reserve → 0 (almost all remaining quota is burnable)

Applied to both windows independently; take the more conservative (lower burnable) as the constraint:
```
burnable = min(burnable_5h, burnable_7d)
```

Work runs only when `burnable > 0`.

### Quota tiers → model routing

| Remaining (above reserve) | Model | Projects allowed |
|---|---|---|
| Flush (>60%) | Claude Sonnet | P1, P2, P3 |
| Comfortable (40–60%) | Claude Sonnet | P1, P2 |
| Moderate (20–40%) | Claude Haiku or Gemini | P1 only |
| Near reserve (<20%) | Gemini / LM Studio | maintenance tasks only |
| At/below reserve | No autonomous work | — |

### Burn-rate gate (for P2/P3)

`sustainable_pct_per_hour = 100 / window_hours`

P2/P3 work only runs when actual burn rate < sustainable pace — i.e., quota is ahead of the smooth curve. P1 work bypasses this gate (runs whenever above the reserve floor).

### Priority tiers (proposed — James to confirm)

| Project | Priority | Rationale |
|---|---|---|
| StageHand | P1 | James's priority — real estate video suite, active discovery |
| nanoclaw | P1 | Maintenance + ongoing infra work |
| AgenticLever | P2 | Business-facing, active build |
| RoadSense | P2 | Going public, active momentum |
| qip-qien | P2 | Live trial — needs progress but review-required gate limits autonomous scope |
| congressional-trading | P3 | Runs autonomously; calibration is in-flight, low need for new LLM spend |
| TradingBot25 | P3 | Deep investment already; review-required, low autonomous-work surface |

### Closed-loop auto-calibration

Track per run:
- Quota spent (from CodexBar delta before/after)
- Output produced (git commits, wiki pages, board moves)
- Did James use Claude within 2h after an autonomous run? (proxy for "left enough headroom")

Weekly review: if James-usage-after-run rate is high → reserve floor was too low → bump it up. If output/quota ratio is consistently low → narrow per-session cap. If P1 projects are stalling → loosen moderate-tier threshold.

The loop surfaces a weekly "autopilot health" note (not a full brief — one line per signal that changed).

### Config file

`/workspace/agent/autopilot-config.json` — all thresholds live here so any run can tweak them without code changes.

```json
{
  "reserve_5h_pct": 25,
  "reserve_7d_pct": 15,
  "per_session_cap_pct": 10,
  "project_priorities": {
    "StageHand": "P1",
    "nanoclaw": "P1",
    "AgenticLever": "P2",
    "RoadSense": "P2",
    "qip-qien": "P2",
    "congressional-trading": "P3",
    "TradingBot25": "P3"
  },
  "flush_threshold": 60,
  "comfortable_threshold": 40,
  "moderate_threshold": 20
}
```

## Implementation plan

1. **Config + quota reader** — `autopilot-config.json` + script that reads CodexBar endpoint and computes current tier and burn rate
2. **Routing logic** — given tier + priority + project next-action, decide: proceed / skip / use alternate model
3. **Task executor** — runs one unit of work per session, commits result, updates board
4. **Scheduled trigger** — every 30 min, runs quota check; fires task if conditions met
5. **Outcome tracker** — logs quota delta + output produced per run
6. **Weekly calibration** — reviews outcome log, suggests threshold changes

## Notes

- 2026-07-07 — James approved design. Start with intelligent defaults, evolve to closed-loop. Config file approach so thresholds are tweakable without code changes.
- 2026-07-07 — James set priorities (inverted from initial proposal): P1 = StageHand, nanoclaw; P2 = AgenticLever, RoadSense, qip-qien; P3 = congressional-trading, TradingBot25.
- CodexBar endpoint: `http://host.docker.internal:7842/usage` — confirmed live and accessible from container.
- LM Studio confirmed live at `http://host.docker.internal:1234` with Gemma 4 12B + 26B models loaded.

{
"reserve_5h_pct": 25,
"reserve_7d_pct": 15,
-  "per_session_cap_pct": 10,
+
"project_priorities": {
"StageHand": "P1",
"nanoclaw": "P1",


Expand Roadsense to new areas and plan public launch


Edit

- 2026-07-07 — James approved design. Start with intelligent defaults, evolve to closed-loop. Config file approach so thresholds are tweakable without code changes.
- 2026-07-07 — James set priorities (inverted from initial proposal): P1 = StageHand, nanoclaw; P2 = AgenticLever, RoadSense, qip-qien; P3 = congressional-trading, TradingBot25.

- 2026-07-07 — James approved design. Start with intelligent defaults, evolve to closed-loop. Config file approach so thresholds are tweakable without code changes.
- 2026-07-07 — James set priorities (inverted from initial proposal): P1 = StageHand, nanoclaw; P2 = AgenticLever, RoadSense, qip-qien; P3 = congressional-trading, TradingBot25.
- 2026-07-07 — Reserve revised to adaptive formula (scales linearly to 0 as window resets). Per-session cap removed — no artificial throttle on session size.


The file /workspace/extra/second-brain/portfolio/projects/portfolio-autopilot.md has been updated successfully. (file state is current in your context — no need to Read it back)

- 2026-07-07 — James approved design. Start with intelligent defaults, evolve to closed-loop. Config file approach so thresholds are tweakable without code changes.
- 2026-07-07 — James set priorities (inverted from initial proposal): P1 = StageHand, nanoclaw; P2 = AgenticLever, RoadSense, qip-qien; P3 = congressional-trading, TradingBot25.
- 2026-07-07 — Reserve revised to adaptive formula (scales linearly to 0 as window resets). Per-session cap removed — no artificial throttle on session size.

# Portfolio Autopilot

- Repo: n/a — lives in Nano's scheduled tasks + config
- Goal: Quota-aware autonomous work loop that advances portfolio projects using excess LLM capacity, while preserving a hard reserve for James's interactive use. Self-adjusting over time.
- Stage: spec
- Guard: autonomous-ok
- Metric: portfolio stages advanced per week at ≤ 15% of James's interactive quota consumed
- Blocked on James: confirm priority tiers (P1/P2/P3) for each project
- Next action: build routing logic + config file; wire to CodexBar endpoint at host.docker.internal:7842

## Design

### Three levers (all configurable)

| Lever | Default | Purpose |
|---|---|---|
| Base reserve (5h window) | 25% | Interactive buffer — scales down as reset approaches |
| Base reserve (7d window) | 15% | Interactive buffer — scales down as reset approaches |

**No per-session cap.** If there's burnable quota and work to do, use it.

### Adaptive reserve formula

```
adaptive_reserve_pct = base_reserve_pct × (resets_in_sec / window_duration_sec)
burnable_pct         = remaining_pct − adaptive_reserve_pct
```

This means:
- Start of window: full reserve held
- Mid-window: reserve shrinks proportionally
- Near reset: reserve → 0 (almost all remaining quota is burnable)

Applied to both windows independently; take the more conservative (lower burnable) as the constraint:
```
burnable = min(burnable_5h, burnable_7d)
```

Work runs only when `burnable > 0`.

### Quota tiers → model routing

| Remaining (above reserve) | Model | Projects allowed |
|---|---|---|
| Flush (>60%) | Claude Sonnet | P1, P2, P3 |
| Comfortable (40–60%) | Claude Sonnet | P1, P2 |
| Moderate (20–40%) | Claude Haiku or Gemini | P1 only |
| Near reserve (<20%) | Gemini / LM Studio | maintenance tasks only |
| At/below reserve | No autonomous work | — |

### Burn-rate gate (for P2/P3)

`sustainable_pct_per_hour = 100 / window_hours`

P2/P3 work only runs when actual burn rate < sustainable pace — i.e., quota is ahead of the smooth curve. P1 work bypasses this gate (runs whenever above the reserve floor).

### Priority tiers (proposed — James to confirm)

| Project | Priority | Rationale |
|---|---|---|
| StageHand | P1 | James's priority — real estate video suite, active discovery |
| nanoclaw | P1 | Maintenance + ongoing infra work |
| AgenticLever | P2 | Business-facing, active build |
| RoadSense | P2 | Going public, active momentum |
| qip-qien | P2 | Live trial — needs progress but review-required gate limits autonomous scope |
| congressional-trading | P3 | Runs autonomously; calibration is in-flight, low need for new LLM spend |
| TradingBot25 | P3 | Deep investment already; review-required, low autonomous-work surface |

### Closed-loop auto-calibration

Track per run:
- Quota spent (from CodexBar delta before/after)
- Output produced (git commits, wiki pages, board moves)
- Did James use Claude within 2h after an autonomous run? (proxy for "left enough headroom")

Weekly review: if James-usage-after-run rate is high → reserve floor was too low → bump it up. If output/quota ratio is consistently low → narrow per-session cap. If P1 projects are stalling → loosen moderate-tier threshold.

The loop surfaces a weekly "autopilot health" note (not a full brief — one line per signal that changed).

### Config file

`/workspace/agent/autopilot-config.json` — all thresholds live here so any run can tweak them without code changes.

```json
{
  "reserve_5h_pct": 25,
  "reserve_7d_pct": 15,

  "project_priorities": {
    "StageHand": "P1",
    "nanoclaw": "P1",
    "AgenticLever": "P2",
    "RoadSense": "P2",
    "qip-qien": "P2",
    "congressional-trading": "P3",
    "TradingBot25": "P3"
  },
  "flush_threshold": 60,
  "comfortable_threshold": 40,
  "moderate_threshold": 20
}
```

## Implementation plan

1. **Config + quota reader** — `autopilot-config.json` + script that reads CodexBar endpoint and computes current tier and burn rate
2. **Routing logic** — given tier + priority + project next-action, decide: proceed / skip / use alternate model
3. **Task executor** — runs one unit of work per session, commits result, updates board
4. **Scheduled trigger** — every 30 min, runs quota check; fires task if conditions met
5. **Outcome tracker** — logs quota delta + output produced per run
6. **Weekly calibration** — reviews outcome log, suggests threshold changes

## Notes

- 2026-07-07 — James approved design. Start with intelligent defaults, evolve to closed-loop. Config file approach so thresholds are tweakable without code changes.
- 2026-07-07 — James set priorities (inverted from initial proposal): P1 = StageHand, nanoclaw; P2 = AgenticLever, RoadSense, qip-qien; P3 = congressional-trading, TradingBot25.
- CodexBar endpoint: `http://host.docker.internal:7842/usage` — confirmed live and accessible from container.
- LM Studio confirmed live at `http://host.docker.internal:1234` with Gemma 4 12B + 26B models loaded.

+- 2026-07-07 — Reserve revised to adaptive formula (scales linearly to 0 as window resets). Per-session cap removed — no artificial throttle on session size.


<message to="telegram-mg-17796">Updated. The revised design:

**Adaptive reserve:** `reserve = base_pct × (time_remaining / window_duration)`. Near reset, the reserve shrinks toward zero so quota doesn't get stranded. A 25% base reserve at the start of a 5h window becomes ~2% in the last 15 minutes — basically "use it all."

**No per-session cap.** If there's work and quota, run the work. The only throttle is the adaptive reserve floor.

The math runs independently on both the 5h and 7d windows and takes whichever is more constrained. So near a 5h reset with plenty of 7d remaining, you can still sprint through the last of the 5h quota.

Design is solid. Starting to write the actual routing script now.</message>


<context timezone="America/Chicago" />
<message id="1080" from="telegram-mg-17796" sender="James Lopez" time="Jul 7, 2026, 9:12 AM">another thing I was thinking about was some activities are more costly than others like planning. We probably want to use smarter more capable models at a higher level of effort because that’s gonna save us time in bad implementation or poor design decisions in any sort of things and so and that’s fine. That should be like expected in actually desirable to use to do planning on smarter models at higher effort levels and so those we also I think need to think about where the human in the loop is. I’m trying to reduce the human in loop but also there’s gonna be important checkpoints where we need the human in the loop in terms of like reviewing the plan. Perhaps if it’s a new plan doing New like Greenfield work they probably needs human review before starting implementation and so frontloading the planning done by the more intelligent model to then have that ready so that me the human loop can go review the plan, think about that think more deeply think about the open questions we need to also do a red team review on that we need to implement that as part of the sort of the end of the plan creation before the human gets involved.</message>


James is adding important nuance about the task execution model:

1. **Task cost tiers**: Different tasks have different compute costs
   - Planning = expensive (use capable models, high effort) — BUT this is intentional and desirable
   - Implementation = medium
   - Simple tasks = cheap (can use Gemini/LM Studio)

2. **Human-in-the-loop checkpoints**:
   - Greenfield / new plans → human review before implementation starts
   - The "plan review" step is a checkpoint where James needs to evaluate before coding begins
   - Brownfield/continuation work might not need a checkpoint

3. **Red-team step**: Before showing a plan to James, the system should automatically red-team it (a second pass critique of the plan — find flaws, surface open questions, identify risks).

The red-team phase should be built directly into the planning workflow rather than handled separately. For new work, the sequence flows through planning with a capable model, then a critique pass to identify gaps and risks, before sending both the plan and critique to James for approval. Continuing work might skip or lighten this checkpoint. This also reshapes the routing logic—it's not just about selecting models based on quota, but orchestrating different task types and their associated workflows.

I'm mapping out a taxonomy of task types with their model requirements, effort levels, and whether they need human sign-off. Each type has specific characteristics: greenfield planning needs the strongest model and always requires James's review, while red-teaming runs automatically after planning, and implementation, research, and maintenance tasks operate autonomously at lower effort levels with cheaper models.

The workflow flows from planning through auto red-team critique, then packages everything for James's approval before implementation kicks off. The key insight is that investing quota upfront in solid planning prevents expensive rework later, making it more efficient overall.

I need to update the routing config to map task types to models separately from just quota tiers, then get back to James with a focused summary of these design decisions.


### Quota tiers → model routing

| Remaining (above reserve) | Model | Projects allowed |
|---|---|---|
| Flush (>60%) | Claude Sonnet | P1, P2, P3 |
| Comfortable (40–60%) | Claude Sonnet | P1, P2 |
| Moderate (20–40%) | Claude Haiku or Gemini | P1 only |
| Near reserve (<20%) | Gemini / LM Studio | maintenance tasks only |
| At/below reserve | No autonomous work | — |

### Task types + model assignment

Task type determines model and effort *regardless of quota tier* — planning always uses the best available model because the cost of a bad plan exceeds the cost of the tokens.

| Task type | Model | Effort | Notes |
|---|---|---|---|
| Plan (greenfield) | Claude Sonnet | high | Always capable model — non-negotiable |
| Red-team review | Claude Sonnet | medium | Auto-runs after every plan, before human sees it |
| Plan (continuation) | Claude Sonnet | medium | Existing direction, lower risk |
| Implementation | Claude Sonnet or Haiku | medium | Quota-tier dependent |
| Research / analysis | Gemini or Haiku | low | Bulk work, quota-saving |
| Maintenance | LM Studio or Haiku | low | Formatting, summaries, simple edits |

### Human-in-the-loop checkpoints

**Greenfield work** (new plan, new feature, new project direction):
```
Plan → Auto red-team → Package (plan + critique + open questions) → Send to James → James approves → Implementation
```
Implementation does not start until James explicitly approves. The package sent to James includes: the plan, the red-team's blind spots and risk flags, and a numbered list of open questions.

**Continuing work** (clear next action on an existing spec):
```
Implement → Report outcome → James can redirect if needed
```
No checkpoint required for `autonomous-ok` projects with a well-defined next action.

**review-required projects** (TradingBot25, qip-qien):
All changes go as PRs; James approves merges. No auto-merge ever.

### Quota tiers → implementation model routing

Planning always uses capable model (see above). Implementation routing adapts to quota:

| Burnable quota | Implementation model | Projects allowed |
|---|---|---|
| High (>50% burnable) | Claude Sonnet | P1, P2, P3 |
| Medium (20–50%) | Claude Sonnet | P1, P2 |
| Low (5–20%) | Gemini or Haiku | P1 only |
| Minimal (<5%) | LM Studio | maintenance only |
| Zero | No autonomous work | — |


### Task types + model assignment

Task type determines model and effort *regardless of quota tier* — planning always uses the best available model because the cost of a bad plan exceeds the cost of the tokens.

| Task type | Model | Effort | Notes |
|---|---|---|---|
| Plan (greenfield) | Claude Sonnet | high | Always capable model — non-negotiable |
| Red-team review | Claude Sonnet | medium | Auto-runs after every plan, before human sees it |
| Plan (continuation) | Claude Sonnet | medium | Existing direction, lower risk |
| Implementation | Claude Sonnet or Haiku | medium | Quota-tier dependent |
| Research / analysis | Gemini or Haiku | low | Bulk work, quota-saving |
| Maintenance | LM Studio or Haiku | low | Formatting, summaries, simple edits |

### Human-in-the-loop checkpoints

**Greenfield work** (new plan, new feature, new project direction):
```
Plan → Auto red-team → Package (plan + critique + open questions) → Send to James → James approves → Implementation
```
Implementation does not start until James explicitly approves. The package sent to James includes: the plan, the red-team's blind spots and risk flags, and a numbered list of open questions.

**Continuing work** (clear next action on an existing spec):
```
Implement → Report outcome → James can redirect if needed
```
No checkpoint required for `autonomous-ok` projects with a well-defined next action.

**review-required projects** (TradingBot25, qip-qien):
All changes go as PRs; James approves merges. No auto-merge ever.

### Quota tiers → implementation model routing

Planning always uses capable model (see above). Implementation routing adapts to quota:

| Burnable quota | Implementation model | Projects allowed |
|---|---|---|
| High (>50% burnable) | Claude Sonnet | P1, P2, P3 |
| Medium (20–50%) | Claude Sonnet | P1, P2 |
| Low (5–20%) | Gemini or Haiku | P1 only |
| Minimal (<5%) | LM Studio | maintenance only |
| Zero | No autonomous work | — |

# Portfolio Autopilot

- Repo: n/a — lives in Nano's scheduled tasks + config
- Goal: Quota-aware autonomous work loop that advances portfolio projects using excess LLM capacity, while preserving a hard reserve for James's interactive use. Self-adjusting over time.
- Stage: spec
- Guard: autonomous-ok
- Metric: portfolio stages advanced per week at ≤ 15% of James's interactive quota consumed
- Blocked on James: confirm priority tiers (P1/P2/P3) for each project
- Next action: build routing logic + config file; wire to CodexBar endpoint at host.docker.internal:7842

## Design

### Three levers (all configurable)

| Lever | Default | Purpose |
|---|---|---|
| Base reserve (5h window) | 25% | Interactive buffer — scales down as reset approaches |
| Base reserve (7d window) | 15% | Interactive buffer — scales down as reset approaches |

**No per-session cap.** If there's burnable quota and work to do, use it.

### Adaptive reserve formula

```
adaptive_reserve_pct = base_reserve_pct × (resets_in_sec / window_duration_sec)
burnable_pct         = remaining_pct − adaptive_reserve_pct
```

This means:
- Start of window: full reserve held
- Mid-window: reserve shrinks proportionally
- Near reset: reserve → 0 (almost all remaining quota is burnable)

Applied to both windows independently; take the more conservative (lower burnable) as the constraint:
```
burnable = min(burnable_5h, burnable_7d)
```

Work runs only when `burnable > 0`.

### Quota tiers → model routing

| Remaining (above reserve) | Model | Projects allowed |
|---|---|---|
| Flush (>60%) | Claude Sonnet | P1, P2, P3 |
| Comfortable (40–60%) | Claude Sonnet | P1, P2 |
| Moderate (20–40%) | Claude Haiku or Gemini | P1 only |
| Near reserve (<20%) | Gemini / LM Studio | maintenance tasks only |
| At/below reserve | No autonomous work | — |

### Burn-rate gate (for P2/P3)

`sustainable_pct_per_hour = 100 / window_hours`

P2/P3 work only runs when actual burn rate < sustainable pace — i.e., quota is ahead of the smooth curve. P1 work bypasses this gate (runs whenever above the reserve floor).

### Priority tiers (proposed — James to confirm)

| Project | Priority | Rationale |
|---|---|---|
| StageHand | P1 | James's priority — real estate video suite, active discovery |
| nanoclaw | P1 | Maintenance + ongoing infra work |
| AgenticLever | P2 | Business-facing, active build |
| RoadSense | P2 | Going public, active momentum |
| qip-qien | P2 | Live trial — needs progress but review-required gate limits autonomous scope |
| congressional-trading | P3 | Runs autonomously; calibration is in-flight, low need for new LLM spend |
| TradingBot25 | P3 | Deep investment already; review-required, low autonomous-work surface |

### Closed-loop auto-calibration

Track per run:
- Quota spent (from CodexBar delta before/after)
- Output produced (git commits, wiki pages, board moves)
- Did James use Claude within 2h after an autonomous run? (proxy for "left enough headroom")

Weekly review: if James-usage-after-run rate is high → reserve floor was too low → bump it up. If output/quota ratio is consistently low → narrow per-session cap. If P1 projects are stalling → loosen moderate-tier threshold.

The loop surfaces a weekly "autopilot health" note (not a full brief — one line per signal that changed).

### Config file

`/workspace/agent/autopilot-config.json` — all thresholds live here so any run can tweak them without code changes.

```json
{
  "reserve_5h_pct": 25,
  "reserve_7d_pct": 15,

  "project_priorities": {
    "StageHand": "P1",
    "nanoclaw": "P1",
    "AgenticLever": "P2",
    "RoadSense": "P2",
    "qip-qien": "P2",
    "congressional-trading": "P3",
    "TradingBot25": "P3"
  },
  "flush_threshold": 60,
  "comfortable_threshold": 40,
  "moderate_threshold": 20
}
```

## Implementation plan

1. **Config + quota reader** — `autopilot-config.json` + script that reads CodexBar endpoint and computes current tier and burn rate
2. **Routing logic** — given tier + priority + project next-action, decide: proceed / skip / use alternate model
3. **Task executor** — runs one unit of work per session, commits result, updates board
4. **Scheduled trigger** — every 30 min, runs quota check; fires task if conditions met
5. **Outcome tracker** — logs quota delta + output produced per run
6. **Weekly calibration** — reviews outcome log, suggests threshold changes

## Notes

- 2026-07-07 — James approved design. Start with intelligent defaults, evolve to closed-loop. Config file approach so thresholds are tweakable without code changes.
- 2026-07-07 — James set priorities (inverted from initial proposal): P1 = StageHand, nanoclaw; P2 = AgenticLever, RoadSense, qip-qien; P3 = congressional-trading, TradingBot25.
- 2026-07-07 — Reserve revised to adaptive formula (scales linearly to 0 as window resets). Per-session cap removed — no artificial throttle on session size.
- CodexBar endpoint: `http://host.docker.internal:7842/usage` — confirmed live and accessible from container.
- LM Studio confirmed live at `http://host.docker.internal:1234` with Gemma 4 12B + 26B models loaded.

-| Remaining (above reserve) | Model | Projects allowed |

+Task type determines model and effort *regardless of quota tier* — planning always uses the best available model because the cost of a bad plan exceeds the cost of the tokens.

+| Plan (greenfield) | Claude Sonnet | high | Always capable model — non-negotiable |

+| Red-team review | Claude Sonnet | medium | Auto-runs after every plan, before human sees it |

+| Plan (continuation) | Claude Sonnet | medium | Existing direction, lower risk |

+| Implementation | Claude Sonnet or Haiku | medium | Quota-tier dependent |

+| Research / analysis | Gemini or Haiku | low | Bulk work, quota-saving |

+| Maintenance | LM Studio or Haiku | low | Formatting, summaries, simple edits |

+**Greenfield work** (new plan, new feature, new project direction):

+Plan → Auto red-team → Package (plan + critique + open questions) → Send to James → James approves → Implementation

+Implementation does not start until James explicitly approves. The package sent to James includes: the plan, the red-team's blind spots and risk flags, and a numbered list of open questions.

+**Continuing work** (clear next action on an existing spec):

+Implement → Report outcome → James can redirect if needed

+No checkpoint required for `autonomous-ok` projects with a well-defined next action.

+**review-required projects** (TradingBot25, qip-qien):

+All changes go as PRs; James approves merges. No auto-merge ever.

+### Quota tiers → implementation model routing

+Planning always uses capable model (see above). Implementation routing adapts to quota:

+| Burnable quota | Implementation model | Projects allowed |

-| Flush (>60%) | Claude Sonnet | P1, P2, P3 |

-| Comfortable (40–60%) | Claude Sonnet | P1, P2 |

-| Moderate (20–40%) | Claude Haiku or Gemini | P1 only |

-| Near reserve (<20%) | Gemini / LM Studio | maintenance tasks only |

-| At/below reserve | No autonomous work | — |

+| High (>50% burnable) | Claude Sonnet | P1, P2, P3 |

+| Medium (20–50%) | Claude Sonnet | P1, P2 |

+| Low (5–20%) | Gemini or Haiku | P1 only |

+| Minimal (<5%) | LM Studio | maintenance only |


Conversation compacted


This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.

Summary:
1. Primary Request and Intent:
James Lopez (telegram-mg-17796) has been managing his personal project portfolio through Nano (the AI agent). Key requests in this session:
- Confirm/update portfolio cards for RoadSense (going public), MentorCore (parked), congressional-trading (calibration), StageHand (new real estate video project)
- Spin up a coding agent (cw-coder) to fix congressional-trading win-rate calculation and build a backtest sweep script
- Ingest new Anthropic GitHub repos into the wiki
- Set up a /workspace/extra/projects/ mount so Nano can access all local repos directly (ongoing, not yet working)
- Design and build a quota-aware autonomous work scheduling system (portfolio-autopilot) that:
  - Uses an adaptive reserve floor that scales to zero as window resets
  - Has no per-session cap
  - Prioritizes P1=StageHand/nanoclaw, P2=AgenticLever/RoadSense/qip-qien, P3=congressional-trading/TradingBot25
  - Uses capable models for planning always; lighter models for implementation based on quota
  - Requires human checkpoint (plan + auto red-team) before greenfield implementation starts
  - Is self-adjusting/closed-loop over time

2. Key Technical Concepts:
- NanoClaw agent platform running on Mac Studio; containers with volume mounts
- Portfolio board: /workspace/extra/second-brain/portfolio/BOARD.md + projects/*.md cards
- CodexBar companion server: http://host.docker.internal:7842/usage (live, confirmed)
  - Returns: providers with windows [{label, pct, resets_in_sec, duration_sec}]
- LM Studio: http://host.docker.internal:1234 (Gemma 4 12B + 26B loaded)
- congressional-trading scoring: 0-13 scale, 8 components, notify threshold=9.0
- Win-rate bug fix: price lookups now anchored to notification date (not today)
- Adaptive reserve formula: `adaptive_reserve = base_pct × (resets_in_sec / window_duration_sec)`
- Burnable quota: `min(remaining_5h - adaptive_reserve_5h, remaining_7d - adaptive_reserve_7d)`
- Task type taxonomy: plan (always capable+high effort) → auto red-team → human checkpoint → implement
- Container config: agent group ID ag-1779666729747-9n7kga, DB at ~/projects/nanoclaw/data/v2.db
- ncl commands: `ncl groups config get --id <id>`, `ncl groups restart --id <id> --rebuild`
- Mount allowlist: ~/.config/nanoclaw/mount-allowlist.json

3. Files and Code Sections:

- `/workspace/extra/second-brain/portfolio/BOARD.md`
  - Master board tracking all projects. Updated throughout session.
  - Current rows: portfolio-autopilot (spec), qip-qien (feedback), congressional-trading (test), FindRefurbishedAppleProduct (discovery), TradingBot25 (build), AgenticLever (build), pool-monitor (maintain), StageHand (discovery), MentorCore (research), RoadSense (spec), nanoclaw (build)

- `/workspace/extra/second-brain/portfolio/projects/RoadSense.md`
  - Goal: personal motorcycle road discovery engine, pilot=Austin area, going public
  - Stage: spec; scoping doc written; display bug + multi-region pipeline are next actions

- `/workspace/extra/second-brain/portfolio/roadsense-public-launch-scope.md`
  - Full public launch scope: ~6-9 weeks, multi-region data pipeline (2-4 weeks), auth (1-2 weeks), hosting (1 week), GTM via r/advriding + ADV Rider

- `/workspace/extra/second-brain/portfolio/projects/congressional-trading.md`
  - 3-phase calibration task documented: Phase 1 fix (done, commit 14b4cc9), Phase 2 sweep script (done, commit b434e5b), Phase 3 monitoring
  - James needs to run `python scripts/backtest_sweep.py` locally

- `/workspace/extra/second-brain/portfolio/projects/StageHand.md`
  - New card: real estate video suite; 3 video types; HyperFrames experiment notes; competitive analysis pending

- `/workspace/extra/second-brain/portfolio/projects/MentorCore.md`
  - Parked, stage=research, no next action

- `/workspace/extra/second-brain/portfolio/projects/nanoclaw.md`
  - Reddit OAuth dropped (Reddit locked down app creation; low signal content)
  - Next action: portfolio-autopilot routing logic (P1 infra)

- `/workspace/extra/second-brain/portfolio/projects/portfolio-autopilot.md`
  - NEW - full design spec for quota-aware autonomous work scheduler
  - Key sections: adaptive reserve formula, task type taxonomy, human-in-loop checkpoints, priority tiers, config file structure, implementation plan
  - Most recent addition: task types table showing Plan=always capable+high effort, Red-team=auto after plan, Implementation=quota-tier dependent

  Current state of key section (task types):
  ```
  | Task type | Model | Effort | Notes |
  |---|---|---|---|
  | Plan (greenfield) | Claude Sonnet | high | Always capable model — non-negotiable |
  | Red-team review | Claude Sonnet | medium | Auto-runs after every plan, before human sees it |
  | Plan (continuation) | Claude Sonnet | medium | Existing direction, lower risk |
  | Implementation | Claude Sonnet or Haiku | medium | Quota-tier dependent |
  | Research / analysis | Gemini or Haiku | low | Bulk work, quota-saving |
  | Maintenance | LM Studio or Haiku | low | Formatting, summaries, simple edits |
  ```

  Greenfield workflow:
  ```
  Plan → Auto red-team → Package (plan + critique + open questions) → Send to James → James approves → Implementation
  ```

  Adaptive reserve formula:
  ```
  adaptive_reserve_pct = base_reserve_pct × (resets_in_sec / window_duration_sec)
  burnable_pct = remaining_pct − adaptive_reserve_pct
  burnable = min(burnable_5h, burnable_7d)
  ```

  Config file (planned at /workspace/agent/autopilot-config.json):
  ```json
  {
    "reserve_5h_pct": 25,
    "reserve_7d_pct": 15,
    "project_priorities": {
      "StageHand": "P1",
      "nanoclaw": "P1",
      "AgenticLever": "P2",
      "RoadSense": "P2",
      "qip-qien": "P2",
      "congressional-trading": "P3",
      "TradingBot25": "P3"
    },
    "flush_threshold": 60,
    "comfortable_threshold": 40,
    "moderate_threshold": 20
  }
  ```

- `/workspace/extra/second-brain/wiki/tools/Launch Your Agent.md` — NEW
- `/workspace/extra/second-brain/wiki/tools/Claude Tag Plugins.md` — NEW
- `/workspace/extra/second-brain/wiki/Home.md` — Updated with both new pages
- `/workspace/extra/second-brain/wiki/Wiki Ingest Log.md` — Updated with sweep #23

4. Errors and Fixes:
- **ncl groups update for mounts**: Error "Nothing to update" — no CLI verb for mounts; fixed by direct SQLite update
- **Wrong DB file** (nanoclaw.db): "no such table: container_configs" — fixed by finding correct DB (v2.db) via `find` + `.tables` check
- **SQL quoting error** with `datetime(''now'')`: Fixed by using heredoc (`<< 'EOF'`) instead of inline double-quoted string
- **ncl groups restart without --id**: Error "--id is required"; fixed by adding `--id ag-1779666729747-9n7kga`
- **Projects mount still not appearing after restart**: Allowlist file likely not created before restart; provided allowlist creation command + rebuild command. After rebuild, mount still not in /workspace/extra/ — unresolved.
- **gh CLI not available**: Used WebFetch instead of GitHub CLI for README fetching

5. Problem Solving:
- **Congressional trading calibration**: Discovered win-rate bug (price anchoring), cw-coder fixed it and built sweep script; James needs to run locally
- **CodexBar endpoint discovery**: Used projects index server (port 8080, directory listing) to read SETUP.md, found port 7842, confirmed server live
- **Portfolio-autopilot design**: Iteratively refined with James — key insights: adaptive reserve (his idea), no per-session cap (his), task-type-based model selection (his), auto red-team before human review (his latest addition)
- **Projects mount**: SQL in correct DB, allowlist created, rebuild done, but mount not appearing — may need investigation into whether allowlist path matches or container networking issue

6. All User Messages:
- "Roadsense, it's currently a personal passion project that has been tuned to be functional with the area near me as a pilot..."
- "MentorCore is just a fun side project I haven't had the time to validate."
- "For going public on RoadSense, I think we should. The work for that will be needs for that needs to be scoped."
- "Yes please" [to draft scoping doc]
- "For congressional trading, do we have decisions that need to be made? Examples of signals that would have made it through the filter in the past, or should have? Can we gut check or backtest those?"
- "I've got no feel for that, can we collect the data then back into what values to use? Or should we choose reasonable defaults then check and see if more tightening or loosening is needed? What's the 'agentic' approach?"
- "Yes" [to spinning up coding agent]
- "Can you spin up a coding agent to do this please. You manage which model and effort level and continuing the work across usage windows."
- "Oh you're coding in the cloud, can you code locally going forward?"
- "You're already running on the same Mac from the projects/nanoclaw/ folder these other projects are all in other folders in the projects/ folder. I can move folders around or provide permissions as needed"
- "Go ahead and" [approve mount config change]
- [SQL parse error message from terminal]
- [ncl restart error output]
- [ncl restart success with restarted:1]
- "Sorry I don't recall" [how second-brain mount was originally added]
- [Various terminal outputs during mount debugging]
- "I was thinking that we have 3 current frontier model LLM subscriptions... We also have in the CodexBar-ESP32 project a way to see how much of the 5h and 7d quota is left..."
- "There's probably multiple ways to find this address..."
- "Can you check maybe the ESR ESP 32 the code part is P 32 think maybe it's not actually running the local service..."
- "Yes, I am interested in building this. Let's like plan out the design of it a little bit first..."
- "Yeah, please add this to the board for a near-term thing let's spec us out and mock up the routing logic..."
- "Actually, let's invert those priority stuff. I think let's put stage hand in 9 o'clock maintenance as the highest priority and congressional trading and trading bought. Let's do those as the lowest priority and magenta lever..."
- "Yeah, for the 9 o'clock maintenance I just mean the general sort of task that we're working on right now of implementing orchestration and routing and usage..."
- "another thing I was thinking about was some activities are more costly than others like planning. We probably want to use smarter more capable models at a higher level of effort..."

7. Pending Tasks:
- **Build portfolio-autopilot**: routing logic script, config file, CodexBar integration, scheduled trigger, outcome tracker, weekly calibration loop
- **Projects mount**: Investigate why mount isn't appearing despite correct DB config, allowlist, and rebuild
- **RoadSense**: Fix display bug; start multi-region pipeline work
- **StageHand**: Competitive analysis (waiting on James's Instagram competitor links)
- **qip-qien**: Confirm goal and trial status (unconfirmed card)
- **TradingBot25**: Confirm active/paused (unconfirmed card)
- **FindRefurbishedAppleProduct**: Confirm alert vs public site (unconfirmed card)
- **congressional-trading**: James runs `python scripts/backtest_sweep.py` locally; Nano sets up Phase 3 monitoring after results received
- **CodexBar launchd**: Install plist for auto-start (optional, server currently running)

8. Current Work:
Immediately before this summary, was updating portfolio-autopilot.md to incorporate James's latest design refinement about task types and human-in-the-loop checkpoints:

Added to the spec:
- Task type taxonomy table (Plan greenfield = always capable+high effort; Red-team = auto after plan; Implementation = quota-tier dependent; Maintenance = LM Studio)
- Greenfield workflow: `Plan → Auto red-team → Package → James approves → Implementation`
- Human-in-the-loop checkpoint rules by project guard type
- Revised implementation model routing table based on burnable quota level

The last edit added the task types + human checkpoint section to portfolio-autopilot.md and the next step is building the actual routing implementation.

9. Optional Next Step:
Continue building the portfolio-autopilot routing logic. James's most recent message: "another thing I was thinking about was some activities are more costly than others like planning. We probably want to use smarter more capable models at a higher level of effort because that's gonna save us time in bad implementation or poor design decisions... frontloading the planning done by the more intelligent model to then have that ready so that me the human loop can go review the plan, think about that... we need to also do a red team review on that we need to implement that as part of the sort of the end of the plan creation before the human gets involved."

The spec now reflects all of James's requirements. Next: build the actual routing script — starting with the config file at `/workspace/agent/autopilot-config.json` and then the quota reader + routing logic that polls `http://host.docker.internal:7842/usage`.

---
You MUST wrap all responses in <message to="name">...</message> blocks.
Available destinations: `cli-local`, `cw-coder`, `ingest-into-wiki`, `telegram-mg-17796`, `telegram-mg-17800`.

If you need specific details from before compaction (like exact code snippets, error messages, or content you generated), read the full transcript at: /home/node/.claude/projects/-workspace-agent/8263edf6-0dc2-4e24-b715-88fe9965e122.jsonl
Continue the conversation from where it left off without asking the user any further questions. Resume directly — do not acknowledge the summary, do not recap what was happening, do not preface with "I'll continue" or similar. Pick up the last task as if the break never happened.


# Portfolio Autopilot

- Repo: n/a — lives in Nano's scheduled tasks + config
- Goal: Quota-aware autonomous work loop that advances portfolio projects using excess LLM capacity, while preserving a hard reserve for James's interactive use. Self-adjusting over time.
- Stage: spec
- Guard: autonomous-ok
- Metric: portfolio stages advanced per week at ≤ 15% of James's interactive quota consumed
- Blocked on James: confirm priority tiers (P1/P2/P3) for each project
- Next action: build routing logic + config file; wire to CodexBar endpoint at host.docker.internal:7842

## Design

### Three levers (all configurable)

| Lever | Default | Purpose |
|---|---|---|
| Base reserve (5h window) | 25% | Interactive buffer — scales down as reset approaches |
| Base reserve (7d window) | 15% | Interactive buffer — scales down as reset approaches |

**No per-session cap.** If there's burnable quota and work to do, use it.

### Adaptive reserve formula

```
adaptive_reserve_pct = base_reserve_pct × (resets_in_sec / window_duration_sec)
burnable_pct         = remaining_pct − adaptive_reserve_pct
```

This means:
- Start of window: full reserve held
- Mid-window: reserve shrinks proportionally
- Near reset: reserve → 0 (almost all remaining quota is burnable)

Applied to both windows independently; take the more conservative (lower burnable) as the constraint:
```
burnable = min(burnable_5h, burnable_7d)
```

Work runs only when `burnable > 0`.

### Task types + model assignment

Task type determines model and effort *regardless of quota tier* — planning always uses the best available model because the cost of a bad plan exceeds the cost of the tokens.

| Task type | Model | Effort | Notes |
|---|---|---|---|
| Plan (greenfield) | Claude Sonnet | high | Always capable model — non-negotiable |
| Red-team review | Claude Sonnet | medium | Auto-runs after every plan, before human sees it |
| Plan (continuation) | Claude Sonnet | medium | Existing direction, lower risk |
| Implementation | Claude Sonnet or Haiku | medium | Quota-tier dependent |
| Research / analysis | Gemini or Haiku | low | Bulk work, quota-saving |
| Maintenance | LM Studio or Haiku | low | Formatting, summaries, simple edits |

### Human-in-the-loop checkpoints

**Greenfield work** (new plan, new feature, new project direction):
```
Plan → Auto red-team → Package (plan + critique + open questions) → Send to James → James approves → Implementation
```
Implementation does not start until James explicitly approves. The package sent to James includes: the plan, the red-team's blind spots and risk flags, and a numbered list of open questions.

**Continuing work** (clear next action on an existing spec):
```
Implement → Report outcome → James can redirect if needed
```
No checkpoint required for `autonomous-ok` projects with a well-defined next action.

**review-required projects** (TradingBot25, qip-qien):
All changes go as PRs; James approves merges. No auto-merge ever.

### Quota tiers → implementation model routing

Planning always uses capable model (see above). Implementation routing adapts to quota:

| Burnable quota | Implementation model | Projects allowed |
|---|---|---|
| High (>50% burnable) | Claude Sonnet | P1, P2, P3 |
| Medium (20–50%) | Claude Sonnet | P1, P2 |
| Low (5–20%) | Gemini or Haiku | P1 only |
| Minimal (<5%) | LM Studio | maintenance only |
| Zero | No autonomous work | — |

### Burn-rate gate (for P2/P3)

`sustainable_pct_per_hour = 100 / window_hours`

P2/P3 work only runs when actual burn rate < sustainable pace — i.e., quota is ahead of the smooth curve. P1 work bypasses this gate (runs whenever above the reserve floor).

### Priority tiers (proposed — James to confirm)

| Project | Priority | Rationale |
|---|---|---|
| StageHand | P1 | James's priority — real estate video suite, active discovery |
| nanoclaw | P1 | Maintenance + ongoing infra work |
| AgenticLever | P2 | Business-facing, active build |
| RoadSense | P2 | Going public, active momentum |
| qip-qien | P2 | Live trial — needs progress but review-required gate limits autonomous scope |
| congressional-trading | P3 | Runs autonomously; calibration is in-flight, low need for new LLM spend |
| TradingBot25 | P3 | Deep investment already; review-required, low autonomous-work surface |

### Closed-loop auto-calibration

Track per run:
- Quota spent (from CodexBar delta before/after)
- Output produced (git commits, wiki pages, board moves)
- Did James use Claude within 2h after an autonomous run? (proxy for "left enough headroom")

Weekly review: if James-usage-after-run rate is high → reserve floor was too low → bump it up. If output/quota ratio is consistently low → narrow per-session cap. If P1 projects are stalling → loosen moderate-tier threshold.

The loop surfaces a weekly "autopilot health" note (not a full brief — one line per signal that changed).

### Config file

`/workspace/agent/autopilot-config.json` — all thresholds live here so any run can tweak them without code changes.

```json
{
  "reserve_5h_pct": 25,
  "reserve_7d_pct": 15,

  "project_priorities": {
    "StageHand": "P1",
    "nanoclaw": "P1",
    "AgenticLever": "P2",
    "RoadSense": "P2",
    "qip-qien": "P2",
    "congressional-trading": "P3",
    "TradingBot25": "P3"
  },
  "flush_threshold": 60,
  "comfortable_threshold": 40,
  "moderate_threshold": 20
}
```

## Implementation plan

1. **Config + quota reader** — `autopilot-config.json` + script that reads CodexBar endpoint and computes current tier and burn rate
2. **Routing logic** — given tier + priority + project next-action, decide: proceed / skip / use alternate model
3. **Task executor** — runs one unit of work per session, commits result, updates board
4. **Scheduled trigger** — every 30 min, runs quota check; fires task if conditions met
5. **Outcome tracker** — logs quota delta + output produced per run
6. **Weekly calibration** — reviews outcome log, suggests threshold changes

## Notes

- 2026-07-07 — James approved design. Start with intelligent defaults, evolve to closed-loop. Config file approach so thresholds are tweakable without code changes.
- 2026-07-07 — James set priorities (inverted from initial proposal): P1 = StageHand, nanoclaw; P2 = AgenticLever, RoadSense, qip-qien; P3 = congressional-trading, TradingBot25.
- 2026-07-07 — Reserve revised to adaptive formula (scales linearly to 0 as window resets). Per-session cap removed — no artificial throttle on session size.
- CodexBar endpoint: `http://host.docker.internal:7842/usage` — confirmed live and accessible from container.
- LM Studio confirmed live at `http://host.docker.internal:1234` with Gemma 4 12B + 26B models loaded.


# Portfolio Board

One row per active project. Cards with full detail live in `projects/`.
Operating model + decisions: [PLAN.md](PLAN.md).
Stages: `research → discovery → spec → build → test → feedback → maintain`.
Guard: `review-required` (James approves merges) or `autonomous-ok` (agents proceed, report after).

**Seeded 2026-07-06 from git activity — stages and next actions are drafts until James confirms.**

| Project | Stage | Guard | Next action | Blocked on James | Last activity |
|---|---|---|---|---|---|
| [portfolio-autopilot](projects/portfolio-autopilot.md) | spec | autonomous-ok | Build config + quota reader + routing logic | confirm P1/P2/P3 tiers | 2026-07-07 |
| [qip-qien](projects/qip-qien.md) | feedback | review-required | Confirm goal + trial status in brief | confirm card | 2026-07-06 |
| [congressional-trading](projects/congressional-trading.md) | test | autonomous-ok | Define precision target; run backtest | exit criteria for shadow mode | 2026-07-06 |
| [FindRefurbishedAppleProduct](projects/FindRefurbishedAppleProduct.md) | discovery | autonomous-ok | Confirm goal — brand new repo | confirm card | 2026-07-06 |
| [TradingBot25](projects/TradingBot25.md) | build | review-required | Confirm goal + current strategy state | confirm card | 2026-06-26 |
| [AgenticLever](projects/AgenticLever.md) | build | autonomous-ok | Confirm goal | confirm card | 2026-06-29 |
| [pool-monitor](projects/pool-monitor.md) | maintain | autonomous-ok | None — steady state? confirm | confirm card | 2026-06-17 |
| [StageHand](projects/StageHand.md) | discovery | autonomous-ok | Competitive analysis (James sharing links) | share Instagram competitor links | 2026-07-06 |
| [MentorCore](projects/MentorCore.md) | research | autonomous-ok | Parked — no bandwidth | none | 2026-06-11 |
| [RoadSense](projects/RoadSense.md) | spec | autonomous-ok | Scope public-launch work + GTM; fix display bug | none | 2026-06-06 |
| [nanoclaw](projects/nanoclaw.md) | build | autonomous-ok | portfolio-autopilot routing logic (P1 infra) | none | 2026-07-07 |

## Decision queue

Questions for James, appended by Nano; answered items move to the card's Notes and are removed here.

- (empty — first daily brief will ask to confirm the seeded cards above)

## Recently shipped

- (empty)


# nanoclaw

- Repo: `~/projects/nanoclaw` · https://github.com/jamesjlopez/nanoclaw
- Goal: the agent platform itself — powers this whole operating model
- Stage: build
- Guard: autonomous-ok
- Metric: _undefined — James to name (candidate: decision questions James actually answers/week — proxy for brief usefulness)_
- Blocked on James: none
- Next action: portfolio-autopilot routing logic (P1 infra work); commit Slack channel files when convenient

## Notes
- 2026-07-06 — Migrated Mini → Studio (see nanoclaw memory: machine-migration-gotchas). Daily brief + portfolio board went live today.
- 2026-07-06 — Reddit ingestion merged 2026-06-13 but non-functional: unauthenticated `.json` API is 403-blocked. **Dropped** 2026-07-07 — Reddit now requires manual approval for new OAuth apps (scraping crackdown). James doesn't find Reddit content high-signal enough to pursue. Dead feature, no further action.


# FindRefurbishedAppleProduct

- Repo: `~/projects/FindRefurbishedAppleProduct`
- Goal: _to confirm — brand new (initial commit today)_
- Stage: discovery (seeded — confirm)
- Guard: autonomous-ok
- Metric: _undefined_
- Blocked on James: confirm this card
- Next action: confirm goal and whether this is a monitor (alert on refurb stock?) or a site

## Notes
- 2026-07-06 — Seeded from git activity: "Initial commit: refurbished Apple ..." (same-day).


# AgenticLever

- Repo: `~/projects/AgenticLever` · https://github.com/jamesjlopez/AgenticLever-website
- Goal: _to confirm — solution-selling site with question-led discovery flow?_
- Stage: build (seeded — confirm)
- Guard: autonomous-ok
- Metric: _undefined — James to name (candidates: interest sign-ups/week, discovery-flow completion rate)_
- Blocked on James: confirm this card
- Next action: confirm goal

## Notes
- 2026-07-06 — Seeded from git activity: "feat: add NTFY_CLICK_URL support" (2026-06-29). Working tree has uncommitted changes.


<system>Your response was not delivered — it was not wrapped in <message to="name">...</message> blocks. All output must be wrapped: use <message to="name"> for content to send, or <internal> for scratchpad. Your destinations: cli-local, cw-coder, ingest-into-wiki, telegram-mg-17796, telegram-mg-17800. Please re-send your response with the correct wrapping.</system>


Let me pick up where I left off. The last task was to build the portfolio-autopilot routing implementation. I need to:

1. Create `/workspace/agent/autopilot-config.json` - the config file
2. Build the quota reader + routing logic script

Let me start by creating these files.


ToolSearch

Question: select:SendMessage


Question: select:SendMessage


Prompt: <system>Your response was not delivered — it was not wrapped in <message to="name">...</message> blocks. All output must be wrapped: use <message to="name"> for content to send, or <internal> for scratchpad. Your destinations: cli-local, cw-coder, ingest-into-wiki, telegram-mg-17796, telegram-mg-17800. Please re-send your response with the correct wrapping.</system>


I need to build the portfolio-autopilot routing implementation. Let me create the config file and the routing logic script. I'll start by writing the config file and then the quota reader + routing script.

Let me create these files:
1. `/workspace/agent/autopilot-config.json` - config file
2. `/workspace/agent/autopilot.mjs` - main routing/quota logic


Write

{
  "reserve_5h_pct": 25,
  "reserve_7d_pct": 15,

  "project_priorities": {
    "StageHand": "P1",
    "nanoclaw": "P1",
    "AgenticLever": "P2",
    "RoadSense": "P2",
    "qip-qien": "P2",
    "congressional-trading": "P3",
    "TradingBot25": "P3"
  },

  "flush_threshold": 60,
  "comfortable_threshold": 40,
  "moderate_threshold": 20,

  "codexbar_url": "http://host.docker.internal:7842/usage",
  "lm_studio_url": "http://host.docker.internal:1234"
}


File created successfully at: /workspace/agent/autopilot-config.json (file state is current in your context — no need to Read it back)


/**
 * Portfolio Autopilot — quota reader + routing logic
 *
 * Reads CodexBar usage, computes burnable quota, and outputs a routing decision:
 * which projects are eligible to run and at what model tier.
 *
 * Usage:
 *   node autopilot.mjs              → prints routing summary to stdout
 *   node autopilot.mjs --json       → prints routing decision as JSON
 *   node autopilot.mjs --check      → prints only burnable pct (for scripts)
 */

import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";

const __dirname = dirname(fileURLToPath(import.meta.url));
const config = JSON.parse(
  readFileSync(join(__dirname, "autopilot-config.json"), "utf8")
);

const JSON_MODE = process.argv.includes("--json");
const CHECK_MODE = process.argv.includes("--check");

// ─── Quota Reader ────────────────────────────────────────────────────────────

async function fetchUsage() {
  const res = await fetch(config.codexbar_url);
  if (!res.ok) throw new Error(`CodexBar returned ${res.status}`);
  return res.json();
}

/**
 * Find the 5h and 7d windows from CodexBar response.
 * Returns { pct, resets_in_sec, duration_sec } for each.
 */
function extractWindows(data) {
  // CodexBar returns providers array; find Claude provider, then its windows
  const providers = data.providers ?? data;
  const claudeProvider = Array.isArray(providers)
    ? providers.find((p) => /claude/i.test(p.name ?? p.label ?? ""))
    : null;

  const windows = claudeProvider?.windows ?? data.windows ?? [];

  const w5h = windows.find(
    (w) => w.duration_sec <= 5 * 3600 + 60 && w.duration_sec >= 5 * 3600 - 60
  );
  const w7d = windows.find(
    (w) =>
      w.duration_sec <= 7 * 86400 + 300 && w.duration_sec >= 7 * 86400 - 300
  );

  if (!w5h || !w7d) {
    // Fallback: pick shortest and longest available windows
    const sorted = [...windows].sort((a, b) => a.duration_sec - b.duration_sec);
    return {
      w5h: sorted[0] ?? null,
      w7d: sorted[sorted.length - 1] ?? null,
      raw: windows,
    };
  }

  return { w5h, w7d, raw: windows };
}

// ─── Adaptive Reserve Formula ────────────────────────────────────────────────

/**
 * adaptive_reserve_pct = base_reserve_pct × (resets_in_sec / window_duration_sec)
 * burnable_pct = remaining_pct − adaptive_reserve_pct
 */
function computeBurnable(window, basePct) {
  if (!window) return 0;
  const remaining = window.pct ?? 100;
  const timeRatio = window.resets_in_sec / window.duration_sec;
  const adaptiveReserve = basePct * timeRatio;
  return Math.max(0, remaining - adaptiveReserve);
}

// ─── Tier Routing ────────────────────────────────────────────────────────────

/**
 * Given burnable %, return the quota tier label and which project priorities run.
 */
function quotaTier(burnablePct) {
  if (burnablePct <= 0) {
    return { tier: "zero", label: "No autonomous work", allowedPriorities: [] };
  }
  if (burnablePct < 5) {
    return {
      tier: "minimal",
      label: "Maintenance only (LM Studio)",
      allowedPriorities: ["P1"],
      implementationModel: "lm-studio",
    };
  }
  if (burnablePct < 20) {
    return {
      tier: "low",
      label: "P1 only — Gemini/Haiku",
      allowedPriorities: ["P1"],
      implementationModel: "gemini-or-haiku",
    };
  }
  if (burnablePct < 50) {
    return {
      tier: "medium",
      label: "P1+P2 — Claude Sonnet",
      allowedPriorities: ["P1", "P2"],
      implementationModel: "claude-sonnet",
    };
  }
  return {
    tier: "high",
    label: "P1+P2+P3 — Claude Sonnet",
    allowedPriorities: ["P1", "P2", "P3"],
    implementationModel: "claude-sonnet",
  };
}

/**
 * Sustainable burn rate gate for P2/P3.
 * Returns true if current burn rate is ahead of the smooth curve.
 */
function sustainableGatePasses(window) {
  if (!window) return false;
  const windowHours = window.duration_sec / 3600;
  const sustainablePctPerHour = 100 / windowHours;
  const elapsedHours = (window.duration_sec - window.resets_in_sec) / 3600;
  const expectedConsumed = sustainablePctPerHour * elapsedHours;
  const actualConsumed = 100 - (window.pct ?? 100);
  // Passes if actual consumption is BELOW expected (ahead of smooth curve)
  return actualConsumed < expectedConsumed;
}

// ─── Project Routing ─────────────────────────────────────────────────────────

function routeProjects(tier, burnablePct, w7d) {
  const sustainableGate = w7d ? sustainableGatePasses(w7d) : true;
  const projects = [];

  for (const [project, priority] of Object.entries(config.project_priorities)) {
    const allowed = tier.allowedPriorities.includes(priority);
    if (!allowed) {
      projects.push({ project, priority, decision: "skip", reason: "quota tier" });
      continue;
    }

    // P2/P3 require sustainable gate (P1 bypasses)
    if (priority !== "P1" && !sustainableGate) {
      projects.push({
        project,
        priority,
        decision: "skip",
        reason: "burn rate above sustainable pace",
      });
      continue;
    }

    projects.push({
      project,
      priority,
      decision: "proceed",
      implementationModel: tier.implementationModel,
    });
  }

  return projects;
}

// ─── Main ────────────────────────────────────────────────────────────────────

async function main() {
  let data;
  try {
    data = await fetchUsage();
  } catch (err) {
    const out = { error: `Failed to reach CodexBar: ${err.message}`, burnable: 0 };
    if (JSON_MODE) console.log(JSON.stringify(out));
    else console.error(out.error);
    process.exit(1);
  }

  const { w5h, w7d, raw } = extractWindows(data);

  const burnable5h = computeBurnable(w5h, config.reserve_5h_pct);
  const burnable7d = computeBurnable(w7d, config.reserve_7d_pct);
  const burnable = Math.min(burnable5h, burnable7d);

  if (CHECK_MODE) {
    console.log(burnable.toFixed(1));
    return;
  }

  const tier = quotaTier(burnable);
  const projects = routeProjects(tier, burnable, w7d);

  const result = {
    timestamp: new Date().toISOString(),
    windows: {
      "5h": w5h
        ? {
            remaining_pct: w5h.pct,
            resets_in_sec: w5h.resets_in_sec,
            burnable_pct: +burnable5h.toFixed(1),
          }
        : null,
      "7d": w7d
        ? {
            remaining_pct: w7d.pct,
            resets_in_sec: w7d.resets_in_sec,
            burnable_pct: +burnable7d.toFixed(1),
          }
        : null,
    },
    burnable_pct: +burnable.toFixed(1),
    tier: tier.tier,
    tier_label: tier.label,
    projects,
  };

  if (JSON_MODE) {
    console.log(JSON.stringify(result, null, 2));
    return;
  }

  // Human-readable summary
  console.log(`\n=== Portfolio Autopilot — ${result.timestamp} ===\n`);

  if (w5h) {
    const hrs5 = Math.round(w5h.resets_in_sec / 60);
    console.log(
      `5h window:  ${w5h.pct?.toFixed(1) ?? "?"}% remaining  (resets in ${hrs5}m)  →  burnable: ${burnable5h.toFixed(1)}%`
    );
  }
  if (w7d) {
    const days7 = Math.round(w7d.resets_in_sec / 3600);
    console.log(
      `7d window:  ${w7d.pct?.toFixed(1) ?? "?"}% remaining  (resets in ${days7}h)  →  burnable: ${burnable7d.toFixed(1)}%`
    );
  }

  console.log(`\nEffective burnable: ${burnable.toFixed(1)}%`);
  console.log(`Quota tier: ${tier.tier.toUpperCase()} — ${tier.label}\n`);

  const proceeding = projects.filter((p) => p.decision === "proceed");
  const skipped = projects.filter((p) => p.decision === "skip");

  if (proceeding.length === 0) {
    console.log("No projects eligible to run.");
  } else {
    console.log("Eligible projects:");
    for (const p of proceeding) {
      console.log(`  ✓ ${p.project} (${p.priority}) — model: ${p.implementationModel}`);
    }
  }

  if (skipped.length > 0) {
    console.log("\nSkipped:");
    for (const p of skipped) {
      console.log(`  ✗ ${p.project} (${p.priority}) — ${p.reason}`);
    }
  }

  console.log("");
}

main();


File created successfully at: /workspace/agent/autopilot.mjs (file state is current in your context — no need to Read it back)

/**
 * Portfolio Autopilot — quota reader + routing logic
 *
 * Reads CodexBar usage, computes burnable quota, and outputs a routing decision:
 * which projects are eligible to run and at what model tier.
 *
 * Usage:
 *   node autopilot.mjs              → prints routing summary to stdout
 *   node autopilot.mjs --json       → prints routing decision as JSON
 *   node autopilot.mjs --check      → prints only burnable pct (for scripts)
 */

import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";

const __dirname = dirname(fileURLToPath(import.meta.url));
const config = JSON.parse(
  readFileSync(join(__dirname, "autopilot-config.json"), "utf8")
);

const JSON_MODE = process.argv.includes("--json");
const CHECK_MODE = process.argv.includes("--check");

// ─── Quota Reader ────────────────────────────────────────────────────────────

async function fetchUsage() {
  const res = await fetch(config.codexbar_url);
  if (!res.ok) throw new Error(`CodexBar returned ${res.status}`);
  return res.json();
}

/**
 * Find the 5h and 7d windows from CodexBar response.
 * Returns { pct, resets_in_sec, duration_sec } for each.
 */
function extractWindows(data) {
  // CodexBar returns providers array; find Claude provider, then its windows
  const providers = data.providers ?? data;
  const claudeProvider = Array.isArray(providers)
    ? providers.find((p) => /claude/i.test(p.name ?? p.label ?? ""))
    : null;

  const windows = claudeProvider?.windows ?? data.windows ?? [];

  const w5h = windows.find(
    (w) => w.duration_sec <= 5 * 3600 + 60 && w.duration_sec >= 5 * 3600 - 60
  );
  const w7d = windows.find(
    (w) =>
      w.duration_sec <= 7 * 86400 + 300 && w.duration_sec >= 7 * 86400 - 300
  );

  if (!w5h || !w7d) {
    // Fallback: pick shortest and longest available windows
    const sorted = [...windows].sort((a, b) => a.duration_sec - b.duration_sec);
    return {
      w5h: sorted[0] ?? null,
      w7d: sorted[sorted.length - 1] ?? null,
      raw: windows,
    };
  }

  return { w5h, w7d, raw: windows };
}

// ─── Adaptive Reserve Formula ────────────────────────────────────────────────

/**
 * adaptive_reserve_pct = base_reserve_pct × (resets_in_sec / window_duration_sec)
 * burnable_pct = remaining_pct − adaptive_reserve_pct
 */
function computeBurnable(window, basePct) {
  if (!window) return 0;
  const remaining = window.pct ?? 100;
  const timeRatio = window.resets_in_sec / window.duration_sec;
  const adaptiveReserve = basePct * timeRatio;
  return Math.max(0, remaining - adaptiveReserve);
}

// ─── Tier Routing ────────────────────────────────────────────────────────────

/**
 * Given burnable %, return the quota tier label and which project priorities run.
 */
function quotaTier(burnablePct) {
  if (burnablePct <= 0) {
    return { tier: "zero", label: "No autonomous work", allowedPriorities: [] };
  }
  if (burnablePct < 5) {
    return {
      tier: "minimal",
      label: "Maintenance only (LM Studio)",
      allowedPriorities: ["P1"],
      implementationModel: "lm-studio",
    };
  }
  if (burnablePct < 20) {
    return {
      tier: "low",
      label: "P1 only — Gemini/Haiku",
      allowedPriorities: ["P1"],
      implementationModel: "gemini-or-haiku",
    };
  }
  if (burnablePct < 50) {
    return {
      tier: "medium",
      label: "P1+P2 — Claude Sonnet",
      allowedPriorities: ["P1", "P2"],
      implementationModel: "claude-sonnet",
    };
  }
  return {
    tier: "high",
    label: "P1+P2+P3 — Claude Sonnet",
    allowedPriorities: ["P1", "P2", "P3"],
    implementationModel: "claude-sonnet",
  };
}

/**
 * Sustainable burn rate gate for P2/P3.
 * Returns true if current burn rate is ahead of the smooth curve.
 */
function sustainableGatePasses(window) {
  if (!window) return false;
  const windowHours = window.duration_sec / 3600;
  const sustainablePctPerHour = 100 / windowHours;
  const elapsedHours = (window.duration_sec - window.resets_in_sec) / 3600;
  const expectedConsumed = sustainablePctPerHour * elapsedHours;
  const actualConsumed = 100 - (window.pct ?? 100);
  // Passes if actual consumption is BELOW expected (ahead of smooth curve)
  return actualConsumed < expectedConsumed;
}

// ─── Project Routing ─────────────────────────────────────────────────────────

function routeProjects(tier, burnablePct, w7d) {
  const sustainableGate = w7d ? sustainableGatePasses(w7d) : true;
  const projects = [];

  for (const [project, priority] of Object.entries(config.project_priorities)) {
    const allowed = tier.allowedPriorities.includes(priority);
    if (!allowed) {
      projects.push({ project, priority, decision: "skip", reason: "quota tier" });
      continue;
    }

    // P2/P3 require sustainable gate (P1 bypasses)
    if (priority !== "P1" && !sustainableGate) {
      projects.push({
        project,
        priority,
        decision: "skip",
        reason: "burn rate above sustainable pace",
      });
      continue;
    }

    projects.push({
      project,
      priority,
      decision: "proceed",
      implementationModel: tier.implementationModel,
    });
  }

  return projects;
}

// ─── Main ────────────────────────────────────────────────────────────────────

async function main() {
  let data;
  try {
    data = await fetchUsage();
  } catch (err) {
    const out = { error: `Failed to reach CodexBar: ${err.message}`, burnable: 0 };
    if (JSON_MODE) console.log(JSON.stringify(out));
    else console.error(out.error);
    process.exit(1);
  }

  const { w5h, w7d, raw } = extractWindows(data);

  const burnable5h = computeBurnable(w5h, config.reserve_5h_pct);
  const burnable7d = computeBurnable(w7d, config.reserve_7d_pct);
  const burnable = Math.min(burnable5h, burnable7d);

  if (CHECK_MODE) {
    console.log(burnable.toFixed(1));
    return;
  }

  const tier = quotaTier(burnable);
  const projects = routeProjects(tier, burnable, w7d);

  const result = {
    timestamp: new Date().toISOString(),
    windows: {
      "5h": w5h
        ? {
            remaining_pct: w5h.pct,
            resets_in_sec: w5h.resets_in_sec,
            burnable_pct: +burnable5h.toFixed(1),
          }
        : null,
      "7d": w7d
        ? {
            remaining_pct: w7d.pct,
            resets_in_sec: w7d.resets_in_sec,
            burnable_pct: +burnable7d.toFixed(1),
          }
        : null,
    },
    burnable_pct: +burnable.toFixed(1),
    tier: tier.tier,
    tier_label: tier.label,
    projects,
  };

  if (JSON_MODE) {
    console.log(JSON.stringify(result, null, 2));
    return;
  }

  // Human-readable summary
  console.log(`\n=== Portfolio Autopilot — ${result.timestamp} ===\n`);

  if (w5h) {
    const hrs5 = Math.round(w5h.resets_in_sec / 60);
    console.log(
      `5h window:  ${w5h.pct?.toFixed(1) ?? "?"}% remaining  (resets in ${hrs5}m)  →  burnable: ${burnable5h.toFixed(1)}%`
    );
  }
  if (w7d) {
    const days7 = Math.round(w7d.resets_in_sec / 3600);
    console.log(
      `7d window:  ${w7d.pct?.toFixed(1) ?? "?"}% remaining  (resets in ${days7}h)  →  burnable: ${burnable7d.toFixed(1)}%`
    );
  }

  console.log(`\nEffective burnable: ${burnable.toFixed(1)}%`);
  console.log(`Quota tier: ${tier.tier.toUpperCase()} — ${tier.label}\n`);

  const proceeding = projects.filter((p) => p.decision === "proceed");
  const skipped = projects.filter((p) => p.decision === "skip");

  if (proceeding.length === 0) {
    console.log("No projects eligible to run.");
  } else {
    console.log("Eligible projects:");
    for (const p of proceeding) {
      console.log(`  ✓ ${p.project} (${p.priority}) — model: ${p.implementationModel}`);
    }
  }

  if (skipped.length > 0) {
    console.log("\nSkipped:");
    for (const p of skipped) {
      console.log(`  ✗ ${p.project} (${p.priority}) — ${p.reason}`);
    }
  }

  console.log("");
}

main();


Now let me test it against the live CodexBar endpoint:


Error: Exit code 1
(node:1743) [UNDICI-EHPA] Warning: EnvHttpProxyAgent is experimental, expect them to change at any time.
(Use `node --trace-warnings ...` to show where the warning was created)
Failed to reach CodexBar: fetch failed


Bash

curl -s http://host.docker.internal:7842/usage | head -c 500 2>&1


{"ok": true, "updated_at": 1783433792, "providers": [{"name": "Claude", "attention": {"state": "idle", "count": 0, "primary_project": "", "projects": [], "reason": "", "updated_at": 1783433792}, "windows": [{"label": "5h", "pct": 0, "resets_in_sec": -1, "duration_sec": 18000}, {"label": "7d", "pct": 13, "resets_in_sec": 369807, "duration_sec": 604800}]}, {"name": "Codex", "attention": {"state": "idle", "count": 0, "primary_project": "", "projects": [], "reason": "", "updated_at": 1783433792}, "w


The CodexBar endpoint is live. The issue with `node autopilot.mjs` must be the proxy agent intercepting `fetch` requests. The response shows `pct: 0` for the 5h window and `resets_in_sec: -1`, which means the 5h window is fully consumed (0% remaining) and has already reset. The 7d window has 13% remaining with 369807 seconds (~4.3 days) until reset.

Wait, actually `resets_in_sec: -1` is interesting - that might mean the window has already reset/expired, or it means unlimited?
Looking at the window data more closely, the 5h window shows 0% with a negative reset timer and an 18000-second duration, while the 7d window has 13% remaining and resets in about 4.3 days. The negative reset value for the 5h window is puzzling—it could indicate the window has already expired or is ready for immediate reset. The real blocker seems to be the EnvHttpProxyAgent intercepting the fetch requests, so I need to find a way to bypass that.


NO_PROXY=host.docker.internal node /workspace/agent/autopilot.mjs 2>&1


(node:1772) [UNDICI-EHPA] Warning: EnvHttpProxyAgent is experimental, expect them to change at any time.
(Use `node --trace-warnings ...` to show where the warning was created)

=== Portfolio Autopilot — 2026-07-07T14:16:43.010Z ===

5h window:  0.0% remaining  (resets in 0m)  →  burnable: 0.0%
7d window:  13.0% remaining  (resets in 103h)  →  burnable: 3.8%

Effective burnable: 0.0%
Quota tier: MINIMAL — Maintenance only (LM Studio)

Eligible projects:
  ✓ StageHand (P1) — model: lm-studio
  ✓ nanoclaw (P1) — model: lm-studio

Skipped:
  ✗ AgenticLever (P2) — quota tier
  ✗ RoadSense (P2) — quota tier
  ✗ qip-qien (P2) — quota tier
  ✗ congressional-trading (P3) — quota tier
  ✗ TradingBot25 (P3) — quota tier


It works! The 5h window is at 0% (James has used it up), but the 7d window has 13% remaining with 3.8% burnable. The effective burnable is 0% because the 5h burnable is 0, so we're at "minimal" tier - maintenance only with LM Studio. Wait, but `burnable: 0.0%` but tier is `MINIMAL` - let me check the logic.

Oh wait - the logic has:
```
if (burnablePct <= 0) → "zero" → no autonomous work
if (burnablePct < 5) → "minimal" → maintenance only
```

But burnable is 0.0%, which should be `<= 0` → `zero`. But it's showing `MINIMAL`. The issue is that 0.0 is being floored to 0 but the `<= 0` check is failing because 0 is not `< 0`. Wait, the check is `<= 0` which should catch exactly 0.

Actually wait - the burnable5h is 0 (because pct is 0 and adaptive reserve would also be 0 since resets_in_sec is -1). Let me recalculate what's happening with the tier assignment logic.

So the adaptive reserve calculation with a negative resets_in_sec is creating a small positive value instead of zero, which pushes burnable5h just above zero. That's why it's not matching the zero case. I need to handle the case where resets_in_sec is non-positive as a special condition indicating the window has already expired or reset.

I also need to add NO_PROXY handling to prevent proxy issues in the Docker environment, so I'll set that environment variable at the start of the script.

The key fix is updating the burnable computation to treat resets_in_sec <= 0 as a reset state where the timeRatio should be zero, making the reserve collapse to zero and burnable effectively zero as well.


It's working — the routing logic is live. Let me fix two small issues: the proxy bypass needs to be baked in, and `resets_in_sec: -1` (window expired) should clamp to zero burnable:


Edit

/**
 * Portfolio Autopilot — quota reader + routing logic
 *
 * Reads CodexBar usage, computes burnable quota, and outputs a routing decision:
 * which projects are eligible to run and at what model tier.
 *
 * Usage:
 *   node autopilot.mjs              → prints routing summary to stdout
 *   node autopilot.mjs --json       → prints routing decision as JSON
 *   node autopilot.mjs --check      → prints only burnable pct (for scripts)
 */

import { readFileSync } from "fs";

/**
 * Portfolio Autopilot — quota reader + routing logic
 *
 * Reads CodexBar usage, computes burnable quota, and outputs a routing decision:
 * which projects are eligible to run and at what model tier.
 *
 * Usage:
 *   node autopilot.mjs              → prints routing summary to stdout
 *   node autopilot.mjs --json       → prints routing decision as JSON
 *   node autopilot.mjs --check      → prints only burnable pct (for scripts)
 */

// Bypass proxy for local Docker host
process.env.NO_PROXY = [process.env.NO_PROXY, "host.docker.internal"]
  .filter(Boolean)
  .join(",");

import { readFileSync } from "fs";


The file /workspace/agent/autopilot.mjs has been updated successfully. (file state is current in your context — no need to Read it back)

/**
 * Portfolio Autopilot — quota reader + routing logic
 *
 * Reads CodexBar usage, computes burnable quota, and outputs a routing decision:
 * which projects are eligible to run and at what model tier.
 *
 * Usage:
 *   node autopilot.mjs              → prints routing summary to stdout
 *   node autopilot.mjs --json       → prints routing decision as JSON
 *   node autopilot.mjs --check      → prints only burnable pct (for scripts)
 */

// Bypass proxy for local Docker host
process.env.NO_PROXY = [process.env.NO_PROXY, "host.docker.internal"]
  .filter(Boolean)
  .join(",");

import { readFileSync } from "fs";

/**
 * Portfolio Autopilot — quota reader + routing logic
 *
 * Reads CodexBar usage, computes burnable quota, and outputs a routing decision:
 * which projects are eligible to run and at what model tier.
 *
 * Usage:
 *   node autopilot.mjs              → prints routing summary to stdout
 *   node autopilot.mjs --json       → prints routing decision as JSON
 *   node autopilot.mjs --check      → prints only burnable pct (for scripts)
 */

import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";

const __dirname = dirname(fileURLToPath(import.meta.url));
const config = JSON.parse(
  readFileSync(join(__dirname, "autopilot-config.json"), "utf8")
);

const JSON_MODE = process.argv.includes("--json");
const CHECK_MODE = process.argv.includes("--check");

// ─── Quota Reader ────────────────────────────────────────────────────────────

async function fetchUsage() {
  const res = await fetch(config.codexbar_url);
  if (!res.ok) throw new Error(`CodexBar returned ${res.status}`);
  return res.json();
}

/**
 * Find the 5h and 7d windows from CodexBar response.
 * Returns { pct, resets_in_sec, duration_sec } for each.
 */
function extractWindows(data) {
  // CodexBar returns providers array; find Claude provider, then its windows
  const providers = data.providers ?? data;
  const claudeProvider = Array.isArray(providers)
    ? providers.find((p) => /claude/i.test(p.name ?? p.label ?? ""))
    : null;

  const windows = claudeProvider?.windows ?? data.windows ?? [];

  const w5h = windows.find(
    (w) => w.duration_sec <= 5 * 3600 + 60 && w.duration_sec >= 5 * 3600 - 60
  );
  const w7d = windows.find(
    (w) =>
      w.duration_sec <= 7 * 86400 + 300 && w.duration_sec >= 7 * 86400 - 300
  );

  if (!w5h || !w7d) {
    // Fallback: pick shortest and longest available windows
    const sorted = [...windows].sort((a, b) => a.duration_sec - b.duration_sec);
    return {
      w5h: sorted[0] ?? null,
      w7d: sorted[sorted.length - 1] ?? null,
      raw: windows,
    };
  }

  return { w5h, w7d, raw: windows };
}

// ─── Adaptive Reserve Formula ────────────────────────────────────────────────

/**
 * adaptive_reserve_pct = base_reserve_pct × (resets_in_sec / window_duration_sec)
 * burnable_pct = remaining_pct − adaptive_reserve_pct
 */
function computeBurnable(window, basePct) {
  if (!window) return 0;
  const remaining = window.pct ?? 100;
  const timeRatio = window.resets_in_sec / window.duration_sec;
  const adaptiveReserve = basePct * timeRatio;
  return Math.max(0, remaining - adaptiveReserve);
}

// ─── Tier Routing ────────────────────────────────────────────────────────────

/**
 * Given burnable %, return the quota tier label and which project priorities run.
 */
function quotaTier(burnablePct) {
  if (burnablePct <= 0) {
    return { tier: "zero", label: "No autonomous work", allowedPriorities: [] };
  }
  if (burnablePct < 5) {
    return {
      tier: "minimal",
      label: "Maintenance only (LM Studio)",
      allowedPriorities: ["P1"],
      implementationModel: "lm-studio",
    };
  }
  if (burnablePct < 20) {
    return {
      tier: "low",
      label: "P1 only — Gemini/Haiku",
      allowedPriorities: ["P1"],
      implementationModel: "gemini-or-haiku",
    };
  }
  if (burnablePct < 50) {
    return {
      tier: "medium",
      label: "P1+P2 — Claude Sonnet",
      allowedPriorities: ["P1", "P2"],
      implementationModel: "claude-sonnet",
    };
  }
  return {
    tier: "high",
    label: "P1+P2+P3 — Claude Sonnet",
    allowedPriorities: ["P1", "P2", "P3"],
    implementationModel: "claude-sonnet",
  };
}

/**
 * Sustainable burn rate gate for P2/P3.
 * Returns true if current burn rate is ahead of the smooth curve.
 */
function sustainableGatePasses(window) {
  if (!window) return false;
  const windowHours = window.duration_sec / 3600;
  const sustainablePctPerHour = 100 / windowHours;
  const elapsedHours = (window.duration_sec - window.resets_in_sec) / 3600;
  const expectedConsumed = sustainablePctPerHour * elapsedHours;
  const actualConsumed = 100 - (window.pct ?? 100);
  // Passes if actual consumption is BELOW expected (ahead of smooth curve)
  return actualConsumed < expectedConsumed;
}

// ─── Project Routing ─────────────────────────────────────────────────────────

function routeProjects(tier, burnablePct, w7d) {
  const sustainableGate = w7d ? sustainableGatePasses(w7d) : true;
  const projects = [];

  for (const [project, priority] of Object.entries(config.project_priorities)) {
    const allowed = tier.allowedPriorities.includes(priority);
    if (!allowed) {
      projects.push({ project, priority, decision: "skip", reason: "quota tier" });
      continue;
    }

    // P2/P3 require sustainable gate (P1 bypasses)
    if (priority !== "P1" && !sustainableGate) {
      projects.push({
        project,
        priority,
        decision: "skip",
        reason: "burn rate above sustainable pace",
      });
      continue;
    }

    projects.push({
      project,
      priority,
      decision: "proceed",
      implementationModel: tier.implementationModel,
    });
  }

  return projects;
}

// ─── Main ────────────────────────────────────────────────────────────────────

async function main() {
  let data;
  try {
    data = await fetchUsage();
  } catch (err) {
    const out = { error: `Failed to reach CodexBar: ${err.message}`, burnable: 0 };
    if (JSON_MODE) console.log(JSON.stringify(out));
    else console.error(out.error);
    process.exit(1);
  }

  const { w5h, w7d, raw } = extractWindows(data);

  const burnable5h = computeBurnable(w5h, config.reserve_5h_pct);
  const burnable7d = computeBurnable(w7d, config.reserve_7d_pct);
  const burnable = Math.min(burnable5h, burnable7d);

  if (CHECK_MODE) {
    console.log(burnable.toFixed(1));
    return;
  }

  const tier = quotaTier(burnable);
  const projects = routeProjects(tier, burnable, w7d);

  const result = {
    timestamp: new Date().toISOString(),
    windows: {
      "5h": w5h
        ? {
            remaining_pct: w5h.pct,
            resets_in_sec: w5h.resets_in_sec,
            burnable_pct: +burnable5h.toFixed(1),
          }
        : null,
      "7d": w7d
        ? {
            remaining_pct: w7d.pct,
            resets_in_sec: w7d.resets_in_sec,
            burnable_pct: +burnable7d.toFixed(1),
          }
        : null,
    },
    burnable_pct: +burnable.toFixed(1),
    tier: tier.tier,
    tier_label: tier.label,
    projects,
  };

  if (JSON_MODE) {
    console.log(JSON.stringify(result, null, 2));
    return;
  }

  // Human-readable summary
  console.log(`\n=== Portfolio Autopilot — ${result.timestamp} ===\n`);

  if (w5h) {
    const hrs5 = Math.round(w5h.resets_in_sec / 60);
    console.log(
      `5h window:  ${w5h.pct?.toFixed(1) ?? "?"}% remaining  (resets in ${hrs5}m)  →  burnable: ${burnable5h.toFixed(1)}%`
    );
  }
  if (w7d) {
    const days7 = Math.round(w7d.resets_in_sec / 3600);
    console.log(
      `7d window:  ${w7d.pct?.toFixed(1) ?? "?"}% remaining  (resets in ${days7}h)  →  burnable: ${burnable7d.toFixed(1)}%`
    );
  }

  console.log(`\nEffective burnable: ${burnable.toFixed(1)}%`);
  console.log(`Quota tier: ${tier.tier.toUpperCase()} — ${tier.label}\n`);

  const proceeding = projects.filter((p) => p.decision === "proceed");
  const skipped = projects.filter((p) => p.decision === "skip");

  if (proceeding.length === 0) {
    console.log("No projects eligible to run.");
  } else {
    console.log("Eligible projects:");
    for (const p of proceeding) {
      console.log(`  ✓ ${p.project} (${p.priority}) — model: ${p.implementationModel}`);
    }
  }

  if (skipped.length > 0) {
    console.log("\nSkipped:");
    for (const p of skipped) {
      console.log(`  ✗ ${p.project} (${p.priority}) — ${p.reason}`);
    }
  }

  console.log("");
}

main();

+process.env.NO_PROXY = [process.env.NO_PROXY, "host.docker.internal"]


function computeBurnable(window, basePct) {
  if (!window) return 0;
  const remaining = window.pct ?? 100;
  const timeRatio = window.resets_in_sec / window.duration_sec;
  const adaptiveReserve = basePct * timeRatio;
  return Math.max(0, remaining - adaptiveReserve);
}

function computeBurnable(window, basePct) {
  if (!window) return 0;
  const remaining = window.pct ?? 100;
  if (remaining <= 0) return 0;
  // resets_in_sec <= 0 means window expired/resetting — treat as full timeRatio approaching 0
  const resets = Math.max(0, window.resets_in_sec);
  const timeRatio = resets / window.duration_sec;
  const adaptiveReserve = basePct * timeRatio;
  return Math.max(0, remaining - adaptiveReserve);
}


function computeBurnable(window, basePct) {
  if (!window) return 0;
  const remaining = window.pct ?? 100;
  if (remaining <= 0) return 0;
  // resets_in_sec <= 0 means window expired/resetting — treat as full timeRatio approaching 0
  const resets = Math.max(0, window.resets_in_sec);
  const timeRatio = resets / window.duration_sec;
  const adaptiveReserve = basePct * timeRatio;
  return Math.max(0, remaining - adaptiveReserve);
}

/**
 * Portfolio Autopilot — quota reader + routing logic
 *
 * Reads CodexBar usage, computes burnable quota, and outputs a routing decision:
 * which projects are eligible to run and at what model tier.
 *
 * Usage:
 *   node autopilot.mjs              → prints routing summary to stdout
 *   node autopilot.mjs --json       → prints routing decision as JSON
 *   node autopilot.mjs --check      → prints only burnable pct (for scripts)
 */

// Bypass proxy for local Docker host
process.env.NO_PROXY = [process.env.NO_PROXY, "host.docker.internal"]
  .filter(Boolean)
  .join(",");

import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";

const __dirname = dirname(fileURLToPath(import.meta.url));
const config = JSON.parse(
  readFileSync(join(__dirname, "autopilot-config.json"), "utf8")
);

const JSON_MODE = process.argv.includes("--json");
const CHECK_MODE = process.argv.includes("--check");

// ─── Quota Reader ────────────────────────────────────────────────────────────

async function fetchUsage() {
  const res = await fetch(config.codexbar_url);
  if (!res.ok) throw new Error(`CodexBar returned ${res.status}`);
  return res.json();
}

/**
 * Find the 5h and 7d windows from CodexBar response.
 * Returns { pct, resets_in_sec, duration_sec } for each.
 */
function extractWindows(data) {
  // CodexBar returns providers array; find Claude provider, then its windows
  const providers = data.providers ?? data;
  const claudeProvider = Array.isArray(providers)
    ? providers.find((p) => /claude/i.test(p.name ?? p.label ?? ""))
    : null;

  const windows = claudeProvider?.windows ?? data.windows ?? [];

  const w5h = windows.find(
    (w) => w.duration_sec <= 5 * 3600 + 60 && w.duration_sec >= 5 * 3600 - 60
  );
  const w7d = windows.find(
    (w) =>
      w.duration_sec <= 7 * 86400 + 300 && w.duration_sec >= 7 * 86400 - 300
  );

  if (!w5h || !w7d) {
    // Fallback: pick shortest and longest available windows
    const sorted = [...windows].sort((a, b) => a.duration_sec - b.duration_sec);
    return {
      w5h: sorted[0] ?? null,
      w7d: sorted[sorted.length - 1] ?? null,
      raw: windows,
    };
  }

  return { w5h, w7d, raw: windows };
}

// ─── Adaptive Reserve Formula ────────────────────────────────────────────────

/**
 * adaptive_reserve_pct = base_reserve_pct × (resets_in_sec / window_duration_sec)
 * burnable_pct = remaining_pct − adaptive_reserve_pct
 */
function computeBurnable(window, basePct) {
  if (!window) return 0;
  const remaining = window.pct ?? 100;
  const timeRatio = window.resets_in_sec / window.duration_sec;
  const adaptiveReserve = basePct * timeRatio;
  return Math.max(0, remaining - adaptiveReserve);
}

// ─── Tier Routing ────────────────────────────────────────────────────────────

/**
 * Given burnable %, return the quota tier label and which project priorities run.
 */
function quotaTier(burnablePct) {
  if (burnablePct <= 0) {
    return { tier: "zero", label: "No autonomous work", allowedPriorities: [] };
  }
  if (burnablePct < 5) {
    return {
      tier: "minimal",
      label: "Maintenance only (LM Studio)",
      allowedPriorities: ["P1"],
      implementationModel: "lm-studio",
    };
  }
  if (burnablePct < 20) {
    return {
      tier: "low",
      label: "P1 only — Gemini/Haiku",
      allowedPriorities: ["P1"],
      implementationModel: "gemini-or-haiku",
    };
  }
  if (burnablePct < 50) {
    return {
      tier: "medium",
      label: "P1+P2 — Claude Sonnet",
      allowedPriorities: ["P1", "P2"],
      implementationModel: "claude-sonnet",
    };
  }
  return {
    tier: "high",
    label: "P1+P2+P3 — Claude Sonnet",
    allowedPriorities: ["P1", "P2", "P3"],
    implementationModel: "claude-sonnet",
  };
}

/**
 * Sustainable burn rate gate for P2/P3.
 * Returns true if current burn rate is ahead of the smooth curve.
 */
function sustainableGatePasses(window) {
  if (!window) return false;
  const windowHours = window.duration_sec / 3600;
  const sustainablePctPerHour = 100 / windowHours;
  const elapsedHours = (window.duration_sec - window.resets_in_sec) / 3600;
  const expectedConsumed = sustainablePctPerHour * elapsedHours;
  const actualConsumed = 100 - (window.pct ?? 100);
  // Passes if actual consumption is BELOW expected (ahead of smooth curve)
  return actualConsumed < expectedConsumed;
}

// ─── Project Routing ─────────────────────────────────────────────────────────

function routeProjects(tier, burnablePct, w7d) {
  const sustainableGate = w7d ? sustainableGatePasses(w7d) : true;
  const projects = [];

  for (const [project, priority] of Object.entries(config.project_priorities)) {
    const allowed = tier.allowedPriorities.includes(priority);
    if (!allowed) {
      projects.push({ project, priority, decision: "skip", reason: "quota tier" });
      continue;
    }

    // P2/P3 require sustainable gate (P1 bypasses)
    if (priority !== "P1" && !sustainableGate) {
      projects.push({
        project,
        priority,
        decision: "skip",
        reason: "burn rate above sustainable pace",
      });
      continue;
    }

    projects.push({
      project,
      priority,
      decision: "proceed",
      implementationModel: tier.implementationModel,
    });
  }

  return projects;
}

// ─── Main ────────────────────────────────────────────────────────────────────

async function main() {
  let data;
  try {
    data = await fetchUsage();
  } catch (err) {
    const out = { error: `Failed to reach CodexBar: ${err.message}`, burnable: 0 };
    if (JSON_MODE) console.log(JSON.stringify(out));
    else console.error(out.error);
    process.exit(1);
  }

  const { w5h, w7d, raw } = extractWindows(data);

  const burnable5h = computeBurnable(w5h, config.reserve_5h_pct);
  const burnable7d = computeBurnable(w7d, config.reserve_7d_pct);
  const burnable = Math.min(burnable5h, burnable7d);

  if (CHECK_MODE) {
    console.log(burnable.toFixed(1));
    return;
  }

  const tier = quotaTier(burnable);
  const projects = routeProjects(tier, burnable, w7d);

  const result = {
    timestamp: new Date().toISOString(),
    windows: {
      "5h": w5h
        ? {
            remaining_pct: w5h.pct,
            resets_in_sec: w5h.resets_in_sec,
            burnable_pct: +burnable5h.toFixed(1),
          }
        : null,
      "7d": w7d
        ? {
            remaining_pct: w7d.pct,
            resets_in_sec: w7d.resets_in_sec,
            burnable_pct: +burnable7d.toFixed(1),
          }
        : null,
    },
    burnable_pct: +burnable.toFixed(1),
    tier: tier.tier,
    tier_label: tier.label,
    projects,
  };

  if (JSON_MODE) {
    console.log(JSON.stringify(result, null, 2));
    return;
  }

  // Human-readable summary
  console.log(`\n=== Portfolio Autopilot — ${result.timestamp} ===\n`);

  if (w5h) {
    const hrs5 = Math.round(w5h.resets_in_sec / 60);
    console.log(
      `5h window:  ${w5h.pct?.toFixed(1) ?? "?"}% remaining  (resets in ${hrs5}m)  →  burnable: ${burnable5h.toFixed(1)}%`
    );
  }
  if (w7d) {
    const days7 = Math.round(w7d.resets_in_sec / 3600);
    console.log(
      `7d window:  ${w7d.pct?.toFixed(1) ?? "?"}% remaining  (resets in ${days7}h)  →  burnable: ${burnable7d.toFixed(1)}%`
    );
  }

  console.log(`\nEffective burnable: ${burnable.toFixed(1)}%`);
  console.log(`Quota tier: ${tier.tier.toUpperCase()} — ${tier.label}\n`);

  const proceeding = projects.filter((p) => p.decision === "proceed");
  const skipped = projects.filter((p) => p.decision === "skip");

  if (proceeding.length === 0) {
    console.log("No projects eligible to run.");
  } else {
    console.log("Eligible projects:");
    for (const p of proceeding) {
      console.log(`  ✓ ${p.project} (${p.priority}) — model: ${p.implementationModel}`);
    }
  }

  if (skipped.length > 0) {
    console.log("\nSkipped:");
    for (const p of skipped) {
      console.log(`  ✗ ${p.project} (${p.priority}) — ${p.reason}`);
    }
  }

  console.log("");
}

main();

-  const timeRatio = window.resets_in_sec / window.duration_sec;

+  // resets_in_sec <= 0 means window expired/resetting — treat as full timeRatio approaching 0

+  const resets = Math.max(0, window.resets_in_sec);

+  const timeRatio = resets / window.duration_sec;


(node:1796) [UNDICI-EHPA] Warning: EnvHttpProxyAgent is experimental, expect them to change at any time.
(Use `node --trace-warnings ...` to show where the warning was created)

=== Portfolio Autopilot — 2026-07-07T14:17:14.606Z ===

5h window:  0.0% remaining  (resets in 0m)  →  burnable: 0.0%
7d window:  13.0% remaining  (resets in 103h)  →  burnable: 3.8%

Effective burnable: 0.0%
Quota tier: ZERO — No autonomous work

No projects eligible to run.

Skipped:
  ✗ StageHand (P1) — quota tier
  ✗ nanoclaw (P1) — quota tier
  ✗ AgenticLever (P2) — quota tier
  ✗ RoadSense (P2) — quota tier
  ✗ qip-qien (P2) — quota tier
  ✗ congressional-trading (P3) — quota tier
  ✗ TradingBot25 (P3) — quota tier


The routing logic is working correctly now. The 5h window is at 0% (fully consumed), so burnable is 0, and the tier is ZERO — no autonomous work. This is the correct output.

Now let me also test the `--json` flag:


Clean output — tier ZERO because the 5h window is fully consumed right now. Let me also test the JSON mode and write the scheduled trigger script:


Bash

node /workspace/agent/autopilot.mjs --json 2>/dev/null


Now let me write the scheduling trigger script — the piece that runs every 30 min, checks quota, and wakes the agent only when there's work to do:


Write

/**
 * autopilot-trigger.mjs — scheduled trigger script for portfolio-autopilot
 *
 * Runs every 30 min via nanoclaw schedule_task. Returns:
 *   { wakeAgent: true, data: {...} }  — if burnable > 0 and eligible projects exist
 *   { wakeAgent: false }              — if quota is zero or no eligible projects
 */

process.env.NO_PROXY = [process.env.NO_PROXY, "host.docker.internal"]
  .filter(Boolean)
  .join(",");

import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";

const __dirname = dirname(fileURLToPath(import.meta.url));
const config = JSON.parse(
  readFileSync(join(__dirname, "autopilot-config.json"), "utf8")
);

async function fetchUsage() {
  const res = await fetch(config.codexbar_url);
  if (!res.ok) throw new Error(`CodexBar returned ${res.status}`);
  return res.json();
}

function extractWindows(data) {
  const providers = data.providers ?? data;
  const claudeProvider = Array.isArray(providers)
    ? providers.find((p) => /claude/i.test(p.name ?? p.label ?? ""))
    : null;
  const windows = claudeProvider?.windows ?? data.windows ?? [];
  const sorted = [...windows].sort((a, b) => a.duration_sec - b.duration_sec);
  const w5h = sorted.find(
    (w) => Math.abs(w.duration_sec - 18000) < 300
  ) ?? sorted[0];
  const w7d = sorted.find(
    (w) => Math.abs(w.duration_sec - 604800) < 600
  ) ?? sorted[sorted.length - 1];
  return { w5h, w7d };
}

function computeBurnable(window, basePct) {
  if (!window) return 0;
  const remaining = window.pct ?? 100;
  if (remaining <= 0) return 0;
  const resets = Math.max(0, window.resets_in_sec);
  const timeRatio = resets / window.duration_sec;
  return Math.max(0, remaining - basePct * timeRatio);
}

function quotaTier(burnablePct) {
  if (burnablePct <= 0) return { tier: "zero", allowedPriorities: [], implementationModel: null };
  if (burnablePct < 5) return { tier: "minimal", allowedPriorities: ["P1"], implementationModel: "lm-studio" };
  if (burnablePct < 20) return { tier: "low", allowedPriorities: ["P1"], implementationModel: "gemini-or-haiku" };
  if (burnablePct < 50) return { tier: "medium", allowedPriorities: ["P1", "P2"], implementationModel: "claude-sonnet" };
  return { tier: "high", allowedPriorities: ["P1", "P2", "P3"], implementationModel: "claude-sonnet" };
}

function sustainableGatePasses(window) {
  if (!window) return false;
  const windowHours = window.duration_sec / 3600;
  const elapsedHours = (window.duration_sec - Math.max(0, window.resets_in_sec)) / 3600;
  const expectedConsumed = (100 / windowHours) * elapsedHours;
  const actualConsumed = 100 - (window.pct ?? 100);
  return actualConsumed < expectedConsumed;
}

async function main() {
  let data;
  try {
    data = await fetchUsage();
  } catch (err) {
    // Can't reach CodexBar — don't wake agent, try next cycle
    console.log(JSON.stringify({ wakeAgent: false, reason: `codexbar_unreachable: ${err.message}` }));
    return;
  }

  const { w5h, w7d } = extractWindows(data);
  const burnable5h = computeBurnable(w5h, config.reserve_5h_pct);
  const burnable7d = computeBurnable(w7d, config.reserve_7d_pct);
  const burnable = Math.min(burnable5h, burnable7d);

  const tier = quotaTier(burnable);
  if (tier.tier === "zero") {
    console.log(JSON.stringify({ wakeAgent: false, reason: "no_burnable_quota" }));
    return;
  }

  const sustainableGate = w7d ? sustainableGatePasses(w7d) : true;

  const eligibleProjects = Object.entries(config.project_priorities)
    .filter(([_, priority]) => tier.allowedPriorities.includes(priority))
    .filter(([_, priority]) => priority === "P1" || sustainableGate)
    .map(([project, priority]) => ({ project, priority }));

  if (eligibleProjects.length === 0) {
    console.log(JSON.stringify({ wakeAgent: false, reason: "no_eligible_projects" }));
    return;
  }

  console.log(
    JSON.stringify({
      wakeAgent: true,
      data: {
        burnable_pct: +burnable.toFixed(1),
        tier: tier.tier,
        implementationModel: tier.implementationModel,
        eligibleProjects,
        windows: {
          "5h": w5h ? { remaining_pct: w5h.pct, resets_in_sec: w5h.resets_in_sec } : null,
          "7d": w7d ? { remaining_pct: w7d.pct, resets_in_sec: w7d.resets_in_sec } : null,
        },
      },
    })
  );
}

main();