Skip to content

feat: client portal + Bing Ads pipeline - #2

Closed
clicktoacquire wants to merge 299 commits into
mainfrom
feat/client-dashboard-portal
Closed

clicktoacquire wants to merge 299 commits into
mainfrom
feat/client-dashboard-portal

Conversation

@clicktoacquire

Copy link
Copy Markdown
Owner

Summary

  • Client portal reports page with 8 KPI cards, date range selector, daily performance table
  • Client auth + RBAC (client role, JWT client_id scoping, middleware isolation)
  • BQ entity_type=campaign filter on all daily_metrics queries
  • Portal links on clients list + detail pages
  • Bing Ads daily ingest pipeline (931 lines, OAuth+REST, partition-safe BQ writes)

Test plan

  • Dashboard builds cleanly
  • Deployed to Vercel
  • Verify portal report with real BQ data
  • Test client login flow
  • Bing pipeline dry-run once OAuth creds provisioned

ClintMoody and others added 30 commits April 16, 2026 12:58
…um#117)

validateOrgName enforced lowercase which rejects mixed-case org names
(e.g. AcmeCorp) that predate the strict lowercase policy. The env
resolution layer should not re-validate what the filesystem already
accepted at org-creation time. Restrict to unsafe-character rejection
(path traversal, whitespace) only. Lowercase enforcement stays at the
CLI --org parse layer where user input is normalised before use.

Fixes bus commands that fail with "invalid org" after upstream added
strict validateOrgName to resolveEnv, even for org directories that
exist on disk with mixed casing.

