Skip to content

feat: ✨ targets system — full implementation with two-stage evaluation, topic queue, and UI#72

Merged
nj-io merged 36 commits intodevelopfrom
feat/targets
Apr 1, 2026
Merged

feat: ✨ targets system — full implementation with two-stage evaluation, topic queue, and UI#72
nj-io merged 36 commits intodevelopfrom
feat/targets

Conversation

@nj-io
Copy link
Copy Markdown
Owner

@nj-io nj-io commented Apr 1, 2026

Summary

Complete targets system implementation: multi-account, multi-destination content flows with per-target content strategies. 177 commits, 310 files changed, +47K lines.

Two-Stage Evaluation Architecture

  • Interval gating: AnalyzerOutcome with should_evaluate field. Commits deferred with processed=1 (invisible to scheduler drain). Stage 2 only runs when threshold crossed.
  • Raw diff removed from evaluator prompt when stage 1 analysis present — stage 2 works from digested analysis only
  • Empty strategies guard prevents "default" strategy name leak

Topic Queue Overhaul

  • Dismissed status: topics can be rejected without deletion. Excluded from evaluator context, tag matching, and auto-seeding. Greyed collapsed section in UI.
  • Strategy-scoped seeding: positioning strategies get product topics from brief, code-driven get implementation topics from commit tags
  • Topic granularity (low/medium/high): LLM-assisted extraction produces short titles + descriptions
  • Draft Now expanded to uncovered topics (not just holding)
  • UI: rank numbers, dismiss button, drag-drop scoped to strategy group, useToast on mutations, created_by badges

Core Pipeline (Phases 1-6)

  • Config model, 5 new DB tables, ContentSource registry, target routing, shared-group drafting
  • Freeform tags replace fixed episode types
  • CommitAnalyzer (stage 1), deep context assembly, post metadata
  • Per-account gap enforcement, queue notifications, error feed

Interfaces

  • 9 CLI command groups, ~30 API endpoints, 11+ web UI components
  • Pipeline diagnostics, activity indicator, task stage tracking, event log

Infrastructure

  • OAuth 2.0, preview mode, unified logging, LLM strategy classification, bot background tasks

Merge with develop

  • Resolved 7 conflicts from model decomposition (7 submodules) + trigger decomposition (6 submodules)
  • Our additions placed into correct submodule files per CODING_PRACTICES
  • No-truncation policy for strategy reasoning preserved

Test plan

  • pytest: 3397 passed, 0 failures
  • tsc --noEmit: clean
  • ruff + all pre-commit hooks pass
  • Playwright: interval gating, topic queue, strategy CRUD, error feed
  • CLI: topics dismiss, list --include-dismissed, draft-now on uncovered

🤖 Generated with Claude Code

nj-io and others added 30 commits March 29, 2026 12:51
Wizard (Change 1):
- CardSelect supports multiSelect prop (toggle cards on/off)
- WizardData.strategyIds is now string[] (was single string)
- Primary strategy (first selected) drives voice/audience customization
- Secondary strategies written with template defaults
- Voice/Audience steps show "(for {strategy name})" label
- Summary lists all with "(primary)" badge
- isSkippable checks all selected, not just first

CLI (Change 2):
- --strategy / -s flag (repeatable) on quickstart. Default: building-public
- --branch / -b flag sets trigger_branch after registration
- Validates strategy IDs against known templates (excludes "custom")
- Writes all strategies to content_strategies with template defaults
- strategies + trigger_branch in JSON output
- Help text with usage examples

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Small dot next to "Social Hook" in nav bar. Activates after 5s of
sustained pipeline/task events (avoids flashing on quick ops), fades
out 5s after last event or on task completion. CSS transitions for
smooth fade-in/fade-out. Links to /system page.

