Summary
Proposing Tickets/Goals/Heartbeats unification with a shared Telegram approval gate — implemented and running in production on a fork, but built on top of custom infrastructure that doesn't exist in develop yet (details below). Opening this as a discussion/proposal before investing in a real port, since the underlying dependency gap is bigger than the feature itself.
Problem
Today (in the fork, and likely in develop too — happy to be corrected) there's no single mechanism for an autonomous Goal to become tracked, assigned work with human oversight on the risky parts:
- A human-authored Goal has no automatic path to becoming actionable tickets.
- Ticket → Goal progress rollup had (in the fork's history) multiple independent writers to
goals.current_value, causing drift/double-counting.
- Nothing stops an autonomous agent from publishing to a public channel (social media, external comms) without a human in the loop, or from proposing new sub-goals recursively without bound.
What was built (fork, production-tested)
- Single source of truth for
goals.current_value — one function (_recompute_goal_from_tickets, COUNT-based over tickets.status IN ('resolved','closed')) replaces what were previously 3-4 independent writers (a SQLite trigger + manual sync + raw PATCH). Verified idempotent on reopen/re-resolve.
goal-planner heartbeat — wakes on a goal_created event (top-level Goals only, guarded by parent_goal_id IS NULL to prevent recursive decomposition), reads .claude/agents/*.md to pick each ticket's assignee via closed-set validation (invalid slug routes to a triage bucket instead of orphaning the ticket).
- Self-healing review loop — a ticket marked
review gets a structured pass/fail verdict before reaching resolved; bounded retries (2 bounces) before parking for a human, with the bounce counter reset scoped to the last manual reopen (not global).
- Shared approval gate (
pending_approvals table, one mechanism for two use cases):
- Publish-gate: an agent capable of publishing externally never resolves a ticket without an explicit Telegram approve/reject. Fail-closed by default (only an explicit
publish_intent: false bypasses).
- Decomposition-gate: an agent proposing a sub-goal (not just tickets) requires a second approval before any ticket is created from that sub-goal. Approving creates the tickets directly from the already-approved payload — deliberately not by re-firing the
goal_created event, since that would bypass the recursion guard (approval-triggered re-wake is a separate code path from goal creation, and only the latter was originally guarded — this was caught in review before it shipped).
- Dedicated bridge token for the approval-decision endpoint, isolated from the general admin API token and from the agent subprocess's own environment (an agent can't read its own approval-bridge credential and self-approve).
- Never fabricates a "published" success — the actual external-publish action returns
published=false honestly when no automated integration exists for a channel, rather than claiming success without confirmation. (This traces back to a real incident where an orchestrator once reported "posted successfully" without actually posting — the whole approval-gate design exists to make that class of bug structurally harder.)
Full ADR-style writeup (architecture decisions, threat model, alternatives considered) available if useful — happy to share.
Why this is a discussion, not a PR yet
The feature is built on top of two things this fork added independently that don't exist in develop:
- A multi-provider fallback/routing layer for the agentic subprocess calls (~900 lines) —
develop's heartbeat runner invokes the claude binary directly, single-provider.
- An outcome-driven heartbeat notification system (~800 lines) replacing a generic "heartbeat ran" ping with structured, agent-reported outcomes (work/skip/blocked) that this feature's review-loop and approval-gate build directly on top of.
Porting just the tickets/goals/approval-gate feature without also reconciling those two pieces isn't realistic — they're load-bearing. Before investing effort in a real port (or a from-scratch reimplementation against develop's simpler heartbeat runner), I'd like maintainer input:
- Is a multi-provider fallback layer something
develop wants at all, or is single-provider (claude CLI) a deliberate simplicity choice?
- Is the tickets/goals/heartbeats unification concept (independent of my specific implementation) something that fits the roadmap?
- Any existing design docs/discussions on this that I should read before proposing an implementation?
Open to whatever shape of contribution makes sense here — a design doc, a smaller isolated piece first, or shelving it if it doesn't fit direction.
Summary
Proposing Tickets/Goals/Heartbeats unification with a shared Telegram approval gate — implemented and running in production on a fork, but built on top of custom infrastructure that doesn't exist in
developyet (details below). Opening this as a discussion/proposal before investing in a real port, since the underlying dependency gap is bigger than the feature itself.Problem
Today (in the fork, and likely in
developtoo — happy to be corrected) there's no single mechanism for an autonomous Goal to become tracked, assigned work with human oversight on the risky parts:goals.current_value, causing drift/double-counting.What was built (fork, production-tested)
goals.current_value— one function (_recompute_goal_from_tickets, COUNT-based overtickets.status IN ('resolved','closed')) replaces what were previously 3-4 independent writers (a SQLite trigger + manual sync + raw PATCH). Verified idempotent on reopen/re-resolve.goal-plannerheartbeat — wakes on agoal_createdevent (top-level Goals only, guarded byparent_goal_id IS NULLto prevent recursive decomposition), reads.claude/agents/*.mdto pick each ticket's assignee via closed-set validation (invalid slug routes to a triage bucket instead of orphaning the ticket).reviewgets a structured pass/fail verdict before reachingresolved; bounded retries (2 bounces) before parking for a human, with the bounce counter reset scoped to the last manual reopen (not global).pending_approvalstable, one mechanism for two use cases):publish_intent: falsebypasses).goal_createdevent, since that would bypass the recursion guard (approval-triggered re-wake is a separate code path from goal creation, and only the latter was originally guarded — this was caught in review before it shipped).published=falsehonestly when no automated integration exists for a channel, rather than claiming success without confirmation. (This traces back to a real incident where an orchestrator once reported "posted successfully" without actually posting — the whole approval-gate design exists to make that class of bug structurally harder.)Full ADR-style writeup (architecture decisions, threat model, alternatives considered) available if useful — happy to share.
Why this is a discussion, not a PR yet
The feature is built on top of two things this fork added independently that don't exist in
develop:develop's heartbeat runner invokes theclaudebinary directly, single-provider.Porting just the tickets/goals/approval-gate feature without also reconciling those two pieces isn't realistic — they're load-bearing. Before investing effort in a real port (or a from-scratch reimplementation against
develop's simpler heartbeat runner), I'd like maintainer input:developwants at all, or is single-provider (claudeCLI) a deliberate simplicity choice?Open to whatever shape of contribution makes sense here — a design doc, a smaller isolated piece first, or shelving it if it doesn't fit direction.