v3/docs/adr/ADR-316-advisor-copilot-tip-insight-ticker.md
src/funnel/insights.ts, priority-ordered candidates: CVEs pending, uncommitted changes, power-saver active, flywheel status), ADR-313/ADR-314/ADR-315 (the consent-domain pattern this ADR follows exactly)"Integrate ruflo's copilot advisor into the promo ticker."
There is no existing "ruflo copilot advisor" feature — grounded against the real corpus (RuvNet Brain search_ruvnet, direct source reads), the closest real, shipped primitive is ADR-172's Fable Advisor Harness (src/services/fable-harness.ts): a cost-disciplined, opt-in, budget-capped headless claude -p --model claude-fable-5 judge/reflector, currently consumed only by the memory-distillation oracle (distill-oracle.ts) for judging trajectory completions and GEPA reflective mutation. Nothing wires it into the statusline. ADR-147 (Copilot SDK adapter) and agent-harness-generator's Copilot-as-host ADR are both unrelated — different products (GitHub Copilot integration, a third agent platform), not an advisor tip.
This ADR is the missing wiring: reuse the existing Fable harness to generate ONE proactive, actionable tip and surface it through the local insight ticker already built this session — without violating either subsystem's existing invariants:
insights.ts) is deliberately synchronous, local, $0 — its own doc comment states "NEVER a network call from here." A Fable call is a real network-bound claude -p spawn with real cost, so it categorically cannot live in the ticker's hot path.consent + 24h TTL gate (advisor-tip.ts)
│ (only when both pass)
▼
FableHarness.adviseCoPilotTip(structuralSnapshot) [services/fable-harness.ts]
│ writes
▼
~/.ruflo/advisor-tip.json { _ts, headline, detail }
│ read synchronously, $0, no network
▼
insights.ts's advisorTipInsight() → local insight ticker → statusline
FableHarness.adviseCoPilotTip() (services/fable-harness.ts)A third entry point alongside judgeBatch/reflectFailures, same cost/cwd/parsing discipline (fresh empty temp cwd, --append-system-prompt, --max-budget-usd, stdin-piped payload — never an argv positional). Takes a CoPilotSnapshot (security status, swarm state, git-uncommitted count, context-window %) and returns one CoPilotTip { headline, detail, confidence } or null. The system prompt explicitly instructs the model: no prompt/command/file content is present in the input, never invent facts not present in the snapshot, and an empty array is a valid "nothing worth saying" answer — not a parse failure.
funnel/advisor-tip.ts — the safety wrapperThree hard gates before any spend:
advisor-tips domain (types.ts/consent.ts), never bundled with sponsored-downtime/power-saver/training-data-sharing or anything else.readStateJson-backed cache, checked BEFORE constructing the harness. At most one real spend per ADVISOR_REFRESH_TTL_MS (24h) regardless of session count. A "no tip" answer still stamps _ts so the same question isn't re-asked (and re-billed) within the same window.ADVISOR_DEFAULT_BUDGET_USD (0.40 — a single unbatched call uses the ~$0.34 clean-cwd cost anchor, not the ~$0.02 batched anchor which only applies across ~20 items), overridable via RUFLO_ADVISOR_MAX_BUDGET_USD, enforced by FableHarness's own --max-budget-usd pass-through to the real claude -p call.src/commands/advisor.ts)ruflo advisor enable --yes / disable / status, mirroring the disclosure-then-confirm shape of proxy.ts's sponsored/power-saver/training-share subcommands. Unlike those, advisor has no proxy-config.toml mirror — this feature never touches the meta-proxy Rust binary; it calls claude -p directly, client-side only. Registered as its own top-level lazy-loaded command (commands/index.ts), not nested under proxy.
ruflo hooks refresh-advisor (new subcommand in commands/hooks.ts, alongside the existing refresh-funnel) builds a snapshot via the shared funnel/local-signals.ts helpers (see below) and calls refreshAdvisorTipIfStale. hook-handler.cjs's session-restore handler spawns this detached + unref'd, same pattern as spawnDetachedFunnelRefresh — this is load-bearing: a fire-and-forget call from a short-lived subprocess never completes (the exact bug the promo-cache fix addressed earlier this session), so the refresh needs a properly-awaited, longer-lived invocation. Safe to call unconditionally on every session-restore: the TTL/consent checks inside refreshAdvisorTipIfStale make an unconsented or already-fresh install a fast local file read, never a network call.
funnel/local-signals.ts, new)getSecurityStatus(), getSwarmStatus(), getGitUncommittedCount() were previously private closures inside commands/hooks.ts's statuslineCommand action. Extracted to a shared module so the statusline and the (separately-invoked) refresh-advisor CLI subcommand can never silently drift on what these signals mean — a single source of truth, same behavior, zero duplication.
insights.ts)advisorTipInsight() — priority 45 (between uncommitted-changes at 50 and flywheel-status at 40) — re-checks hasConsent('advisor-tips') even though it's just reading a cache: this is what actually enforces "disable means off" the instant a user revokes consent, without needing to also delete the cache file on disable.
Both advisor-tip.ts's isCacheStale/readAdvisorTip and the pre-existing flywheelInsight in insights.ts used !cache._ts to check for a missing timestamp. Since 0 is falsy in JS, a legitimate _ts: 0 (epoch) is indistinguishable from "no timestamp at all" — surfaced by a test using new Date(0) for a deterministic clock. Fixed in both places: typeof cache._ts !== 'number'. Benign in production (Date.now() is never 0), but a real latent defect, now closed in both call sites since it's the identical pattern.
claude -p wrapper.local-signals.ts extraction is a pure refactor (no behavior change) that removes a source of future drift between the statusline and any other consumer of the same structural signals.ADVISOR_REFRESH_TTL_MS per-user, or feeding contextPctUsed into the snapshot (the type field exists in CoPilotSnapshot but nothing populates it yet — deliberately left for a future iteration rather than wiring a rough estimate now).training-data-sharing) this ADR's advisor-tips domain mirrors exactlysrc/funnel/insights.ts — the local insight ticker this ADR adds a fifth source to