Co-authored-by: Bob <bob@agents.cortextos>
…sh alert (grandamenium#109)

The SessionEnd crash-alert hook currently pages Telegram on every
non-crash end type (planned-restart, session-refresh, daemon-stop,
user-*) regardless of time, and has no deduplication. A single
misclassified crash loop (e.g. the Claude Code CLI weekly-limit wall,
which surfaces as "You've used 100% of your weekly limit · resets Xpm"
rather than an API error body) results in a 🚨 CRASH Telegram alert
every 30 minutes all night.

This changes the hook to:

1. Detect Anthropic rate-limit and CLI weekly-limit signatures in the
   tail of stdout.log and reclassify bare `crash` exits as a new
   `rate-limited` end type. The calmer ⏳ message is emitted instead of
   🚨 CRASH, and the exit is subject to quiet hours. Patterns scanned:
   overloaded_error, rate_limit_error, rate limit, too many requests,
   quota exceeded, usage limit, weekly limit, 5-hour limit, and the
   Claude Code CLI status-bar regex /used \d+% of your/.

2. Apply quiet hours (22:00–07:00 America/Los_Angeles) to every routine
   end type: planned-restart, session-refresh, daemon-stop, user-*, and
   rate-limited. Real unexpected `crash` events still page at night —
   that is the only thing worth waking the operator for.

3. Deduplicate identical (agent, type) alerts within a 10-minute window
   via a .crash_alert_dedup.json state file. A broken watchdog loop
   produces at most one notification instead of a buzz storm.

All events continue to be appended to crashes.log regardless of whether
the Telegram alert is actually sent, so post-hoc visibility is
preserved.

The quiet-hour window and dedup TTL are defined as module constants
(QUIET_HOUR_START_LA = 22, QUIET_HOUR_END_LA = 7, DEDUP_WINDOW_MS = 10
minutes) so they are easy to adjust. Timezone resolution uses Intl via
toLocaleString('en-US', { timeZone: 'America/Los_Angeles' }), which
does not depend on the host TZ.

Tested against cortextos on revops-global — stopped a 7×30-minute
overnight alert loop on an agent that had hit the Claude weekly limit.
…randamenium#123)

Community-contributed skill belongs in community/skills/ not
templates/agent/.claude/skills/ per project policy.

Original PR: grandamenium#92

Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Community-contributed skill belongs in community/skills/ not
templates/agent/.claude/skills/ per project policy.

Original PR: grandamenium#93

Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Community-contributed skill belongs in community/skills/ not
templates/agent/.claude/skills/ per project policy.

Original PR: grandamenium#95

Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Community-contributed skill belongs in community/skills/ not
templates/agent/.claude/skills/ per project policy.

Original PR: grandamenium#96

Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… template (grandamenium#127)

Community-contributed skill belongs in community/skills/ per policy.
As an M2C1-related skill, also added to templates/m2c1-worker/.claude/skills/
(new m2c1-worker template directory, created as part of this fix).

Original PR: grandamenium#97

Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…andamenium#128)

Adds mandatory planning phase (PLAN.md → send → await PLAN_APPROVED)
before any source files are written, and a corresponding supervisor
review checklist with approve/reject bus commands.

Changes apply to templates/agent/.claude/skills/m2c1-worker/SKILL.md
(existing template edit — no path change needed per policy).
Also mirrored to new templates/m2c1-worker/.claude/skills/m2c1-worker/.

Original PR: grandamenium#98

Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…nium#130)

Community-contributed skill belongs in community/skills/ not
templates/agent/.claude/skills/ per project policy.

Original PR: grandamenium#102

Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…randamenium#129)

Adds self-monitoring stuck detection (5 consecutive identical tool
calls triggers STUCK ALERT bus message to supervisor) and matching
supervisor handling section with specific directive guidance.

Changes apply to templates/agent/.claude/skills/m2c1-worker/SKILL.md
(existing template edit — no path change needed per policy).
Also mirrored to templates/m2c1-worker/.claude/skills/m2c1-worker/.

Original PR: grandamenium#99

Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…nsitive fs

existsSync('orgs/acmecorp') returns true on macOS/Windows even when
the directory was created as 'orgs/AcmeCorp', causing normalizeOrgName
to incorrectly return the input as-is instead of the canonical on-disk
casing. Replace the existsSync fast-path with readdirSync+entries.includes()
so the exact-case check operates on the actual directory listing rather
than the filesystem's case-folded lookup.

Also fix the case-sensitive-fs test to skip gracefully on macOS where
mkdirSync('acmecorp') throws EEXIST when 'AcmeCorp' already exists.
…ort (grandamenium#134)

Both PreCompact hooks could hit the settings.json timeout (10s/15s) and
cause Claude Code to abort compaction entirely, leaving agents stuck at
100% context with no recovery path.

- hook-compact-telegram: add 5s AbortController to fetch() so the Telegram
  call always resolves before the 10s hook timeout fires
- hook-extract-facts: race readStdin() against a 10s timer so the hook
  exits cleanly even if Claude Code does not close stdin, well within the
  15s hook timeout

Fixes the "no compaction message in 24h" regression introduced after grandamenium#116.

Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…emplate (grandamenium#137)

* fix(hooks): remove hook-extract-facts from PreCompact — never worked, revert to simple notification

hook-extract-facts assumed Claude Code sends a summary via stdin at
PreCompact time. It does not — it sends transcript_path. The hook has
never written a single facts file. Removing it from all agent and
template settings.json files.

PreCompact now has only hook-compact-telegram (with the 5s AbortController
timeout from PR grandamenium#134). Simple and reliable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(cli): add CLAUDE_CODE_DISABLE_1M_CONTEXT=true to new agent .env template

Claude Code v2.1.111+ gives Sonnet 4.6 a 1M context window by default.
Without extra usage billing enabled, compaction fails with:
"Extra usage is required for 1M context"

All agents need CLAUDE_CODE_DISABLE_1M_CONTEXT=true in their .env to revert
to standard 200k context and keep auto-compaction working. Adding it to the
generated .env so every new agent gets it automatically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(cli): note Opus exception in CLAUDE_CODE_DISABLE_1M_CONTEXT comment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Approval-gated virtual Visa card issuance for autonomous agent purchases.
Uses AgentCard.sh MCP tools for card creation, checkout, and lifecycle.
Depends on the approvals skill for human-in-the-loop authorization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… revert to simple notification (grandamenium#135)

hook-extract-facts assumed Claude Code sends a summary via stdin at
PreCompact time. It does not — it sends transcript_path. The hook has
never written a single facts file. Removing it from all agent and
template settings.json files.

PreCompact now has only hook-compact-telegram (with the 5s AbortController
timeout from PR grandamenium#134). Simple and reliable.

Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ses wrong URL (grandamenium#139)

* fix(dashboard): org filter not applied on page load; login redirect uses wrong URL

- DashboardShell restores org from localStorage on mount but never
  navigates to sync the URL, so the server renders with all agents
  while the selector shows the saved org. Add a mount-only useEffect
  that calls router.replace() when the saved org differs from the
  current ?org= param.

- login/page.tsx: after a successful redirect:follow sign-in, navigate
  to '/' instead of res.url. res.url resolves to an absolute URL which
  can be http://localhost:3000/ behind a reverse proxy, causing
  navigation to the wrong host. '/' always resolves to the current
  origin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(dashboard): address PR review — URL authoritative for org; preserve callbackUrl

dashboard-shell.tsx: Remove the mount-only useEffect that synced
localStorage to the URL. That approach clobbered deep links with
explicit ?org= params, caused a double server render and visible
flash, and introduced a race with OrgSelector. Instead, read
window.location.search directly in the useState initializer so the
URL is authoritative at hydration time, with localStorage as fallback.
No navigation side effect needed.

login/page.tsx: Hardcoding '/' lost the callbackUrl set by middleware
when bouncing unauthenticated users to login. Read callbackUrl from
the current URL's search params and validate it is a same-origin path
(starts with / but not //) before navigating. Falls back to / if
absent or invalid, which also guards against open-redirect via
callbackUrl=//evil.com.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When a Telegram message is delivered to an agent, prepend the last 6
back-and-forth messages (inbound + outbound, sorted by timestamp) as
a [Recent conversation:] block. This gives the agent visibility into
what was said immediately before the current message, eliminating the
context gap when multiple senders (director, analyst, etc.) write to
the same chat and the human replies.

- logging.ts: add buildRecentHistory() reads inbound/outbound JSONL logs
- fast-checker.ts: add optional recentHistory param to formatTelegramTextMessage
- agent-manager.ts: call buildRecentHistory and pass to formatter
…log.json churn

Two fixes applied to PR grandamenium#140 (feat: recent conversation history):

1. logging.ts: replace hardcoded 'Greg' speaker label with
   process.env.ADMIN_USERNAME ?? 'user'. The original used a personal
   name that would appear verbatim in every cortextOS installation.

2. community/catalog.json: revert the cosmetic reformatter churn
   (em-dash → \u2014, inline tags → multi-line). That change was
   unrelated to the feature and inflated the diff by 150 lines.

The feature itself (buildRecentHistory, formatTelegramTextMessage injection,
agent-manager wiring) is preserved as-is from the original PR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…enium#121)

Adds two files:
- scripts/setup-hooks.sh: one-time setup script. Run after cloning to
  install git hooks into .git/hooks/.
- scripts/hooks/pre-push: pre-push hook that runs npm run build &&
  npm test before any push. Aborts the push if either step fails.

Motivation: two CI failures overnight were caused by TypeScript errors
and missing dashboard deps that were not caught locally before pushing.
James directive: fail locally, not on CI.

Usage:
  bash scripts/setup-hooks.sh   # run once after cloning

Note: git hooks are not versioned (.git/hooks/ is not committed).
setup-hooks.sh bridges this by copying the versioned hook sources into
the local .git/hooks directory. Each contributor runs setup-hooks.sh
once — CI is not affected.

Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…issue grandamenium#110) (grandamenium#120)

Before this change, gap detection silently skipped any cron with no
fire record in cron-state.json. On fresh deploy or after a daemon
restart, all entries are missing, so the detection loop ran every 10
minutes but never fired a single nudge — creating guaranteed dead
zones as long as the first cron fire had not yet been recorded.

Fix: pass loopStartedAt (the timestamp when scheduleGapDetection was
called) into runGapDetectionLoop. When no record exists for a cron,
use loopStartedAt as the implicit last_fire. Gap detection then fires
a nudge if the cron has not fired within 2x its interval AFTER the
daemon started — directly addressing the cold-start dead-zone.

Closes grandamenium#110.

Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ions to the agent (grandamenium#141)

Adds support for Telegram's message_reaction update type so agents can
see when a user reacts to one of the bot's messages. Reactions are
injected into the PTY as a one-line "=== REACTION from ... ===" banner
so the agent can follow up on acks or clarify after a negative signal.

Changes:
1. src/telegram/api.ts: append 'message_reaction' to allowed_updates in
   getUpdates. Without this Telegram filters reactions out server-side
   and the poller never sees them.
2. src/types/index.ts: new TelegramMessageReaction interface and
   TelegramReactionType tagged union (emoji | custom_emoji). Optional
   message_reaction field on TelegramUpdate.
3. src/telegram/poller.ts: new ReactionHandler type, onReaction()
   registrar, and pollOnce() routing with the same
   try/catch/handlerFailed + offset-after-handler contract as the
   existing message and callback_query paths. A reaction-handler throw
   leaves the update un-acknowledged so Telegram re-delivers it.
4. src/daemon/fast-checker.ts: new FastChecker.formatTelegramReaction()
   static formatter. Handles three shapes: added reaction (render
   new_reaction emojis), removed reaction (render "removed <old>"),
   custom_emoji placeholder (since we do not resolve custom_emoji_id).
5. src/daemon/agent-manager.ts: poller.onReaction() wire-in alongside
   the existing onMessage/onCallback handlers. Same ALLOWED_USER gate,
   same dedup via FastChecker.isDuplicate, same queueTelegramMessage
   PTY injection path — reactions flow through the same plumbing as
   text messages so they inherit rate limits and ordering.

Tests: 2 new in tests/unit/telegram/poller.test.ts (route to handler
+ offset advance, handler-throw does not advance offset) + 4 in
tests/unit/daemon/fast-checker.test.ts (single emoji, multiple
concurrent emojis, removed reaction, custom_emoji placeholder).

Full suite 664/664 green, tsc clean, build clean.

Co-authored-by: Bob <bob@agents.cortextos>
…randamenium#142)

The regex in the goals generate-md subcommand rejected any argument
containing an uppercase character, which made it impossible to run
`cortextos goals generate-md --agent <name> --org <CamelCaseOrg>` —
the exact case for mixed-case org directories that predate the
strict-lowercase policy.

Fix: change `/^[a-z0-9_-]+$/` to `/^[A-Za-z0-9_-]+$/` on both the
agent and org arguments. The rest of the validator (length,
dash/underscore allowlist, rejection of path traversal or spaces)
is unchanged — this relaxation is scoped to the case class.

Matches the same fix already applied to src/utils/validate.ts. The
goals subcommand has its own inline check and was missed by that
earlier patch.

Verified: `cortextos goals generate-md --agent <name> --org <CamelCaseOrg>`
now returns "Generated GOALS.md for <name>" instead of the
"alphanumeric/dash/underscore only" error.

Full suite green, tsc clean, build clean.

Co-authored-by: Bob <bob@agents.cortextos>
…ing global list (grandamenium#143)

`cortextos bus manage-cycle list <agent>` was returning every cycle in
config.json regardless of the agent argument. Cycle-to-agent
attribution in storage was already correct (each cycle record carries
its own `agent` field) — the bug was purely in the list path's
ignoring the filter.

Fix: in the `list` case of `manageCycle()`, filter `config.cycles` by
`options.agent` when supplied. When the caller omits the agent, the
full list is still returned so existing callers that deliberately
want a global view keep working (back-compat).

Adds one unit test in tests/sprint3-experiments.test.ts covering: two
cycles under 'alice' + one under a second agent, list --agent=alice
returns exactly the alice pair, list --agent=other-agent returns just
the other cycle, list with no filter returns all three (back-compat).

Full suite 665/665 green, tsc clean.

Co-authored-by: Bob <bob@agents.cortextos>
…onnecting-IP fallback, rate-limit error message (grandamenium#144)

Three fixes for issue grandamenium#138:

1. .env.local.example: add commented TRUST_PROXY and AUTH_URL entries with
   explanations so users know to set these when deploying behind Cloudflare
   Tunnel or any other reverse proxy.

2. auth.ts: fall back to CF-Connecting-IP header when TRUST_PROXY is unset.
   CF-Connecting-IP is injected by Cloudflare and not spoofable from outside
   Cloudflare's network, so it is safe to use without TRUST_PROXY=true.
   Prevents all login attempts from bucketing as 0.0.0.0 behind a quick tunnel.

3. login/page.tsx: map CallbackRouteError to a human-readable message.
   "Too many attempts. Please wait a few minutes and try again." instead
   of the raw NextAuth error code which gave no indication of the real cause.

Closes grandamenium#138.

Co-authored-by: James Goldbach <cortextos@Jamess-Mac-mini.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…-v5v3 + GHSA-458j-xx4x-4375 (grandamenium#146)

REGRESSION: upstream merge a3002be overwrote the previous next@16.2.3 fix
(577f16e), reverting dashboard to next@16.2.1 which is vulnerable to
GHSA-q4gf-8mx6-v5v3 (Server Components DoS, HIGH).

This commit:
- Bumps next to 16.2.4 (exact pin, no caret — prevents future drift)
- npm audit fix also bumps hono to >=4.12.14 clearing GHSA-458j-xx4x-4375
  (JSX SSR HTML injection, MODERATE)
- Post-fix: npm audit reports 0 vulnerabilities
- 665/665 tests pass

Co-authored-by: friday <ClintMoody@users.noreply.github.com>
…randamenium#147)

Upstream merges can silently downgrade a security-patched dependency
by carrying a pinned old version. Without an automated check, the
regression goes unnoticed until the next manual audit.

Fix: add `npm audit --audit-level=moderate` step to the upstream
merge workflow, running AFTER npm install and BEFORE build/test.
If audit reports any moderate+ vulnerability: block the merge,
record advisory IDs + affected packages + severity, report to the
orchestrator for manual resolution.

Updated in 2 tracked files:
- community/skills/framework-upstream-auto-update/SKILL.md
- templates/analyst/.claude/skills/upstream-sync/SKILL.md

FRIDAY's live copy (orgs/*/agents/friday/.claude/skills/upstream-sync)
also updated locally but gitignored from the upstream repo.

Co-authored-by: Bob <ClintMoody@users.noreply.github.com>
…oded-name-and-catalog-churn

fix(pr140): replace hardcoded speaker name, revert catalog churn
…ict-org-case-sensitivity

fix(org): use readdirSync for exact-case match — fixes macOS case-insensitive fs
…ession-restore

fix(daemon): prevent duplicate crons on rapid session restarts
clicktoacquire and others added 26 commits June 12, 2026 08:08
…amenium#563)

* feat(usage-monitor): unified Claude Max + Codex usage tracking

- bus/check-usage-api.sh: add Codex live usage % via chatgpt.com/backend-api/wham/usage
  - New _codex_wham_usage() fetches primary_window.used_percent (5h) and
    secondary_window.used_percent (7d) directly from OpenAI's undocumented
    wham/usage endpoint (found in Codex Rust binary)
  - Auto-refreshes Codex OAuth token from ~/.codex/auth.json when < 5 min remaining
  - Output now includes codex.utilization_5h, codex.utilization_7d, codex.limit_reached,
    codex.reset_5h_seconds, codex.reset_7d_seconds alongside existing SQLite token counts
  - Adds Codex threshold alerts (limit_reached, >=80% 7d, >=90% 5h) matching Claude Max
  - Codex wham/usage result cached 5 min at $CTX_ROOT/state/usage/codex-wham-cache.json

- templates/analyst/config.json: add usage-monitor cron (2h interval)
  - Checks both Claude Max and Codex utilization on every fire
  - Alerts on new 10% thresholds, 80% 7d CODE RED, 90% 5h warning

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(usage-monitor): fix sprint1 test failures — bash ref + cron count

- Replace `bash $CTX_FRAMEWORK_ROOT/bus/check-usage-api.sh` with
  `cortextos bus check-usage-api` in analyst template prompt
- Update sprint1 cron count assertion: 5→6, add usage-monitor to expected names
- Remove hardcoded --chat-id from template prompt (use env var default)

Co-Authored-By: Claude Sonnet 4.6 <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>
…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>
When an agent's MCP server hangs during init (e.g. connecting to a dead
endpoint), the PTY stays alive but never bootstraps — the daemon reports
"running" while the session is effectively dead. This adds a configurable
boot watchdog (default 300s) that kills the hung PTY, arms .force-fresh
to skip --continue, and restarts clean.

Root cause: googli hit 71h session expiry, --continue restart spawned
into an Obsidian MCP connecting to 127.0.0.1:27124 (not running),
blocking init indefinitely. Diagnosed by dexter, fix by coder.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…#620)

Cherry-picked from upstream d53e293. Resolves conflict with boot watchdog
by taking upstream's spawn-verify restructure; boot watchdog re-added in
follow-up commit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…he brief-alive corpse (grandamenium#620 follow-up)

The soak corpse-E2E found the immediate pid-check missed the real gen-B
shape: a node-pty corpse holds a briefly-alive WRAPPER pid that passes the
instant probe, then dies as the exec fails inside — so it slipped to
fast-checker C (abort only) or crash-recovery, never spawn-failed+alert.

Bootstrap-completion is now the semantic line:
- SETTLE: after spawn, poll pid-aliveness across a bounded INJECTABLE window
  (AgentProcess.spawnSettleMs, default 500ms) so a brief-alive wrapper that
  dies is caught and routed to the budget, not declared Running.
- UNIFIED BUDGET: ANY pre-bootstrap exit (settle-caught or later via
  handleExit) routes to onPreBootstrapExit → retry up to MAX_SPAWN_ATTEMPTS
  → SPAWN-FAILED + fleet alert + STOP. spawnAttempts persists across retries.
- handleExit is one branch: everBootstrapped ? crash-recovery : pre-bootstrap
  budget. markBootstrapped() (from the fast-checker on real bootstrap) latches
  everBootstrapped and resets the budget. This REPLACES (not double-bounds)
  the old path where a pre-boot corpse crash-looped up to max_crashes_per_day
  (~10, silent, as 'crashed'); crash-recovery now serves post-bootstrap only.
- fast-checker aborts on terminal spawn-failed (not a momentary mid-retry
  death), and a timeout with a dead process no longer 'proceeds anyway'.
Re-integrates boot_timeout_seconds watchdog (5685bdc) into the
restructured spawn-verify flow from upstream. Same behavior: kills
and force-fresh restarts agents that don't bootstrap within timeout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…randamenium#339)

A crash-recovery restart re-spawns the PTY but nothing verifies the
--continue session actually RESUMES: the fast-checker runs waitForBootstrap
once per discovery, not per crash-restart (empirically anchored on cdd8fc6),
so a restart into a non-responsive / onboarding-wedged session is undetected
(observed live: a kill -9'd agent came back 'Running' but sat idle on an
onboarding TUI, never heartbeating, for 26min).

Mechanism B (wedge-detection): after each crash-recovery restart, require a
heartbeat STRICTLY NEWER than the restart within wedgeDetectMs (5min fixed).
A healthy resume writes one within ~1-2min via its boot/resume routine; a
wedged session never does. Absent => arm .force-fresh + exactly ONE re-restart
=> still wedged => ONE operator alert (restart-wedged class, 15-min dedup).

last_task} in the agent state dir, piggybacking the SessionEnd crash-alert
hook's classification (overwritten each end, mirrors crashes.log) so the
dashboard can distinguish planned restarts from crashes without log-parsing.

Part-1 (.session-refresh marker before stop) is already satisfied in the base
(marker write precedes stop() in sessionRefresh; existing regression test
covers it). Codex session-cap rollover routes through the SAME generic
sessionRefresh (max_session timer, all runtimes) — no codex-specific path.

Also env-pins the PRE-EXISTING notifyAgents transport tests (cdd8fc6, not this
patch): they asserted only the CTX_FRAMEWORK_ROOT-unset fallback branch and
broke when CTX_FRAMEWORK_ROOT was set. Now each case pins the var and BOTH
branches (process.execPath+dist/cli.js when set; cortextos-on-PATH fallback)
are asserted explicitly, so the suite is env-independent.

Tests: wedge-detection resume-vs-wedge acceptance (force-fresh re-restart cap=1
-> ONE alert), exit-reason.json schema/overwrite/best-effort, notifyAgents both
transport branches. tsc 0; targeted daemon+hooks green 43/43 BOTH env-scrubbed
AND with CTX_FRAMEWORK_ROOT set.

NOTE: N=5min and the restart-wedged alert use cdd8fc6-native primitives, not
the grandamenium#621 staleness module / grandamenium#620 spawn-failure-alerter (different PR bases) —
unification points called out in the delivery notes.
…-scope

Original B:F-09 wanted MCP-flap detection in handleExit (mirror the image-poison
crash detector). Repro on claude v2.1.170 FALSIFIED that premise: a broken
.mcp.json (malformed JSON AND valid-but-unreachable-server) no longer crashes —
claude boots DEGRADED with a non-fatal warning and runs WITHOUT its MCP tools.
There is no crash, so handleExit can't fire; that mechanism is obsolete on
current claude (see MCP-FLAP-REPRO-FINDING.md).

Re-scoped to surface the degraded state (an agent silently running without its
MCP tools is the same silent-failure class, just non-fatal):
- checkMcpSetupWarningOnBoot(): after bootstrap, scan the boot stdout tail (ANSI
  stripped) for the exact captured warning structure — /setup issues:.*MCP.*\/doctor/
  — anchored on the setup-issues + MCP + /doctor triple so normal output that
  merely mentions mcp can't false-positive (no guessed signatures; this is the
  real captured line).
- ONE operator alert naming .mcp.json + a /doctor hint. Surface-only: NO
  restart/backoff (the agent is alive).
- Once-per-incident dedup via a latch CLEARED on a warning-free boot. The
  post-bootstrap placement gives the clean latch-clear the handleExit path
  structurally couldn't — so no flat-4h fallback is needed here.
- Wired from the fast-checker right after 'Bootstrap complete'.

Tests (4, green dual-env scrubbed + CTX-set): degraded → ONE alert naming
.mcp.json; clean → zero; false-positive guard (output mentioning mcp + /doctor
but not the triple → no alert); once-per-incident (re-degraded no re-alert, a
clean boot re-arms). fast-checker mock gains the method.
…Area 4.3, B:F-04)

resetCrashCountIfNewDay ran ONLY from the crash path (handleExit), so a HALTED
agent — which stops crashing once the daemon stops restarting it — never got its
budget reset on a new day. In a long-running daemon across midnight, an agent
halted yesterday stayed halted indefinitely until a manual .crash_count_today
wipe.

Fix:
- resetCrashBudgetIfNewDay(): non-incrementing budget reset for start() + the
  daily check (the crash-path resetCrashCountIfNewDay keeps its +1 semantics).
  Only WRITES when clearing a genuine stale date (same-day = load, absent = no-op).
- start() clears a stale budget up front → first start of a new day gets a fresh
  budget with no manual wipe.
- Top-of-hour daily timer (aligned to :00 so all agents tick together at the
  day-roll; crossed-UTC-day guard; UTC toISOString day, no TZ logic). On the roll
  a crash-budget HALTED agent is auto-unhalted + restarted.
- G2: agents carrying operator-intent markers (.user-disable/.user-stop) are NEVER
  auto-restarted on the day-roll — they stay down.
- G1: auto-unhalt notices BATCH across agents — a module-level collector + 10s
  debounce emits ONE telegram naming all unhalted agents + count, never per-agent
  (same shape as the spawn-failure alerter).

Tests (4, green dual-env scrubbed + CTX-set): start-path stale-date → today:0;
daily-check new-day → reset+unhalt+restart; G2 user-disabled halted agent stays
DOWN on day-roll; G1 two halted agents → ONE batched notice. Existing
daemon-shutdown test drove the only-write-on-stale-clear refinement.
…+ first-heartbeat clear (grandamenium#445)

Replay of PR grandamenium#445 (asachs01) onto main 2ae6356. Net effect of the PR's 4 commits (993dafe/6bd4f92/7f32046/3faca36): write .session-refresh marker so SessionEnd hook does not classify --continue rollovers as crashes; no-unlink marker handling + first-heartbeat clear; clearEndMarkers timing-race hardening. Applied 3-way clean onto current main (no conflicts).

Co-Authored-By: asachs01 <asachs01@users.noreply.github.com>
grandamenium#609 grandamenium#202)

Orphaned lifecycle markers made the crash-alert hook misread a genuine
crash as an intentional stop, silently masking dead agents (overnight:
14.5h). Four fixes, preserving the deliberate firing#2 grace window:

(c) enable consumes stale markers: cortextos enable now deletes a prior
disable/stop/restart's .user-disable/.user-stop/.restart-planned/etc
before the daemon start, so a re-enabled agent's first crash is seen.

(d) daemon boot sweep: discoverAndStart reaps lifecycle markers older
than the 300s hook TTL across every agent (in-flight ones < TTL are left
to the heartbeat grace path; .daemon-crashed excluded). Each clear logs
agent/marker/age/content — the observability for this class. This is
what self-heals an orphaned-marker agent on daemon restart.

(a) the orphaned .restart-planned from a crash-before-heartbeat (or a
re-armed restart loop refreshing its mtime past the grace window) is
reaped by (d) at the next boot; misclassification is bounded by the
existing 300s TTL.

(b) hard-restart now actually kills a wedged PTY: the wrapper forwards
the kill signal (was silently dropped), and stop() escalates to SIGKILL
on the whole PROCESS GROUP if the child survives SIGTERM — reaping
descendants a bare kill would orphan (orphaned PTY children are the
suspected OS resource-exhaustion mechanism).

Single canonical marker list exported from heartbeat.ts. Tests cover the
disable->enable cycle, the stale-vs-in-flight sweep + its log line, the
.daemon-crashed exclusion, and the process-group SIGKILL + fallback.
…ndamenium#619/grandamenium#202 (b) completion)

The group SIGKILL (process.kill(-pid)) only reaps the leader's process
group. A child that put itself in its OWN process group (job control,
setpgid, detached helper) survives it and orphans to pid 1 — the exact
own-pgroup case the teardown audit found uncovered. Snapshot the leader's
descendant tree by ppid BEFORE the graceful kill (while still attached),
then on teardown SIGKILL each survivor by pid in addition to the group
signal, so no descendant orphans regardless of its process group.
6.1 (A:F-08): poll getUpdates with a 25s long-poll window instead of
timeout=1 short-polling, cutting idle Telegram API traffic ~25x with no
added message latency. The long-poll window must sit below the getUpdates
HTTP abort, so TelegramAPI.getUpdates now scales its AbortSignal timeout
to (timeout+5)s (>=15s) via a new optional post() timeout param; other
methods keep the 15s default.

6.3 (C#1): split the previously-conflated poll-error path via a pure
planPollError() helper. 409 Conflict self-dies (unchanged); 429 honours
Telegram's 'retry after N' value; network/timeout errors get exponential
backoff (1s..60s cap) with a circuit-breaker log at 5 consecutive
failures. Per-cycle jitter de-syncs the fleet on shared outages. The
network streak resets on any cycle that reaches Telegram (success or 429).
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>
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>
Add stdin/file body input paths to `cortextos bus send-message` so a
message body is never interpolated into a shell-expanded argument. A body
passed positionally is subject to the caller's shell expansion (backticks,
$()) BEFORE the CLI runs — the root of the 2026-06-02 command-injection
incident where a bus message body command-substituted and force-moved a
branch. The send layer itself was already safe (quoted array -> exec node);
the only effective fix is an input path that bypasses argument-level shell
expansion, which stdin/file provide without mangling legitimate code-in-body
content (body-content stripping would corrupt it and cannot prevent
caller-side expansion).

- resolveMessageBody() helper: stdin > --body-file > positional <text>
- CLI: --body-stdin, --body-file <path>; <text> now optional
- bus/send-message.sh: flag passthrough for the safe forms
- tests: 6 cases (precedence, literal backtick/$() preservation, empty stdin, error)
- docs in --help + wrapper header mandating single-quote-or-stdin for code bodies

Closes the command-injection-via-message-body class. Verified: typecheck
clean, 14/14 message tests pass, smoke test confirms $()/backticks stored
literally with no execution.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The fleet-health watchdog used a FLAT 5h staleness threshold, so an agent
dead 3.5h read healthy (paul, 01:00Z) — a flat threshold can't tell a
5-min-cadence agent from a 4h-cadence one. Derive the threshold from the
agent's own heartbeat loop_interval:

  threshold = max( min(3x interval, 6h), 1.5x interval, 15m )

The 1.5x floor is the cap-bite guard: without it a cadence at/above the 6h
cap (e.g. a 6h heartbeat cron) would flag at its own normal beat. Missing or
unparseable loop_interval falls back to a conservative 2h, logged once per
process so a misconfigured agent surfaces without spamming every cycle.

Wires computeStaleThresholdMs into collectMetrics (src/bus/metrics.ts),
replacing the flat 5h. Existing metrics-staleness tests still pass (no
loop_interval => 2h fallback: fresh healthy, 6h-old stale).

Tests: trio (3x-band / cap-bite guard / 15m floor + 2h fallback) +
paul-regression (1h cadence, dead 3.5h => stale; the old flat 5h read it
healthy).
Spawn-verify retry loop intercepts pre-bootstrap exits — tests that
simulate post-bootstrap crashes need markBootstrapped() so exits route
through crash-recovery instead of spawn-retry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add /portal/[clientId]/reports with 8 KPI cards, spend-by-platform,
  zero-conversion campaigns, CPA vs target, daily performance table,
  and date-range selector (7/14/30/60/90 days)
- Add entity_type='campaign' filter to all daily_metrics queries in
  bq-clients.ts and portal-questions.ts (prevents ~4x spend inflation)
- Add 'client' role to RBAC (types, abilities, auth JWT, middleware)
- Add client_id column to users table for binding clients to their data
- Client-role middleware routing: restrict to own portal, redirect on
  admin route access
- Add /api/client-users endpoint for admin to create client logins
- Update portal layout with nav links (reports, creatives, billing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add "Portal" column to clients list with View Portal link
- Add Client Portal section on client detail page with report link
- Both link to /portal/[clientId]/reports (the new reports page)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TypeScript script pulls CampaignPerformanceReport via Bing Ads
Reporting API v13, writes to click-to-acquire.analytics.daily_metrics
with platform='bing', entity_type='campaign'. Covers both sub-accounts
(Click To Acquire + OC Repipes). Includes shell wrapper and launchd
plist for daily 06:00 ET execution.

Requires: MSADS_CLIENT_ID, MSADS_CLIENT_SECRET, MSADS_REFRESH_TOKEN
in orgs/click-to-acquire/.env (OAuth refresh token flow).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Custom cookies config overrode Auth.js defaults without secure:true,
causing __Secure- prefixed cookies to be rejected by browsers on HTTPS
(Vercel). Also fixed submitToAuthJs using stale csrfTokenRef instead of
the freshly-fetched submitToken variable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
clicktoacquire pushed a commit that referenced this pull request Jul 2, 2026
fix(fast-checker): remove stdout.log size check from isAgentActive() — fixes permanent typing indicator
@clicktoacquire
clicktoacquire deleted the feat/client-dashboard-portal branch July 2, 2026 02:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.