Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ed6f146
feat: ✨ multi-strategy wizard selection + CLI quickstart flags
nj-io Mar 29, 2026
292e7eb
feat: ✨ nav bar activity indicator — pulsing dot during LLM work
nj-io Mar 29, 2026
5846757
fix: wire ActivityIndicator into nav bar
nj-io Mar 29, 2026
218b57a
fix: 🐛 activity indicator — center in nav, only visible during work
nj-io Mar 29, 2026
6fe935f
fix: activity indicator absolute center of navbar
nj-io Mar 29, 2026
3add077
fix: 🐛 stale task cleanup uses DecisionType enum, not hardcoded strings
nj-io Mar 29, 2026
c815cb7
fix: ✨ error copy button + activity indicator shows on page refresh
nj-io Mar 29, 2026
cdb9817
chore: remove dev-only test button from activity indicator
nj-io Mar 29, 2026
9a76a15
fix: 🐛 activity indicator only tracks task lifecycle, not pipeline st…
nj-io Mar 29, 2026
8bc081f
feat: ✨ topic-drafter flow fixes, batch evaluate endpoint, commit log UX
nj-io Mar 31, 2026
4a596db
fix: 🐛 preview drafts respect platform tier, shared group uses single…
nj-io Mar 31, 2026
31b82c5
fix: 🐛 target strategy validation accepts built-in templates, fix nam…
nj-io Mar 31, 2026
6fa262e
fix: 🐛 batch evaluate separates trigger from deferred, reuses existin…
nj-io Mar 31, 2026
74e29a0
feat: ✨ import history limit — restrict number of commits imported
nj-io Mar 31, 2026
862f50c
feat: ✨ import limit in UI modal + reasoning expands on row click
nj-io Mar 31, 2026
59cc13a
fix: 🐛 trigger decision gets batch_id + content_source rendered safely
nj-io Mar 31, 2026
0251030
fix: 🐛 paid tier prompt encourages rich single posts over threads
nj-io Mar 31, 2026
4e6f8a4
refactor: ✨ pipeline phase separation — batch membership at cycle cre…
nj-io Mar 31, 2026
0b9c110
refactor: ✨ extract ensure_project_brief as shared discovery check
nj-io Mar 31, 2026
d865600
feat: ✨ reusable task stage tracking — per-stage progress on UI elements
nj-io Mar 31, 2026
c6b31b1
feat: ✨ evaluation cycle status count links to drafts page
nj-io Mar 31, 2026
f34927c
fix: 🐛 reasoning column shows full text on expand, remove duplicate f…
nj-io Mar 31, 2026
ea7bf8f
fix: 🐛 reasoning + angle columns use ExpandableText, remove 500-char …
nj-io Mar 31, 2026
02604f1
fix: 🐛 preview_mode checks OAuth credentials, not just account existence
nj-io Mar 31, 2026
0969dec
fix: 🐛 preview_mode checks account-level OAuth credentials
nj-io Mar 31, 2026
f5b6654
feat: ✨ migrate bot LLM calls to background tasks with stage tracking
nj-io Apr 1, 2026
e5b863b
feat: ✨ frontend tracks bot LLM tasks — stage labels + error handling
nj-io Apr 1, 2026
27daefc
refactor: simplify bot LLM migration — fix stale closure, extract hel…
nj-io Apr 1, 2026
bd4e4b9
fix: 🐛 text prompt stays visible during background task processing
nj-io Apr 1, 2026
e176aa9
fix: 🐛 handlePromote tracks background task instead of clearing immed…
nj-io Apr 1, 2026
c4227b6
fix: mypy type ignore for _dispatch_chat_message return
nj-io Apr 1, 2026
e9d68f3
Merge remote-tracking branch 'origin/develop' into feat/targets
nj-io Apr 1, 2026
1cf713f
fix: restore no-truncation policy for strategy reasoning
nj-io Apr 1, 2026
146eab2
fix: add task stage tracking to evaluate_batch in trigger_batch.py
nj-io Apr 1, 2026
b845e24
fix: CI failures — regenerate CLI docs, fix mypy return type
nj-io Apr 1, 2026
e7d64bf
test: ✅ add 19 high-value tests for targets pipeline coverage
nj-io Apr 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
761 changes: 761 additions & 0 deletions docs/workbench-architecture.html

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions site-docs/cli/decision.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,33 @@ Decision management.

---

### `social-hook decision batch-evaluate`

Evaluate multiple imported/deferred decisions as a single batch.

Groups the decisions and runs a combined evaluation through the full
pipeline (commit analysis, evaluation, drafting, notifications).
All decisions must belong to the same project and have status
'imported' or 'deferred_eval' (without an existing batch_id).

