  // Log pre-run quota snapshot for closed-loop calibration
  try {
    const snapshot = {
      ts: new Date().toISOString(),
      type: "pre_run",
      remaining_5h_pct: w5h ? +remaining(w5h).toFixed(1) : null,
      remaining_7d_pct: w7d ? +remaining(w7d).toFixed(1) : null,
      burnable_pct: +burnable.toFixed(1),
      reserve_5h_pct: config.reserve_5h_pct,
      reserve_7d_pct: config.reserve_7d_pct,
      headroom_5h_adequate: w5h ? remaining(w5h) >= config.reserve_5h_pct : null,
      headroom_7d_adequate: w7d ? remaining(w7d) >= config.reserve_7d_pct : null,
    };
    appendFileSync(join(__dirname, "autopilot-quota-log.jsonl"), JSON.stringify(snapshot) + "\n");
  } catch { /* non-fatal — don't block the wake */ }

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

/**
 * autopilot-trigger.mjs — scheduled trigger script for portfolio-autopilot
 *
 * Returns { wakeAgent: true, data } when burnable quota exists and eligible projects are ready.
 * Returns { wakeAgent: false, reason, next_reset_at? } otherwise.
 *
 * NOTE: CodexBar `pct` = percent USED. remaining = 100 - pct.
 */

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

import { readFileSync, existsSync, appendFileSync } 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")
);

// Skip-lock: agent writes this file after an all-blocked cycle to suppress
// repeated trigger wakes until the quota window resets.
const SKIP_LOCK_PATH = join(__dirname, "autopilot-skip-lock.json");
if (existsSync(SKIP_LOCK_PATH)) {
  try {
    const lock = JSON.parse(readFileSync(SKIP_LOCK_PATH, "utf8"));
    if (lock.skip_lock_until && new Date(lock.skip_lock_until) > new Date()) {
      console.log(JSON.stringify({ wakeAgent: false, reason: "skip_lock_active", skip_lock_until: lock.skip_lock_until }));
      process.exit(0);
    }
  } catch { /* malformed lock file — ignore and proceed */ }
}

function remaining(w) { return 100 - (w?.pct ?? 100); }
function effectiveResetsIn(w) { return (!w || w.resets_in_sec < 0) ? (w?.duration_sec ?? 0) : w.resets_in_sec; }
function nextResetAt(w) { if (!w) return null; return new Date(Date.now() + effectiveResetsIn(w) * 1000).toISOString(); }

function extractWindows(windows = []) {
  return {
    w5h: windows.find((w) => Math.abs(w.duration_sec - 18000) < 300) ?? null,
    w7d: windows.find((w) => Math.abs(w.duration_sec - 604800) < 600) ?? null,
  };
}

