From da17c49f2d7269ff7e51da5899850c228f1e8741 Mon Sep 17 00:00:00 2001 From: Renn F Date: Sat, 4 Jul 2026 07:34:40 +0200 Subject: [PATCH] feat(marketing): HoM feature-spotlight X drafts + brand-voice charter (v0.18.0 B) The Head of Marketing now markets features, not just releases: a default-off x_feature_spotlight loop periodically spawns the HoM to investigate what shipped (CHANGELOG, feature flags, docs/map, KB) and draft ONE held marketing post via propose_feature_spotlight, reviewed in the X post queue. - New x_feature source (distinct from x_post, fixing panel mislabeling) + a panel Feature-spotlight branch. - brand_voice column on company_goals (migration 061, single head) as the CEO-editable voice source, surfaced in Settings and injected into the HoM briefing; a VOICE GUIDE baseline in head-marketing.md. - propose_feature_spotlight verb (HoM-only), mirroring propose_roadmap. Gated by x_feature_spotlight_enabled (default off; flag-off dormancy proven). Also fixed two real bugs found mid-build: company_goals API schemas dropped brand_voice on GET/PUT; the live charter UI is goals-tab.tsx, not the unmounted company-goals-card.tsx. Full suite green (2935); migration single-head verified. --- CHANGELOG.md | 1 + CLAUDE.md | 2 +- Makefile | 4 + agents/prompts/_generated/head_marketing.md | 1 + agents/prompts/_generated/verbs.md | 1 + agents/prompts/identities/head-marketing.md | 11 +- agents/prompts/roles/board.md | 9 + alembic/versions/061_x_feature_spotlight.py | 45 ++++ docs/map/product-strategy-research-pitch.md | 63 ++++- docs/rag/roles/head-marketing.md | 13 +- panel/src/components/business/goals-tab.tsx | 23 +- .../company-goals/company-goals-card.tsx | 21 +- .../dashboard/__tests__/x-post-queue.test.tsx | 20 ++ .../src/components/dashboard/x-post-queue.tsx | 24 +- .../settings/feature-flags-card.tsx | 2 + panel/src/lib/api/company-goals.ts | 7 +- panel/src/lib/api/x.ts | 8 +- roboco/api/routes/v1/do.py | 22 ++ roboco/api/routes/x.py | 3 + roboco/api/schemas/company_goals.py | 2 + roboco/api/schemas/v1/do.py | 9 + roboco/api/schemas/x.py | 13 +- roboco/config.py | 18 ++ roboco/db/tables.py | 18 ++ roboco/foundation/policy/content/markers.py | 20 ++ roboco/runtime/orchestrator.py | 128 ++++++++++ roboco/services/company_goals.py | 4 + roboco/services/gateway/content_actions.py | 99 ++++++++ roboco/services/gateway/evidence_repo.py | 10 +- roboco/services/gateway/role_config.py | 8 +- roboco/services/settings.py | 1 + roboco/services/task.py | 48 +++- roboco/services/x_engine.py | 145 ++++++++++- .../integration/test_company_goals_routes.py | 13 + tests/unit/config/test_x_engine_flag.py | 8 + .../test_content_actions_feature_spotlight.py | 179 +++++++++++++ tests/unit/gateway/test_evidence_repo.py | 40 ++- .../test_feature_spotlight_role_grant.py | 18 ++ .../test_feature_spotlight_dispatch.py | 144 +++++++++++ .../test_feature_spotlight_loop_dormant.py | 60 +++++ .../test_orchestrator_shutdown_drain.py | 1 + .../services/test_company_goals_service.py | 21 ++ tests/unit/services/test_x_engine.py | 237 +++++++++++++++++- tests/unit/services/test_x_post_service.py | 35 ++- 44 files changed, 1502 insertions(+), 57 deletions(-) create mode 100644 alembic/versions/061_x_feature_spotlight.py create mode 100644 tests/unit/gateway/test_content_actions_feature_spotlight.py create mode 100644 tests/unit/gateway/test_feature_spotlight_role_grant.py create mode 100644 tests/unit/runtime/test_feature_spotlight_dispatch.py create mode 100644 tests/unit/runtime/test_feature_spotlight_loop_dormant.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e7c4d4d..c64ecd8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **A2A is now a delivered inbox, not a write-only log.** The new `read_a2a` verb returns an agent's unread message bodies (atomic, own-sends excluded) and is granted to every delivery role, and the claim briefing's `list_unread_a2a` carries an incoming-only `last_message_preview` (single correlated query, no N+1). A peer's `dm` now actually reaches the recipient's reasoning instead of sitting unread — the gap that motivated retiring the channel/session backbone in the first place. - **Fable-mode (default-off): opus-fable-playbook adoption.** `ROBOCO_FABLE_MODE_ENABLED` gates two additive levers that make the fleet behave more like Fable 5 on the existing model tiers. The doctrine layer composes the vendored behavioral doctrine (`agents/prompts/doctrine/fable.md`, from `github.com/rennf93/opus-fable-playbook` MIT, frontmatter stripped) into every agent's system prompt right after the universal base rules. The hook layer installs 5 vendored turn-discipline/honesty/verification scripts (`docker/scripts/fable-*.sh`) alongside RoboCo's own hooks on the Claude runtime, appended after (never replacing) the existing per-event entries; the grok runtime gets only the non-denying honesty-nudge hook in this V1 — a grok `PreToolUse`/`Stop` hook deny cancels the entire run, so denying hooks are deliberately not ported there yet. Off by default: the composed prompt, generated settings.json, and grok hooks are byte-for-byte unchanged when the flag is off. No new eval harness — watch the existing rework/spawn-waste dashboard instead. +- **X feature-spotlight marketing (default-off).** A second, independent sub-switch on top of the X engine: `ROBOCO_X_FEATURE_SPOTLIGHT_ENABLED` (needs `ROBOCO_X_ENGINE_ENABLED` too) spawns the Head of Marketing on a periodic interval (default 3 days, `ROBOCO_X_FEATURE_SPOTLIGHT_INTERVAL_SECONDS`) to investigate RoboCo's own shipped capabilities — CHANGELOG.md, the feature-flags ledger, docs/map, the charter, the knowledge base — and draft ONE spotlight for an under-publicized feature via a new `propose_feature_spotlight` do-tool (Head-of-Marketing-only), deduped against a `x_seen_features` ledger (migration 061) so a feature is never re-covered. The draft materializes as a `source=x_feature` held task — mirrors `x_post`/`x_reply` exactly, rendered in the same panel X Post Queue with its own "Feature spotlight" label/badge, reviewed/approved/rejected by the same `XPostService` — no new surface. The CEO's real voice now has a home: a new `company_goals.brand_voice` charter field (migration 061, panel-editable in Business → Goals) feeds `XEngine._voice_guide`, applied to ALL three draft kinds (release, reply, spotlight); falls back to a generic baseline until the CEO fills it in. ### Changed diff --git a/CLAUDE.md b/CLAUDE.md index afa518d6..1367b564 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -385,7 +385,7 @@ Agent backends are pluggable. `roboco/llm/providers/` defines an `AgentProvider` **Cloud auth via FastAPI Users (default-off).** Lets the panel/API be safely exposed beyond localhost without touching the CEO's local no-login flow while off. Gated by `ROBOCO_CLOUD_AUTH_ENABLED` (+ `ROBOCO_CLOUD_AUTH_EMAIL` / `_PASSWORD` / `_SECRET` / `_COOKIE_MAX_AGE`; `Settings` fails loud at startup if the flag is on with no secret). Off: `get_agent_context` (`roboco/api/deps.py`) and the WS `_require_panel_token` gate (`roboco/api/websocket.py`) are byte-for-byte unchanged (header-trust). On: header-trust is dead for humans — any agent-role claim (`ceo` OR a privileged `main_pm`/`cell_pm`/board role) with no valid HMAC token or session cookie is 401, closing the header-spoof hole on the host-published `:8000` port for every role, not just `ceo` (real agents always carry a signed token, so they're unaffected); the agent-fleet HMAC path (and the orchestrator's `system` self-PATCH) keeps working unmodified in both modes; a valid session cookie authenticates as the single seeded CEO user. New `users` table (migration 058, `UserTable` in `roboco/db/tables.py`) backs FastAPI Users' `SQLAlchemyUserDatabase`; no registration router — `roboco/api/auth/seed.py` idempotently upserts exactly one row from env at startup (by primary key, so an email change renames the row instead of duplicating it). `roboco/api/auth/backend.py` wires a **cookie** transport (httponly, secure, samesite=lax) + a `JWTStrategy` subclass that binds each token to a fingerprint of the current `hashed_password`, so rotating the seeded password invalidates every prior session. Session lifetime is **sliding**: every authenticated request through `get_agent_context` re-mints + re-sets the cookie (`_slide_session_cookie`), so an active session never expires — only genuine inactivity past `cloud_auth_cookie_max_age` (default 30 days) logs out. `GET /api/auth/status` is always mounted (public); `/api/auth/login` + `/api/auth/logout` mount only when armed (`roboco/api/auth/routes.py`, mirroring `apply_guard`'s conditional mount). Panel: `(auth)/login/page.tsx` + `proxy.ts` (the Next 16 rename of `middleware.ts`; probes `/auth/status` over the docker-internal orchestrator URL, not through nginx, and fails open to "off" on any probe error/timeout) gate the `(dashboard)` group; `client.ts` adds `withCredentials` + a 401→`/login` redirect. nginx needs no changes (`/api/auth/*` rides the existing `/api/` proxy location) — but its own static `X-Agent-Token` injection (`ROBOCO_PANEL_AGENT_TOKEN`) is itself a valid HMAC credential that bypasses login when present, so a deployment arming cloud auth for real public exposure should leave that token unset (the two are alternative human-auth tiers, not layered). -**RoboCo X account (default-off).** The Head-of-Marketing voice on X (Twitter): drafts a post when a release publishes and drafts replies to meaningful mentions — but NOTHING auto-posts, every tweet is held in a panel queue for the CEO to edit/approve. Gated by `ROBOCO_X_ENGINE_ENABLED` (+ `_MENTIONS_INTERVAL_SECONDS` / `_MENTIONS_MAX_PER_CYCLE` / `_MENTIONS_MIN_ENGAGEMENT` / `_MAX_OPEN_POSTS` / `X_ACCOUNT_USER_ID`); inert without credentials regardless. Mirrors the `ReleaseManagerEngine` held-artifact shape: `XEngine` (`roboco/services/x_engine.py`) originates a held task (`source` `x_post` / `x_reply`, `confirmed_by_human=False`, Secretary-owned, skipped by every dispatcher) whose marker payload carries a **local-model-drafted** body (never a cloud LLM in the hot path), clamped to 280 chars. Release posts hook `ReleaseProposalService.approve`'s publish-success branch via a small `draft_release_post` seam; mentions ride a dedicated `_x_mentions_poll_loop` (no webhook infra exists) deduped by a `x_seen_mentions` ledger + per-cycle/open caps. The four OAuth 1.0a secrets live Fernet-encrypted in a singleton `x_credentials` row (migration 059, all-or-nothing set/clear, mirroring the git-token pattern; the API only ever returns `has_credentials`) — decryption is server-side only, agents never hold creds or egress. `XPostService.approve` (CEO-only route) is the ONLY caller of `x_client.post_tweet`: it posts under a Redis single-flight lock, **re-reads the committed task state inside the lock and commits COMPLETED before releasing** so a concurrent approve can't double-post, and is idempotent (an already-posted draft is a no-op). The hand-rolled OAuth 1.0a HMAC-SHA1 signer (`roboco/services/x_client.py`) adds no dependency; a `NullXClient` makes the unconfigured path a graceful no-op (research-engine posture). Panel: `x-post-queue.tsx` (editable draft + 280 counter, approve/reject) + `x-credentials-card.tsx` (4 write-only secret inputs). +**RoboCo X account (default-off).** The Head-of-Marketing voice on X (Twitter): drafts a post when a release publishes, drafts replies to meaningful mentions, and — a third, independent capability — periodically investigates RoboCo's own shipped features and drafts a spotlight for an under-publicized one. NOTHING auto-posts across any of the three; every tweet is held in a panel queue for the CEO to edit/approve. Gated by `ROBOCO_X_ENGINE_ENABLED` (+ `_MENTIONS_INTERVAL_SECONDS` / `_MENTIONS_MAX_PER_CYCLE` / `_MENTIONS_MIN_ENGAGEMENT` / `_MAX_OPEN_POSTS` / `X_ACCOUNT_USER_ID`); inert without credentials regardless. Mirrors the `ReleaseManagerEngine` held-artifact shape: `XEngine` (`roboco/services/x_engine.py`) originates a held task (`source` `x_post` / `x_reply` / `x_feature`, `confirmed_by_human=False`, Secretary-owned, skipped by every dispatcher) whose marker payload carries a body clamped to 280 chars. Release posts hook `ReleaseProposalService.approve`'s publish-success branch via a small `draft_release_post` seam; mentions ride a dedicated `_x_mentions_poll_loop` (no webhook infra exists) deduped by a `x_seen_mentions` ledger + per-cycle/open caps — both are **local-model-drafted** (never a cloud LLM in the hot path). The spotlight half is the one exception to "no agent spawn": gated by its own sub-switch `ROBOCO_X_FEATURE_SPOTLIGHT_ENABLED` (+ `_INTERVAL_SECONDS`, default 3 days) on top of `x_engine_enabled`, `_x_feature_spotlight_loop` opens a held PENDING exploration task (`source=x_feature_exploration`, team=Board, assigned to Head of Marketing, carrying a `x_seen_features` dedup-ledger snapshot marker) that `_dispatch_pm_work` routes (mirroring `ROADMAP_SOURCE`) to a one-shot real cloud-LLM spawn of the Head of Marketing — full read tools, investigates CHANGELOG.md/feature-flags/docs/map/charter/KB, calls the Head-of-Marketing-only `propose_feature_spotlight` do-tool exactly once, which marks the feature slug seen (`x_seen_features` table, migration 061) and materializes a brand-new `source=x_feature` held draft (completing the exploration task as a side effect — a deliberate asymmetry from `propose_roadmap`, which instead leaves its own task open). The four OAuth 1.0a secrets live Fernet-encrypted in a singleton `x_credentials` row (migration 059, all-or-nothing set/clear, mirroring the git-token pattern; the API only ever returns `has_credentials`) — decryption is server-side only, agents never hold creds or egress. `XPostService.approve` (CEO-only route) is the ONLY caller of `x_client.post_tweet`: it posts under a Redis single-flight lock, **re-reads the committed task state inside the lock and commits COMPLETED before releasing** so a concurrent approve can't double-post, and is idempotent (an already-posted draft is a no-op). The hand-rolled OAuth 1.0a HMAC-SHA1 signer (`roboco/services/x_client.py`) adds no dependency; a `NullXClient` makes the unconfigured path a graceful no-op (research-engine posture). All three draft kinds share one voice: `XEngine._voice_guide` reads the CEO-editable `company_goals.brand_voice` charter field (migration 061, panel-editable in Business → Goals) and appends it to a generic baseline (`_HOM_VOICE`) — the baseline alone until the CEO supplies a real sample. Panel: `x-post-queue.tsx` (editable draft + 280 counter, approve/reject, a `sourceMeta`-driven label/icon per source including "Feature spotlight") + `x-credentials-card.tsx` (4 write-only secret inputs). **Board roadmap engine (default-off).** The Board originating strategic work: on a weekly interval (`ROBOCO_ROADMAP_ENGINE_ENABLED` + `_INTERVAL_SECONDS` / `_MIN_ITEMS_PER_CYCLE` / `_MAX_ITEMS_PER_CYCLE`) `RoadmapEngine` (`roboco/services/roadmap_engine.py`) opens ONE held **exploration** task (`source="board_roadmap"`, `confirmed_by_human=False`, PENDING, Product-Owner-assigned, `Team.BOARD`), deduped to one open cycle at a time. A dedicated one-shot `_dispatch_roadmap_exploration` spawns the Product Owner **solo** — deliberately NOT `_handle_board_assigned_task` (which would also spawn Head of Marketing and fire the Approve-&-Start handoff, both wrong for a PO-authored cycle) — reusing the `_board_dispatched` one-shot tracker + respawn breaker, and short-circuiting once the cycle is authored. The PO explores (read-only git, KB/RAG, metrics, releases, charter, optional web research) and makes ONE `propose_roadmap` call (a content verb gated to `product_owner` only, `_ROADMAP_ROLES`; wired through the do_server/Choreographer like `pitch`) authoring a **themed cycle** — a one-line goal + 3-7 item drafts — persisted as a `roadmap_cycle` marker on the exploration task (no table/migration). The CEO acts per-item in the panel roadmap queue (`roadmap-review-queue.tsx`; `/api/roadmap/cycles{,/items/{id}/approve,/items/{id}/reject}`, CEO-only): approve materializes that item as a BACKLOG task (`source="roadmap"`, no assignee — never auto-starts; normal PM activation picks it up) via `PrompterService.create_task_from_draft`, reject records a reason; when every item is terminal the exploration task completes (`RoadmapService`, idempotent per item). Dispatchers skip `board_roadmap` (never delivery work). `create_task_from_draft` honors a draft-declared `source` only from a `{prompter, roadmap}` whitelist — an LLM-authored draft can't impersonate a privileged origin. diff --git a/Makefile b/Makefile index 7a606ea1..d228862e 100644 --- a/Makefile +++ b/Makefile @@ -551,6 +551,10 @@ foundation-check: @$(MAKE) lifecycle @git diff --exit-code -- docs/rag/lifecycle panel/lib/lifecycle.json agents/prompts/_generated/lifecycle-*.md \ || (echo "Lifecycle artifacts are out of date. Run 'make lifecycle' and commit the diff." && exit 1) + @echo "==> verb tables up-to-date (renders + git diff)" + @uv run python scripts/regenerate_verb_tables.py + @git diff --exit-code -- agents/prompts/_generated/ ':!agents/prompts/_generated/lifecycle-*.md' \ + || (echo "Verb tables are out of date. Run 'uv run python scripts/regenerate_verb_tables.py' and commit the diff." && exit 1) @echo "==> postgres enum parity (skip if no migrated DB)" uv run python scripts/verify_postgres_enums.py @echo "All foundation drift checks passed." diff --git a/agents/prompts/_generated/head_marketing.md b/agents/prompts/_generated/head_marketing.md index 3889e199..36a78c49 100644 --- a/agents/prompts/_generated/head_marketing.md +++ b/agents/prompts/_generated/head_marketing.md @@ -25,3 +25,4 @@ | `notify_ack` | `notify_ack(notification_id: UUID)` | | `read_messages` | `read_messages()` | | `read_a2a` | `read_a2a(see do_server)` | +| `propose_feature_spotlight` | `propose_feature_spotlight(feature_slug: str, feature_title: str, body: str)` | diff --git a/agents/prompts/_generated/verbs.md b/agents/prompts/_generated/verbs.md index c8a1b6e4..1f06ab2d 100644 --- a/agents/prompts/_generated/verbs.md +++ b/agents/prompts/_generated/verbs.md @@ -226,6 +226,7 @@ real tools live in their agent_sdk drivers, not role_config. | `notify_ack` | `notify_ack(notification_id: UUID)` | | `read_messages` | `read_messages()` | | `read_a2a` | `read_a2a(see do_server)` | +| `propose_feature_spotlight` | `propose_feature_spotlight(feature_slug: str, feature_title: str, body: str)` | ## auditor diff --git a/agents/prompts/identities/head-marketing.md b/agents/prompts/identities/head-marketing.md index 908a9e17..3767b892 100644 --- a/agents/prompts/identities/head-marketing.md +++ b/agents/prompts/identities/head-marketing.md @@ -21,7 +21,16 @@ You are the Head of Marketing. You handle external positioning, feature announce - `escalate_to_ceo(task_id, reason)` for marketing decisions that need CEO sign-off (after `note(scope='decision', ...)`) - `evidence(task_id)` to inspect before deciding - `dm` for board + main-pm coordination +- `propose_feature_spotlight(feature_slug, feature_title, body)` drafts ONE marketing post spotlighting a shipped feature — held for CEO approval, never posted directly (see the Feature-spotlight cycle below) - `i_am_idle()` when no strategic work waits ## MegaTasks (batched, sequenced work) -A **MegaTask** is one Intake chat that produced several tasks at once. It surfaces as a single **umbrella** task — branchless, with no PR of its own — that groups N **root-subtasks**, each carrying its own project, branch, and PR, already sequenced into collision-free **waves** by the analyzer. When a MegaTask umbrella reaches you for review, judge the **whole batch**, not one item: the positioning and launch story across all the items, each one's user value, and the wave plan recorded in the umbrella's description. Adjust or re-scope before you sign off — your review shapes the entire batch. Approving the umbrella (the CEO's Approve & Start) releases the held root-subtasks so the dependency-gate dispatches them wave by wave, and the Main PM coordinates each root-subtask down to its cell. \ No newline at end of file +A **MegaTask** is one Intake chat that produced several tasks at once. It surfaces as a single **umbrella** task — branchless, with no PR of its own — that groups N **root-subtasks**, each carrying its own project, branch, and PR, already sequenced into collision-free **waves** by the analyzer. When a MegaTask umbrella reaches you for review, judge the **whole batch**, not one item: the positioning and launch story across all the items, each one's user value, and the wave plan recorded in the umbrella's description. Adjust or re-scope before you sign off — your review shapes the entire batch. Approving the umbrella (the CEO's Approve & Start) releases the held root-subtasks so the dependency-gate dispatches them wave by wave, and the Main PM coordinates each root-subtask down to its cell. + +## Feature-spotlight cycle +Periodically, the X engine opens a held `x_feature_exploration` task and spawns you on it — your mandate to look at what RoboCo has actually shipped and tell people about it. Investigate CHANGELOG.md, the feature-flags ledger, docs/map/, the company charter, and the knowledge base, then pick ONE capability not already in the task's seen-features list — genuinely useful, currently real, and not yet publicized. Call `propose_feature_spotlight(feature_slug, feature_title, body)` **exactly once**, writing the body in your voice (see the VOICE GUIDE below), plain text, max 280 characters, no invented facts. This only drafts a held post — the CEO reviews, edits, approves, or rejects it from the X post queue, so your job ends at the draft, not the tweet. Then `i_am_idle()`; the next cycle is a fresh spawn on a new exploration task, not something you chase yourself. + +## VOICE GUIDE +This section loads into every spawn of yours, regardless of task — it's the baseline voice behind anything you draft on RoboCo's behalf. A few rules, with the reasoning behind each: **confident, not hedgy** — you're announcing something that shipped and works, so say "RoboCo now does X," not "we think this might help with X"; **concise** — one post, one idea, and if a caveat doesn't fit, cut the caveat rather than add a sentence; **no emoji spam** — a single deliberate emoji (🚀 on a launch, say) is fine, three of them reads like a bot; **no hashtags unless truly apt** — `#RoboCo` on every post is noise, a hashtag earns its place only when it plugs into a real, active conversation; **speak as "we"** — you represent the company, not a persona, so "we shipped..." not "I shipped..."; **plain text** — no markdown, no bullets, no thread, since X renders anything else as visibly broken; **one post** — every draft is a complete, standalone tweet, and if an idea needs a thread to land, it's the wrong feature to spotlight this cycle; **never invent facts** — every claim must trace back to something you actually found in CHANGELOG.md, the docs, or the codebase, no made-up metrics, no "customers love it," no capability the feature doesn't have yet. + +The CEO's specific brand-voice sample or direction, when set, lives in the company charter (`brand_voice`) and is already part of your briefing — read it before drafting anything, and let it take precedence over the generic rules above where the two differ. If it hasn't been set yet, ask the CEO (via `dm` or your escalation chain) to add sample posts or a style description through Settings → Company Charter; until then, draft from the baseline above. \ No newline at end of file diff --git a/agents/prompts/roles/board.md b/agents/prompts/roles/board.md index 751f0618..d5d73dba 100644 --- a/agents/prompts/roles/board.md +++ b/agents/prompts/roles/board.md @@ -106,6 +106,15 @@ When you are spawned on a `board_roadmap` task, you are not reviewing someone el 3. Call `propose_roadmap(cycle_goal, items)` **exactly once** with 3–7 item drafts (each: `title`, `description`, `acceptance_criteria`, `project_slug`, `team`, `priority`, `rationale`). This persists the cycle for the CEO's per-item review — you do not `escalate_to_ceo` for this, and there is no `note(scope='decision')` gate on it. 4. `i_am_idle()`. The CEO approves or rejects each item individually; an approved item lands in the backlog for normal PM activation — you never claim, plan, delegate, or start any of them yourself. +## Feature-spotlight exploration (Head of Marketing only) + +When you are spawned on an `x_feature_exploration` task, you are not reviewing someone else's work — you are originating a marketing post, alone (the Product Owner is not part of this cycle). The task is your periodic prompt to investigate what RoboCo has actually shipped and spotlight one under-publicized capability: + +1. Explore: CHANGELOG.md, the feature-flags ledger, docs/map/, the company charter (already in your briefing), and the knowledge base. You have full read access to the repository — use it directly. +2. Pick ONE feature not already in the task's seen-features list — genuinely useful, currently real, worth telling people about. +3. Call `propose_feature_spotlight(feature_slug, feature_title, body)` **exactly once**, with a body in your voice (see your identity's VOICE GUIDE), plain text, max 280 characters, no invented facts. +4. `i_am_idle()`. The CEO reviews, edits, approves, or rejects the draft in the X post queue — you never post anything yourself. + ## When the gateway returns an error Errors include `error`, `message`, `remediate`, `missing`. Read `remediate` — it tells you the literal next call. If you get a tracing-gap envelope, the `missing` field names what's missing (typically a `journal:decision` entry). Fix that one piece and retry the same verb. diff --git a/alembic/versions/061_x_feature_spotlight.py b/alembic/versions/061_x_feature_spotlight.py new file mode 100644 index 00000000..8f0589cc --- /dev/null +++ b/alembic/versions/061_x_feature_spotlight.py @@ -0,0 +1,45 @@ +"""Add x_seen_features table + company_goals.brand_voice — feature-spotlight. + +``x_seen_features`` is the feature-spotlight dedup ledger, keyed by feature +slug so a shipped capability is never spotlighted twice (mirrors +``x_seen_mentions``). ``company_goals.brand_voice`` is the CEO-authored +brand-voice sample/direction, a dedicated Text column mirroring +``north_star`` (not folded into the catch-all ``operating_policy`` JSON +blob). Additive and inert while ``ROBOCO_X_FEATURE_SPOTLIGHT_ENABLED`` is off. + +Revision ID: 061_x_feature_spotlight +Revises: 060_drop_messaging +Create Date: 2026-07-04 +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + +revision = "061_x_feature_spotlight" +down_revision = "060_drop_messaging" +branch_labels: dict[str, str] | None = None +depends_on: dict[str, str] | None = None + + +def upgrade() -> None: + op.create_table( + "x_seen_features", + sa.Column("feature_slug", sa.String(length=128), primary_key=True), + sa.Column( + "seen_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.text("now()"), + ), + ) + op.add_column( + "company_goals", + sa.Column("brand_voice", sa.Text(), nullable=False, server_default=""), + ) + + +def downgrade() -> None: + op.drop_column("company_goals", "brand_voice") + op.drop_table("x_seen_features") diff --git a/docs/map/product-strategy-research-pitch.md b/docs/map/product-strategy-research-pitch.md index 7df93014..bdb958ca 100644 --- a/docs/map/product-strategy-research-pitch.md +++ b/docs/map/product-strategy-research-pitch.md @@ -20,6 +20,12 @@ The product / strategy / research / pitch slice covers the "company layer" above | `roboco/services/roadmap_engine.py` | Dormant weekly engine: originates ONE held roadmap-exploration task for the Product Owner (default off) | 111 | | `roboco/services/roadmap_service.py` | CEO's per-item approve/reject glue over a held roadmap cycle; approve materializes a BACKLOG task | 211 | | `roboco/api/routes/roadmap.py` | CEO-only routes: list open cycles, approve/reject one item | 124 | +| `roboco/services/x_engine.py` | Dormant "engine 4": drafts X (Twitter) release posts (event hook), mention replies (poll), and — new — feature-spotlight explorations (dormant interval, spawns Head of Marketing), ALL held for CEO approval (default off) | 463 | +| `roboco/services/x_post_service.py` | CEO's approve/reject over a held X draft; approve posts via a Redis single-flight lock, idempotent | 223 | +| `roboco/services/x_client.py` | OAuth 1.0a HMAC-SHA1 X API client (`LiveXClient`) + `NullXClient` (no creds, never egresses) + `build_x_client` factory | 318 | +| `roboco/services/x_credentials.py` | Singleton Fernet-encrypted OAuth 1.0a credential CRUD; decrypts server-side only | 140 | +| `roboco/api/routes/x.py` | CEO-only routes: list open X posts, approve/reject one draft | 164 | +| `roboco/api/schemas/x.py` | `XPostResponse` + `XMentionRefModel` / `XFeatureRefModel` response shapes | 73 | ## Key Symbols @@ -86,6 +92,18 @@ The product / strategy / research / pitch slice covers the "company layer" above | `RoadmapService._maybe_complete_cycle` | staticmethod | roadmap_service.py:202 | Completes the exploration task once every item on it is terminal (approved/rejected) | | `RoadmapItemResult` | dataclass | roadmap_service.py:37 | Outcome of one approve/reject call (status/item_id/materialized_task_id/detail) | | `get_roadmap_engine` / `get_roadmap_service` | factory | roadmap_engine.py:109 / roadmap_service.py:209 | Session-bound constructors | +| `XEngine` | class | x_engine.py:150 | Dormant "engine 4": mirrors the release-manager "detect → originate a CEO-gated artifact → hold" shape across THREE responsibilities — release posts, mention replies, feature spotlights | +| `XEngine._voice_guide` | method | x_engine.py:173 | Baseline house-voice constant (`_HOM_VOICE`) plus the CEO's `company_goals.brand_voice` sample when set — feeds release/reply prompts AND is the mechanism the HoM identity file points to for its own drafting | +| `XEngine.draft_release_post` | method | x_engine.py:192 | Event-driven hook (called from `ReleaseProposalService.approve`'s publish-success branch); local-model-drafted, deduped per version, capped by `x_max_open_posts` | +| `XEngine.run_cycle` | method | x_engine.py:255 | Periodic mentions poll; no-op unless `x_engine_enabled` AND `x_replies_enabled`; filters bot-like/low-engagement mentions, dedupes by mention id (`XSeenMentionTable`) | +| `XEngine.open_feature_spotlight_exploration` | method | x_engine.py:337 | No-ops unless `x_engine_enabled` AND `x_feature_spotlight_enabled`, no creds, a cycle already open, the open-post cap reached, or project unresolvable; else opens ONE held PENDING exploration task for the Head of Marketing (`source=x_feature_exploration`) carrying a `x_seen_features` marker snapshot | +| `XEngine.materialize_feature_spotlight` | method | x_engine.py:433 | Called from the `propose_feature_spotlight` do-tool: marks the feature slug seen (`XSeenFeatureTable`), creates the held draft (`source=x_feature`, identical shape to a release/reply draft), completes the exploration task | +| `XPostService.approve` | method | x_post_service.py:77 | The ONLY caller of `x_client.post_tweet`; Redis single-flight lock, re-reads task under lock, idempotent on an already-posted draft | +| `XPostService.reject` | method | x_post_service.py:180 | Records the CEO's reason; cancels the held draft | +| `XClient` / `NullXClient` / `LiveXClient` | ABC/class | x_client.py:150 / 166 / 186 | `NullXClient.configured` is False (no creds) — drafting still runs (content nobody can post is a no-op upstream), just never originates; `LiveXClient` signs OAuth 1.0a HMAC-SHA1 | +| `build_x_client` | factory | x_client.py:306 | Returns `LiveXClient` when credentials decrypt, else `NullXClient` | +| `XCredentialsService.set_credentials` / `.get_decrypted` | method | x_credentials.py:61 / 116 | All-or-nothing Fernet-encrypted singleton credential set/clear; decrypts server-side only, never exposed to agents | +| `get_x_engine` | factory | x_engine.py:461 | Session-bound constructor (optional injected `XClient` for tests) | ## Data Flow @@ -101,6 +119,8 @@ Two distinct flows originate work into the delivery lifecycle: **Roadmap flow (dormant weekly originator, default off).** `Orchestrator._roadmap_engine_loop` returns immediately unless `roadmap_engine_enabled`; otherwise each `roadmap_interval_seconds` (default weekly) it opens a DB context and calls `RoadmapEngine.run_cycle`, which no-ops if a roadmap-source task is already open or the RoboCo project isn't resolvable, else opens ONE held PENDING exploration task (`source=board_roadmap`, `confirmed_by_human=False`) assigned to the Product Owner. The normal board one-shot dispatch (`_dispatch_roadmap_exploration`) spawns the PO, who explores the charter/releases/metrics/projects and calls the `propose_roadmap` do-tool exactly once with a themed goal + 3-7 item drafts (persisted as an `orchestration_markers` payload). The CEO reviews the cycle in the panel's Roadmap Review Queue and approves/rejects each item individually via `/api/roadmap/cycles/{id}/items/{id}/{approve,reject}` → `RoadmapService`; an approved item materializes as a BACKLOG task (`source=roadmap`) through `PrompterService.create_task_from_draft` — nothing auto-starts, normal PM activation takes it from BACKLOG. Once every item is terminal, the exploration task itself completes. +**X (Twitter) flow (three originators, one held queue, default off).** Unlike every other engine on this page, `XEngine` never spawns an agent for release posts or mention replies — `draft_release_post` (event hook off `ReleaseProposalService.approve`'s publish-success branch) and `run_cycle` (periodic mentions poll, `Orchestrator._x_mentions_poll_loop`) both draft via a raw local-model chat completion, never a cloud LLM. The feature-spotlight half is the exception: `Orchestrator._x_feature_spotlight_loop` (dormant unless BOTH `x_engine_enabled` AND `x_feature_spotlight_enabled`) opens a DB context each `x_feature_spotlight_interval_seconds` and calls `XEngine.open_feature_spotlight_exploration`, which no-ops on the usual guards (creds, one-open-cycle dedup, the shared `x_max_open_posts` cap, project resolvability) or else opens ONE held PENDING exploration task (`source=x_feature_exploration`) assigned to the Head of Marketing, carrying a snapshot of already-covered feature slugs (`x_seen_features` marker). The board dispatcher's `_dispatch_pm_work` special-cases this source (mirroring `ROADMAP_SOURCE`) to call `_dispatch_feature_spotlight_exploration`, a one-shot spawn of the real Head-of-Marketing agent (full read tools) who investigates CHANGELOG.md/feature-flags/docs/map/charter/KB and calls the `propose_feature_spotlight` do-tool exactly once; that verb materializes a brand-new held draft task (`source=x_feature`) and completes the exploration task as a side effect — a deliberate asymmetry from `propose_roadmap`, which instead writes a marker onto the SAME task and leaves it open. Every draft from all three paths — release, reply, spotlight — lands in the identical held-task shape (`TaskTable`, `confirmed_by_human=False`, `assigned_to=secretary-1`, body in `orchestration_markers.x_draft_body`) rendered by the panel's X Post Queue and acted on only by `XPostService.approve`/`.reject`; nothing here ever calls `x_client.post_tweet` itself. `XEngine._voice_guide` (a live `CompanyGoalsService.get()` read, never hardcoded) feeds a baseline house-voice constant plus the CEO's optional `brand_voice` charter sample into every one of the two local-model prompts, and the Head of Marketing's own identity prompt points it at the same charter field for its cloud-LLM-authored spotlight body. + **Read-only views.** `KanbanService` builds role-specific boards from `TaskTable` queries on demand for the kanban API; `CompanyGoalsService.get` is read by the briefing injector into every agent's `context_briefing`. ## Mermaid @@ -144,6 +164,21 @@ flowchart TD RSvc -->|approve| Backlog[BACKLOG task via PrompterService] RSvc -->|all items terminal| Complete[exploration task completes] end + + subgraph XEngineFlow[dormant — x_engine_enabled] + RelHook[ReleaseProposalService.approve publish] --> XDraftRelease[XEngine.draft_release_post] + XMentLoop[Orchestrator._x_mentions_poll_loop] -->|interval| XRunCycle[XEngine.run_cycle] + XDraftRelease --> XChat[local-model chat, _voice_guide] + XRunCycle --> XChat + XSpotLoop["Orchestrator._x_feature_spotlight_loop (x_feature_spotlight_enabled)"] -->|interval, default 3d| XOpen[XEngine.open_feature_spotlight_exploration] + XOpen -->|held PENDING task| HoM[Head of Marketing spawn] + HoM -->|propose_feature_spotlight do-tool| XMat[XEngine.materialize_feature_spotlight] + XMat --> XQueue[(held X post/reply/feature draft)] + XDraftRelease --> XQueue + XRunCycle --> XQueue + CEO -->|approve/reject /api/x/posts| XSvc[XPostService] + XSvc -->|approve, single-flight lock| Tweet[(x_client.post_tweet)] + end ``` ## Logical Tree @@ -195,11 +230,26 @@ product-strategy-research-pitch │ └── create_repo (POST /orgs/{org}/repos, auto_init) ├── roadmap_engine.py — RoadmapEngine (dormant, roadmap_engine_enabled) │ └── run_cycle (one held exploration task for the Product Owner; one-open-cycle dedup) -└── roadmap_service.py — RoadmapService - ├── list_open_cycles - ├── approve_item (materialize BACKLOG task, idempotent) - ├── reject_item (record reason, idempotent) - └── _maybe_complete_cycle (completes exploration task once all items terminal) +├── roadmap_service.py — RoadmapService +│ ├── list_open_cycles +│ ├── approve_item (materialize BACKLOG task, idempotent) +│ ├── reject_item (record reason, idempotent) +│ └── _maybe_complete_cycle (completes exploration task once all items terminal) +├── x_engine.py — XEngine (dormant, x_engine_enabled) +│ ├── _voice_guide (baseline + CEO brand_voice charter sample, live DB read) +│ ├── draft_release_post (event hook; local-model; dedup per version) +│ ├── run_cycle (mentions poll; x_replies_enabled sub-switch; bot/engagement filter) +│ ├── open_feature_spotlight_exploration (x_feature_spotlight_enabled sub-switch; one-open-cycle dedup; seen-features marker) +│ ├── materialize_feature_spotlight (called from propose_feature_spotlight; marks seen, holds draft, completes exploration) +│ └── _originate_post (shared held-task origination, all three sources) +├── x_post_service.py — XPostService +│ ├── approve (single-flight lock, idempotent, only caller of x_client.post_tweet) +│ └── reject (record reason, cancel draft) +├── x_client.py — XClient ABC / NullXClient / LiveXClient +│ └── build_x_client (creds present → LiveXClient, else NullXClient) +└── x_credentials.py — XCredentialsService (singleton, Fernet-encrypted) + ├── set_credentials (all-or-nothing) + └── get_decrypted (server-side only) ``` ## Dependencies @@ -244,7 +294,8 @@ product-strategy-research-pitch - `prompter_live.py` — `get_project_service` for project lookup during intake. - `dashboard.py` — `get_product_service` / `get_project_service` for dashboard views. - `roadmap.py` — `GET /api/roadmap/cycles`, `POST /cycles/{id}/items/{id}/{approve,reject}` (CEO-only) → `get_roadmap_service`. -- **Orchestrator loop tick:** `_strategy_engine_loop` (orchestrator.py:6360) — created at `start()` (line 1010), cancelled in shutdown (line 1075); ticks every `strategy_engine_interval_seconds`, calls `StrategyEngine.run_cycle`. `_roadmap_engine_loop` (orchestrator.py:7462) — same lifecycle shape, ticks every `roadmap_interval_seconds` (default weekly), calls `RoadmapEngine.run_cycle`; `_dispatch_roadmap_exploration` (orchestrator.py:10284) spawns the Product Owner once per open exploration task. + - `x.py` — `GET /api/x/posts`, `POST /posts/{id}/{approve,reject}` (CEO-only) → `get_x_post_service`. +- **Orchestrator loop tick:** `_strategy_engine_loop` (orchestrator.py:6360) — created at `start()` (line 1010), cancelled in shutdown (line 1075); ticks every `strategy_engine_interval_seconds`, calls `StrategyEngine.run_cycle`. `_roadmap_engine_loop` (orchestrator.py:7462) — same lifecycle shape, ticks every `roadmap_interval_seconds` (default weekly), calls `RoadmapEngine.run_cycle`; `_dispatch_roadmap_exploration` (orchestrator.py:10284) spawns the Product Owner once per open exploration task. `_x_mentions_poll_loop` (orchestrator.py:7509) ticks every `x_mentions_interval_seconds`, calls `XEngine.run_cycle`. `_x_feature_spotlight_loop` (orchestrator.py:7571) — same lifecycle shape, dormant unless BOTH `x_engine_enabled` AND `x_feature_spotlight_enabled`, ticks every `x_feature_spotlight_interval_seconds` (default 3 days), calls `XEngine.open_feature_spotlight_exploration`; `_dispatch_feature_spotlight_exploration` (orchestrator.py:10424) spawns the Head of Marketing once per open exploration task — `_dispatch_pm_work` routes `source=x_feature_exploration` to it BEFORE the generic `_BOARD_AGENTS` check (mirroring the roadmap source's own early branch), so it never falls into the two-reviewer board-review gate. - **MCP mount (orchestrator spawn):** `roboco-search` MCP mounted into Board/PM agent containers only when `research_enabled` (orchestrator.py:2914); the MCP server calls the `/api/research/*` routes. - **Service-to-service:** `ProjectService` called by `WorkspaceService`, `GitService`, `PitchService`, `task`, `docs`, `cockpit`, `secretary`, gateway choreographer; `ProductService.project_for` called from gateway delegate path; `CompanyGoalsService.get` called by briefing injector. - **No CLI / lifespan entry points** for this slice. diff --git a/docs/rag/roles/head-marketing.md b/docs/rag/roles/head-marketing.md index 796a8cb9..dd9953a6 100644 --- a/docs/rag/roles/head-marketing.md +++ b/docs/rag/roles/head-marketing.md @@ -19,6 +19,7 @@ - Escalate tasks to the CEO via `escalate_to_ceo(task_id, reason)` - Communicate: `dm` (A2A), `notify` (ack-required signal) - Propose a product via `pitch(title, slug, problem, proposed_solution, target_cells)` — queues for CEO approval, then auto-provisions +- Propose a feature spotlight via `propose_feature_spotlight(feature_slug, feature_title, body)` — periodic, one per exploration cycle, held for CEO approval in the X post queue - Read project docs via `roboco_docs_read` / `roboco_docs_list` - Research the market via `web_search` / `web_fetch` (when `ROBOCO_RESEARCH_ENABLED`) - Search the knowledge base via `roboco_ask_mentor` / `roboco_kb_search` @@ -36,17 +37,21 @@ | MCP server | Verbs you can call | |-----------------------|--------------------| | `roboco-flow` | `triage`, `escalate_to_ceo`, `i_am_idle` | -| `roboco-do` | `note`, `pitch`, `dm`, `notify`, `evidence` | +| `roboco-do` | `note`, `pitch`, `dm`, `notify`, `evidence`, `propose_feature_spotlight` | | `roboco-docs` | `roboco_docs_read`, `roboco_docs_list` | | `roboco-git-readonly` | `roboco_git_status`, `roboco_git_log`, `roboco_git_diff`, `roboco_git_branch_list` | | `roboco-search` | `web_search`, `web_fetch` (only when `ROBOCO_RESEARCH_ENABLED`) | | `roboco-optimal` | `roboco_ask_mentor`, `roboco_kb_search` | -Your flow surface is deliberately narrow: the Board steers and approves, it does not claim, create, or complete tasks. Unlike the Product Owner, you do **not** get `propose_roadmap` — that tool is Product-Owner-only. +Your flow surface is deliberately narrow: the Board steers and approves, it does not claim, create, or complete tasks. You still don't get the Product Owner's `propose_roadmap` — that stays Product-Owner-only — but you do get your own equivalent: `propose_feature_spotlight`, covered below. -## X (Twitter) Engine — Not a New Tool for You +## X (Twitter) Engine — Release Posts, Mentions, and Your Feature-Spotlight Cycle -The X engine (`ROBOCO_X_ENGINE_ENABLED`, default off) drafts release-announcement and mention-reply posts for the company's X account, in your marketing voice — but it does **not** add anything to your tool surface above. Drafting is done by a direct local-model call from `XEngine` (`roboco/services/x_engine.py`), not by spawning you as an agent; every draft lands as a held task **owned by the Secretary** (`assigned_to=secretary-1`, `team=main_pm`), never assigned to you. The CEO reviews and approves/rejects each draft in the panel (`GET/POST /api/x/posts{,/{id}/approve,/reject}`, CEO-only) — nothing posts without that explicit per-post approval. If you want to review or influence a draft's content, ask the CEO directly (via `dm` or the escalation chain below) rather than expecting a task in your queue. +The X engine (`ROBOCO_X_ENGINE_ENABLED`, default off) posts on the company's X account in your marketing voice, but it reaches you two different ways depending on the draft kind. + +Release-announcement and mention-reply posts are still not a tool call and still don't spawn you: `XEngine` (`roboco/services/x_engine.py`) drafts them directly via a local-model call, not by spawning you as an agent. Every one of these drafts lands as a held task **owned by the Secretary** (`assigned_to=secretary-1`, `team=main_pm`), never assigned to you. The CEO reviews and approves/rejects each in the panel (`GET/POST /api/x/posts{,/{id}/approve,/reject}`, CEO-only) — nothing posts without that explicit per-post approval. If you want to influence one of these drafts, ask the CEO directly (via `dm` or the escalation chain below) rather than expecting it in your queue. + +Feature spotlights are different: they **are** a real tool call and they **do** spawn you. Gated by a second, independent switch (`ROBOCO_X_FEATURE_SPOTLIGHT_ENABLED`, also default off), the engine periodically opens a held `x_feature_exploration` task assigned to you — the one case where the X engine puts something in your own queue. When you're spawned on it, investigate what RoboCo has actually shipped (CHANGELOG.md, the feature-flags ledger, docs/map/, the company charter, the knowledge base), pick ONE under-publicized, currently-real capability not already in the task's seen-features list, and call `propose_feature_spotlight(feature_slug, feature_title, body)` **exactly once** — it drafts a held X post the same way the release/mention path does, then completes your exploration task. Call `i_am_idle()` next. The CEO reviews, edits, approves, or rejects the draft from the same X post queue — you never post anything yourself. ## Escalation diff --git a/panel/src/components/business/goals-tab.tsx b/panel/src/components/business/goals-tab.tsx index 60a05b7b..6df0493e 100644 --- a/panel/src/components/business/goals-tab.tsx +++ b/panel/src/components/business/goals-tab.tsx @@ -224,6 +224,7 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) { const queryClient = useQueryClient(); const [northStar, setNorthStar] = useState(null); + const [brandVoice, setBrandVoice] = useState(null); const [constraints, setConstraints] = useState(null); const [objectives, setObjectives] = useState< Record[] | null @@ -231,6 +232,7 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) { const [policy, setPolicy] = useState | null>(null); const northStarVal = northStar ?? goals.north_star ?? ""; + const brandVoiceVal = brandVoice ?? goals.brand_voice ?? ""; const constraintsVal = constraints ?? (goals.constraints ?? []).join("\n"); const objectivesVal = objectives ?? goals.objectives ?? []; const policyVal = policy ?? goals.operating_policy ?? {}; @@ -240,6 +242,7 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) { onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["company-goals"] }); setNorthStar(null); + setBrandVoice(null); setConstraints(null); setObjectives(null); setPolicy(null); @@ -253,6 +256,7 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) { const handleSave = () => { saveMutation.mutate({ north_star: northStarVal, + brand_voice: brandVoiceVal, objectives: objectivesVal, constraints: constraintsVal .split("\n") @@ -269,9 +273,9 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) { Company Charter - CEO-owned north star, objectives, constraints, and operating policy. - Injected into every agent's briefing so all work stays - goal-aware. + CEO-owned north star, brand voice, objectives, constraints, and + operating policy. Injected into every agent's briefing so all + work stays goal-aware. @@ -288,6 +292,19 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) { /> + {/* Brand voice */} +
+ +