Example: social-hook decision batch-evaluate dec_abc123 dec_def456

**Arguments:**

| Name | Required | Description |
|------|----------|-------------|
| `decision_ids` | yes | Decision IDs to evaluate as a batch |

**Options:**

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--project`, `-p` | string | | Project path (default: cwd) |
| `--yes`, `-y` | boolean | false | Skip confirmation |
| `--json` | boolean | false | Output as JSON |

---

### `social-hook decision delete`

Delete a decision and its associated drafts.
Expand Down
6 changes: 4 additions & 2 deletions site-docs/cli/project.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,21 @@ Examples:

Import historical git commits as imported decisions.

Imports all past commits so the dashboard shows the full project timeline.
Imports past commits so the dashboard shows the project timeline.
Imported commits are NOT evaluated — use retrigger to evaluate them later.

Examples:
social-hook project import-commits
social-hook project import-commits --branch main
social-hook project import-commits --id project_abc123
social-hook project import-commits --limit 50
social-hook project import-commits --branch main --limit 100

**Options:**

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--branch`, `-b` | string | | Import only this branch |
| `--limit`, `-n` | integer | | Import only the N most recent commits |
| `--id`, `-i` | string | | Project ID |
| `--json` | boolean | false | Output as JSON |

Expand Down
2 changes: 2 additions & 0 deletions site-docs/cli/root-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ and generates an introductory draft — all in one command.
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--key` | string | | Anthropic API key (skips prompt) |
| `--strategy`, `-s` | string | | Content strategy template ID (repeatable). Default: building-public. |
| `--branch`, `-b` | string | | Set a trigger branch filter on the project after registration. |
| `--evaluate-last` | integer | 0 | Evaluate last N commits for additional drafts (max 5) |
| `--yes`, `-y` | boolean | false | Skip all confirmation prompts |
| `--json` | boolean | false | JSON output |
Expand Down
9 changes: 9 additions & 0 deletions src/social_hook/bot/buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,15 @@ def btn_media_gen_spec(
)
spec_tool = build_spec_generation_tool(tool_name, schema)
client = create_client(config.models.drafter, config)

_task_id = kwargs.get("task_id")
if _task_id:
from social_hook.db import operations as _stage_ops

_stage_ops.emit_task_stage(
conn, _task_id, "generating", "Generating media spec", draft.project_id
)

response = client.complete(
messages=[{"role": "user", "content": prompt}],
tools=[spec_tool],
Expand Down
34 changes: 29 additions & 5 deletions src/social_hook/bot/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,10 @@ def handle_command(


def handle_message(
msg: InboundMessage, adapter: MessagingAdapter, config: Any | None = None
msg: InboundMessage,
adapter: MessagingAdapter,
config: Any | None = None,
task_id: str | None = None,
) -> None:
"""Handle a free-text message by routing through Gatekeeper.

Expand All @@ -325,7 +328,7 @@ def handle_message(
pending = get_pending_reply(chat_id)
if pending:
clear_pending_reply(chat_id)
_handle_pending_reply(adapter, chat_id, pending, text, config)
_handle_pending_reply(adapter, chat_id, pending, text, config, task_id=task_id)
return

try:
Expand Down Expand Up @@ -450,6 +453,13 @@ def handle_message(
except Exception:
logger.debug("Failed to store inbound chat message", exc_info=True)

if task_id:
from social_hook.db import operations as ops

ops.emit_task_stage(
_context_conn, task_id, "routing", "Understanding message", project_id or ""
)

gatekeeper = Gatekeeper(client)
route = gatekeeper.route(
user_message=text,
Expand Down Expand Up @@ -483,6 +493,7 @@ def handle_message(
draft=draft_obj,
project_id=project_id,
db=db,
task_id=task_id,
)

# Store outbound response
Expand Down Expand Up @@ -510,14 +521,14 @@ def handle_message(
_send(adapter, chat_id, f"Error processing message: {e}")


def _handle_pending_reply(adapter, chat_id, pending, text, config):
def _handle_pending_reply(adapter, chat_id, pending, text, config, task_id=None):
"""Dispatch a pending reply to the appropriate handler."""
if pending.type == "edit_text":
_save_edit(adapter, chat_id, pending.draft_id, text)
elif pending.type == "schedule_custom":
_save_custom_schedule(adapter, chat_id, pending.draft_id, text, config)
elif pending.type == "edit_angle":
_save_angle(adapter, chat_id, pending.draft_id, text)
_save_angle(adapter, chat_id, pending.draft_id, text, task_id=task_id)
elif pending.type == "reject_note":
_save_rejection_note(adapter, chat_id, pending.draft_id, text, config)
elif pending.type == "edit_media_spec":
Expand Down Expand Up @@ -815,7 +826,7 @@ def _apply_expert_result(
return True


def _save_angle(adapter, chat_id, draft_id, text):
def _save_angle(adapter, chat_id, draft_id, text, task_id=None):
"""Use Expert agent to redraft content with a new angle."""
from social_hook.config.yaml import load_full_config
from social_hook.db import get_draft
Expand Down Expand Up @@ -843,6 +854,11 @@ def _save_angle(adapter, chat_id, draft_id, text):

summary = ops.get_project_summary(conn, draft.project_id)

if task_id:
ops.emit_task_stage(
conn, task_id, "redrafting", "Redrafting with new angle", draft.project_id
)

expert = Expert(client)
result = expert.handle(
draft=draft,
Expand Down Expand Up @@ -1016,6 +1032,7 @@ def _handle_expert_escalation(
draft: Any = None,
project_id: str | None = None,
db: Any = None,
task_id: str | None = None,
) -> str | None:
"""Handle an Expert escalation. Returns response text for chat history.

Expand Down Expand Up @@ -1067,6 +1084,13 @@ def _handle_expert_escalation(
except Exception:
logger.debug("Failed to resolve expert context", exc_info=True)

if task_id and db:
from social_hook.db import operations as _stage_ops

_stage_ops.emit_task_stage(
db.conn, task_id, "thinking", "Drafting response", project_id or ""
)

result = expert.handle(
draft=draft,
user_message=user_message,
Expand Down
47 changes: 46 additions & 1 deletion src/social_hook/cli/cycles.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def show(
# Get related decisions (via commit_hash pattern for synthetic decisions)
decision_rows = conn.execute(
"""
SELECT id, decision, reasoning, commit_hash
SELECT id, decision, reasoning, commit_hash, targets
FROM decisions
WHERE project_id = ? AND (
commit_hash LIKE 'combine:%' OR
Expand All @@ -149,6 +149,30 @@ def show(
).fetchall()
decisions = [dict(r) for r in decision_rows]

# Extract per-strategy outcomes from decision targets
strategy_outcomes: dict[str, dict] = {}
for d in decisions:
targets_raw = d.get("targets")
if not targets_raw:
continue
targets_data = safe_json_loads(targets_raw, "decision.targets", default={})
for strat_name, outcome in targets_data.items():
if isinstance(outcome, dict):
strategy_outcomes[strat_name] = outcome

# Batch-resolve topic names for any topic_ids in strategy outcomes
topic_ids = {o.get("topic_id") for o in strategy_outcomes.values() if o.get("topic_id")}
topic_names: dict[str, str] = {}
if topic_ids:
valid_ids = [tid for tid in topic_ids if tid is not None]
if valid_ids:
placeholders = ",".join("?" * len(valid_ids))
topic_rows = conn.execute(
f"SELECT id, topic FROM content_topics WHERE id IN ({placeholders})",
valid_ids,
).fetchall()
topic_names = {r[0]: r[1] for r in topic_rows}

# Get batched decisions (deferred commits included in this cycle)
batched_rows = conn.execute(
"SELECT id, decision, commit_hash, commit_message FROM decisions WHERE batch_id = ?",
Expand All @@ -157,10 +181,19 @@ def show(
batched = [dict(r) for r in batched_rows]

if json_output:
# Enrich strategy outcomes with resolved topic names
enriched_outcomes = {}
for sn, outcome in strategy_outcomes.items():
entry = dict(outcome)
tid = entry.get("topic_id")
if tid and tid in topic_names:
entry["topic_name"] = topic_names[tid]
enriched_outcomes[sn] = entry
typer.echo(
json_mod.dumps(
{
"cycle": cycle.to_dict(),
"strategy_outcomes": enriched_outcomes,
"drafts": drafts,
"decisions": decisions,
"batched_commits": batched,
Expand All @@ -177,6 +210,18 @@ def show(
typer.echo(f" Ref: {cycle.trigger_ref}")
typer.echo(f" Created: {cycle.created_at}")

if strategy_outcomes:
typer.echo(f"\n Strategy Outcomes ({len(strategy_outcomes)}):")
for sn, outcome in strategy_outcomes.items():
action = outcome.get("action", "?")
reason = (outcome.get("reason") or "")[:50]
topic_id = outcome.get("topic_id")
topic_part = ""
if topic_id:
name = topic_names.get(topic_id, topic_id[:14])
topic_part = f" topic={name}"
typer.echo(f" {sn:<20} {action:<10} {reason}{topic_part}")

if batched:
typer.echo(f"\n Batched commits ({len(batched)}):")
for b in batched:
Expand Down
Loading
Loading