function computeBurnable(w, basePct) {
  if (!w) return 0;
  const rem = remaining(w);
  if (rem <= 0) return 0;
  const timeRatio = effectiveResetsIn(w) / w.duration_sec;
  return Math.max(0, rem - 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(w) {
  if (!w) return false;
  const elapsedHours = (w.duration_sec - effectiveResetsIn(w)) / 3600;
  const expectedConsumed = (100 / (w.duration_sec / 3600)) * elapsedHours;
  return (w.pct ?? 0) < expectedConsumed; // pct = used%
}

async function main() {
  let data;
  try {
    const res = await fetch(config.codexbar_url);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    data = await res.json();
  } catch (err) {
    console.log(JSON.stringify({ wakeAgent: false, reason: `codexbar_unreachable: ${err.message}` }));
    return;
  }

  const providers = data.providers ?? [];
  const claudeProvider = providers.find((p) => /claude/i.test(p.name ?? "")) ?? null;
  const { w5h, w7d } = extractWindows(claudeProvider?.windows ?? []);

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

  // Next reset time — for the constraining window, so the agent can schedule an immediate wake
  const constraintWindow = burnable5h <= burnable7d ? w5h : w7d;
  const next_reset_at = nextResetAt(constraintWindow);

  // All-provider snapshot (for context in the agent prompt)
  const trackedSnapshot = providers.map((p) => {
    const { w5h: p5h, w7d: p7d } = extractWindows(p.windows ?? []);
    return {
      name: p.name,
      tracked: true,
      "5h_remaining_pct": p5h ? remaining(p5h) : null,
      "7d_remaining_pct": p7d ? remaining(p7d) : null,
    };
  });
  const untrackedSnapshot = (config.untracked_providers ?? []).map((p) => ({
    name: p.name,
    tracked: false,
    note: p.note,
  }));
  const providerSnapshot = [...trackedSnapshot, ...untrackedSnapshot];

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

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

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

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

main();

+  // Log pre-run quota snapshot for closed-loop calibration

+      remaining_5h_pct: w5h ? +remaining(w5h).toFixed(1) : null,

+      remaining_7d_pct: w7d ? +remaining(w7d).toFixed(1) : null,

+      burnable_pct: +burnable.toFixed(1),

+      reserve_5h_pct: config.reserve_5h_pct,

+      reserve_7d_pct: config.reserve_7d_pct,

+      headroom_5h_adequate: w5h ? remaining(w5h) >= config.reserve_5h_pct : null,

+      headroom_7d_adequate: w7d ? remaining(w7d) >= config.reserve_7d_pct : null,

+    appendFileSync(join(__dirname, "autopilot-quota-log.jsonl"), JSON.stringify(snapshot) + "\n");

+  } catch { /* non-fatal — don't block the wake */ }


Ingest Claude blog post on marketing operations automation


Now update the calibration script with headroom analysis:


1	#!/usr/bin/env node
2	/**
3	 * autopilot-calibrate.mjs
4	 * Weekly calibration report — reads autopilot-run-log.jsonl and sends a
5	 * Telegram summary to James via mcp__nanoclaw__send_message.
6	 *
7	 * Designed to run as a scheduled task (weekly, Monday 08:00 CT).
8	 * Can also be run manually: node autopilot-calibrate.mjs [--dry-run]
9	 */
10
11	import { readFileSync, existsSync } from 'fs';
12	import { fileURLToPath } from 'url';
13	import { dirname, join } from 'path';
14
15	const __dir = dirname(fileURLToPath(import.meta.url));
16	const LOG_PATH = join(__dir, 'autopilot-run-log.jsonl');
17	const DRY_RUN = process.argv.includes('--dry-run');
18
19	const DAYS = 7;
20	const now = new Date();
21	const cutoff = new Date(now.getTime() - DAYS * 24 * 60 * 60 * 1000);
22
23	// --- Load log ---
24	let runs = [];
25	if (existsSync(LOG_PATH)) {
26	  runs = readFileSync(LOG_PATH, 'utf8')
27	    .trim().split('\n').filter(Boolean)
28	    .map(l => JSON.parse(l));
29	}
30
31	const recent = runs.filter(r => new Date(r.timestamp) >= cutoff);
32	const total = recent.length;
33
34	// Aggregate stats
35	const byOutcome = {};
36	const byProject = {};
37	const tierMap = { minimal: 1, low: 2, medium: 3, high: 4 };
38	let skippedRuns = 0;
39
40	for (const r of recent) {
41	  byOutcome[r.outcome] = (byOutcome[r.outcome] || 0) + 1;
42	  byProject[r.project] = (byProject[r.project] || 0) + 1;
43	  if (r.outcome === 'skipped') skippedRuns++;
44	}
45
46	const completedRuns = byOutcome['completed'] || 0;
47	const successRate = total > 0 ? Math.round((completedRuns / total) * 100) : 0;
48
49	// Project activity
50	const projectLines = Object.entries(byProject)
51	  .sort((a, b) => b[1] - a[1])
52	  .map(([p, n]) => `  • ${p}: ${n} run${n === 1 ? '' : 's'}`)
53	  .join('\n');
54
55	// All-time totals
56	const allTotal = runs.length;
57	const allCompleted = runs.filter(r => r.outcome === 'completed').length;
58
59	// Build message
60	const lines = [
61	  `📊 *Autopilot Weekly Calibration* (last ${DAYS} days)`,
62	  '',
63	  `Runs: ${total} · Completed: ${completedRuns} · Success rate: ${successRate}%`,
64	  `All-time: ${allTotal} runs, ${allCompleted} completed`,
65	  '',
66	];
67
68	if (total === 0) {
69	  lines.push('No runs in the last 7 days.');
70	} else {
71	  lines.push('*Active projects:*');
72	  lines.push(projectLines);
73	}
74
75	// Flag if success rate is low
76	if (total > 0 && successRate < 70) {
77	  lines.push('');
78	  lines.push(`⚠ Success rate below 70% — review run log for recurring blockers.`);
79	}
80
81	// Flag if no runs recently (last 2 days)
82	const recentCutoff = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
83	const veryRecent = runs.filter(r => new Date(r.timestamp) >= recentCutoff);
84	if (veryRecent.length === 0 && total > 0) {
85	  lines.push('');
86	  lines.push('⚠ No runs in the last 2 days — autopilot may be stalled.');
87	}
88
89	const message = lines.join('\n');
90
91	if (DRY_RUN) {
92	  console.log('--- DRY RUN (would send to telegram-mg-17796) ---');
93	  console.log(message);
94	  process.exit(0);
95	}
96
97	// Output JSON for the agent to pick up and send
98	console.log(JSON.stringify({ wakeAgent: true, data: { message, total, completedRuns, successRate } }));
99

#!/usr/bin/env node
/**
 * autopilot-calibrate.mjs
 * Weekly calibration report — reads autopilot-run-log.jsonl and sends a
 * Telegram summary to James via mcp__nanoclaw__send_message.
 *
 * Designed to run as a scheduled task (weekly, Monday 08:00 CT).
 * Can also be run manually: node autopilot-calibrate.mjs [--dry-run]
 */

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

const __dir = dirname(fileURLToPath(import.meta.url));
const LOG_PATH = join(__dir, 'autopilot-run-log.jsonl');
const DRY_RUN = process.argv.includes('--dry-run');

const DAYS = 7;
const now = new Date();
const cutoff = new Date(now.getTime() - DAYS * 24 * 60 * 60 * 1000);

// --- Load log ---
let runs = [];
if (existsSync(LOG_PATH)) {
  runs = readFileSync(LOG_PATH, 'utf8')
    .trim().split('\n').filter(Boolean)
    .map(l => JSON.parse(l));
}

const recent = runs.filter(r => new Date(r.timestamp) >= cutoff);
const total = recent.length;

// Aggregate stats
const byOutcome = {};
const byProject = {};
const tierMap = { minimal: 1, low: 2, medium: 3, high: 4 };
let skippedRuns = 0;

for (const r of recent) {
  byOutcome[r.outcome] = (byOutcome[r.outcome] || 0) + 1;
  byProject[r.project] = (byProject[r.project] || 0) + 1;
  if (r.outcome === 'skipped') skippedRuns++;
}

const completedRuns = byOutcome['completed'] || 0;
const successRate = total > 0 ? Math.round((completedRuns / total) * 100) : 0;

// Project activity
const projectLines = Object.entries(byProject)
  .sort((a, b) => b[1] - a[1])
  .map(([p, n]) => `  • ${p}: ${n} run${n === 1 ? '' : 's'}`)
  .join('\n');

// All-time totals
const allTotal = runs.length;
const allCompleted = runs.filter(r => r.outcome === 'completed').length;

// Build message
const lines = [
  `📊 *Autopilot Weekly Calibration* (last ${DAYS} days)`,
  '',
  `Runs: ${total} · Completed: ${completedRuns} · Success rate: ${successRate}%`,
  `All-time: ${allTotal} runs, ${allCompleted} completed`,
  '',
];

if (total === 0) {
  lines.push('No runs in the last 7 days.');
} else {
  lines.push('*Active projects:*');
  lines.push(projectLines);
}

// Flag if success rate is low
if (total > 0 && successRate < 70) {
  lines.push('');
  lines.push(`⚠ Success rate below 70% — review run log for recurring blockers.`);
}

// Flag if no runs recently (last 2 days)
const recentCutoff = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
const veryRecent = runs.filter(r => new Date(r.timestamp) >= recentCutoff);
if (veryRecent.length === 0 && total > 0) {
  lines.push('');
  lines.push('⚠ No runs in the last 2 days — autopilot may be stalled.');
}

const message = lines.join('\n');

if (DRY_RUN) {
  console.log('--- DRY RUN (would send to telegram-mg-17796) ---');
  console.log(message);
  process.exit(0);
}

// Output JSON for the agent to pick up and send
console.log(JSON.stringify({ wakeAgent: true, data: { message, total, completedRuns, successRate } }));


Write

#!/usr/bin/env node
/**
 * autopilot-calibrate.mjs
 * Weekly calibration report — reads autopilot-run-log.jsonl + autopilot-quota-log.jsonl
 * and sends a Telegram summary with headroom analysis to James.
 *
 * Designed to run as a scheduled task (weekly, Monday 08:00 CT).
 * Can also be run manually: node autopilot-calibrate.mjs [--dry-run]
 */

import { readFileSync, existsSync, writeFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';

const __dir = dirname(fileURLToPath(import.meta.url));
const LOG_PATH = join(__dir, 'autopilot-run-log.jsonl');
const QUOTA_LOG_PATH = join(__dir, 'autopilot-quota-log.jsonl');
const CONFIG_PATH = join(__dir, 'autopilot-config.json');
const DRY_RUN = process.argv.includes('--dry-run');

const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
const DAYS = 7;
const now = new Date();
const cutoff = new Date(now.getTime() - DAYS * 24 * 60 * 60 * 1000);

// --- Load run log ---
let runs = [];
if (existsSync(LOG_PATH)) {
  runs = readFileSync(LOG_PATH, 'utf8')
    .trim().split('\n').filter(Boolean)
    .map(l => { try { return JSON.parse(l); } catch { return null; } })
    .filter(Boolean);
}

// Normalize timestamp field (some entries use 'ts', some use 'timestamp')
runs = runs.map(r => ({ ...r, _ts: r.timestamp || r.ts }));

const recent = runs.filter(r => r._ts && new Date(r._ts) >= cutoff);
const total = recent.length;

const byOutcome = {};
const byProject = {};
for (const r of recent) {
  const outcome = r.outcome || 'unknown';
  byOutcome[outcome] = (byOutcome[outcome] || 0) + 1;
  if (r.project && r.project !== 'none') {
    byProject[r.project] = (byProject[r.project] || 0) + 1;
  }
}

const skippedRuns = byOutcome['skipped'] || 0;
// Count "done" outcomes: phase1_done, phase2_done, completed, etc.
const completedRuns = Object.entries(byOutcome)
  .filter(([k]) => k !== 'skipped' && k !== 'unknown')
  .reduce((s, [, v]) => s + v, 0);
const successRate = total > 0 ? Math.round((completedRuns / total) * 100) : 0;

const projectLines = Object.entries(byProject)
  .sort((a, b) => b[1] - a[1])
  .map(([p, n]) => `  • ${p}: ${n} run${n === 1 ? '' : 's'}`)
  .join('\n');

const allTotal = runs.length;
const allCompleted = runs.filter(r => {
  const o = r.outcome || '';
  return o !== 'skipped' && o !== 'unknown' && o !== '';
}).length;

// --- Load quota log for headroom analysis ---
let quotaEntries = [];
if (existsSync(QUOTA_LOG_PATH)) {
  quotaEntries = readFileSync(QUOTA_LOG_PATH, 'utf8')
    .trim().split('\n').filter(Boolean)
    .map(l => { try { return JSON.parse(l); } catch { return null; } })
    .filter(Boolean);
}

const recentQuota = quotaEntries.filter(q => q.ts && new Date(q.ts) >= cutoff);
const postRuns = recentQuota.filter(q => q.type === 'post_run');
const preRuns = recentQuota.filter(q => q.type === 'pre_run');

// Headroom analysis
let headroomLines = [];
if (postRuns.length > 0) {
  const adequateCount5h = postRuns.filter(q => q.headroom_5h_adequate === true).length;
  const adequateRate5h = Math.round((adequateCount5h / postRuns.length) * 100);
  const avgRemaining5h = postRuns.reduce((s, q) => s + (q.remaining_5h_pct ?? 0), 0) / postRuns.length;

  const adequateCount7d = postRuns.filter(q => q.headroom_7d_adequate === true).length;
  const avgRemaining7d = postRuns.reduce((s, q) => s + (q.remaining_7d_pct ?? 0), 0) / postRuns.length;

  headroomLines.push(`*Headroom after runs* (${postRuns.length} samples):`);
  headroomLines.push(`  5h window: avg ${avgRemaining5h.toFixed(0)}% remaining, adequate ${adequateRate5h}% of the time (floor=${config.reserve_5h_pct}%)`);
  headroomLines.push(`  7d window: avg ${avgRemaining7d.toFixed(0)}% remaining, adequate ${Math.round(adequateCount7d/postRuns.length*100)}% of the time (floor=${config.reserve_7d_pct}%)`);

  // Recommend reserve adjustment
  const suggestions = [];
  if (adequateRate5h < 80 && avgRemaining5h < config.reserve_5h_pct + 5) {
    suggestions.push(`  ⚠ 5h reserve too low — consider raising reserve_5h_pct from ${config.reserve_5h_pct}% to ${config.reserve_5h_pct + 5}%`);
  } else if (adequateRate5h === 100 && avgRemaining5h > config.reserve_5h_pct + 25) {
    suggestions.push(`  💡 5h reserve may be too conservative — could lower reserve_5h_pct from ${config.reserve_5h_pct}% to ${config.reserve_5h_pct - 5}%`);
  }
  if (suggestions.length > 0) headroomLines.push(...suggestions);
} else if (preRuns.length > 0) {
  headroomLines.push(`*Headroom:* ${preRuns.length} pre-run snapshots, 0 post-run. Call \`node autopilot-headroom.mjs\` at end of each run to build history.`);
} else {
  headroomLines.push(`*Headroom:* No quota snapshots yet — data accumulates as runs complete.`);
}

// --- Build message ---
const lines = [
  `📊 *Autopilot Weekly Calibration* (last ${DAYS} days)`,
  '',
  `Runs: ${total} · Completed: ${completedRuns} · Skipped: ${skippedRuns} · Success rate: ${successRate}%`,
  `All-time: ${allTotal} runs, ${allCompleted} completed`,
  '',
];

if (total === 0) {
  lines.push('No runs in the last 7 days.');
} else {
  if (Object.keys(byProject).length > 0) {
    lines.push('*Active projects:*');
    lines.push(projectLines);
    lines.push('');
  }
  lines.push(...headroomLines);
}

// Flags
if (total > 0 && successRate < 50) {
  lines.push('');
  lines.push(`⚠ Success rate below 50% — review run log for recurring blockers.`);
}

const recentCutoff2d = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
const veryRecent = runs.filter(r => r._ts && new Date(r._ts) >= recentCutoff2d);
if (veryRecent.length === 0 && total > 0) {
  lines.push('');
  lines.push('⚠ No runs in the last 2 days — autopilot may be stalled.');
}

const message = lines.join('\n');

if (DRY_RUN) {
  console.log('--- DRY RUN ---');
  console.log(message);
  process.exit(0);
}

// Output JSON for the agent to pick up and send
console.log(JSON.stringify({ wakeAgent: true, data: { message, total, completedRuns, successRate } }));


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

#!/usr/bin/env node
/**
 * autopilot-calibrate.mjs
 * Weekly calibration report — reads autopilot-run-log.jsonl + autopilot-quota-log.jsonl
 * and sends a Telegram summary with headroom analysis to James.
 *
 * Designed to run as a scheduled task (weekly, Monday 08:00 CT).
 * Can also be run manually: node autopilot-calibrate.mjs [--dry-run]
 */

import { readFileSync, existsSync, writeFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';

const __dir = dirname(fileURLToPath(import.meta.url));
const LOG_PATH = join(__dir, 'autopilot-run-log.jsonl');
const QUOTA_LOG_PATH = join(__dir, 'autopilot-quota-log.jsonl');
const CONFIG_PATH = join(__dir, 'autopilot-config.json');
const DRY_RUN = process.argv.includes('--dry-run');

const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
const DAYS = 7;
const now = new Date();
const cutoff = new Date(now.getTime() - DAYS * 24 * 60 * 60 * 1000);

// --- Load run log ---
let runs = [];
if (existsSync(LOG_PATH)) {
  runs = readFileSync(LOG_PATH, 'utf8')
    .trim().split('\n').filter(Boolean)
    .map(l => { try { return JSON.parse(l); } catch { return null; } })
    .filter(Boolean);
}

// Normalize timestamp field (some entries use 'ts', some use 'timestamp')
runs = runs.map(r => ({ ...r, _ts: r.timestamp || r.ts }));

const recent = runs.filter(r => r._ts && new Date(r._ts) >= cutoff);
const total = recent.length;

const byOutcome = {};
const byProject = {};
for (const r of recent) {
  const outcome = r.outcome || 'unknown';
  byOutcome[outcome] = (byOutcome[outcome] || 0) + 1;
  if (r.project && r.project !== 'none') {
    byProject[r.project] = (byProject[r.project] || 0) + 1;
  }
}

const skippedRuns = byOutcome['skipped'] || 0;
// Count "done" outcomes: phase1_done, phase2_done, completed, etc.
const completedRuns = Object.entries(byOutcome)
  .filter(([k]) => k !== 'skipped' && k !== 'unknown')
  .reduce((s, [, v]) => s + v, 0);
const successRate = total > 0 ? Math.round((completedRuns / total) * 100) : 0;

const projectLines = Object.entries(byProject)
  .sort((a, b) => b[1] - a[1])
  .map(([p, n]) => `  • ${p}: ${n} run${n === 1 ? '' : 's'}`)
  .join('\n');

const allTotal = runs.length;
const allCompleted = runs.filter(r => {
  const o = r.outcome || '';
  return o !== 'skipped' && o !== 'unknown' && o !== '';
}).length;

// --- Load quota log for headroom analysis ---
let quotaEntries = [];
if (existsSync(QUOTA_LOG_PATH)) {
  quotaEntries = readFileSync(QUOTA_LOG_PATH, 'utf8')
    .trim().split('\n').filter(Boolean)
    .map(l => { try { return JSON.parse(l); } catch { return null; } })
    .filter(Boolean);
}

const recentQuota = quotaEntries.filter(q => q.ts && new Date(q.ts) >= cutoff);
const postRuns = recentQuota.filter(q => q.type === 'post_run');
const preRuns = recentQuota.filter(q => q.type === 'pre_run');

// Headroom analysis
let headroomLines = [];
if (postRuns.length > 0) {
  const adequateCount5h = postRuns.filter(q => q.headroom_5h_adequate === true).length;
  const adequateRate5h = Math.round((adequateCount5h / postRuns.length) * 100);
  const avgRemaining5h = postRuns.reduce((s, q) => s + (q.remaining_5h_pct ?? 0), 0) / postRuns.length;

  const adequateCount7d = postRuns.filter(q => q.headroom_7d_adequate === true).length;
  const avgRemaining7d = postRuns.reduce((s, q) => s + (q.remaining_7d_pct ?? 0), 0) / postRuns.length;

  headroomLines.push(`*Headroom after runs* (${postRuns.length} samples):`);
  headroomLines.push(`  5h window: avg ${avgRemaining5h.toFixed(0)}% remaining, adequate ${adequateRate5h}% of the time (floor=${config.reserve_5h_pct}%)`);
  headroomLines.push(`  7d window: avg ${avgRemaining7d.toFixed(0)}% remaining, adequate ${Math.round(adequateCount7d/postRuns.length*100)}% of the time (floor=${config.reserve_7d_pct}%)`);

  // Recommend reserve adjustment
  const suggestions = [];
  if (adequateRate5h < 80 && avgRemaining5h < config.reserve_5h_pct + 5) {
    suggestions.push(`  ⚠ 5h reserve too low — consider raising reserve_5h_pct from ${config.reserve_5h_pct}% to ${config.reserve_5h_pct + 5}%`);
  } else if (adequateRate5h === 100 && avgRemaining5h > config.reserve_5h_pct + 25) {
    suggestions.push(`  💡 5h reserve may be too conservative — could lower reserve_5h_pct from ${config.reserve_5h_pct}% to ${config.reserve_5h_pct - 5}%`);
  }
  if (suggestions.length > 0) headroomLines.push(...suggestions);
} else if (preRuns.length > 0) {
  headroomLines.push(`*Headroom:* ${preRuns.length} pre-run snapshots, 0 post-run. Call \`node autopilot-headroom.mjs\` at end of each run to build history.`);
} else {
  headroomLines.push(`*Headroom:* No quota snapshots yet — data accumulates as runs complete.`);
}

// --- Build message ---
const lines = [
  `📊 *Autopilot Weekly Calibration* (last ${DAYS} days)`,
  '',
  `Runs: ${total} · Completed: ${completedRuns} · Skipped: ${skippedRuns} · Success rate: ${successRate}%`,
  `All-time: ${allTotal} runs, ${allCompleted} completed`,
  '',
];

if (total === 0) {
  lines.push('No runs in the last 7 days.');
} else {
  if (Object.keys(byProject).length > 0) {
    lines.push('*Active projects:*');
    lines.push(projectLines);
    lines.push('');
  }
  lines.push(...headroomLines);
}

// Flags
if (total > 0 && successRate < 50) {
  lines.push('');
  lines.push(`⚠ Success rate below 50% — review run log for recurring blockers.`);
}

const recentCutoff2d = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
const veryRecent = runs.filter(r => r._ts && new Date(r._ts) >= recentCutoff2d);
if (veryRecent.length === 0 && total > 0) {
  lines.push('');
  lines.push('⚠ No runs in the last 2 days — autopilot may be stalled.');
}

const message = lines.join('\n');

if (DRY_RUN) {
  console.log('--- DRY RUN ---');
  console.log(message);
  process.exit(0);
}

// Output JSON for the agent to pick up and send
console.log(JSON.stringify({ wakeAgent: true, data: { message, total, completedRuns, successRate } }));

- * Weekly calibration report — reads autopilot-run-log.jsonl and sends a

- * Telegram summary to James via mcp__nanoclaw__send_message.

+ * Weekly calibration report — reads autopilot-run-log.jsonl + autopilot-quota-log.jsonl

+ * and sends a Telegram summary with headroom analysis to James.

-import { readFileSync, existsSync } from 'fs';

+import { readFileSync, existsSync, writeFileSync } from 'fs';

+const QUOTA_LOG_PATH = join(__dir, 'autopilot-quota-log.jsonl');

+const CONFIG_PATH = join(__dir, 'autopilot-config.json');

+const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));

+    .map(l => { try { return JSON.parse(l); } catch { return null; } })

-const recent = runs.filter(r => new Date(r.timestamp) >= cutoff);

+// Normalize timestamp field (some entries use 'ts', some use 'timestamp')

+runs = runs.map(r => ({ ...r, _ts: r.timestamp || r.ts }));

+const recent = runs.filter(r => r._ts && new Date(r._ts) >= cutoff);

-const tierMap = { minimal: 1, low: 2, medium: 3, high: 4 };

-  byOutcome[r.outcome] = (byOutcome[r.outcome] || 0) + 1;

-  byProject[r.project] = (byProject[r.project] || 0) + 1;

-  if (r.outcome === 'skipped') skippedRuns++;

+  const outcome = r.outcome || 'unknown';

+  byOutcome[outcome] = (byOutcome[outcome] || 0) + 1;

+  if (r.project && r.project !== 'none') {

+    byProject[r.project] = (byProject[r.project] || 0) + 1;

-const completedRuns = byOutcome['completed'] || 0;

+const skippedRuns = byOutcome['skipped'] || 0;

+// Count "done" outcomes: phase1_done, phase2_done, completed, etc.

+const completedRuns = Object.entries(byOutcome)

+  .filter(([k]) => k !== 'skipped' && k !== 'unknown')

const projectLines = Object.entries(byProject)

-const allCompleted = runs.filter(r => r.outcome === 'completed').length;

+const allCompleted = runs.filter(r => {

+  return o !== 'skipped' && o !== 'unknown' && o !== '';

+// --- Load quota log for headroom analysis ---

+  quotaEntries = readFileSync(QUOTA_LOG_PATH, 'utf8')

+    .trim().split('\n').filter(Boolean)

+const recentQuota = quotaEntries.filter(q => q.ts && new Date(q.ts) >= cutoff);

+const postRuns = recentQuota.filter(q => q.type === 'post_run');

+const preRuns = recentQuota.filter(q => q.type === 'pre_run');

+  const adequateCount5h = postRuns.filter(q => q.headroom_5h_adequate === true).length;

+  const adequateRate5h = Math.round((adequateCount5h / postRuns.length) * 100);

+  const avgRemaining5h = postRuns.reduce((s, q) => s + (q.remaining_5h_pct ?? 0), 0) / postRuns.length;

+  const adequateCount7d = postRuns.filter(q => q.headroom_7d_adequate === true).length;

+  const avgRemaining7d = postRuns.reduce((s, q) => s + (q.remaining_7d_pct ?? 0), 0) / postRuns.length;

+  headroomLines.push(`*Headroom after runs* (${postRuns.length} samples):`);

+  headroomLines.push(`  5h window: avg ${avgRemaining5h.toFixed(0)}% remaining, adequate ${adequateRate5h}% of the time (floor=${config.reserve_5h_pct}%)`);

+  headroomLines.push(`  7d window: avg ${avgRemaining7d.toFixed(0)}% remaining, adequate ${Math.round(adequateCount7d/postRuns.length*100)}% of the time (floor=${config.reserve_7d_pct}%)`);

+  if (adequateRate5h < 80 && avgRemaining5h < config.reserve_5h_pct + 5) {

+    suggestions.push(`  ⚠ 5h reserve too low — consider raising reserve_5h_pct from ${config.reserve_5h_pct}% to ${config.reserve_5h_pct + 5}%`);

+  } else if (adequateRate5h === 100 && avgRemaining5h > config.reserve_5h_pct + 25) {

+    suggestions.push(`  💡 5h reserve may be too conservative — could lower reserve_5h_pct from ${config.reserve_5h_pct}% to ${config.reserve_5h_pct - 5}%`);

+  if (suggestions.length > 0) headroomLines.push(...suggestions);

+  headroomLines.push(`*Headroom:* ${preRuns.length} pre-run snapshots, 0 post-run. Call \`node autopilot-headroom.mjs\` at end of each run to build history.`);

+  headroomLines.push(`*Headroom:* No quota snapshots yet — data accumulates as runs complete.`);

-  `Runs: ${total} · Completed: ${completedRuns} · Success rate: ${successRate}%`,

+  `Runs: ${total} · Completed: ${completedRuns} · Skipped: ${skippedRuns} · Success rate: ${successRate}%`,

lines.push('No runs in the last 7 days.');

+  if (Object.keys(byProject).length > 0) {

-  lines.push(`⚠ Success rate below 70% — review run log for recurring blockers.`);

+  lines.push(`⚠ Success rate below 50% — review run log for recurring blockers.`);

-// Flag if no runs recently (last 2 days)

-const recentCutoff = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);

-const veryRecent = runs.filter(r => new Date(r.timestamp) >= recentCutoff);

+const recentCutoff2d = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);

+const veryRecent = runs.filter(r => r._ts && new Date(r._ts) >= recentCutoff2d);

if (veryRecent.length === 0 && total > 0) {

-  console.log('--- DRY RUN (would send to telegram-mg-17796) ---');

#!/usr/bin/env node
/**
 * autopilot-calibrate.mjs
 * Weekly calibration report — reads autopilot-run-log.jsonl and sends a
 * Telegram summary to James via mcp__nanoclaw__send_message.
 *
 * Designed to run as a scheduled task (weekly, Monday 08:00 CT).
 * Can also be run manually: node autopilot-calibrate.mjs [--dry-run]
 */

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

const __dir = dirname(fileURLToPath(import.meta.url));
const LOG_PATH = join(__dir, 'autopilot-run-log.jsonl');
const DRY_RUN = process.argv.includes('--dry-run');

const DAYS = 7;
const now = new Date();
const cutoff = new Date(now.getTime() - DAYS * 24 * 60 * 60 * 1000);

// --- Load log ---
let runs = [];
if (existsSync(LOG_PATH)) {
  runs = readFileSync(LOG_PATH, 'utf8')
    .trim().split('\n').filter(Boolean)
    .map(l => JSON.parse(l));
}

const recent = runs.filter(r => new Date(r.timestamp) >= cutoff);
const total = recent.length;

// Aggregate stats
const byOutcome = {};
const byProject = {};
const tierMap = { minimal: 1, low: 2, medium: 3, high: 4 };
let skippedRuns = 0;

for (const r of recent) {
  byOutcome[r.outcome] = (byOutcome[r.outcome] || 0) + 1;
  byProject[r.project] = (byProject[r.project] || 0) + 1;
  if (r.outcome === 'skipped') skippedRuns++;
}

const completedRuns = byOutcome['completed'] || 0;
const successRate = total > 0 ? Math.round((completedRuns / total) * 100) : 0;

// Project activity
const projectLines = Object.entries(byProject)
  .sort((a, b) => b[1] - a[1])
  .map(([p, n]) => `  • ${p}: ${n} run${n === 1 ? '' : 's'}`)
  .join('\n');

// All-time totals
const allTotal = runs.length;
const allCompleted = runs.filter(r => r.outcome === 'completed').length;

// Build message
const lines = [
  `📊 *Autopilot Weekly Calibration* (last ${DAYS} days)`,
  '',
  `Runs: ${total} · Completed: ${completedRuns} · Success rate: ${successRate}%`,
  `All-time: ${allTotal} runs, ${allCompleted} completed`,
  '',
];

if (total === 0) {
  lines.push('No runs in the last 7 days.');
} else {
  lines.push('*Active projects:*');
  lines.push(projectLines);
}

// Flag if success rate is low
if (total > 0 && successRate < 70) {
  lines.push('');
  lines.push(`⚠ Success rate below 70% — review run log for recurring blockers.`);
}

// Flag if no runs recently (last 2 days)
const recentCutoff = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
const veryRecent = runs.filter(r => new Date(r.timestamp) >= recentCutoff);
if (veryRecent.length === 0 && total > 0) {
  lines.push('');
  lines.push('⚠ No runs in the last 2 days — autopilot may be stalled.');
}

const message = lines.join('\n');

if (DRY_RUN) {
  console.log('--- DRY RUN (would send to telegram-mg-17796) ---');
  console.log(message);
  process.exit(0);
}

// Output JSON for the agent to pick up and send
console.log(JSON.stringify({ wakeAgent: true, data: { message, total, completedRuns, successRate } }));


Bash

node /workspace/agent/autopilot-calibrate.mjs --dry-run 2>&1

Test calibration script dry run


(node:60543) [UNDICI-EHPA] Warning: EnvHttpProxyAgent is experimental, expect them to change at any time.
(Use `node --trace-warnings ...` to show where the warning was created)
--- DRY RUN ---
📊 *Autopilot Weekly Calibration* (last 7 days)

Runs: 24 · Completed: 11 · Skipped: 13 · Success rate: 46%
All-time: 24 runs, 11 completed

*Active projects:*
  • nanoclaw: 6 runs
  • RoadSense: 1 run
  • StageHand: 1 run

*Headroom:* No quota snapshots yet — data accumulates as runs complete.

⚠ Success rate below 50% — review run log for recurring blockers.


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:

This is an ongoing agentic session for James Lopez's nanoclaw AI assistant platform. Multiple autopilot cycles ran on Jul 12, 2026:

- **Jul 12, 11:30 AM CT** — Autopilot trigger (83.3% burnable, high tier): congressional-trading Phase 1 (win_rate fixed-horizon fix)
- **Jul 12, 12:00 PM CT** — Autopilot trigger (57.5% burnable, high tier): congressional-trading Phase 2 (backtest sweep script)
- **Jul 12, 12:30 PM / 12:36 PM CT** — Duplicate autopilot triggers (75% burnable): congressional-trading Phase 3 (backfill + full sweep)
- **Jul 12, 5:00 PM CT** — Autopilot trigger (79.8% burnable, high tier): portfolio-autopilot closed-loop calibration (CURRENT)

All P1/P2 projects remain blocked on James. Congressional-trading is now also blocked (awaiting James's threshold decision). The only autonomous-ok project with a clear next action is **portfolio-autopilot** ("Closed-loop calibration — track whether James used Claude within 2h after autonomous run; adjust reserve floor if headroom was too tight").

2. Key Technical Concepts:

- **Portfolio Autopilot**: Quota-aware autonomous work loop; reads BOARD.md; writes skip-locks; logs to `autopilot-run-log.jsonl`
- **Closed-loop calibration**: Track post-run quota headroom; compare vs reserve floor; feed back into weekly calibration to recommend reserve_5h_pct / reserve_7d_pct adjustments
- **autopilot-quota-log.jsonl**: New log file capturing quota snapshots (pre_run from trigger, post_run from headroom script)
- **CodexBar**: `http://host.docker.internal:7842/usage` — quota tracking endpoint. Node `fetch` (undici) fails due to HTTPS proxy interception; must use Node's built-in `http` module
- **Congressional-trading backtest sweep**: 117 configurations (threshold 7-13 × conviction LOW/MEDIUM/HIGH × edge 2-4); full replay against 6,493 trades, 15 months, 960k price rows
- **Key finding**: Current production config (HIGH conviction, thresh=9.0, edge≥3) = 44% precision, -0.96% median excess vs SPY. Best: thresh=11.5, LOW conviction, edge≥2 → 54%, +0.67%
- **Surprising insight**: HIGH conviction signals underperform — LLM identifies obvious catalysts already priced in; base score more predictive than LLM overlay
- **price_history backfill**: Batched yfinance download (50 tickers/batch, 1,378 tickers, 39 seconds, 960,111 rows)
- **TDD pattern**: RED tests first (ImportError), then GREEN implementation
- **Run log inconsistency**: Some entries use `ts` field, some use `timestamp`; calibrate.mjs now normalizes both

3. Files and Code Sections:

- **`/workspace/agent/autopilot-headroom.mjs`** (CREATED)
  - New script for capturing post-run quota snapshots
  - Uses Node `http` module (NOT `fetch` — fetch intercepted by proxy)
  - Appends to `autopilot-quota-log.jsonl`
  - Supports `--type pre_run|post_run|check` and `--dry-run`
  - Key code:
```javascript
function httpGet(url) {
  return new Promise((resolve, reject) => {
    import("http").then(({ default: http }) => {
      const req = http.get(url, (res) => {
        let body = "";
        res.on("data", (c) => (body += c));
        res.on("end", () => {
          try { resolve(JSON.parse(body)); }
          catch (e) { reject(new Error(`JSON parse error: ${e.message}`)); }
        });
      });
      req.on("error", reject);
      req.setTimeout(5000, () => { req.destroy(); reject(new Error("timeout")); });
    });
  });
}
```
  - Output snapshot format:
```json
{
  "ts": "2026-07-12T17:32:44.867Z",
  "type": "post_run",
  "remaining_5h_pct": 82,
  "remaining_7d_pct": 92,
  "burnable_pct": 78.8,
  "reserve_5h_pct": 25,
  "reserve_7d_pct": 15,
  "headroom_5h_adequate": true,
  "headroom_7d_adequate": true
}
```

- **`/workspace/agent/autopilot-trigger.mjs`** (PATCHED)
  - Added `appendFileSync` to the `fs` import
  - Added pre-run quota snapshot logging before the `console.log(JSON.stringify({wakeAgent: true, ...}))` call:
```javascript
// Log pre-run quota snapshot for closed-loop calibration
try {
  const snapshot = {
    ts: new Date().toISOString(),
    type: "pre_run",
    remaining_5h_pct: w5h ? +remaining(w5h).toFixed(1) : null,
    remaining_7d_pct: w7d ? +remaining(w7d).toFixed(1) : null,
    burnable_pct: +burnable.toFixed(1),
    reserve_5h_pct: config.reserve_5h_pct,
    reserve_7d_pct: config.reserve_7d_pct,
    headroom_5h_adequate: w5h ? remaining(w5h) >= config.reserve_5h_pct : null,
    headroom_7d_adequate: w7d ? remaining(w7d) >= config.reserve_7d_pct : null,
  };
  appendFileSync(join(__dirname, "autopilot-quota-log.jsonl"), JSON.stringify(snapshot) + "\n");
} catch { /* non-fatal — don't block the wake */ }
```

- **`/workspace/agent/autopilot-calibrate.mjs`** (REWRITTEN)
  - Now reads both `autopilot-run-log.jsonl` AND `autopilot-quota-log.jsonl`
  - Normalizes `ts` vs `timestamp` field in run log entries
  - Counts completed outcomes broadly (phase1_done, phase2_done, etc.)
  - New headroom analysis section:
    - Reads `post_run` quota snapshots
    - Computes avg remaining_5h_pct, adequacy rate
    - Suggests raising reserve if adequacy < 80% and avg remaining < reserve+5
    - Suggests lowering reserve if adequacy = 100% and avg remaining > reserve+25
  - Dry run output:
```
📊 *Autopilot Weekly Calibration* (last 7 days)
Runs: 24 · Completed: 11 · Skipped: 13 · Success rate: 46%
All-time: 24 runs, 11 completed
*Active projects:*
  • nanoclaw: 6 runs
  • RoadSense: 1 run
  • StageHand: 1 run
*Headroom:* No quota snapshots yet — data accumulates as runs complete.
⚠ Success rate below 50% — review run log for recurring blockers.
```

- **`/workspace/extra/projects/congressional-trading/tools/win_rate.py`** (Phase 1, DONE)
  - Added `compute_fixed_horizon()` — uses price_history table, price at tx_date+30d
  - `compute()` dispatches to fixed-horizon when price_history populated; rolling fallback otherwise
  - Committed to `feat/agentic-alert-loop`

- **`/workspace/extra/projects/congressional-trading/tests/test_win_rate.py`** (Phase 1, DONE)
  - 8 TDD tests: `FixedHorizonTests` + `ComputeDispatchTests`, all GREEN

- **`/workspace/extra/projects/congressional-trading/scripts/backtest_sweep.py`** (Phase 2, DONE)
  - Sweeps score_threshold (7-13), conviction_min (LOW/MEDIUM/HIGH), edge_min (2/3/4)
  - Quick mode: signal_outcomes; Full mode: price_history replay
  - `--backfill` flag triggers price_history.py then full sweep
  - `--csv PATH` writes CSV

- **`/workspace/extra/projects/congressional-trading/data/backtest_sweep_2026-07-12.csv`** (Phase 3, DONE)
  - 117 config rows; full replay results
  - Current config: 44.4% precision, -0.96% median excess
  - Best config (thresh=11.5, LOW, edge≥2): 53.9% precision, +0.67% median excess, 398/yr

- **`/workspace/extra/second-brain/portfolio/projects/congressional-trading.md`** (UPDATED)
  - Phase 1, 2, 3 all marked ✅ DONE with details
  - Next action: "James to pick thresholds from sweep results → merge to main"
  - Blocked on James: "threshold decision"

- **`/workspace/extra/second-brain/portfolio/BOARD.md`** (UPDATED multiple times)
  - congressional-trading: "James to pick thresholds from sweep results → merge to main | threshold decision | 2026-07-12"

- **`/workspace/agent/autopilot-run-log.jsonl`** (UPDATED multiple times)
  - Note: inconsistent schema — older entries use `timestamp`, newer use `ts`. Calibrate.mjs now handles both.

4. Errors and Fixes:

- **`fetch` fails for CodexBar in `autopilot-headroom.mjs`**: Error was `HTTPParserError: Response does not match the HTTP/1.1 protocol` — an HTTPS proxy intercepts the request and returns TLS bytes. Fixed by replacing `fetch()` with Node's built-in `http` module (`import("http").then(...)`). Curl works fine; `http` module works fine; only undici/fetch is affected.

- **"File has not been read yet" errors** (from previous session): Trying to edit files not yet read. Fixed by reading before editing.

- **`python` not found in container**: Container uses `python3`. All Python commands use `python3`.

- **yfinance not installed**: Installed with `pip3 install yfinance --break-system-packages --trusted-host pypi.org --trusted-host files.pythonhosted.org`.

- **Run log schema inconsistency**: Some entries have `timestamp` field, some have `ts`. `autopilot-calibrate.mjs` now normalizes with `const _ts = r.timestamp || r.ts`.

- **Success rate miscounted in calibrate**: Original script only counted `outcome === 'completed'` but newer run log entries use `phase1_done`, `phase2_done`, etc. Fixed to count all non-skipped, non-unknown outcomes as completed.

5. Problem Solving:

- **Congressional-trading calibration (complete)**: All 3 phases done. Price history backfilled (960k rows, 39s via batched yfinance). Full backtest sweep shows current production config underperforms alternatives. Conviction filter is counterproductive — HIGH conviction signals have LOWER precision than LOW/MEDIUM, suggesting LLM-identified "obvious catalysts" are already priced in. Recommendation sent to James.

- **Portfolio-autopilot closed-loop (in progress)**: The missing piece was tracking post-run quota headroom. Built three-piece solution: headroom snapshot script, trigger pre-run logging, calibrate headroom analysis. The dry run confirms the calibrate script works.

- **All P1/P2 projects blocked**: The autopilot has been correctly picking P3 work (congressional-trading) and now portfolio-autopilot infrastructure work. Sent James a Telegram/Slack summary of the congressional-trading findings with two specific questions.

6. All User Messages:

- <task from="telegram-mg-17796" time="Jul 12, 2026, 11:30 AM">Portfolio Autopilot run triggered. burnable_pct: 83.3, tier: high... [congressional-trading Phase 1 cycle]</task>

- <task from="telegram-mg-17796" time="Jul 12, 2026, 12:00 PM">Portfolio Autopilot run triggered. burnable_pct: 57.5, tier: high... [congressional-trading Phase 2 cycle]</task>

- <system>Your response was not delivered — not wrapped in message blocks. All output must be wrapped...</system>

- <task from="telegram-mg-17796" time="Jul 12, 2026, 12:36 PM">Portfolio Autopilot run triggered. burnable_pct: 75, tier: high... next_reset_at: 2026-07-12T18:00:00.514Z... [congressional-trading Phase 3 cycle]</task>

- <task from="telegram-mg-17796" time="Jul 12, 2026, 12:30 PM">Portfolio Autopilot run triggered. burnable_pct: 75, tier: high... next_reset_at: 2026-07-12T18:00:01.044Z... [duplicate trigger, same cycle]</task>

- <task from="telegram-mg-17796" time="Jul 12, 2026, 5:00 PM">Portfolio Autopilot run triggered. burnable_pct: 79.8, tier: high, next_reset_at: 2026-07-18T20:59:59.520Z... [portfolio-autopilot closed-loop cycle, CURRENT]</task>

7. Pending Tasks:

- **Complete portfolio-autopilot closed-loop cycle** (in progress):
  - Run `node autopilot-headroom.mjs` (not dry-run) to log first real post-run quota snapshot
  - Update `/workspace/extra/second-brain/portfolio/projects/portfolio-autopilot.md` Next action
  - Update `/workspace/extra/second-brain/portfolio/BOARD.md` Last activity for portfolio-autopilot
  - Append entry to `/workspace/agent/autopilot-run-log.jsonl`
  - Schedule next reset-wake for `2026-07-18T20:59:59.520Z`
  - Send 2-3 line outcome note to `slack-infrastructure`

- **Congressional-trading**: Waiting for James to answer two questions:
  1. Drop `NOTIFY_MIN_CONVICTION` from HIGH to LOW (or remove)?
  2. Raise threshold from 9.0 to 11.5?
  Then merge `feat/agentic-alert-loop` → main

- **Decision queue still open**: nanoclaw metric, RoadSense 5 launch questions, StageHand 5 toolchain questions, various card confirmations

8. Current Work:

Working on **portfolio-autopilot closed-loop calibration** (Jul 12, 5:00 PM CT autopilot trigger, next_reset_at: 2026-07-18T20:59:59.520Z).

Three files were just created/modified:

1. **`/workspace/agent/autopilot-headroom.mjs`** — newly created, captures post-run quota snapshot:
```javascript
// Uses http module (not fetch — proxy intercepts fetch)
function httpGet(url) { ... }

async function main() {
  data = await httpGet(config.codexbar_url);
  // Computes remaining_5h_pct, remaining_7d_pct, burnable, headroom_adequate
  const snapshot = { ts, type, remaining_5h_pct, remaining_7d_pct, burnable_pct,
    reserve_5h_pct, reserve_7d_pct, headroom_5h_adequate, headroom_7d_adequate };
  appendFileSync(QUOTA_LOG_PATH, JSON.stringify(snapshot) + "\n");
}
```

2. **`/workspace/agent/autopilot-trigger.mjs`** — patched to write pre-run snapshot before waking agent

3. **`/workspace/agent/autopilot-calibrate.mjs`** — rewritten to read quota-log and add headroom analysis with reserve adjustment suggestions

Dry run just completed successfully:
```
📊 *Autopilot Weekly Calibration* (last 7 days)
Runs: 24 · Completed: 11 · Skipped: 13 · Success rate: 46%
...
*Headroom:* No quota snapshots yet — data accumulates as runs complete.
```

The next step is to finalize the cycle: run the headroom script to capture first real snapshot, update project card + BOARD.md, log the run, schedule reset-wake for 2026-07-18T20:59:59.520Z, send outcome note.

9. Optional Next Step:

Complete the portfolio-autopilot closed-loop calibration cycle. The immediate next steps (in order):

1. Run `node /workspace/agent/autopilot-headroom.mjs` (no --dry-run) to capture the first real post-run quota snapshot into `autopilot-quota-log.jsonl`
2. Update `/workspace/extra/second-brain/portfolio/projects/portfolio-autopilot.md` Next action to reflect what's been done and what's next (perhaps "Monitor headroom log; next calibration will have real data")
3. Update `BOARD.md` Last activity for portfolio-autopilot to 2026-07-12
4. Append to `autopilot-run-log.jsonl`
5. Call `mcp__nanoclaw__schedule_task` with `processAfter: "2026-07-18T20:59:59.520Z"` and `script: "node /workspace/agent/autopilot-trigger.mjs 2>/dev/null"`
6. Send 2-3 line outcome to `slack-infrastructure`

From the current task: "Next action: closed-loop calibration (track whether James used Claude within 2h after autonomous run; adjust reserve floor if headroom was too tight)"

The calibrate script dry run showed "No quota snapshots yet — data accumulates as runs complete" — meaning the headroom script needs to be actually run (non-dry-run) to begin building the dataset.

---

You MUST wrap all responses in <message to="name">...</message> blocks.
Available destinations: `cli-local`, `cw-coder`, `ingest-into-wiki`, `slack-agenticlever-website`, `slack-infrastructure`, `slack-mg-17835`, `slack-mg-17835-2`, `slack-mg-17835-3`, `slack-mg-17835-4`, `slack-mg-17835-5`, `slack-mg-17835-6`, `slack-qip-qien`, `slack-roadsense`, `slack-stagehand`, `slack-tradingbot25`, `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/033a4a00-9171-4e08-9a79-6b99f177355d.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.


/**
 * autopilot-trigger.mjs — scheduled trigger script for portfolio-autopilot
 *
 * Returns { wakeAgent: true, data } when burnable quota exists and eligible projects are ready.
 * Returns { wakeAgent: false, reason, next_reset_at? } otherwise.
 *
 * NOTE: CodexBar `pct` = percent USED. remaining = 100 - pct.
 */

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

import { readFileSync, existsSync, appendFileSync } 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")
);

// Skip-lock: agent writes this file after an all-blocked cycle to suppress
// repeated trigger wakes until the quota window resets.
const SKIP_LOCK_PATH = join(__dirname, "autopilot-skip-lock.json");
if (existsSync(SKIP_LOCK_PATH)) {
  try {
    const lock = JSON.parse(readFileSync(SKIP_LOCK_PATH, "utf8"));
    if (lock.skip_lock_until && new Date(lock.skip_lock_until) > new Date()) {
      console.log(JSON.stringify({ wakeAgent: false, reason: "skip_lock_active", skip_lock_until: lock.skip_lock_until }));
      process.exit(0);
    }
  } catch { /* malformed lock file — ignore and proceed */ }
}

function remaining(w) { return 100 - (w?.pct ?? 100); }
function effectiveResetsIn(w) { return (!w || w.resets_in_sec < 0) ? (w?.duration_sec ?? 0) : w.resets_in_sec; }
function nextResetAt(w) { if (!w) return null; return new Date(Date.now() + effectiveResetsIn(w) * 1000).toISOString(); }

function extractWindows(windows = []) {
  return {
    w5h: windows.find((w) => Math.abs(w.duration_sec - 18000) < 300) ?? null,
    w7d: windows.find((w) => Math.abs(w.duration_sec - 604800) < 600) ?? null,
  };
}

function computeBurnable(w, basePct) {
  if (!w) return 0;
  const rem = remaining(w);
  if (rem <= 0) return 0;
  const timeRatio = effectiveResetsIn(w) / w.duration_sec;
  return Math.max(0, rem - 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(w) {
  if (!w) return false;
  const elapsedHours = (w.duration_sec - effectiveResetsIn(w)) / 3600;
  const expectedConsumed = (100 / (w.duration_sec / 3600)) * elapsedHours;
  return (w.pct ?? 0) < expectedConsumed; // pct = used%
}

async function main() {
  let data;
  try {
    const res = await fetch(config.codexbar_url);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    data = await res.json();
  } catch (err) {
    console.log(JSON.stringify({ wakeAgent: false, reason: `codexbar_unreachable: ${err.message}` }));
    return;
  }

  const providers = data.providers ?? [];
  const claudeProvider = providers.find((p) => /claude/i.test(p.name ?? "")) ?? null;
  const { w5h, w7d } = extractWindows(claudeProvider?.windows ?? []);

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

  // Next reset time — for the constraining window, so the agent can schedule an immediate wake
  const constraintWindow = burnable5h <= burnable7d ? w5h : w7d;
  const next_reset_at = nextResetAt(constraintWindow);

  // All-provider snapshot (for context in the agent prompt)
  const trackedSnapshot = providers.map((p) => {
    const { w5h: p5h, w7d: p7d } = extractWindows(p.windows ?? []);
    return {
      name: p.name,
      tracked: true,
      "5h_remaining_pct": p5h ? remaining(p5h) : null,
      "7d_remaining_pct": p7d ? remaining(p7d) : null,
    };
  });
  const untrackedSnapshot = (config.untracked_providers ?? []).map((p) => ({
    name: p.name,
    tracked: false,
    note: p.note,
  }));
  const providerSnapshot = [...trackedSnapshot, ...untrackedSnapshot];

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

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

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

  // Log pre-run quota snapshot for closed-loop calibration
  try {
    const snapshot = {
      ts: new Date().toISOString(),
      type: "pre_run",
      remaining_5h_pct: w5h ? +remaining(w5h).toFixed(1) : null,
      remaining_7d_pct: w7d ? +remaining(w7d).toFixed(1) : null,
      burnable_pct: +burnable.toFixed(1),
      reserve_5h_pct: config.reserve_5h_pct,
      reserve_7d_pct: config.reserve_7d_pct,
      headroom_5h_adequate: w5h ? remaining(w5h) >= config.reserve_5h_pct : null,
      headroom_7d_adequate: w7d ? remaining(w7d) >= config.reserve_7d_pct : null,
    };
    appendFileSync(join(__dirname, "autopilot-quota-log.jsonl"), JSON.stringify(snapshot) + "\n");
  } catch { /* non-fatal — don't block the wake */ }

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

main();


#!/usr/bin/env node
/**
 * autopilot-headroom.mjs — post-run quota snapshot for closed-loop calibration
 *
 * Call this at the END of each autopilot agent turn to capture how much quota
 * remains after the run. autopilot-calibrate.mjs reads these snapshots to
 * detect whether the reserve floor is set correctly.
 *
 * Usage:
 *   node autopilot-headroom.mjs [--type post_run|pre_run|check]
 *   node autopilot-headroom.mjs --dry-run
 *
 * Appends one JSON line to autopilot-quota-log.jsonl.
 * Prints a summary to stdout.
 */

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

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

const __dir = dirname(fileURLToPath(import.meta.url));
const CONFIG_PATH = join(__dir, "autopilot-config.json");
const QUOTA_LOG_PATH = join(__dir, "autopilot-quota-log.jsonl");

const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
const DRY_RUN = process.argv.includes("--dry-run");
const TYPE = (() => {
  const idx = process.argv.indexOf("--type");
  return idx >= 0 ? process.argv[idx + 1] : "post_run";
})();

function remaining(w) { return 100 - (w?.pct ?? 100); }
function effectiveResetsIn(w) {
  return (!w || w.resets_in_sec < 0) ? (w?.duration_sec ?? 0) : w.resets_in_sec;
}
function computeBurnable(w, basePct) {
  if (!w) return 0;
  const rem = remaining(w);
  if (rem <= 0) return 0;
  const timeRatio = effectiveResetsIn(w) / w.duration_sec;
  return Math.max(0, rem - basePct * timeRatio);
}
function extractWindows(windows = []) {
  return {
    w5h: windows.find((w) => Math.abs(w.duration_sec - 18000) < 300) ?? null,
    w7d: windows.find((w) => Math.abs(w.duration_sec - 604800) < 600) ?? null,
  };
}

function httpGet(url) {
  return new Promise((resolve, reject) => {
    import("http").then(({ default: http }) => {
      const req = http.get(url, (res) => {
        let body = "";
        res.on("data", (c) => (body += c));
        res.on("end", () => {
          try { resolve(JSON.parse(body)); }
          catch (e) { reject(new Error(`JSON parse error: ${e.message}`)); }
        });
      });
      req.on("error", reject);
      req.setTimeout(5000, () => { req.destroy(); reject(new Error("timeout")); });
    });
  });
}

async function main() {
  let data;
  try {
    data = await httpGet(config.codexbar_url);
  } catch (err) {
    console.error(`CodexBar unreachable: ${err.message}`);
    process.exit(1);
  }

  const providers = data.providers ?? [];
  const claude = providers.find((p) => /claude/i.test(p.name ?? "")) ?? null;
  const { w5h, w7d } = extractWindows(claude?.windows ?? []);

  const remaining5h = w5h ? remaining(w5h) : null;
  const remaining7d = w7d ? remaining(w7d) : null;
  const burnable5h = w5h ? computeBurnable(w5h, config.reserve_5h_pct) : null;
  const burnable7d = w7d ? computeBurnable(w7d, config.reserve_7d_pct) : null;
  const burnable = (burnable5h !== null && burnable7d !== null)
    ? Math.min(burnable5h, burnable7d) : (burnable5h ?? burnable7d ?? 0);

  const snapshot = {
    ts: new Date().toISOString(),
    type: TYPE,
    remaining_5h_pct: remaining5h !== null ? +remaining5h.toFixed(1) : null,
    remaining_7d_pct: remaining7d !== null ? +remaining7d.toFixed(1) : null,
    burnable_pct: +burnable.toFixed(1),
    reserve_5h_pct: config.reserve_5h_pct,
    reserve_7d_pct: config.reserve_7d_pct,
    // headroom_adequate: true if remaining was above reserve (James had buffer available)
    headroom_5h_adequate: remaining5h !== null ? remaining5h >= config.reserve_5h_pct : null,
    headroom_7d_adequate: remaining7d !== null ? remaining7d >= config.reserve_7d_pct : null,
  };

  const summary = [
    `[headroom/${TYPE}] 5h: ${remaining5h?.toFixed(1) ?? "?"}% remaining`,
    `(reserve=${config.reserve_5h_pct}%, adequate=${snapshot.headroom_5h_adequate})`,
    `| 7d: ${remaining7d?.toFixed(1) ?? "?"}%`,
    `(reserve=${config.reserve_7d_pct}%, adequate=${snapshot.headroom_7d_adequate})`,
    `| burnable=${burnable.toFixed(1)}%`,
  ].join(" ");

  console.log(summary);

  if (!DRY_RUN) {
    appendFileSync(QUOTA_LOG_PATH, JSON.stringify(snapshot) + "\n");
  } else {
    console.log("[dry-run] would append:", JSON.stringify(snapshot));
  }
}

main();


# 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) | build | autonomous-ok | Closed-loop calibration (post-run headroom tracking) | none | 2026-07-08 |
| [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 | James to pick thresholds from sweep results → merge to main | threshold decision | 2026-07-12 |
| [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 | Await James answers on 5 toolchain questions; Local Recs next if green-lit | voice, agent, posting, pricing, Local Recs y/n | 2026-07-09 |
| [MentorCore](projects/MentorCore.md) | research | autonomous-ok | Parked — no bandwidth | none | 2026-06-11 |
| [RoadSense](projects/RoadSense.md) | spec | autonomous-ok | Answer 5 launch questions → unblock implementation | auth model, city, GIS license, creator posture, metric | 2026-07-08 |
| [nanoclaw](projects/nanoclaw.md) | build | autonomous-ok | James to run deploy in Studio terminal; confirm metric | none | 2026-07-08 |

## Decision queue

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

- **nanoclaw metric:** Proposed metric = "decisions James answers per week from daily brief" (proxy for brief usefulness). Confirm or rename? (Card currently undefined.)
- **RoadSense launch questions (5):** auth model, generalization city, GIS license, creator posture, 30-day success metric. Sent via Telegram 2026-07-08.

## Recently shipped

- **nanoclaw** 2026-07-08 — fix(router): mention-sticky Slack session scoping committed


# congressional-trading

- Repo: `~/projects/congressional-trading` · https://github.com/jamesjlopez/congressional-trading
- Goal: Score congressional trade disclosures (0–13 scale) and push high-conviction buy signals via ntfy when score ≥ 9 + quality gates pass. In shadow/measurement mode — scoring and logging signals but not pushing live notifications yet.
- Stage: test
- Guard: autonomous-ok
- Metric: Signal precision (% of signals that are wins at 30/60d) over shadow period — threshold TBD by James
- Blocked on James: define exit criteria — what win-rate and sample size graduates it to live?
- Next action: James to review sweep findings and decide: (a) drop conviction gate LOW/MEDIUM? (b) raise threshold to 11.5? Then merge feat/agentic-alert-loop → main

## Notes
- 2026-07-06 — Seeded from git activity: "Keep shadow measurement active during..." (same-day commit). Runs on always-on machine.
- 2026-07-06 — Explored codebase: scoring algorithm is 0–13 scale with 8 components (committee, cluster, size, small-cap bonus, options bonus, sell pressure, strategic LLM score, disruption LLM score). Notification threshold = 9.0 after timeliness multiplier. Max 1 notification/run. 14k+ historical trades stored. `signal_outcomes` table tracks 30/60d returns. Key open issue: win-rate uses current price snapshot, not fixed-horizon (backlog item). Forward-looking predictor module also exists.
- 2026-07-06 — James approved agentic calibration approach: backtest first → data-derived thresholds → ongoing automated monitoring. Scoped as 3 phases below.

### Calibration task — 3 phases

**Phase 1: Fix win-rate calculation** ✅ DONE 2026-07-12
- Added `compute_fixed_horizon()` to `tools/win_rate.py`: uses `price_history` table (price at tx_date+30d vs tx_date) — stable classification, not today's rolling price
- `compute()` dispatches to fixed-horizon when `price_history` is populated; rolling fallback for fresh installs
- 8 TDD tests added to `tests/test_win_rate.py`, all GREEN
- Committed: `feat/agentic-alert-loop` — "fix(win_rate): use fixed-horizon returns instead of rolling price snapshot"

**Phase 2: Backtest sweep script** ✅ DONE 2026-07-12
- `scripts/backtest_sweep.py` added. Sweeps score threshold (7–13), conviction (LOW/MEDIUM/HIGH), edge-factor min (2/3/4).
- Quick mode: reads signal_outcomes (n=36, no price_history needed). Full mode: replays 7k+ trades from price_history.
- Quick mode ran: signals scored 10.8–14.5. Threshold 12.5+ improves precision 58%→67% and cuts max drawdown from -40.7% to -7.7%.
- **Price_history backfill needed for full sweep** (python scripts/backtest_sweep.py --backfill --csv results.csv) — yfinance not installed in container.
- Committed: `feat/agentic-alert-loop` — "feat(backtest): add threshold/conviction/edge sweep script"

**Phase 3: Backfill + full sweep** ✅ DONE 2026-07-12
- Batched yfinance backfill: 960k price rows across 1,378 tickers in 39s
- Ran full replay sweep: 6,493 trades, 15 months, 117 config combinations
- CSV saved to `data/backtest_sweep_2026-07-12.csv`
- **Key finding**: Current config (HIGH conviction, thresh=9.0, edge≥3) = 44% precision, -0.96% median excess vs SPY
- **Best config**: threshold=11.5, LOW conviction, edge≥2 → 54% precision, +0.67% median excess, 398 signals/year
- **Surprising**: HIGH conviction = WORSE performance. Unresearched signals at high base scores outperform LLM-tagged HIGH conviction ones — suggesting base score is more predictive than LLM overlay.
- Results sent to James via Slack 2026-07-12. Waiting for threshold decision to implement config changes.

**Phase 4: Ongoing automated outcome monitoring** (closes the loop)
- Schedule a weekly Nano task: read `signal_outcomes`, compute rolling 30/60d precision for last 10/20/all-time signals
- If precision drops below threshold: surface tuning suggestion to James via Telegram
- If precision is holding: brief weekly note in daily brief or silence
- Estimated effort: ~2 hours to set up scheduled task


<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, slack-agenticlever-website, slack-infrastructure, slack-mg-17835, slack-mg-17835-2, slack-mg-17835-3, slack-mg-17835-4, slack-mg-17835-5, slack-mg-17835-6, slack-qip-qien, slack-roadsense, slack-stagehand, slack-tradingbot25, telegram-mg-17796, telegram-mg-17800. Please re-send your response with the correct wrapping.</system>


Let me pick up where the previous session left off. I need to complete the portfolio-autopilot closed-loop calibration cycle. The steps are:

1. Run `node /workspace/agent/autopilot-headroom.mjs` (no --dry-run) to capture the first real post-run quota snapshot
2. Update portfolio-autopilot.md Next action
3. Update BOARD.md Last activity for portfolio-autopilot
4. Append to autopilot-run-log.jsonl
5. Schedule reset-wake for 2026-07-18T20:59:59.520Z
6. Send outcome note to slack-infrastructure

I'll start by running the headroom script and handle the other updates in parallel where I can.


ToolSearch

Question: select:SendMessage,TodoWrite,mcp__nanoclaw__schedule_task


Question: select:SendMessage,TodoWrite,mcp__nanoclaw__schedule_task


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, slack-agenticlever-website, slack-infrastructure, slack-mg-17835, slack-mg-17835-2, slack-mg-17835-3, slack-mg-17835-4, slack-mg-17835-5, slack-mg-17835-6, slack-qip-qien, slack-roadsense, slack-stagehand, slack-tradingbot25, telegram-mg-17796, telegram-mg-17800. Please re-send your response with the correct wrapping.</system>


Let me complete the portfolio-autopilot closed-loop calibration cycle. I need to:
1. Run `node /workspace/agent/autopilot-headroom.mjs` to capture the first real post-run quota snapshot
2. Update portfolio-autopilot.md
3. Update BOARD.md
4. Append to autopilot-run-log.jsonl
5. Schedule reset-wake for 2026-07-18T20:59:59.520Z
6. Send outcome note to slack-infrastructure

Let me start with running the headroom script and doing the other things in parallel.


TodoWrite

Run autopilot-headroom.mjs to capture first real post-run quota snapshot

Update portfolio-autopilot.md and BOARD.md

Append run entry to autopilot-run-log.jsonl

Schedule next reset-wake for 2026-07-18T20:59:59.520Z

Send outcome note to slack-infrastructure


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Run autopilot-headroom.mjs to capture first real post-run quota snapshot

Update portfolio-autopilot.md and BOARD.md

Append run entry to autopilot-run-log.jsonl

Schedule next reset-wake for 2026-07-18T20:59:59.520Z

Send outcome note to slack-infrastructure


Bash

Run headroom script to capture first real post-run quota snapshot


(node:60614) [UNDICI-EHPA] Warning: EnvHttpProxyAgent is experimental, expect them to change at any time.
(Use `node --trace-warnings ...` to show where the warning was created)
[headroom/post_run] 5h: 73.0% remaining (reserve=25%, adequate=true) | 7d: 92.0% (reserve=15%, adequate=true) | burnable=71.0%


The headroom script ran successfully. Now let me read the current state of the files I need to update and then do the rest of the steps.


TodoWrite

Run autopilot-headroom.mjs to capture first real post-run quota snapshot

Update portfolio-autopilot.md and BOARD.md

Append run entry to autopilot-run-log.jsonl

Schedule next reset-wake for 2026-07-18T20:59:59.520Z

Send outcome note to slack-infrastructure


1	# Portfolio Autopilot
2
3	- Repo: n/a — lives in Nano's scheduled tasks + config
4	- 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.
5	- Stage: build
6	- Guard: autonomous-ok
7	- Metric: portfolio stages advanced per week at ≤ 15% of James's interactive quota consumed
8	- Blocked on James: none
9	- Next action: closed-loop calibration (track whether James used Claude within 2h after autonomous run; adjust reserve floor if headroom was too tight)
10
11	## Design
12
13	### Three levers (all configurable)
14
15	| Lever | Default | Purpose |
16	|---|---|---|
17	| Base reserve (5h window) | 25% | Interactive buffer — scales down as reset approaches |
18	| Base reserve (7d window) | 15% | Interactive buffer — scales down as reset approaches |
19
20	**No per-session cap.** If there's burnable quota and work to do, use it.
21
22	### Adaptive reserve formula
23
24	```
25	adaptive_reserve_pct = base_reserve_pct × (resets_in_sec / window_duration_sec)
26	burnable_pct         = remaining_pct − adaptive_reserve_pct
27	```
28
29	This means:
30	- Start of window: full reserve held
31	- Mid-window: reserve shrinks proportionally
32	- Near reset: reserve → 0 (almost all remaining quota is burnable)
33
34	Applied to both windows independently; take the more conservative (lower burnable) as the constraint:
35	```
36	burnable = min(burnable_5h, burnable_7d)
37	```
38
39	Work runs only when `burnable > 0`.
40
41	### Task types + model assignment
42
43	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.
44
45	| Task type | Model | Effort | Notes |
46	|---|---|---|---|
47	| Plan (greenfield) | Claude Sonnet | high | Always capable model — non-negotiable |
48	| Red-team review | Claude Sonnet | medium | Auto-runs after every plan, before human sees it |
49	| Plan (continuation) | Claude Sonnet | medium | Existing direction, lower risk |
50	| Implementation | Claude Sonnet or Haiku | medium | Quota-tier dependent |
51	| Research / analysis | Gemini or Haiku | low | Bulk work, quota-saving |
52	| Maintenance | LM Studio or Haiku | low | Formatting, summaries, simple edits |
53
54	### Human-in-the-loop checkpoints
55
56	**Greenfield work** (new plan, new feature, new project direction):
57	```
58	Plan → Auto red-team → Package (plan + critique + open questions) → Send to James → James approves → Implementation
59	```
60	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.
61
62	**Continuing work** (clear next action on an existing spec):
63	```
64	Implement → Report outcome → James can redirect if needed
65	```
66	No checkpoint required for `autonomous-ok` projects with a well-defined next action.
67
68	**review-required projects** (TradingBot25, qip-qien):
69	All changes go as PRs; James approves merges. No auto-merge ever.
70
71	### Quota tiers → implementation model routing
72
73	Planning always uses capable model (see above). Implementation routing adapts to quota:
74
75	| Burnable quota | Implementation model | Projects allowed |
76	|---|---|---|
77	| High (>50% burnable) | Claude Sonnet | P1, P2, P3 |
78	| Medium (20–50%) | Claude Sonnet | P1, P2 |
79	| Low (5–20%) | Gemini or Haiku | P1 only |
80	| Minimal (<5%) | LM Studio | maintenance only |
81	| Zero | No autonomous work | — |
82
83	### Burn-rate gate (for P2/P3)
84
85	`sustainable_pct_per_hour = 100 / window_hours`
86
87	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).
88
89	### Priority tiers (proposed — James to confirm)
90
91	| Project | Priority | Rationale |
92	|---|---|---|
93	| StageHand | P1 | James's priority — real estate video suite, active discovery |
94	| nanoclaw | P1 | Maintenance + ongoing infra work |
95	| AgenticLever | P2 | Business-facing, active build |
96	| RoadSense | P2 | Going public, active momentum |
97	| qip-qien | P2 | Live trial — needs progress but review-required gate limits autonomous scope |
98	| congressional-trading | P3 | Runs autonomously; calibration is in-flight, low need for new LLM spend |
99	| TradingBot25 | P3 | Deep investment already; review-required, low autonomous-work surface |
100
101	### Closed-loop auto-calibration
102
103	Track per run:
104	- Quota spent (from CodexBar delta before/after)
105	- Output produced (git commits, wiki pages, board moves)
106	- Did James use Claude within 2h after an autonomous run? (proxy for "left enough headroom")
107
108	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.
109
110	The loop surfaces a weekly "autopilot health" note (not a full brief — one line per signal that changed).
111
112	### Config file
113
114	`/workspace/agent/autopilot-config.json` — all thresholds live here so any run can tweak them without code changes.
115
116	```json
117	{
118	  "reserve_5h_pct": 25,
119	  "reserve_7d_pct": 15,
120
121	  "project_priorities": {
122	    "StageHand": "P1",
123	    "nanoclaw": "P1",
124	    "AgenticLever": "P2",
125	    "RoadSense": "P2",
126	    "qip-qien": "P2",
127	    "congressional-trading": "P3",
128	    "TradingBot25": "P3"
129	  },
130	  "flush_threshold": 60,
131	  "comfortable_threshold": 40,
132	  "moderate_threshold": 20
133	}
134	```
135
136	## Implementation plan
137
138	1. **Config + quota reader** — `autopilot-config.json` + script that reads CodexBar endpoint and computes current tier and burn rate
139	2. **Routing logic** — given tier + priority + project next-action, decide: proceed / skip / use alternate model
140	3. **Task executor** — runs one unit of work per session, commits result, updates board
141	4. **Scheduled trigger** — every 30 min, runs quota check; fires task if conditions met
142	5. **Outcome tracker** — logs quota delta + output produced per run
143	6. **Weekly calibration** — reviews outcome log, suggests threshold changes
144
145	## Notes
146
147	- 2026-07-07 — James approved design. Start with intelligent defaults, evolve to closed-loop. Config file approach so thresholds are tweakable without code changes.
148	- 2026-07-07 — James set priorities (inverted from initial proposal): P1 = StageHand, nanoclaw; P2 = AgenticLever, RoadSense, qip-qien; P3 = congressional-trading, TradingBot25.
149	- 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.
150	- 2026-07-07 — Core build complete: `autopilot-config.json` (thresholds + priority map), `autopilot.mjs` (quota reader + routing logic, human-readable + JSON mode), `autopilot-trigger.mjs` (pre-script for scheduler — returns wakeAgent true/false). Scheduled task `task-1783433888329-w9msxw` runs every 30 min.
151	- 2026-07-08 — Outcome tracker built: `autopilot-summary.mjs` (stats by outcome/project/tier/model, recent history, --json + --last N flags). Weekly calibration built: `autopilot-calibrate.mjs` (7-day stats, sends Telegram summary). Scheduled weekly Mondays 08:00 CT.
152	- 2026-07-08 — Skip-lock added to `autopilot-trigger.mjs`: agent writes `autopilot-skip-lock.json` after all-blocked cycles to suppress repeated 30-min trigger wakes. Prevents wasted cycles when all projects are blocked on James.
153	- CodexBar endpoint: `http://host.docker.internal:7842/usage` — confirmed live and accessible from container.
154	- LM Studio confirmed live at `http://host.docker.internal:1234` with Gemma 4 12B + 26B models loaded.
155

# 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: build
- Guard: autonomous-ok
- Metric: portfolio stages advanced per week at ≤ 15% of James's interactive quota consumed
- Blocked on James: none
- Next action: closed-loop calibration (track whether James used Claude within 2h after autonomous run; adjust reserve floor if headroom was too tight)

## 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.
- 2026-07-07 — Core build complete: `autopilot-config.json` (thresholds + priority map), `autopilot.mjs` (quota reader + routing logic, human-readable + JSON mode), `autopilot-trigger.mjs` (pre-script for scheduler — returns wakeAgent true/false). Scheduled task `task-1783433888329-w9msxw` runs every 30 min.
- 2026-07-08 — Outcome tracker built: `autopilot-summary.mjs` (stats by outcome/project/tier/model, recent history, --json + --last N flags). Weekly calibration built: `autopilot-calibrate.mjs` (7-day stats, sends Telegram summary). Scheduled weekly Mondays 08:00 CT.
- 2026-07-08 — Skip-lock added to `autopilot-trigger.mjs`: agent writes `autopilot-skip-lock.json` after all-blocked cycles to suppress repeated 30-min trigger wakes. Prevents wasted cycles when all projects are blocked on James.
- 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.


Now I need to update the portfolio-autopilot.md card to reflect what's been done and what's next, and update the BOARD.md. Let me also read the current BOARD.md row for portfolio-autopilot.

The headroom snapshot was successfully taken:
- 5h: 73.0% remaining (reserve=25%, adequate=true)
- 7d: 92.0% (reserve=15%, adequate=true)
- burnable=71.0%

Now I need to:
1. Update portfolio-autopilot.md:
   - Next action: "Monitor headroom log as data accumulates; weekly calibration (Mondays 08:00 CT) will recommend reserve adjustments after 5+ post-run snapshots"
   - Add a note about today's work
2. Update BOARD.md last activity to 2026-07-12
3. Append run entry to autopilot-run-log.jsonl
4. Schedule next reset-wake for 2026-07-18T20:59:59.520Z (the 7d window)
5. Send outcome note to slack-infrastructure

Let me update the files in parallel.


Edit

- Blocked on James: none
- Next action: closed-loop calibration (track whether James used Claude within 2h after autonomous run; adjust reserve floor if headroom was too tight)

- Blocked on James: none
- Next action: Monitor headroom log as post-run data accumulates; weekly calibration (Mondays 08:00 CT) will surface reserve adjustment suggestions once 5+ snapshots exist


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)

- Blocked on James: none
- Next action: Monitor headroom log as post-run data accumulates; weekly calibration (Mondays 08:00 CT) will surface reserve adjustment suggestions once 5+ snapshots exist

# 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: build
- Guard: autonomous-ok
- Metric: portfolio stages advanced per week at ≤ 15% of James's interactive quota consumed
- Blocked on James: none
- Next action: closed-loop calibration (track whether James used Claude within 2h after autonomous run; adjust reserve floor if headroom was too tight)

## 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.
- 2026-07-07 — Core build complete: `autopilot-config.json` (thresholds + priority map), `autopilot.mjs` (quota reader + routing logic, human-readable + JSON mode), `autopilot-trigger.mjs` (pre-script for scheduler — returns wakeAgent true/false). Scheduled task `task-1783433888329-w9msxw` runs every 30 min.
- 2026-07-08 — Outcome tracker built: `autopilot-summary.mjs` (stats by outcome/project/tier/model, recent history, --json + --last N flags). Weekly calibration built: `autopilot-calibrate.mjs` (7-day stats, sends Telegram summary). Scheduled weekly Mondays 08:00 CT.
- 2026-07-08 — Skip-lock added to `autopilot-trigger.mjs`: agent writes `autopilot-skip-lock.json` after all-blocked cycles to suppress repeated 30-min trigger wakes. Prevents wasted cycles when all projects are blocked on James.
- 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.

-- Next action: closed-loop calibration (track whether James used Claude within 2h after autonomous run; adjust reserve floor if headroom was too tight)

+- Next action: Monitor headroom log as post-run data accumulates; weekly calibration (Mondays 08:00 CT) will surface reserve adjustment suggestions once 5+ snapshots exist


- 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.

- 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-12 — Closed-loop calibration wired up. Three new components: `autopilot-headroom.mjs` (post-run quota snapshot logger), pre-run snapshot added to `autopilot-trigger.mjs`, and headroom analysis section added to `autopilot-calibrate.mjs`. Snapshots track remaining_5h/7d_pct vs reserve floor; calibration script suggests raising/lowering reserve based on 7-day sample. First post-run snapshot captured: 5h=73% remaining (adequate), 7d=92% (adequate), burnable=71%.