Weekly upstream sync 2026-07-26 (247 commits) - #24
loganbronstein wants to merge 250 commits into
Conversation
When restart-all fires (e.g. dashboard restart), stop() and start() are called near-simultaneously for each agent. startAgent() previously bailed with 'already running' if the agent was still in the registry mid-stop, leaving the agent stopped with no recovery. Fix: add a pendingRestarts set. startAgent() queues the name instead of bailing. stopAgent() checks the set after cleanup and re-launches the agent, making restart-all race-free. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude Code writes ANSI escape codes (spinner, cursor movement) to stdout constantly even when idle/waiting for input. This made stdout.log always grow between polls, causing isAgentActive() to always return true and showing a permanent "typing..." indicator to the user (issue grandamenium#219). The hook-based path below the size check is the correct signal: - lastMessageInjectedAt: set when fast-checker injects a Telegram message - last_idle.flag: written by the Stop hook when Claude finishes a turn - Typing = injection after last idle, within 10 minutes Remove the stdout.log check entirely and rely on the hook-based approach. This gives accurate per-turn typing indicators with zero false positives. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(daemon): queue pending restarts when stop+start race on restart-all
fix(fast-checker): remove stdout.log size check from isAgentActive() — fixes permanent typing indicator
…tation PR #2 removed the stdout.log size check from isAgentActive() but the test still expected the old file-growth behavior. Updated tests to match the new hook-based approach (lastMessageInjectedAt + last_idle.flag). Added two new tests covering the hook-based path: - returns true when message injected and no idle flag yet - returns false when idle flag is newer than last injection Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
test(fast-checker): sync isAgentActive tests with hook-based implementation
The .env file in ~/.cortextos/<instance>/ was being written with the
default mode (typically 644, world-readable). Although the parent
directory is 700 so cross-user access is blocked, .env files
conventionally hold secrets and any future feature writing a token
or credential to this file would silently expose it to any local
process running as the same user.
The fix mirrors the existing pattern used a few lines below for
bus-signing-key (line 269) and dashboard.env (line 306): one chmod
call wrapped in a try/catch to ignore the no-op on Windows.
Verified locally:
- npm test → 382/382 passed
- chmodSync was already imported
- existing .env files are unaffected (the chmod is inside the
`if (!existsSync(envPath))` block)
Refs: BUG-001 in our internal tracker. Found during a fresh-install
audit on a clean macOS install.
Co-authored-by: grandamenium <noreply@anthropic.com>
…ents (#8) The daemon's discoverAndStart loop scans the framework orgs/ directory for agent dirs and starts each one, but never read the instance-level enabled-agents.json that the CLI's `cortextos enable`/`disable` commands and the dashboard's lifecycle API write to. Result: a user could `cortextos disable foo`, see no error, restart the daemon, and find that foo was running again — the disable was a no-op across restarts. The two views never agreed. Symmetrically, `cortextos list-agents` (via `bus/agents.ts:listAgents`) treated enabled-agents.json as authoritative and skipped the directory scan entirely if the file existed. This caused list-agents to miss any agent that the daemon had discovered on disk but that wasn't in the file (e.g., an agent created via `cortextos add-agent` against a different instance, or a directory added manually). This commit aligns both sides on the same logical model: The framework orgs/ directory is the canonical "what exists" set. enabled-agents.json provides explicit user-set overrides on top of that. Default for a discovered agent with no entry in the file is enabled. Two surgical changes: 1. src/daemon/agent-manager.ts: discoverAndStart now reads ${ctxRoot}/config/enabled-agents.json and skips any discovered agent whose entry has `enabled: false`. The existing per-agent config.json `enabled: false` check is preserved (it takes precedence — both gates are checked). A new private helper readInstanceEnableList() encapsulates the file read with safe fallback on missing/corrupt file. 2. src/bus/agents.ts: listAgents now always scans orgs/ directories. The "skip dir scan if enabled-agents.json exists" branch is removed. The file is still read and used to override the default enabled state on each discovered agent. Stale file entries (have an entry but no matching dir) are appended at the end so users can see them and clean them up. Tests added (7 new, all passing locally): tests/unit/bus/agents.test.ts: - shows agents from dir scan even when enabled-agents.json exists - respects enabled: false from enabled-agents.json for agents in dir scan tests/unit/daemon/agent-manager.test.ts (NEW file): - skips agents marked enabled: false in enabled-agents.json - starts all discovered agents when enabled-agents.json is missing - starts all discovered agents when enabled-agents.json is empty {} - still respects per-agent config.json enabled: false (existing behavior) - handles corrupt enabled-agents.json by defaulting to enabled-all Total test count: 382 → 389. All passing. What this PR does NOT change: - cortextos enable/disable: keep writing to enabled-agents.json (now actually respected by the daemon) - cortextos install: keeps creating an empty enabled-agents.json - cortextos add-agent: keeps writing to the file - dashboard: keeps writing to the file via existing API routes - All IPC handlers in src/daemon/ipc-server.ts: unchanged - The schema of enabled-agents.json: unchanged - All other CLI subcommands: unchanged Backward compatible. No migration. Existing installs benefit from the fix without any user action — disable now actually persists across daemon restarts. Refs: BUG-028 in our internal tracker. The structural root cause behind several other entries (BUG-024, BUG-025, possibly BUG-009). Co-authored-by: grandamenium <noreply@anthropic.com>
…es (#9) The canonical curl install pulls install.mjs from a specific branch URL, but install.mjs itself unconditionally clones the default branch (`main`) from CORTEXTOS_REPO. This means there's no way to test a fix in isolation via the curl install path before it merges to main — you have to either manually `git clone --branch` and run install.mjs locally, or wait until your fix is merged to verify it via the canonical install path. This commit adds CORTEXTOS_BRANCH support, mirroring the existing CORTEXTOS_REPO and CORTEXTOS_DIR overrides: CORTEXTOS_BRANCH=fix/some-fix curl -fsSL \ https://raw.githubusercontent.com/grandamenium/cortextos/fix/some-fix/install.mjs \ | node Both URL components (the branch in the curl path AND the env var) point at the same branch — install.mjs is fetched FROM that branch, and then clones it via `git clone --branch ${REPO_BRANCH}`. The branch name is validated against a strict regex (a-zA-Z0-9._/-) to prevent shell injection via the env var. Default behavior unchanged: without CORTEXTOS_BRANCH set, install.mjs clones `main` exactly as before. Backward compatible. This is a methodology improvement that lets us run the standard 2-cycle test loop (install pre-fix from main → merge → install post-fix from main) against any branch in 1 install command instead of needing a 3-step manual git clone workaround for each branch test. Verified locally: - npm test → 389/389 passing (no tests for install.mjs itself; this just confirms nothing else broke) - The diff is text-only changes to install.mjs - Branch validation regex matches all standard git ref characters Co-authored-by: grandamenium <noreply@anthropic.com>
…#10) The /onboarding skill had two related bugs that produced split-brain state on every clean install: BUG-029: every CLI subcommand defaults --instance to literal 'default' when neither --instance nor CTX_INSTANCE_ID is set. The skill set a local INSTANCE_ID variable but never exported it, and only passed --instance to `init` (line 192). add-agent (line 300), enable (line 333), and ecosystem (line 470) all silently wrote to ~/.cortextos/default/ even though the user's actual instance was cortextos1 (or higher). This left the agent registration in default's enabled-agents.json while the daemon ran on cortextos1, splitting the user's view from the daemon's view. BUG-017: the auto-instance-numbering loop unconditionally picked cortextosN even though `cortextos install` (called earlier in the flow) always creates an empty default/ instance. Result: every install ended with TWO instance dirs — an orphaned default/ that nothing used, and a cortextos1/ with the actual state. Repeating the install accumulated orphans (default, cortextos1, cortextos2, ...). The two bugs reinforced each other. With BUG-029 the wrong-instance writes scattered state across the orphan default/ created by BUG-017. Fixes: 1. Auto-numbering now reuses default/ if it exists and is empty (enabled-agents.json content is `{}`, the fresh-install state). Otherwise falls back to the next free cortextosN slot. This means the typical "fresh install + run /onboarding" flow ends with ONE instance dir, not two. 2. Added --instance "${INSTANCE_ID}" to all four CLI calls in the skill: init (already had it), add-agent, enable, ecosystem. This guarantees every write lands in the right instance dir. 3. Added `export CTX_INSTANCE_ID` and `export CTX_ROOT` so any indirect subprocess (e.g. PM2 reading ecosystem.config.js, the bus catalog command, the dashboard env loader) inherits the right instance. Belt and suspenders alongside the explicit flags. 4. Replaced the misleading note that said "CTX_INSTANCE_ID is set automatically by the framework" — it isn't. Replaced with an explicit IMPORTANT block telling future maintainers that every CLI call MUST pass --instance and explaining why. Test plan: - npm test → 389/389 still passing (no source changes; skill is .md) - Manual: ./reset.sh && curl ... | node && claude ~/cortextos && /onboarding → after completion, `ls ~/.cortextos` should show ONE instance dir, not two; that dir's enabled-agents.json should contain the orchestrator entry. Refs: BUG-017, BUG-029 in our internal tracker. Closes both. The structural bug in the daemon (BUG-028) is already fixed in #8 — this PR fixes the user-visible symptoms in the onboarding flow that were producing the same wrong-instance state. Co-authored-by: grandamenium <noreply@anthropic.com>
* fix(daemon): rebuild Telegram poller on restartAgent restartAgent() previously stopped and restarted the agent process and fast checker but never touched the TelegramPoller. After every IPC restart-agent call the old poller kept running in stale state forever and no new poller was created — the agent silently stopped receiving Telegram messages until the daemon itself restarted. Delegate to stopAgent + startAgent so the poller, checker, TelegramAPI, crash callback, and slash-command registration are all rebuilt from fresh .env credentials. Participates in the pendingRestarts race protection (commit 39163d9) for free. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(agent-manager): add regression tests for BUG-007 restartAgent fix Two unit tests pinning the new restartAgent behavior: 1. delegates to stopAgent then startAgent (in order) — verifies the new code path uses the high-level methods (which clean up and rebuild ALL per-agent resources, including the Telegram poller) rather than the partial inline cleanup that previously missed poller, TelegramAPI, crash callbacks, and slash commands. 2. is a no-op when the agent does not exist — verifies the new existence guard so a stale dashboard or IPC client can't crash the daemon by calling restart-agent on a missing name. Mocks reuse the existing AgentProcess/FastChecker/TelegramAPI/ TelegramPoller stubs added for the BUG-028 tests. Total test count: 389 → 391, all passing. --------- Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
`cortextos stop` with no arguments silently stopped every running agent in sequence, cascading the entire fleet. The description incorrectly claimed it would "stop the daemon," which led at least one autonomous agent to run it expecting `pm2 stop cortextos-daemon` semantics and instead kill six production agents. The dangerous behavior now requires an explicit \`--all\` flag. The no-arg form prints a helpful error pointing at the three legitimate intents (stop one, stop all, stop the daemon) and exits with code 2 before any IPC call. Description and argument help text rewritten to be accurate and to explicitly disclaim daemon-stop semantics. Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The current AgentProcess.stop() has a race window: it sets stopping=true, sends Ctrl-C/`/exit`/SIGKILL to the PTY, then sets stopping=false at the end. The PTY's exit callback (set in start() at line 81) fires asynchronously and may run AFTER stopping=false. The handleExit function's `if (this.stopping) return` guard then doesn't catch it, and the function falls through to crash recovery: increments crashCount, sets status='crashed', schedules a setTimeout restart. So an agent we *intentionally* stopped gets resurrected ~5-10 seconds later under the wrong assumption that it crashed. We saw direct evidence of this race in PR #4's cycle 2 daemon log: the AgentManager-level pendingRestarts protection fired TWICE during a single restart sequence, racing two startAgent calls through the same flow. The pendingRestarts logic was a workaround for the underlying race fixed by this PR. The fix: 1. Add `exitPromise` and `resolveExit` fields to AgentProcess. start() creates a fresh promise; the existing onExit handler resolves it AFTER calling handleExit. stop() awaits this promise (with a 5-second safety timeout for hung PTYs) AFTER pty.kill() and BEFORE setting stopping=false. This guarantees the exit handler has fired and seen stopping=true (so it skipped crash recovery) before stopping is reset. 2. Replace sessionRefresh()'s body with `await this.stop(); await this.start()`. The previous inline implementation duplicated the stop logic AND had a separate bug where the OLD pty's exit handler could fire AFTER the NEW pty was set up, nulling out the wrong reference. Delegating to stop() + start() inherits the BUG-011 fix automatically AND eliminates that duplicate-pty bug for free. (Same lesson as PR #4's restartAgent fix.) Tests added (4 new, all passing): tests/unit/daemon/agent-process.test.ts (NEW file): - stop() awaits the PTY exit handler before resolving - stop() does NOT trigger crash recovery on intentional stop (the regression test for the bug we're fixing) - handleExit DOES trigger crash recovery on UNINTENTIONAL exit (regression check — make sure we didn't break real crash recovery) - sessionRefresh() delegates to stop() then start() (in order) Total test count: 391 → 395, all passing. What this PR does NOT change: - The stopping flag's existence or its semantics — the only change is WHEN it gets reset (after the exit fires, not before) - The handleExit function — left untouched, just gets resolveExit called AFTER it - The crash recovery branch — left untouched, regression test pins it - The AgentManager-level pendingRestarts logic — separate concern (BUG-031 if it doesn't go away after this lands) Refs: BUG-011 in our internal tracker. Likely also closes BUG-010 (boris2 SIGHUP code 129 on restart) which was the symptom of the spurious crash recovery firing on an intentional stop, then start() running into a half-cleaned-up state. Co-authored-by: grandamenium <noreply@anthropic.com>
…ASH alarms (BUG-036) (#12) The SessionEnd crash-alert hook (src/hooks/hook-crash-alert.ts) determines whether an agent's exit was a crash by checking for marker files in ~/.cortextos/<inst>/state/<agent>/. Before this fix, `cortextos disable` and `cortextos stop` did not write any marker, so every intentional shutdown was misclassified as a crash and triggered a false 🚨 CRASH alarm via Telegram. This was trust-destroying — once a user saw one false alarm, they stopped trusting all future crash alarms, including real ones. Fix: 1. src/hooks/hook-crash-alert.ts — add two new marker types (.user-disable and .user-stop) with distinct emojis (⏸️ ⏹️) so users can tell at a glance whether they ran disable (semi-permanent) vs stop (transient). 2. src/cli/enable-agent.ts — disableAgentCommand now writes a .user-disable marker via writeDisableMarker() BEFORE the IPC stop-agent call. Helper is exported for unit testing. 3. src/cli/stop.ts — stopCommand now writes a .user-stop marker via writeStopMarker() in both the single-agent and --all branches BEFORE the IPC stop-agent call. Helper is exported for unit testing. 4. tests/unit/cli/lifecycle-markers.test.ts (NEW) — regression tests for both helpers covering: correct path/content, mkdirSync of missing dirs, error swallowing on filesystem failure, no collision between disable and stop markers for the same agent. Pattern matches src/cli/bus.ts:1285-1289 and :1355-1358 (the proven marker-write pattern from soft-restart, verified working in Phase 4 of the core stability test plan). Out of scope for this PR (separate follow-ups): - daemon SIGTERM shutdown path (different code path in agent-manager.ts) - broader lifecycle UX policy / 12-row test matrix (BUG-034) - dashboard restart events (BUG-030) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: grandamenium <noreply@anthropic.com>
…tial, 035) (#13) * fix: batch 7 stability/UX fixes (BUG-002, 013, 016, 019, 033, 034 partial, 035) Multi-bug batch PR consolidating seven independent fixes into one install/onboarding test cycle. Each fix is isolated and individually small; together they close one P0-relevant cluster (false-CRASH elimination), three P1 reliability fixes, two P2 robustness fixes, and one P3 tunable. BUG-002 (P1) — ecosystem.config.js bakes CTX_INSTANCE_ID at PM2 load src/cli/ecosystem.ts now emits raw JS that resolves process.env.CTX_INSTANCE_ID at PM2 startup time, with the install default as a fallback. Instance switching no longer requires re-running cortextos ecosystem. BUG-013 (P2) — corrupt enabled-agents.json silently destroys state src/cli/enable-agent.ts:readEnabledAgents() now backs up corrupt files as enabled-agents.json.broken-<timestamp>, logs a warning, and validates shape (must be a JSON object). Previously, parse failures returned {} silently and the next write overwrote the corrupt file with {}, destroying user state with no warning. BUG-016 (P3) — max_restarts: 10 too low Bumped to 50 in the generated ecosystem.config.js. PM2 max_restarts is independent of in-daemon agent crash counting. BUG-019 (P1) — dashboard runs as npm run dev outside PM2 Generated ecosystem.config.js now includes a cortextos-dashboard PM2 entry alongside cortextos-daemon. Dashboard now gets restart- on-crash, log files in ~/.pm2/logs/, and reboot survival. BUG-033 (P2) — /onboarding polls Telegram AFTER user confirms send Reordered .claude/commands/onboarding.md instructions: the LLM no longer waits for typed confirmation before running the long-poll loop. The 30s timeout on getUpdates IS the user confirmation window. BUG-034 partial (P1) — daemon SIGTERM shutdown false CRASH alarms AgentManager.stopAll() now writes a .daemon-stop marker in each agent state dir BEFORE stopping it. The SessionEnd crash-alert hook reads the marker and reports a clean daemon shutdown notification instead of a false CRASH. Eliminates the per-agent false-crash flood on pm2 restart cortextos-daemon. BUG-035 (P1) — cortextos enable is cwd-dependent src/cli/enable-agent.ts:discoverProjectRoot() now tries CTX_FRAMEWORK_ROOT, then CTX_PROJECT_ROOT, then ~/cortextos, then process.cwd() as a last resort. Same fix applied inline to src/cli/ecosystem.ts. Error message when no .env is found now lists the paths actually checked. Tests: tests/unit/cli/enable-agent-validation.test.ts (NEW) — 12 tests covering discoverProjectRoot precedence and readEnabledAgents validation paths. Build: clean. npm test: 414/414 passing (402 baseline + 12 new). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ecosystem): also require dashboard/node_modules/.bin/next before adding PM2 entry Caught in cycle 2: the BUG-019 fix added a cortextos-dashboard PM2 entry based only on dashboard/package.json existing, but if the user runs cortextos ecosystem before npm install in dashboard/, the dashboard PM2 entry crash-loops with "next: command not found". Tighter check: require both package.json AND node_modules/.bin/next. If the dashboard isn't installed yet, silently skip the PM2 entry. The user can re-run cortextos ecosystem after npm install to add it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: grandamenium <noreply@anthropic.com>
…026, 027) (#14) End-of-night close-out PR consolidating four conservative code fixes plus four pure verification closures. The four code fixes are all small, defensive, or observability improvements. The four verifications confirm that bugs were transitively closed by earlier PRs. BUG-015 (P2) — IPC source logging Added optional `source: string` field to IPCRequest. Daemon now logs every incoming IPC request as `[ipc] <type> <agent> from <source>`. Instrumented all 12 CLI callsites: cortextos status, enable, disable, start, stop (single + --all), bus self-restart, bus soft-restart, bus soft-restart-all, bus status. Older callers fall back to 'unknown'. BUG-021 (P2) — pm2 startup mid-flow blocking /onboarding skill no longer prompts the user to paste a sudo command mid-flow. Phase 9b now captures pm2 startup output silently. Phase 10 delivers the captured sudo command in an OPTIONAL section at the very end, after the user is fully onboarded. Reboot survival becomes opt-in rather than blocking the critical path. BUG-031 (P2) — pendingRestarts regression detector Replaced the dormant pendingRestarts queue actions in agent-manager.ts with console.warn regression-check lines. PR #11 (BUG-011) closed the underlying race that pendingRestarts was working around, so the queue should never fire. Preserved as a safety net + telemetry: if BUG-011 ever regresses, the warning fires immediately and we know to investigate. Once we have weeks of zero-warning production data, we can delete the queue mechanism entirely. BUG-032 (P3) — PTY exits with SIGHUP code 129 Defensive fix in AgentProcess.stop(): - Changed `pty.write('/exit\\r')` -> `pty.write('/exit\\r\\n')` (Claude Code REPL parses CRLF, not lone CR) - Bumped post-/exit wait from 3000ms -> 5000ms (give the child time to flush + exit cleanly before the PTY is torn down) This addresses the most likely contributors to the SIGHUP. If cycle 1/2 still shows code 129, the more aggressive fixes (drain buffer, exit-detection-before-kill) will be a follow-up PR. Verify-and-close (no code changes — exploration confirmed already correct in source): BUG-008 (P1) — Telegram 409 conflict — likely closed by PR #4 (restartAgent rebuilds Telegram poller). Cycle 1 will confirm by scanning for 409 in daemon log after 5 restart cycles. BUG-009 (P2) — "Agent <orgname> not found" — likely closed by PR #8 + #10. Cycle 1 will confirm by scanning daemon log. BUG-026 (P1) — agent-manager path missing org segment — verified correct at agent-manager.ts:96 and :536. Both use the right `join(frameworkRoot, 'orgs', this.org, 'agents', name)` pattern. BUG-027 (P2) — dashboard SQLite filename hardcoded — verified correct at dashboard/src/lib/db.ts:8-12. Already uses templated `cortextos-${instanceId}.db`, not a hardcoded literal. Deferred (not in this PR): BUG-003 — mystery SIGTERM cascade. Needs the 30-min idle soak from Phase 3 of core-stability-test-plan.md. Stays open for a future dedicated soak session. Build: clean. npm test: 414/414 passing (no test additions needed — all changes are observability or defensive). Co-authored-by: grandamenium <noreply@anthropic.com>
…UG-040, closes BUG-038) (#15) BUG-040 — root cause of BUG-038 (pendingRestarts regression detector firing under cortextos bus soft-restart-all): the `stopping` flag in AgentProcess gets cleared when stop()'s 5-second Promise.race timeout fires, NOT when the PTY actually exits. After BUG-032's defensive fix bumped graceful shutdown from 3s to 5s wait, the total elapsed time before pty.kill() is ~6s. Once pty.kill() runs, the PTY can take additional time to actually exit. If that delayed exit fires AFTER stop()'s timeout, handleExit runs with stopping=false and triggers spurious crash recovery — exactly what PR #11 was supposed to prevent. This was a partial regression of BUG-011 caused by BUG-032's defensive fix interacting badly with the 5s safety timeout. Surfaced live in PR #14's fastloop test 2026-04-09: a single pm2 restart of cortextos-daemon produced a crash recovery cascade with two crash counts incrementing during the supposed clean shutdown. Fix: 1. New `stopRequested: boolean` field that persists ACROSS stop()'s return. Set true at the start of stop(), cleared only by handleExit when an intentional exit fires, or by start() at the beginning of a new lifecycle. This is the safety net for late-arriving exits. 2. New `lifecycleGeneration: number` counter incremented on each successful start(). Each PTY's onExit closure captures the generation at spawn time and bails out early if the generation doesn't match — i.e. a new PTY has been spawned since this old one was created. Prevents an old PTY's late exit from triggering crash recovery on the new agent. 3. Bumped Promise.race timeout in stop() from 5s to 15s. The functional correctness no longer depends on this (stopRequested handles late exits) but a generous timeout reduces "Ignoring late exit" log noise from the generation guard. 4. handleExit now checks `stopRequested || stopping` instead of just `stopping`. Either flag short-circuits crash recovery. Clears stopRequested when consumed. Verified end-to-end via fastloop: - Daemon SIGTERM shutdown: clean stop, no crash recovery cascade - 5x cortextos bus soft-restart-all cycles: 0 crash recovery, 0 REGRESSION CHECK warnings, 0 "Ignoring late exit" lines, 5 successful "Restart complete" sequences - Telegram: 5x correct "🔄 commander restarted by user" notifications, zero false 🚨 CRASH alarms - 414/414 tests still passing, no regressions Closes BUG-038 (its symptom) as a side effect of fixing the root cause. Note: BUG-032 (PTY exits with code 129 SIGHUP) is still present — agents still exit with 129 instead of 0 after the graceful shutdown sequence. But with BUG-040 fixed, the daemon correctly recognizes 129 as intentional and does not fire crash recovery. BUG-032 is now purely cosmetic log noise and can be addressed in a separate aggressive-fix follow-up. Co-authored-by: grandamenium <noreply@anthropic.com>
…41) (#16) BUG-041 is a P0 validation mismatch that was discovered live when the CortextDesigner agent (created earlier during a session) tried to use `cortextos bus send-telegram` and failed with: Error: CTX_AGENT_NAME is invalid: Invalid agent name 'CortextDesigner'. Must contain only lowercase letters, numbers, underscores, and hyphens. at resolveEnv (dist/cli.js:4273:13) Root cause: `src/cli/add-agent.ts` performed ZERO validation on the agent name argument. It accepted any string, created the agent directory on disk, wrote to enabled-agents.json, and the daemon happily spawned the agent. But at runtime, every `cortextos bus *` command calls `resolveEnv()` from `src/utils/env.ts`, which strictly validates the agent name via `validateAgentName()` (regex `/^[a-z0-9_-]+$/`). Names that add-agent accepted would then fail every bus call. Result: mixed-case names like 'CortextDesigner' produced half- functional agents — daemon-managed fine, Telegram receiving worked (via FastChecker PTY injection), but Telegram SENDING, inbox reading, task creation, approvals, heartbeats, and every other bus operation were broken. Affected agents were effectively undeployable for real work. Fix: call `validateAgentName()` at the start of the add-agent action, BEFORE any filesystem operations. Invalid names now get rejected upfront with a clear error message that explains the rule and shows valid examples. The validation uses the same `validateAgentName()` function that resolveEnv already uses, so add-agent and resolveEnv are now guaranteed to agree on what a valid name is. Changes: 1. src/cli/add-agent.ts — import validateAgentName, call it as the first thing in the action handler. Clear error on failure with examples: paul, sentinel, cortext-designer, m2c1-worker, agent_1. 2. tests/unit/utils/validate.test.ts — new regression test for mixed-case / PascalCase / CamelCase names. Locks in rejection of CortextDesigner, MyAgent, camelCase, Agent1, tally-Bot, snake_Case. 3. tests/unit/cli/add-agent-validation.test.ts (NEW) — integration test that calls addAgentCommand.parseAsync with invalid names (PascalCase, single uppercase, spaces, path traversal) and asserts process.exit(1) is called with the correct error message and the validation rule reference. Verification (via manual dist copy, no pm2 restart needed since the fix is CLI-only): - 419/419 tests passing (414 baseline + 5 new tests) - LIVE: `cortextos add-agent TestMixedCase --template agent --org testorg` → fails with clear error, no directory created, exit code 1 - LIVE: `cortextos add-agent test-valid-name --template agent --org testorg` → succeeds, normal flow works, agent registered - LIVE: CTX_AGENT_NAME=test-valid-name cortextos bus list-agents → succeeds, proves the fix makes add-agent and resolveEnv consistent - LIVE: CTX_AGENT_NAME=CortextDesigner cortextos bus list-agents → still fails (expected — fix prevents NEW bad agents, existing ones stay broken until recreated with a valid name) Out of scope (hygiene follow-up): inline regex duplicates in src/cli/setup.ts, src/cli/goals.ts, src/bus/agents.ts, src/bus/system.ts should all import validateAgentName from the canonical location but that's a separate cleanup PR. Not required for BUG-041 fix. Co-authored-by: grandamenium <noreply@anthropic.com>
BUG-043 was a P0 architectural bug that prevented multi-org installs from
working. The daemon's AgentManager had a singleton this.org field set from
startup CTX_ORG and used it for every agent lookup:
discoverAgents() scanned only orgs/{this.org}/agents/
startAgent() auto-discovered via join(frameworkRoot, 'orgs', this.org, ...)
env.org passed to AgentProcess was hardcoded this.org
resolvePaths() call used this.org
worker spawning used this.org
Result: a daemon started with CTX_ORG=testorg could never find, spawn, or
operate agents that lived in other orgs (lifeos, cointally, etc). Agents in
those orgs were silently invisible — enabled-agents.json entries pointing
to them would log "Agent directory not found" when cortextos enable fired.
Discovered live during the donna migration: agent 'donna' in orgs/lifeos
could not be enabled because the daemon (CTX_ORG=testorg) searched
orgs/testorg/agents/donna instead of orgs/lifeos/agents/donna.
Fix:
1. discoverAgents() iterates ALL orgs under frameworkRoot/orgs, not just
this.org. Each discovered entry records its org so downstream code
knows where to find the agent dir. Return type changed from
Array<{name, dir, config}> to Array<{name, dir, org, config}>.
2. discoverAndStart() destructures the new org field and passes it as
the 4th argument to startAgent, so startAgent never needs to fall
back to this.org on the discovery path.
3. startAgent() signature extended: startAgent(name, agentDir, config?, org?).
When called from discoverAndStart it receives an explicit org. When
called from the IPC start-agent handler (which only has the agent
name), it uses the new resolveAgentOrg() helper to look up the
correct org from enabled-agents.json, with a filesystem scan
fallback for legacy entries missing the org field.
4. New private helper resolveAgentOrg(name, explicitOrg?) with four-tier
resolution:
a. explicit org arg (from discoverAgents or caller)
b. enabled-agents.json entry's org field
c. filesystem scan — walk orgs/*/agents/ looking for a dir named name
d. legacy fallback: this.org (preserves single-org install behavior)
5. startAgent() uses the resolved org everywhere it previously used
this.org: the join() path for auto-discovery at line 107, the env.org
field at line 126, and the resolvePaths() call at line 130.
6. Worker spawning at line 489 deliberately keeps this.org. Workers are
ephemeral and don't have entries in enabled-agents.json, so there's
no per-agent org to look up. This is documented in the plan and a
safe no-op for multi-org support since workers are spawned by their
parent agent, which has the correct org context.
Tests:
tests/unit/daemon/agent-manager.test.ts — new describe block for BUG-043
with 4 tests:
- discovers agents from ALL orgs, not just the startup org
- passes the correct per-agent org as the 4th arg to startAgent
- respects enabled-agents.json disable-flags across multiple orgs
- returns empty list when orgs/ does not exist (backward compat)
Existing BUG-028 test updated to expect the new 4th arg.
Verification: 423/423 tests passing (419 baseline + 4 new).
Backward compatibility: single-org installs continue to work unchanged.
resolveAgentOrg falls back to this.org when nothing else resolves,
preserving the legacy behavior for installs that only ever have one org.
Unblocks the in-progress 8-agent migration that was halted when the
first agent (donna, in lifeos) couldn't be spawned.
Co-authored-by: grandamenium <noreply@anthropic.com>
… set listAgents() was falling back to process.cwd() whenever scanRoots was empty, even when CTX_FRAMEWORK_ROOT was set to a valid path that just had no orgs/ subdir. This caused 4 test failures in agents.test.ts: vitest runs from the repo root which has a real orgs/ dir, so deleting CTX_FRAMEWORK_ROOT in beforeEach still allowed the cwd fallback to find and return real agents. Fix: only apply the cwd fallback when CTX_FRAMEWORK_ROOT is completely unset (falsy). If a root is explicitly configured, respect it — an empty framework root means zero agents, not "fall back to cwd." Also update the test's beforeEach to set CTX_FRAMEWORK_ROOT to an isolated subdir of testDir (no orgs/ inside) instead of deleting it, so the guard condition is tested correctly. CLAUDE.md: update test count from 381 → 423 (reflects additions in PRs #11-14). 423/423 tests passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The count drifts as tests are added. Removing the specific number avoids stale documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… rendering (#21) Two cosmetic fixes from sentinel's bug audit: 1. SparkLine: change wrapper from <span> to <div> and add minWidth/minHeight constraints to prevent Recharts ResponsiveContainer from computing negative container dimensions. 2. Brand Voice: replace raw <pre> with the existing renderMarkdown() renderer so markdown syntax (bold, lists, headings, etc.) displays as formatted HTML instead of raw text. Extract renderMarkdown to shared lib/render-markdown.tsx for reuse across kb-view and organization-tab. Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(daemon): auto-verify cron restoration after agent bootstrap Agents are supposed to restore crons from config.json on session start, but compliance is unreliable (agents sometimes skip step 6 of the 13-step boot sequence). This adds a framework-level safety net: After the agent finishes its startup turn (detected via last_idle.flag), the daemon reads the crons array from config.json and injects a verification prompt asking the agent to check CronList and restore any missing recurring crons. Safe for both fresh and --continue restarts: waits for idle before injecting, so it never interrupts a mid-conversation agent. Bails out if the agent stops or restarts during the wait window. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(daemon): guard cron injection on timeout + add happy-path test Issue 1: verifyCronsAfterIdle had a timeout fallthrough bug where the while-loop exit via timeout (no idle flag detected) fell through into the injection block, causing a prompt to be injected into an agent mid-work. Fix adds a `foundIdle` boolean — injection is skipped unless the loop explicitly broke on a newer idle timestamp. Issue 2: Add happy-path test for verifyCronsAfterIdle covering the core flow: existsSync returns true with a boot timestamp, then a newer timestamp on the first poll, triggering injection with expected cron names. Uses vi.useFakeTimers() to advance the 15s poll instantly. Also adds the three guard-clause tests (no-crons, once-only, recurring) and wires mockInjectMessage for assertion. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: comprehensive Windows support (14 fixes from PR #6) Ports all 14 Windows compatibility fixes from Erica's PR #6, with the password sync behavior made opt-in (requires SYNC_ADMIN_PASSWORD=true) instead of always-on to prevent silently overwriting dashboard-changed passwords on restart. Fixes: claude.exe resolution in node-pty, shell:true for npm/npx/where on Windows, UTF-8 encoding in PTY, venv Scripts/ vs bin/ paths, embedding model migration, Python venv in install, npm link registration, build tools guidance, favicon, enabled-agents.json path, context window docs, APPDATA gitignore. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Co-Authored-By: Erica <erica@cortextos.com> * fix(auth,pty): fix ADMIN_PASSWORD regression and use claude.cmd on Windows Issue 1: seedAdminUser validated ADMIN_PASSWORD before checking if users already exist, breaking all existing deployments without ADMIN_PASSWORD set. Now returns early when users exist and SYNC_ADMIN_PASSWORD is not true, only validating the password when it is actually needed for seeding or syncing. Issue 2: npm global installs on Windows create .cmd wrapper scripts, not .exe binaries. node-pty's CreateProcess cannot resolve .cmd files from a .exe name. Changed 'claude.exe' to 'claude.cmd' on win32 so node-pty can find and launch the Claude Code CLI correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dashboard): guard journal_mode WAL switch against parallel-worker SQLITE_BUSY Next.js build runs 3 workers that all import db.ts simultaneously. Each opens the DB, acquires a shared lock, then tries PRAGMA journal_mode = WAL (needs exclusive lock). With all three holding shared locks, none can upgrade — busy_timeout eventually expires with SQLITE_BUSY. Fix: catch SQLITE_BUSY from the WAL switch and verify whether another worker already succeeded. If journal_mode is already 'wal', continue; otherwise re-throw. This is a no-op in normal single-process operation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Erica <erica@cortextos.com>
* feat: comprehensive Windows support (14 fixes from PR #6) Ports all 14 Windows compatibility fixes from Erica's PR #6, with the password sync behavior made opt-in (requires SYNC_ADMIN_PASSWORD=true) instead of always-on to prevent silently overwriting dashboard-changed passwords on restart. Fixes: claude.exe resolution in node-pty, shell:true for npm/npx/where on Windows, UTF-8 encoding in PTY, venv Scripts/ vs bin/ paths, embedding model migration, Python venv in install, npm link registration, build tools guidance, favicon, enabled-agents.json path, context window docs, APPDATA gitignore. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Co-Authored-By: Erica <erica@cortextos.com> * fix(auth,pty): fix ADMIN_PASSWORD regression and use claude.cmd on Windows Issue 1: seedAdminUser validated ADMIN_PASSWORD before checking if users already exist, breaking all existing deployments without ADMIN_PASSWORD set. Now returns early when users exist and SYNC_ADMIN_PASSWORD is not true, only validating the password when it is actually needed for seeding or syncing. Issue 2: npm global installs on Windows create .cmd wrapper scripts, not .exe binaries. node-pty's CreateProcess cannot resolve .cmd files from a .exe name. Changed 'claude.exe' to 'claude.cmd' on win32 so node-pty can find and launch the Claude Code CLI correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dashboard): guard journal_mode WAL switch against parallel-worker SQLITE_BUSY Next.js build runs 3 workers that all import db.ts simultaneously. Each opens the DB, acquires a shared lock, then tries PRAGMA journal_mode = WAL (needs exclusive lock). With all three holding shared locks, none can upgrade — busy_timeout eventually expires with SQLITE_BUSY. Fix: catch SQLITE_BUSY from the WAL switch and verify whether another worker already succeeded. If journal_mode is already 'wal', continue; otherwise re-throw. This is a no-op in normal single-process operation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(telegram): use relative paths for media files, add reply context for media messages BUG-046: Claude Code strips absolute file paths from pasted user input, causing agents to receive empty local_file: fields for Telegram photos, documents, voice, and video messages. Convert to relative paths (from agent working dir) before injection so paths survive. Also adds buildReplyContext() to properly handle reply context for media messages — previously only .text was checked, so replies to photos/videos arrived with no indication of what was being replied to. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Erica <erica@cortextos.com>
…(BUG-003) (#18) A second SIGTERM arriving while shutdown() was already in flight would start a parallel stopAll(), causing unpredictable signal cascades across child PTY processes. Add a shuttingDown flag so subsequent signals are logged and ignored. Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…ean exit (BUG-032) (#19) After sending /exit and waiting 5s, the child process has usually exited cleanly. Calling pty.kill() unconditionally on an already-exited PTY tears down the file descriptor and sends SIGHUP (exit code 129). Now we check pty.isAlive() first and only kill if the process is still running. Also adds isAlive to the AgentPTY mock in agent-process tests. Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…d usage API script (grandamenium#37) - BUG-048: session timer now re-reads config.json on each check so a config change after start() takes effect. Prevents fleet-wide simultaneous restarts when max_session_seconds is briefly reduced and then restored after timers are already set. Rescheduled if remaining time > 5s, fires immediately otherwise. - bus/check-usage-api.sh: new script to check Claude Max API usage via OAuth endpoint with 3-minute cache, threshold alerts via Telegram, and --warn-7day / --warn-5h flags. - 441/441 tests passing (2 new BUG-048 regression tests added). Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
#25) Login would hang forever when the dashboard was accessed via a Tailscale IP, LAN address, or reverse proxy. Root cause was three independent layers that all had to be fixed together — verified end-to-end in a headless browser (all three layers have to land for the form to reach the bcrypt check). 1. next.config.ts — allowedDevOrigins. Next.js 15.2+ rejects /_next/* dev-internal requests from non-localhost origins by default. The browser receives the SSR HTML but the client bundle never finishes hydrating, so useEffect never fires, /api/auth/csrf is never fetched, and the form is stuck. Whitelist is now read from DASHBOARD_ALLOWED_DEV_ORIGINS (comma-separated; localhost is always allowed) so each deployment picks its own hosts. 2. login/page.tsx — CSRF token lifetime. Previous attempts stored the token in React state bound with value= or set it imperatively on the hidden input. Both failed: React reconciliation resets uncontrolled input values between the useEffect completion and the next render, so the input never carried the real token at submit time. The token is now fetched once, held in a ref, and injected on the request body at submit. The hidden input in the JSX stays as a placeholder. 3. login/page.tsx — submit body format. The previous implementation used FormData (multipart/form-data), which caused NextAuth to fail CSRF validation with MissingCSRF — the multipart parser did not recover the csrfToken field. Submit now bypasses signIn() and POSTs the credentials callback directly as application/x-www-form-urlencoded, which NextAuth parses correctly. Also removes the now-unused signIn import. Co-authored-by: Clint Moody <ClintMoody@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a session-independent setInterval in FastChecker that fires cortextos bus update-heartbeat every 50 minutes. Covers idle sessions where REPL-bound cron jobs miss the theta wave window. Clears the timer cleanly on stop(). 3 new unit tests (fires, clears, pre-bootstrap guard) — all 430 tests pass. Co-authored-by: Ben Joslin <benjoslin52@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…grandamenium#593) agent-pty.ts hard-coded `--dangerously-skip-permissions` on every Claude agent spawn, which overrides settings.json entirely and disables ALL of Claude Code's permission gating — so the PermissionRequest hook (hook-permission-telegram) can never fire for tool use, regardless of the user's permission config. The Telegram approval gate was effectively dead for Claude agents. Add `AgentConfig.dangerously_skip_permissions` (default true for back-compat — agents have historically run unattended). Setting it to false omits the flag at spawn so Claude Code's permission system engages and the approval hook actually gates tool use. Only the literal boolean false disables the skip; a non-boolean value warns and falls back to skip-on, so a typo can't silently leave an agent ungated. Hermes already omits the flag, so only the claude-code spawn path is affected. This is the spawn-time half of the permission-gate work; the hook-hardening change is the matching correctness fix for the gate this toggle engages. Tests: flag present by default / when true / when undefined; absent when false; non-boolean warns and keeps the flag. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nium#592 follow-up) (grandamenium#597) grandamenium#592 closed the fence/forged-header injection on the inbox + Telegram text paths but did not reach the Telegram media formatters or the .urgent-signal body. External text there is still interpolated raw into a triple-backtick fence / header, so a caption/transcript carrying its own fence can break out and forge daemon containment headers (same class as grandamenium#592). Reuse grandamenium#592's helpers across the remaining untrusted sinks in fast-checker: - formatTelegramPhoto/Document/Voice/Video: caption + transcript via wrapFenceSafe (dynamic unescapable fence), from + file_name via sanitizeForPtyInjection. - checkUrgentSignal: .urgent-signal body via wrapFenceSafe. Keep the codex-app-server consumer compatible: buildMediaPayload re-parses the formatted media block, so its caption/transcript regexes now match a dynamically-sized fence (backreference to the opening run length) instead of a hard-coded ``` — otherwise a caption containing backticks would mis-parse. Tests: media formatters neutralise fence-breakout + forged headers; urgent signal body fenced; codex-app-server parses both dynamic (4-backtick) and plain fences. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…13/#14) (grandamenium#598) Task operations interpolated caller-supplied taskId (and task.assigned_to) into filesystem paths with no validation, so a traversal id/assignee could escape the task tree (read/write/rename/unlink arbitrary *.json / *.jsonl / *.claim). - New validateTaskId() in validate.ts (/^[a-z0-9_-]+$/, matching the task_<epoch>_<rand> generator) — rejects path separators, dots, traversal. - findTaskFile() validates taskId (the chokepoint for update/claim/complete/ check-deps); appendTaskAudit() + readTaskAudit() validate before building audit paths; cli/bus.ts checkDeliverableRequirement() validates before its pre-update/complete lookup (it runs ahead of findTaskFile's guard). - saveOutput() validates taskId AND task.assigned_to (the latter comes from the task JSON and feeds the deliverables path). - archiveTasks()/compactTasks() validate task.id (from the JSON body) before using it for rename/unlink, and reject a non-YYYY-MM completed_at before it feeds the archive filename — both skip the offending task rather than abort. Tests: validateTaskId accept/reject; findTaskFile + readTaskAudit reject traversal; saveOutput rejects traversal taskId + tampered assigned_to and still saves a legit deliverable; archiveTasks skips a traversal-id task without escaping. Follow-ups (out of this traversal-scope branch): derive archive/compact filenames from the on-disk task_*.json name rather than trusting task.id (guards against a valid-but-mismatched id touching the wrong in-tree file); add a log line to the archiveTasks skip path. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eForPtyInjection (grandamenium#596) (grandamenium#603) sanitizeForPtyInjection (the grandamenium#592 follow-up's unfenced-context guard) prefixed forged `=== AGENT MESSAGE` / `=== TELEGRAM` / `Reply using:` headers with [quoted] only when they were preceded by ASCII space/tab ([ \t]*). A downstream parser recognizes the same headers via `.trim()`, which strips the full Unicode White_Space set — so a header led by e.g. NBSP, IDEOGRAPHIC SPACE, or BOM escaped [quoted] here yet was still parser-recognized after trim (asymmetry reported by ClintMoody, follow-up to grandamenium#592 / 20583d3). Widen the leading-whitespace class to the Unicode space chars `.trim()` strips: NBSP, OGHAM SPACE, the U+2000–200A run, NARROW NBSP, MEDIUM MATH SPACE, IDEOGRAPHIC SPACE, and BOM/ZWNBSP. Line terminators stay excluded — the /m anchor already restarts after \n and U+2028/U+2029, \r was folded to \n, and \v/\f are removed by stripControlChars. Adds a 7-char gap matrix (each char x AGENT MESSAGE + Reply-using), a mixed ASCII+Unicode run, and VT/FF + LS/PS regression guards. Verified the old class missed all 7 chars and the widened class quotes them. Suite green, tsc clean. Co-authored-by: Boris <noreply@anthropic.com>
…tization (grandamenium#604) * fix(fast-checker): inject unhandled callbacks to agent instead of dropping Custom inline button flows were hitting the catch-all log line and being silently dropped. Now they are injected as a Telegram message so the agent can handle them. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(security): sanitize callback-injection path against PTY injection The unhandled-callback injection block interpolated the tapping user's first_name and callback_data raw into the '=== TELEGRAM from [USER: ...]' message injected into the agent session. This block predates grandamenium#592 and its sanitizer was never retrofitted, so a forged '=== AGENT MESSAGE' or fence-breakout in a user-controlled first_name rendered un-neutralized. Wrap both senderName and callback_data with sanitizeForPtyInjection, matching the text path. Reachability is gated by ALLOWED_USER (line 562), so practical exposure is self-inflicted on single-user bots; this closes the defense-in-depth gap for unset-allowed-user / multi-user contexts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Boris <noreply@anthropic.com>
* feat: add voice-agent-factory community skill Turns any cortextOS agent into a live ElevenLabs voice agent. Six-phase pipeline: DISCOVER (mine skills/CLIs/MCPs/transcripts) -> ASK -> GENERATE (all code written dynamically per target, policy-gated gateway, server-side invariants) -> TEST (probe-shaped fixtures) -> PROVISION (tier-detect with server-tools fallback) -> VERIFY (real text-only WS conversations, never simulate-conversation which mocks tools). 14 hard-won lessons encoded from two live reference builds. Bundled resources: verified EL API reference, webhook schema + pricing gaps doc, sources. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(voice-agent-factory): address review round 1 — operator gate, MCP attach, runtime detection, fresh-user path - Hard operator gate before Phase 5: no EL provisioning, tunnel, or link sharing on defaults; Phase 2 defaults limited to discovery/generation - MCP path now PATCHes agent prompt mcp_server_ids after POST (append, preserve existing), matching the tool_ids discipline - Runtime detection in Phase 1 with codex-app-server alternate paths and an explicit unsupported-runtime stop instead of silent under-mining - Prerequisites / How to run / What this writes section (incl. unpkg CDN note and no-CDN variant) - GAPS.md: body_params_schema -> request_body_schema (+ response_body_schema docs-only note); REPORT section 8 reconciled with GAPS closures - Neutralized remaining org-voice leaks and research-harness provenance in resources; FROM SCRATCH wording reconciled with lesson-13 coexist rule - catalog.json churn reduced to a minimal +13/-1 diff Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(voice-agent-factory): review round 2 — webhook schema keys, gap-status consistency - GAPS webhook sketch: headers_schema -> request_headers; auth_connection / auth_resolved_params bullet (per WebhookToolApiSchemaConfig-Input) - REPORT closing paragraph now lists only remaining key-test checks; schema + pricing marked closed-in-GAPS / re-confirm at key-test - SOURCES server-tools line reworded to resolved-in-GAPS Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Boris <noreply@anthropic.com>
… heartbeat (grandamenium#667) buildStartupPrompt auto-wrote the .onboarded marker whenever heartbeat.json existed, on the assumption the agent had completed onboarding and just forgot the marker. That silently suppressed FIRST BOOT for agents that were manually scaffolded (heartbeat present) but never actually ran onboarding — the agent would never be told to read ONBOARDING.md. Require the marker to be explicit: a heartbeat alone no longer marks an agent onboarded. An existing .onboarded marker still suppresses FIRST BOOT, so already-onboarded agents are unaffected. This is general daemon behavior; it was surfaced via a manually scaffolded agent but applies to any runtime. Adds two-direction regression coverage in agent-process.test.ts: - heartbeat-only / no marker -> still routes to FIRST BOOT, and no .onboarded is auto-written - existing .onboarded -> FIRST BOOT suppressed Co-authored-by: Boris <noreply@anthropic.com>
…op the slash menu (grandamenium#668) setMyCommands ran once per agent at startup as a single fire-and-forget attempt. When the daemon bounced mid-onboarding (e.g. reloading to pick up a newly created agent) the in-flight request was killed and, with no retry, the slash menu never landed for that bot - registration silently failed. - registerTelegramCommands now retries on transient failures with a short linear backoff (default 3 attempts) so a flaky network or slow API response within a single boot no longer loses the menu. - The daemon caller logs registration failures instead of swallowing them, so a missing menu is visible to operators. Adds retry-path coverage in sprint5-metrics.test.ts (success, retry-then- succeed, exhausted-attempts, empty-noop). Co-authored-by: Boris <noreply@anthropic.com>
…from usage cache (grandamenium#669) * fix(dashboard): auto-populate Max plan usage widget from the live usage cache The Max Plan Usage widget reads state/usage/latest.json, which is only written by a manual `cortextos bus scrape-usage` paste. Without that one-time step the widget shows "Plan usage tracking not configured" indefinitely, even though the usage-monitor cron already maintains a fresh OAuth usage cache. getPlanUsage now falls back to state/usage/api-cache.json when latest.json is absent, mapping its 5h / 7d / 7d-sonnet utilization onto session / week-all-models / week-sonnet (with friendly reset dates and the cache file mtime as the timestamp). latest.json stays primary, so the manual scrape path and the daily history series are unchanged — this only fixes the empty default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(dashboard): add Codex plan usage to the usage widget Surfaces Codex (ChatGPT) plan usage alongside Claude on the analytics page, read from the codex-wham-cache.json that the usage-monitor cron keeps fresh. primary_window maps to the 5h session limit, secondary_window to the 7d limit; reset_at is a unix epoch. Renders a "Codex Plan Usage (<plan>)" card with 7d + 5h bars when the cache is present, and is omitted otherwise. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Boris <noreply@anthropic.com>
This reverts commit 143f915.
…grandamenium#684) Token auto-refreshes via OAuth; the expiry warning is noise. Removes the <24h block that fired every usage-monitor cycle regardless of --chat-id. Co-authored-by: Boris <noreply@anthropic.com>
…rator paths from public repo Remove 13 dev-artifact reports (WINDOWS_INSTALL_REPORT.md + docs/phase-reports/*) that exposed the internal agent roster, cron schedules, operator home paths, and org name; redact hardcoded operator username in two test files; gitignore to prevent re-tracking.
… (SEC-1 L1/L4) (grandamenium#698) L1: .github/workflows/leak-guard.yml runs .github/scripts/leak-guard.sh on every pull_request (fork-safe, no secrets) and push to main — the server-side backstop a local pre-push hook cannot provide (covers fork PRs and UI-merges). Blocks on the operational-LEAK SHAPE (operator home paths, agent-roster+cron-schedule tables, secret-shaped tokens, dev-report artifact paths), NOT on legitimate framework convention (agent-name placeholders, lifeos test fixtures). Ships with a falsifiability test proving it FAILS on a planted leak and PASSES on the clean tree. Intended as a required status check (applied post-merge, L2). L4: broaden .gitignore so root-level *INSTALL_REPORT.md / PHASE*-REPORT.md / docs/phase-reports/ dev artifacts cannot re-track. Remediation follow-up to the 2026-07-01 fleet-metadata leak. Co-authored-by: James Goldbach <cortextos@Mac.fios-router.home>
…dow (grandamenium#685) Make the runtime-agnostic context-handoff mechanism ship enabled by default so any install/pull restarts agents before native compaction, writes a resume-ready handoff doc, and reboots fresh from it. Previously unset ctx_handoff_threshold meant observe-only; now it defaults to warn 30% / handoff 60% of the real model window (opt out with ctx_handoff_threshold <= 0). - Default-on at 60% of model context window (fast-checker) - Wire the statusLine context-status writer into the Claude templates so the used-percentage is reported for the threshold computation - Report current (not lifetime) context usage from the codex adapter - Rate-limit concurrent handoffs with a release-safe lease (cap 2, queue the rest; release by name, session-id independent, on fresh session) - Gate the PTY overflow backstop on real high context to avoid false 100% - Unit truth-table for the default-ON behavior (fast-checker.test.ts): unset threshold => handoff 60 / warn 30, opt-out at <= 0 (observe-only), and an explicit threshold still honored — exercises the real checkContextStatus - Codex handoff restart starts a genuinely NEW thread: only resume a persisted codex thread in continue mode, so a context-handoff (fresh) restart falls to thread/start instead of resuming the old thread. Resuming retained the full context window, so the handoff never lowered usage and the agent immediately re-crossed the threshold and re-fired — a restart treadmill caught by the PR-A live validation. (personal-main already gated this; upstream did not.) - Cooperative-restart loop backstop: the circuit breaker now also counts Tier-2 handoff fires in a persisted 15min window and trips (30min pause + alert) if they reach the cap, so any handoff loop self-limits regardless of cause — the existing breaker only counted Tier-3 force-restarts, not cooperative handoffs. Co-authored-by: Boris <noreply@anthropic.com>
…andamenium#699) * feat(codex): mid-turn message injection via turn/steer Codex agents previously accepted injected messages only after the active turn completed (queueTurn gates on _executing, which clears on turn/completed). Claude agents get instant mid-turn PTY injection, so codex delivery felt like wait-until-stopped on long turns. The codex 0.130.0 app-server protocol ships turn/steer: mid-turn input that is drained into the active turn at the next model step, without aborting in-flight work. This change uses it for parity: - Track activeTurnId from turn/started; clear on turn/completed, error notifications, and kill(). - queueTurn while executing now attempts turn/steer with expectedTurnId as the active-turn precondition. Success means no queue entry; the turn continues with the new context. - Any steer rejection (ExpectedTurnMismatch when the turn just ended, ActiveTurnNotSteerable for review/compact turns, NoActiveTurn, transport errors) falls back to the existing queue, so no message is ever lost. No steer retry on fallback to avoid loops on non-steerable turns. - Idle path unchanged; Claude path untouched. - CODEX_STEER_DISABLED=1 kill-switch reverts to pure queue behavior. Tests: - 6 new unit tests (steer payload, rejection fallback ordering, pre-turn/started race, kill-switch, turn-id lifecycle); adapter suite 71/71. - New env-gated live integration test (CODEX_STEER_LIVE=1) against a real codex app-server: steer accepted mid-turn with same turnId, no second turn/started, steered content present in final output, stale expectedTurnId rejected. 2/2 passing on codex-cli 0.130.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit 9cd1cc2491bd19fa5334deaeffecbd4f9a70a2d7) * fix(codex): honor fresh app-server thread starts (cherry picked from commit d3dbde6eb6bada4269203853656fa797e43e3ef0) * feat(runtime): add native opencode agent adapter (cherry picked from commit bbc8ea4fdd783a348751a705bd20971f547565a7, internal build artifacts under .agent/ dropped) * fix(opencode): use object-form permissions in template (cherry picked from commit 3816bc65ca89c93d6a72261a3602f1bcc23f0227) * fix(opencode): inject inbound messages as raw TUI input (cherry picked from commit e58735daae85b04f6d096d4759973db1443245b6) * fix(daemon): suppress context handoff during fresh-session grace window A fresh codex app-server thread can briefly report prior prompt-cache tokens, producing a transient ~100% context reading on a session that is actually at low context. The fast-checker fired a Tier-2 handoff on that spike, injecting a prompt telling the agent to run `cortextos bus hard-restart`; that cooperative restart bypasses the force-path circuit breaker, yielding a fresh session that reads ~100% again -> restart loop every ~1-2min (observed on codex-worker after a force-fresh boot). Add HANDOFF_GRACE_MS (120s) anchored on session_id change: suppress the Tier-1 warning and Tier-2 handoff while the session is younger than the grace window. The hard API-overflow regex remains ungated so a genuine overflow still force-restarts immediately. Tier-3 deadline is inherently protected (only armed when Tier-2 fires). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit 8b5d445cf71bb2bc396a7f9b57f51f0512474c89) * fix(opencode): wait for real TUI readiness before inbound (cherry picked from commit 78dc26a72e6c4a2f6660f3e0aa33d67008d57cee) * fix(opencode): force telegram replies through bus command (cherry picked from commit 279c76d167e0e09a3791d5873d8b34fde843a544) * fix(opencode): escape shell mode before every inbound injection After OpenCode executes the reply-protocol command (cortextos bus send-telegram) the TUI is left in Shell mode. A subsequent inbound was typed straight at the zsh prompt (`$ === TELEGRAM ...` -> command not found) so the second turn produced no reply. Press Esc before every OpencodePTY injection to exit shell mode / return to chat readiness, settle 150ms, then type the content (existing 300ms deferred Enter kept to preserve the proven first-turn timing). Adds a chained multi-turn regression asserting each inbound gets its own Esc reset. Base AgentPTY (Claude/Codex) is unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit 3d5274e86fae585244951e660730aef8af8c6304) * fix(opencode): detect zsh shell mode and exit-recover before inject OpenCode's own heartbeat/check-inbox crons run terminal commands that leave the TUI at a real zsh prompt. Esc alone does not exit that stuck state, so the next Telegram inbound lands at the shell (command not found) and produces no reply. Detect chat vs shell from the output tail: chat readiness markers => type directly; a bare zsh prompt with no markers => typed exit + Enter recovery, settle, then type. Conservative default is chat, so a spurious exit is never submitted into a real chat box. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit 81f3f7add5009d077c9d224de61d042198e8b202) * fix(daemon): send opencode lifecycle telegram (cherry picked from commit f7e3d4894eed4011ef46f36f2f19166b3cf92140) * fix(opencode): mirror restart lifecycle instructions (cherry picked from commit ba774edfabf4ed0064555616021bfc54f329f343) * fix(opencode): elevate telegram reply context (cherry picked from commit b36b599e2508679d6ec03ab512b404218123419e) * test(daemon): regression for overflow-backstop self-referential false-positive The PTY overflow backstop matched its banner regex as plain text, so any agent that read or quoted the overflow/compaction mechanism force-restarted itself at low context. On 2026-06-26 this cascaded across the fleet (boris -> stephen -> paul) once several agents were tasked to investigate opencode's compaction. Tests use the ACTUAL same-line strings pulled from each agent's stdout and prove teeth per falsifiability: every cascade string trips the OLD (regex-only) detector, and none trip the guarded detector at low context; genuine overflow (pct>=85 or exceeds_200k) still force-restarts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit b07f17349ac346bf10aa7b63861709d2a4a798c1) * feat(opencode): report context status from session tokens (cherry picked from commit 1aac99a4631e5151d848017d0575efc836201d02) * fix(opencode): reset context status on fresh start (cherry picked from commit af6613c8b1922d0b1dda7ca497f7c40ffdfd8b21) * fix(opencode): execute startup prompts immediately (cherry picked from commit baad0719e99d0af3dadad481044564080950844e) * feat(daemon): rate limit context handoffs (cherry picked from commit fd7cd3d5d960f36c93cb8b4dd93a47f3a23d59db) * fix(daemon): send opencode lifecycle telegram on context handoff The opencode (deepseek) runtime does not execute the injected boot-prompt instruction to self-send a contextual 'back —' message on handoff restarts, so opencode went silent on every context-handoff (now default-on). codex-app-server self-sends reliably and is unchanged. The daemon now emits a handoff-flavored 'back online (context handoff)' notification for opencode itself. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit cd8b3677a5bff52526101eaf99b43bf572cb9ce9) * fix(daemon): emit planned-restart msg1 for codex/opencode on handoff The two-message handoff pattern is msg1 (lifecycle notif "🔄 <agent> restarted (planned): context handoff at X%") + msg2 (the agent's own "back — ..." summary). msg1 is emitted by the Claude Code hook hook-crash-alert.ts on PTY exit, which only fires for the claude runtime. codex (codex-app-server) and opencode runtimes do not run Claude Code hooks, so James only ever saw msg1 for claude agents. The daemon's maybeSendRuntimeLifecycleNotification now emits msg1 itself for codex/opencode on a handoff restart, reading the reason from the .restart-planned marker and matching hook-crash-alert.ts:394-397 format byte-for-byte. opencode additionally keeps its daemon-emitted back-online msg2 (deepseek does not self-send); codex self-sends its own msg2 so the daemon emits msg1 only for it (no double-up). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit fcde85a15121eac2d212fb4bd9e750a7490e8e03) * fix(daemon): drop redundant opencode back-online substitute on context handoff opencode now reliably self-sends its own contextual "back — ..." via the handoff boot prompt, so the daemon's "Agent X is back online (context handoff)" substitute produced a redundant 3rd message (msg1 + daemon substitute + self-sent back—). Removing it leaves opencode on the clean 2-message pattern: msg1 (planned-restart lifecycle) + the agent's own "back —", matching codex/claude. Part of the restart-message standardization (James 2026-06-29). Agent-side donna + data-codex AGENTS.md stale-step14 re-sync ships alongside (orgs/, gitignored). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit 628324f3cd066b266e1c765c89170926f6f76d92) * feat(templates): context-handoff lifecycle contract in agent template + parity tests Documents the daemon-driven context-handoff lifecycle in the agent template AGENTS.md (status-line bridge, FastChecker polling, injected lifecycle prompts) and extends runtime parity tests to cover it. (extracted from internal commit ec126b46; framework files only) * fix(daemon): restore cooperative-restart loop backstop alongside handoff grace window The grace-window and lease changes were resolved against a branch that predated the ctxHandoffFires cooperative-restart loop backstop shipped in grandamenium#685; restore it so repeated Tier-2 handoff fires still trip the circuit breaker. Also drop the opencode production-validation docs test: it validates internal build-planning artifacts (.agent/) that are not part of the shipped adapter. * test(daemon): herd lease test crosses the 60% default handoff threshold The six-agent herd test relied on createChecker's 50% default usage, written when the default handoff threshold was 40%. After the 40->60 threshold change no checker crossed the threshold and zero handoff prompts fired. Pin the herd at 70% so the test exercises the lease cap as intended. --------- Co-authored-by: Boris <noreply@anthropic.com>
grandamenium#702) The `opencode` runtime and context-handoff lifecycle shipped in grandamenium#685/grandamenium#699 (CLI accepts `--runtime opencode`, auto-maps `--template agent` to the agent-opencode bootstrap), but the behavioral/discoverability layer never caught up: - The agent-management skill (which teaches agents how to scaffold other agents) hard-coded a 2-way runtime choice (claude-code vs codex-app-server) across all 8 mirrors, leaving `--runtime opencode` orphaned. - README.md and CLAUDE.md never mentioned the opencode runtime or the context-handoff lifecycle, so neither was discoverable from the docs. Changes: - agent-management SKILL.md (8 mirrors): STEP 0 now teaches the 3-runtime choice incl opencode + the `--runtime opencode` flow; runtime rule and summary-table row updated (security mirror gets the STEP 0 block only, as it lacks the other two fragments). - README.md: opencode added to the multi-runtime bullet, templates table, and runtime config table; new paragraph documenting the opencode PTY and the context-handoff lifecycle (ctx_handoff_threshold default 60%). - CLAUDE.md: templates line lists agent-codex + agent-opencode. Docs-only; verified `--runtime`/auto-map/threshold facts against src/cli/add-agent.ts and templates/agent-opencode. No OPENAI_API_KEY or sqlite3 claims (neither is a cortextOS dependency). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…grandamenium#703) The agentic-crm-assistant setup gathered preferences well but hand-waved the actual tool connection: tool-discovery only DETECTED installed tools, and the setup skill's Tools step dead-ended in a generic HUMAN task. So an agent that knew the user's stack still could not get email/calendar/messaging authed. This reworks the connect UX to a research-driven, CLI-first, per-domain loop: - TOOL_CONNECTIONS.md: explicit preference order CLI > connector/MCP > browser for every domain; setup order now names the connect + verify steps; adds a Messaging domain (iMessage local reads, outbound gated by approval rules). - tool-discovery/SKILL.md: rewrites detect-only into the full connect loop — once the user names a service for a domain, prefer a CLI, research it live if unknown, walk the user through install + auth in-conversation (user runs the auth command so no secret hits chat), then verify with a real read before moving on. Worked examples for Google (gog) and iMessage. - agentic-crm-setup/SKILL.md: Tools step now drives that per-domain connect loop; removes the dead-end generic HUMAN task. Docs/skill-only. Frontmatter intact; gog login / gog auth status confirmed to exist (the skill now actually calls them). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… caller's (grandamenium#636) manage-cycle <action> <agent> resolved its working directory from the CALLER's environment and ignored the target agent argument. A cycle "created for skoolio" by an orchestrating agent landed in the caller's own experiments/config.json — a second registry the target agent's autoresearch loop never reads. Result: "created" cycles with zero entries in the file the skill loops on, a silent no-op with no error. Add resolveTargetAgentDir(env, target): resolves the target as a sibling of the caller's agentDir first, then via the projectRoot conventions resolveEnv uses; validates the agent name (path-traversal guard); returns null when no candidate exists so manage-cycle fails loudly on typos instead of writing a registry nobody reads. Self- targeting keeps the previous cwd fallback for bare invocations. Co-authored-by: Boris <noreply@anthropic.com>
…k-guard (SEC-1 L3) (grandamenium#704) * feat(security): auto-install pre-push hook + windowed roster/cron leak-guard (SEC-1 L3) Two defense-in-depth hardenings on top of the SEC-1 leak-guard work (L1 CI scan, L2 branch protection, L4 gitignore): L3(a) — Auto-install the tracked pre-push build+test gate. `scripts/hooks/pre-push` and `scripts/setup-hooks.sh` already ship, but nothing wired the installer up, so fresh clones never got the local gate. - setup-hooks.sh is now NON-CLOBBERING: it installs only when no pre-push hook exists (or the existing one is byte-identical); if a different hook is already present it is left untouched. Safe/idempotent to re-run and never overwrites a user's own hook. - install.mjs (new section 9b) and `cortextos init` both call the installer best-effort and NON-FATAL — a hook-install failure never aborts install or init. Gated on non-Windows + repo presence + installer presence. L3(b) — Windowed roster+cron heuristic in leak-guard.sh. The same-line ROSTER_CRON_RE misses a leaked ops table that splits an agent name and its cron expression across adjacent rows. Added an awk windowed scan (WINDOW=3) that flags a roster name and a cron expression co-occurring within 3 lines, using the exact cron alternatives from the existing RE. It fires only when the same-line check did not, so the class reports at most once per file, and inherits the existing test/fixture skip. Verified no new false positives against the full tree (leak-guard --tree HEAD stays clean). Adds tests/leak-guard.test.sh cases: a multi-line roster+cron table must be flagged; a far-apart name+cron control must stay clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(security): non-clobber guard also catches a broken symlink at hook dest `[[ -e "$dest" ]]` is false for a broken symlink (it follows the link and the target is missing), so a broken-symlink pre-push would fall through the guard and get cp-overwritten — a small hole in the never-overwrite guarantee. Add `|| -L "$dest"` so a broken symlink is treated as an existing hook and left in place. Verified: no-hook installs, identical reports already-installed, different is skipped and preserved, broken symlink is skipped and preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…adless crash-loop) Claude Code 2.1.x shows an interactive "Bypass Permissions mode" acceptance screen on first launch with --dangerously-skip-permissions, defaulting to "1. No, exit". The prior auto-accept sent a bare Enter whenever recent output contained 'trust' OR 'Yes' — and the bypass screen's "Yes, I accept" matched 'Yes', so headless agents selected "No, exit" and exited (code 1), crash-looping and never onboarding. Replace the two fixed-delay bare-Enter sends with a bounded poll that: - distinguishes the bypass screen (Down-arrow + Enter to select "Yes, I accept") from the trust-folder screen (bare Enter), matching co-occurring ANSI-stripped tokens so normal output cannot trigger a stray keystroke; - handles each screen at most once (one-shot guards); - stops on isBootstrapped() and an unconditional 20s backstop. Hardening beyond the original fix: - bootstrap-guard the deferred confirm Enter so a late session bootstrap cannot swallow the CR into the live session; - clear the poll immediately after the bypass Down+Enter is consumed, so the only hazardous injection cannot reach a live session without relying on the case-sensitive 'permissions' status-bar halt (which is fragile to CC TUI text changes — the exact failure mode this incident is about). Extracted as a minimal single-file change from fork PR grandamenium#611 (neoturns/cortextos:fix/headless-agent-startup). The remainder of that PR (bootstrapPattern, Windows binary probing, Telegram-optional onboarding) is already present on main via independently merged changes, so only the bypass-screen handling is new. Original work by neoturns. Co-Authored-By: neoturns <neoturns@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8bc056d2f5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| import { OrgContext } from '../types'; | ||
| import { validateAgentName, validateOrgName } from '../utils/validate'; | ||
|
|
||
| <<<<<<< HEAD |
There was a problem hiding this comment.
Resolve the committed merge conflicts
The sync commits unresolved conflict markers throughout production sources, beginning here and continuing across the CLI, daemon, PTY, bus, Telegram, and type modules. In every build environment, npm run typecheck now fails with numerous TS1185 errors, so the package cannot be built and neither the CLI nor daemon can be released until each conflict is resolved and the markers are removed.
Useful? React with 👍 / 👎.
Resolves the 60-file / 242-hunk conflict from merging upstream/main (a15baad, 247 commits) into the fork. Redone as a clean 3-way merge off base 75a40eb (replaces the marker-committed 8bc056d). Policy: default take upstream; preserve fork ops/product fixes; keep all upstream security fixes. Adopted upstream: opencode runtime (PTY/add-agent/ types/README), turn/steer, statusLine, telegram reply-target logging, security. Preserved fork: BUG-011 durable-quarantine + lifecycle-withhold state machine, RestartIntent atomic restart, TZ pinning (daemon+ecosystem+timezone.ts), KB merge_collections/hybrid_search, three-tier task lookup, idempotent cron add-missing, symlink-canonicalize path guard, vitest scrub-ctx-env, codex-expiry warning. ARCHITECTURAL DECISION (for codex gate): the daemon-lifecycle subsystem is a deep fork-vs-upstream divergence. Kept the fork's context-handoff (modal /compact detector + 401-auth-wedge + intent-based sessionRefresh), which is coupling-locked to the BUG-011 quarantine. DECLINED upstream's overlapping refactor (context-handoff-lease coordination + ContextMonitor + fast-checker reply-context + generalized opencode notification) because it is incompatible with the fork quarantine coupling. Removed the orphaned tests for the declined refactor (context-handoff-lease.test, context-monitor.test, agent-process-opencode.test); context-handoff-lease.ts left in tree (unused) for review. Verify: esbuild build OK, tsc --noEmit clean, 2094/2094 runnable tests pass (3 dashboard files fail to LOAD only on a better-sqlite3 NODE_MODULE_VERSION native-module ABI mismatch — environmental, pre-existing, not this merge). DO NOT MERGE without the codex non-author review gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XBPyrfBfc8EumjREkeFw9R
…rk quarantine
Codex CHANGES-NEEDED gate (pr24-independent-gate-2026-07-26): the upstream
handoff lease/grace/overflow/loop protections WRAP handoff admission + restart
triggering and are compatible with the fork's BUG-011 quarantine, RestartIntent,
/compact modal detector, and 401 auth-wedge recovery. Those are kept UNTOUCHED;
the upstream protections are layered around the existing forceContextRestart()
/ sessionRefresh('fresh') path in src/daemon/fast-checker.ts.
Four protections ported from a15baad:
1. Overflow-banner corroboration — the "extra usage 1M context | conversation
too long compaction" banner now force-restarts ONLY when exceeds_200k_tokens
or reported usage pct >= 85, so an agent that merely reads/discusses the
banner at low context no longer self-restarts.
2. Runtime-aware fresh-session grace — handoffGraceMs(runtime): 10min for
codex-app-server/opencode, 2min otherwise; anchored on ctxSessionStartedAt.
Suppresses warn + Tier-2 handoff while within grace; the hard /compact modal
and corroborated-overflow paths still fire.
3. Fleet handoff LEASE admission via context-handoff-lease.ts — acquire/queue
before Tier-2; release by agent name on a fresh below-threshold session and on
Tier-3 teardown; leaked-lease guard (session-id-independent) for the Claude
null-session_id edge; over-release guard for a lease the live session holds.
4. Persisted cooperative-handoff loop breaker — ctxHandoffFires timestamps in a
15min window, tripped at 3, persisted to <stateDir>/.ctx-circuit.json
(handoffFires field) and reloaded on construction so a --continue/handoff
restart cannot reset it.
Templates / default-on: adopt upstream default-ON threshold resolution — an unset
ctx_handoff_threshold now hands off at the default (30% warn / 60% handoff,
matching a15baad) instead of observe-only, so freshly scaffolded agent-codex and
agent-opencode agents auto-hand-off; an explicit ctx_handoff_threshold <= 0 is the
deliberate opt-out. Fork configured thresholds and the ctx_autoreset_threshold
alias are preserved. Templates left threshold-free so default-on covers them.
Tests: restored + adapted the 3 deleted suites (context-handoff-lease,
context-monitor, agent-process-opencode). The opencode notification tests were
adapted to the fork's real behavior (daemon back-online is codex-only via
maybeSendCodexBootNotification and skipped on handoff restarts; opencode is
prompt-driven) rather than upstream's generalized maybeSendRuntimeLifecycle path.
Added context-threshold-default-on and context-handoff-loop-breaker suites for
the default-on/<=0 opt-out and loop-breaker persistence. Fork modal/401/
quarantine/RestartIntent families kept and passing.
npm run build + tsc --noEmit clean; full suite 2163 passed / 50 skipped, only the
3 pre-existing dashboard better-sqlite3 NODE_MODULE_VERSION ABI load failures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBPyrfBfc8EumjREkeFw9R
…otification codex re-gate on 8a69a38 required this (single blocker). The fork-narrow maybeSendCodexBootNotification (codex-only, skipped on handoff) was reverting an upstream FIX: upstream added daemon-owned lifecycle notifications after production evidence that an OpenCode --continue restart completed with NO Telegram under prompt-only ownership. Asserting "no daemon send" blessed that known failure. Restores upstream maybeSendRuntimeLifecycleNotification + buildPlannedRestartNotification: - OpenCode fresh/continue -> daemon-direct "Agent <name> is back online" - Codex + OpenCode handoff -> daemon-direct planned-restart msg1 from .restart-planned - handoff msg2 stays agent-self-sent via the boot prompt (no redundant generic ping) - Telegram-disabled / no-handle gating preserved (via shouldPromptTelegramOnlineMessage) Restores the upstream assertions in agent-process-opencode.test.ts and agent-process-codex-app-server.test.ts (undoing the fork-narrow rewrite), and updates the two BUG-011 tests (restart-marker-supersede, startagent-spawn-rollback) that spy the notification method to the restored name. agent-process.ts is the only source change; the FastChecker blend from 8a69a38 is untouched. Verify: tsc clean, build success, 0 markers, full test 2163 passed | 50 skipped (only the 3 environmental dashboard better-sqlite3 NODE_MODULE_VERSION load fails). DO NOT MERGE without the codex re-gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XBPyrfBfc8EumjREkeFw9R
8bc056d to
bf89a8f
Compare
|
Superseded by #25 (Weekly upstream sync 2026-08-02, 251 commits = superset of this PR's 247). Closing to avoid two open weekly-sync PRs. NOTE: like #25, this PR is a naive upstream merge that does NOT contain the local round-6 auth-wedge fix — do not merge as-is; the gated reconstruction tracked on #25 is the correct path. (boss agent, 2026-08-02) |
Weekly upstream sync (2026-07-26)
Upstream
grandamenium/cortextosadded 247 commit(s) since the last run.Each one is summarized in plain English below.
What's new
Bug fix in pty: auto-accept Claude Code 2.1.x Bypass Permissions screen (headless crash-loop)
a15baad(grandamenium@a15baad)New feature in security: auto-install pre-push hook + windowed roster/cron leak-guard (SEC-1 L3) (feat(security): auto-install pre-push hook + windowed roster/cron leak-guard (SEC-1 L3) grandamenium/cortextos#704)
5a0882d(grandamenium@5a0882d)Bug fix in bus: manage-cycle operates on the target agent's config, not the caller's (fix(bus): manage-cycle operates on the target agent's config, not the caller's grandamenium/cortextos#636)
bdcbc01(grandamenium@bdcbc01)New feature in crm-assistant: connect + verify tools in setup, not just detect (feat(crm-assistant): connect + verify tools in setup, not just detect grandamenium/cortextos#703)
20543a7(grandamenium@20543a7)Docs: teach opencode runtime in agent-management skill + README/CLAUDE (docs: teach opencode runtime in agent-management skill + README/CLAUDE grandamenium/cortextos#702)
9f01981(grandamenium@9f01981)New feature in daemon: context-handoff lifecycle + native opencode adapter (feat(daemon): context-handoff lifecycle + native opencode adapter grandamenium/cortextos#699)
b15ca01(grandamenium@b15ca01)New feature in daemon: context-handoff mechanism — default-on at 60% model window (feat(daemon): context-handoff mechanism, default-on at 60% window grandamenium/cortextos#685)
99158f8(grandamenium@99158f8)New feature in security: server-side leak-guard CI check + broadened gitignore (SEC-1 L1/L4) (feat(security): server-side leak-guard CI check (SEC-1 L1/L4) grandamenium/cortextos#698)
8267bab(grandamenium@8267bab)Maintenance in security: purge leaked internal fleet-metadata reports and operator paths from public repo
086ba1d(grandamenium@086ba1d)Bug fix in bus: remove Codex token expiry auto-send from check-usage-api.sh (fix(bus): remove Codex token expiry auto-send grandamenium/cortextos#684)
2b6932e(grandamenium@2b6932e)69ae247(grandamenium@69ae247)143f915(grandamenium@143f915)ad4e0d3(grandamenium@ad4e0d3)c7dffe7(grandamenium@c7dffe7)d11d8e0(grandamenium@d11d8e0)46bd910(grandamenium@46bd910)2faa961(grandamenium@2faa961)0db2a84(grandamenium@0db2a84)fc0ac54(grandamenium@fc0ac54)025cce8(grandamenium@025cce8)dab255a(grandamenium@dab255a)db39193(grandamenium@db39193)20583d3(grandamenium@20583d3)4369e94(grandamenium@4369e94)ee21f17(grandamenium@ee21f17)d815993(grandamenium@d815993)b1883f9(grandamenium@b1883f9)381aa49(grandamenium@381aa49)593e0c0(grandamenium@593e0c0)85ddcf7(grandamenium@85ddcf7)initandadd-agent --org(cli:initandadd-agent --orgaccept mixed-case org names that runtime + dashboard then reject grandamenium/cortextos#407) (fix(cli): validate org name in init + add-agent (closes #408) grandamenium/cortextos#548)5f9cc6c(grandamenium@5f9cc6c)d06936d(grandamenium@d06936d)4a0ca24(grandamenium@4a0ca24)05ce125(grandamenium@05ce125)897c5af(grandamenium@897c5af)452a9c7(grandamenium@452a9c7)d81fe55(grandamenium@d81fe55)40bdacd(grandamenium@40bdacd)97b8574(grandamenium@97b8574)53accd4(grandamenium@53accd4)467e977(grandamenium@467e977)e18c99b(grandamenium@e18c99b)cc3fffd(grandamenium@cc3fffd)f2b399a(grandamenium@f2b399a)e68def7(grandamenium@e68def7)519b698(grandamenium@519b698)7eed2e2(grandamenium@7eed2e2)f03de88(grandamenium@f03de88)06774c2(grandamenium@06774c2)19def47(grandamenium@19def47)da76631(grandamenium@da76631)46f8761(grandamenium@46f8761)7751ae4(grandamenium@7751ae4)d7cf5d0(grandamenium@d7cf5d0)8b68e98(grandamenium@8b68e98)9a30342(grandamenium@9a30342)a7a5932(grandamenium@a7a5932)67a6a63(grandamenium@67a6a63)5685bc3(grandamenium@5685bc3)d5c4acd(grandamenium@d5c4acd)1e1224e(grandamenium@1e1224e)5df9d31(grandamenium@5df9d31)8e45560(grandamenium@8e45560)93b8d09(grandamenium@93b8d09)5ccab8c(grandamenium@5ccab8c)fef58bf(grandamenium@fef58bf)56045ea(grandamenium@56045ea)6f4bc20(grandamenium@6f4bc20)525ba48(grandamenium@525ba48)66a9a7a(grandamenium@66a9a7a)2985b18(grandamenium@2985b18)28224eb(grandamenium@28224eb)701161d(grandamenium@701161d)7782a35(grandamenium@7782a35)c074194(grandamenium@c074194)bc71008(grandamenium@bc71008)28ae583(grandamenium@28ae583)f013887(grandamenium@f013887)8817d75(grandamenium@8817d75)c102ab5(grandamenium@c102ab5)3c0385b(grandamenium@3c0385b)ecdd47c(grandamenium@ecdd47c)3a21d47(grandamenium@3a21d47)49e5755(grandamenium@49e5755)eb3ddf3(grandamenium@eb3ddf3)b91261a(grandamenium@b91261a)675943e(grandamenium@675943e)22cc61b(grandamenium@22cc61b)1ae60da(grandamenium@1ae60da)b8696f9(grandamenium@b8696f9)784d6ca(grandamenium@784d6ca)158dcfd(grandamenium@158dcfd)f4ba977(grandamenium@f4ba977)00be9e6(grandamenium@00be9e6)b1eee4b(grandamenium@b1eee4b)cd6454c(grandamenium@cd6454c)95959df(grandamenium@95959df)b81f247(grandamenium@b81f247)dc9e296(grandamenium@dc9e296)a47b1e4(grandamenium@a47b1e4)70d11da(grandamenium@70d11da)1731eb6(grandamenium@1731eb6)aa1d9b5(grandamenium@aa1d9b5)bf2c898(grandamenium@bf2c898)ae64452(grandamenium@ae64452)0328e8e(grandamenium@0328e8e)051b22e(grandamenium@051b22e)3f3d738(grandamenium@3f3d738)5bfda2f(grandamenium@5bfda2f)8d8b872(grandamenium@8d8b872)6ce2022(grandamenium@6ce2022)462efc0(grandamenium@462efc0)035d51e(grandamenium@035d51e)f37dc1e(grandamenium@f37dc1e)a9dde1b(grandamenium@a9dde1b)5ce5ea0(grandamenium@5ce5ea0)bc0f714(grandamenium@bc0f714)eb93da5(grandamenium@eb93da5)362d7a1(grandamenium@362d7a1)f0c5dbb(grandamenium@f0c5dbb)00f79ab(grandamenium@00f79ab)90deac6(grandamenium@90deac6)ab32c68(grandamenium@ab32c68)bede753(grandamenium@bede753)3f338bc(grandamenium@3f338bc)c53a0ca(grandamenium@c53a0ca)d11854a(grandamenium@d11854a)f88e3d3(grandamenium@f88e3d3)f34b329(grandamenium@f34b329)a30f176(grandamenium@a30f176)9fc1c6b(grandamenium@9fc1c6b)7f09aff(grandamenium@7f09aff)6befcfb(grandamenium@6befcfb)eec8745(grandamenium@eec8745)d7aa78b(grandamenium@d7aa78b)3e835bb(grandamenium@3e835bb)164742e(grandamenium@164742e)de521d1(grandamenium@de521d1)22e00be(grandamenium@22e00be)476ea61(grandamenium@476ea61)918d3a8(grandamenium@918d3a8)59913b5(grandamenium@59913b5)dfc5556(grandamenium@dfc5556)2137d48(grandamenium@2137d48)f81a8d0(grandamenium@f81a8d0)27b01a2(grandamenium@27b01a2)c72af5d(grandamenium@c72af5d)788a9e6(grandamenium@788a9e6)6d8cbe1(grandamenium@6d8cbe1)b0c8a0a(grandamenium@b0c8a0a)b7ef973(grandamenium@b7ef973)72f6c29(grandamenium@72f6c29)4fd6e05(grandamenium@4fd6e05)d86d50f(grandamenium@d86d50f)897b8d6(grandamenium@897b8d6)166ebb8(grandamenium@166ebb8)1562fa4(grandamenium@1562fa4)a00ed2c(grandamenium@a00ed2c)fbe58fe(grandamenium@fbe58fe)55cf04a(grandamenium@55cf04a)d77820b(grandamenium@d77820b)860b359(grandamenium@860b359)c7db670(grandamenium@c7db670)86ebce6(grandamenium@86ebce6)8d16468(grandamenium@8d16468)861bd6b(grandamenium@861bd6b)c9291cf(grandamenium@c9291cf)abca0a4(grandamenium@abca0a4)0f45fdd(grandamenium@0f45fdd)a7e278a(grandamenium@a7e278a)63a0d6a(grandamenium@63a0d6a)fcf1145(grandamenium@fcf1145)6869e45(grandamenium@6869e45)764dd84(grandamenium@764dd84)cab0d14(grandamenium@cab0d14)763bfa7(grandamenium@763bfa7)2db9328(grandamenium@2db9328)ace7045(grandamenium@ace7045)528fd71(grandamenium@528fd71)890d69e(grandamenium@890d69e)bbe2187(grandamenium@bbe2187)8ec8cca(grandamenium@8ec8cca)6e36a0f(grandamenium@6e36a0f)7cac57a(grandamenium@7cac57a)98ce453(grandamenium@98ce453)93e1f5b(grandamenium@93e1f5b)c34c11d(grandamenium@c34c11d)376195a(grandamenium@376195a)6cb7244(grandamenium@6cb7244)004b8ee(grandamenium@004b8ee)875a867(grandamenium@875a867)9cf92a8(grandamenium@9cf92a8)495a874(grandamenium@495a874)eaec9c3(grandamenium@eaec9c3)e537d05(grandamenium@e537d05)c73a292(grandamenium@c73a292)fa0b11b(grandamenium@fa0b11b)6820592(grandamenium@6820592)4e2ebb6(grandamenium@4e2ebb6)7732ebd(grandamenium@7732ebd)0ea6b8f(grandamenium@0ea6b8f)7d5ab2b(grandamenium@7d5ab2b)426c410(grandamenium@426c410)00452d6(grandamenium@00452d6)0e3359f(grandamenium@0e3359f)2bc3fc3(grandamenium@2bc3fc3)b4f9eab(grandamenium@b4f9eab)5cfbe62(grandamenium@5cfbe62)d988f59(grandamenium@d988f59)a8e6c28(grandamenium@a8e6c28)ec9d0b5(grandamenium@ec9d0b5)3006ece(grandamenium@3006ece)43d4dfa(grandamenium@43d4dfa)d5b88c0(grandamenium@d5b88c0)a4a322a(grandamenium@a4a322a)d7b531c(grandamenium@d7b531c)a3a75be(grandamenium@a3a75be)2c51dfd(grandamenium@2c51dfd)36a9bcb(grandamenium@36a9bcb)ba4dcac(grandamenium@ba4dcac)fd5a252(grandamenium@fd5a252)3fae1c1(grandamenium@3fae1c1)7109f9a(grandamenium@7109f9a)ca0ea77(grandamenium@ca0ea77)e9b65af(grandamenium@e9b65af)ba52e0c(grandamenium@ba52e0c)8178658(grandamenium@8178658)2f7ee06(grandamenium@2f7ee06)fce35b6(grandamenium@fce35b6)f12f8b4(grandamenium@f12f8b4)8e34742(grandamenium@8e34742)55e710d(grandamenium@55e710d)b151f74(grandamenium@b151f74)4ed0d58(grandamenium@4ed0d58)b0bb43c(grandamenium@b0bb43c)3df05a5(grandamenium@3df05a5)49b61a6(grandamenium@49b61a6)21aeaf0(grandamenium@21aeaf0)2b3409d(grandamenium@2b3409d)aecdcfa(grandamenium@aecdcfa)fdd9599(grandamenium@fdd9599)42e1bba(grandamenium@42e1bba)844ef57(grandamenium@844ef57)1d001cb(grandamenium@1d001cb)3de02fc(grandamenium@3de02fc)94c5845(grandamenium@94c5845)39163d9(grandamenium@39163d9)How to merge
merge allto take everything.merge 1 3 5(numbers from the list above) to cherry-pick those.skipto dismiss this whole PR.This PR was opened automatically by boss every Sunday at 6:30pm Chicago time.