Uses existing useGateway().addListener pattern from slow-task-banner.
Listens for both pipeline stage events and task lifecycle events.
CSS-only animation via Tailwind animate-ping + opacity transitions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Moved from left (next to logo) to center of nav bar. Now fully hidden
(opacity-0 + pointer-events-none) when idle — only fades in after a
background task runs for 5+ seconds. No idle dot visible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use absolute positioning (left-1/2 top-1/2 -translate) on the nav
element (now position:relative) so the dot is dead center regardless
of logo/link widths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The cleanup query looked for 'evaluating' which was removed from the
enum — decisions stuck in 'processing' were never cleaned up on restart.

Now uses DecisionType.PROCESSING.value for all transient decision states.
Also fixed response status on suggestion evaluate endpoint (was still
returning 'evaluating' instead of 'processing'). Moved DecisionType to
top-level import in server.py.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Error feed: Copy button on each error entry copies severity, message,
  component, source, timestamp, and context to clipboard via useToast
- Activity indicator: polls /api/tasks?status=running on mount so the
  animation shows immediately after page refresh if a task is running
  (previously only activated via WebSocket events)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ages

Pipeline stage events (analyzing, evaluating, drafting) fire multiple
times per task without matching completion signals, so the counter
incremented but never decremented. Now only tracks task entity
started/completed/failed which have a proper 1:1 lifecycle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire the full topic-to-drafter pipeline: content source resolution
(previously dead code) now flows through to the drafter prompt,
topic_id and arc_id are set on Draft objects, and topic status
transitions happen at posting time instead of draft creation.

- Show topic IDs + descriptions in evaluator prompt (Fix 1)
- Wire resolved content source through 6-point drafting chain (Fix 2)
- Auto-include topic in context_source when topic_id set (Fix 3)
- Thread topic_id, arc_id, cycle_id to Draft constructor (Fix 4, 14)
- Move topic status transitions to posting time (Fix 5)
- Validate topic_id belongs to strategy (Fix 6)
- Store hold_reason on topics with atomic update_topic_hold (Fix 7)
- Add batch evaluate endpoint, CLI command, and frontend (Change 1)
- Server-side sort/filter on decisions endpoint (Change 2)
- Batch grouping visual with indigo headers (Change 4)
- CLI parity: batch-evaluate + batch grouping (Change 5)
- Extract delete_drafts_for_decision to eliminate 3x duplication

Migration: arc_id on drafts, hold_reason on content_topics,
idx_drafts_topic_id index.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-call variants

Preview drafts were written as X free-tier threads regardless of config
because the drafter received no platform constraints. Shared group
drafting used a "lead platform" string-adapt pattern instead of the
designed single-call multi-variant approach.

- Drafter always includes tier/char-limit in platform description
- Generic preview (no platform) gets explicit "no constraints" guidance
- User message uses platform_config.name not raw target name
- Add PlatformVariant model + variants field to CreateDraftInput
- Multi-platform user message with per-platform tier/intro context
- Rewrite _draft_shared_group: one LLM call, extract variants per platform
- Delete _pick_lead_platform, _adapt_content_for_platform, _unthread_content
- Add "Other Platforms / Preview" + "Multi-Platform Variants" to drafter.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e collision

Strategy validation in _parse_targets only checked config.content_strategies,
rejecting built-in templates like 'technical-deep-dive' that are valid
strategies shown in the UI. Now checks both config overrides and
STRATEGY_TEMPLATES.

Auto-generated target names now include strategy to prevent collisions:
{account}-{strategy}-{destination} instead of {account}-{destination}.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…g decision

The batch evaluate endpoint passed all selected decisions as deferred AND
used the first one as trigger_commit_hash, causing duplicate hashes and a
UNIQUE constraint violation. Now the last decision is the trigger (with
existing_decision_id set so _run_targets_path reuses the row) and the
rest are deferred.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add --limit / -n option to import-commits CLI and limit param to
web API (import-preview + import-commits). Returns the N most recent
commits when set.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Import history modal now has a Limit field — restricts import to
the N most recent commits. Preview updates live as limit changes.

Reasoning column truncates in collapsed rows, shows full text in
expanded detail section.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The trigger commit in a batch evaluation was not getting batch_id set,
so it appeared outside the batch group in the UI. Now all decisions in
a batch (deferred + trigger) share the same batch_id.

Also fix React crash when content_source is an object (ContextSourceSpec)
instead of a string — render the types array as comma-separated text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Paid tier guidance now explicitly tells the drafter to take advantage
of the 25K char limit with rich, detailed content. Prefer single
flowing posts over threads. Intro posts emphasize substance and depth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ation

Move batch membership marking from post-hoc (after _run_targets_path)
to Phase A (cycle creation) and Phase E (decision creation). Deferred
decisions show "Batched" immediately instead of staying as "Processing"
until drafting finishes.

- Add TargetsPathResult dataclass (exit_code, cycle_id, decision_id)
- _run_targets_path returns TargetsPathResult instead of int
- Deferred decisions get batch_id in Phase A (cycle creation)
- Trigger decision gets batch_id in Phase E (after decision insert)
- Remove mark_decisions_processing from evaluate_batch (redundant)
- Delete post-hoc batch marking block (get_recent_cycles re-query)
- Add error recovery: clear batch_id on pipeline failure
- Add DECIDING pipeline stage (decision creation + arc + queue actions)
- Replace _val() with enum_value() from parsing.py

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Auto-discovery (brief creation + stale refresh) was inline in run_trigger,
missing from batch evaluate, CLI batch-evaluate, and scheduler drain.
Extracted as ensure_project_brief() called from all four evaluation paths.

Fresh projects now get a brief regardless of evaluation entry point.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Background tasks can now emit named stages via ops.emit_task_stage().
Frontend useBackgroundTasks tracks current_stage/stage_label in-memory
from SSE events. Per-row buttons show stage labels with timers during
batch evaluation and retrigger.

Backend:
- emit_task_stage() in operations.py (thin wrapper over emit_data_event)
- task_id on TriggerContext + run_trigger for stage tracking
- Pipeline stages (ANALYZING, EVALUATING, DECIDING, DRAFTING) emit both
  pipeline events (toasts) and task stage events (per-task progress)
- Mutable holder pattern for passing task_id to blocking closures

Frontend:
- DataChangeEvent gains stage/stage_label fields
- BackgroundTask gains current_stage/stage_label/stage_started_at
- useBackgroundTasks handles "stage" action in-memory, preserves during refresh
- Per-row buttons use batch task stage for label and timer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The "N pending" / "N posted" text in the evaluation cycles status
column is now a clickable link to the drafts page filtered by project.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rom commit column

Reasoning was duplicated in the expanded commit detail section and
truncated by max-w-[300px] in the reasoning column. Removed the
duplicate box and the width constraint so reasoning displays fully
when the row is expanded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…truncation

Reasoning was truncated to 500 chars before DB storage — removed the
limit so full per-strategy reasoning is preserved. Added ExpandableText
component (ui/) for table cells that clamp to 3 lines collapsed and
wrap fully when expanded. Applied to reasoning and angle columns.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Targets with an account but no OAuth tokens (no credentials set up)
were not getting preview_mode=True. Now checks oauth_tokens table —
if the account has no tokens, the draft is in preview mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Preview mode now triggers when a target's account has no OAuth tokens
in the DB (per-account check, not per-platform). Diagnostics also
report preview mode when account exists but has no credentials.

The accounts_with_creds set is built from oauth_tokens at the caller
level (trigger.py for diagnostics, drafting.py for preview_targets)
to keep diagnostic checks as pure functions per CODING_PRACTICES.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All 4 bot handler LLM calls (Gatekeeper, Expert escalation, angle
redraft, media spec) now run via _run_background_task instead of
blocking the HTTP/WebSocket response for 5-60s.

Backend:
- handle_message + handle_callback accept optional task_id
- Stage events emitted before each LLM call (routing, thinking,
  redrafting, generating) using locally-scoped connections
- /api/message always returns 202 (all messages go through background)
- /api/callback returns 202 for LLM actions (media_gen_spec), sync
  for non-LLM (approve, reject, schedule)
- WebSocket send_message dispatches to background task with deferred
  ack containing task_id

Frontend:
- sendMessage/sendCallback return types include optional task_id
- button-row.tsx guards against undefined events on 202 responses

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
draft-action-panel.tsx: wires useBackgroundTasks for edit_angle and
other text prompts. onTaskCompleted clears pending state and refreshes
draft. Failed tasks show error message.

chat-panel.tsx: tracks chat background tasks via gateway ack envelope
with task_id. Listens for task completion/failure events. Shows toast
on failure. Keeps sending=true until task completes (not on send).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…per, reuse conn

- Fix critical stale closure: chatTaskId changed from useState to useRef
  so the gateway listener always reads the current value
- Extract _dispatch_chat_message() shared by /api/message and WebSocket
- Use db.conn for emit_task_stage in _handle_expert_escalation instead
  of opening a separate connection
- Move generate_id import inside LLM branch of /api/callback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Confirm button and text input disappeared immediately after clicking
because handleTextSubmit cleared textPrompt/submenu in the finally block.
Now the UI stays in loading state until onTaskCompleted fires — the user
sees the Confirm button disabled with actionPending while the LLM works.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…iately

promoteDraft() always returns a task_id (LLM pipeline work) but the
handler was ignoring it and clearing loading state in finally. Now
tracks via useBackgroundTasks — loading persists until the promote
task completes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
nj-io and others added 4 commits April 1, 2026 07:14
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Conflicts:
#	src/social_hook/models/__init__.py
#	src/social_hook/parsing.py
#	src/social_hook/topics.py
#	src/social_hook/trigger.py
#	src/social_hook/web/server.py
#	tests/test_drafting_adapt.py
#	tests/test_drafting_shared_group.py
#	tests/test_phase6_pipeline.py
Develop added 500-char truncation to _combine_strategy_reasoning().
Our branch deliberately keeps full reasoning — truncation loses
per-strategy context that operators need for decision review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Develop's decomposition of trigger.py into submodules lost our
emit_task_stage calls in evaluate_batch(). These track "analyzing"
and "evaluating" stages for the frontend progress indicator.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nj-io nj-io changed the title feat: ✨ targets system — topic-drafter flow, batch evaluate, pipeline stages, reusable task tracking feat: ✨ targets system — full implementation with two-stage evaluation, topic queue, and UI Apr 1, 2026
@codecov-commenter
Copy link
Copy Markdown

nj-io and others added 2 commits April 1, 2026 11:50
- Regenerate site-docs/cli/ for --strategy and --branch quickstart flags
- Fix trigger_batch.py:259 mypy error: annotate exit_code as int

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests derived from TARGETS_DESIGN.md cross-referenced with coverage
gaps. Focus on behaviors that catch real bugs, not trivial plumbing.

Interval gating (4 tests):
- Deferral when count < threshold
- Evaluation at threshold
- Minimum clamp (interval=0 → always evaluate)
- No config defaults to evaluate

Topic queue (7 tests):
- Dismissed topics excluded from tag matching
- Dismissed topics not recreated by seeding
- Strategy-scoped seeding (positioning vs code-driven)
- Draft Now accepts uncovered, rejects dismissed
- Granularity gating (routine blocked at low, notable passes)

Evaluator prompt (3 tests):
- Diff excluded when analysis present
- Diff included without analysis
- Empty strategies raises ConfigError

Config validation (3 tests):
- Invalid topic_granularity raises ConfigError
- Valid values accepted
- Default is "low"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nj-io nj-io merged commit a4ee0a9 into develop Apr 1, 2026
6 checks passed
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.

2 participants