#!/usr/bin/env ruby # frozen_string_literal: true # # wakeup-poll: one pass of the issue-triggered wakeup poller (ADR-0035). # # Reads the global project index (~/.cc-os/projects.json, ADR-0034), and for # each wakeup-opted-in forgejo:/github: project lists open issues (tea/gh), # diffs against last-seen state (~/.cc-os/wakeup-state.json), and for each # NEW issue either spawns a tmux session (ADR-0036) or — the default — # prints the command it WOULD run. hitl/unlabeled issues are notify-only: # appended to ~/.cc-os/wakeup-summary.md (pull-only, notification policy v2). # # Usage: # wakeup-poll # dry-run: print planned actions, touch no state # wakeup-poll --spawn # actually create tmux sessions + persist state # # Fails soft everywhere: missing index, unroutable projects, tea/gh errors, # and absent tmux each degrade to a printed note, never a crash. The poller # uses tea/gh's existing logins only and never reads ~/.credentials. # Pilot only — install no cron around this until ADR-0035 is Accepted. require "json" require "open3" require "shellwords" require_relative "../lib/backlog" spawn_mode = ARGV.include?("--spawn") def note(msg) = puts("wakeup-poll: #{msg}") def list_issues(kind, slug) cmd = case kind when "forgejo" then ["tea", "issues", "list", "--repo", slug, "--state", "open", "--output", "json"] when "github" then ["gh", "issue", "list", "--repo", slug, "--state", "open", "--json", "number,title,labels,body,url"] end out, err, status = Open3.capture3(*cmd) return nil unless status.success? rows = JSON.parse(out) # Normalize gh's shape to tea's {index, title, labels}. rows.map do |r| { "index" => (r["index"] || r["number"]).to_s, "title" => r["title"], "labels" => r["labels"].is_a?(Array) ? r["labels"].map { |l| l["name"] }.join(",") : r["labels"].to_s, "body" => r["body"].to_s, "url" => r["url"].to_s } end rescue StandardError => e warn "wakeup-poll: #{slug}: #{e.class}: #{e.message}" nil end index = Backlog::ProjectIndex.new projects = index.all if projects.empty? note "project index empty or unreadable — nothing to do" exit 0 end config_reader = lambda do |path| file = File.join(path, ".cc-os", "config") File.exist?(file) ? File.read(file) : "" rescue StandardError "" end eligible = Backlog::Wakeup.eligible_projects(projects, config_reader: config_reader) skipped = projects.keys - eligible.map { |p| p[:path] } skipped.each { |path| note "skip #{path} (#{projects[path]['tracker'] || 'no tracker'}: not a wakeup-eligible forgejo/github project, or wakeup=true not set)" } if eligible.empty? note "no opted-in forgejo/github projects — nothing to do" exit 0 end state = Backlog::Wakeup::State.new summary_path = File.join(File.dirname(Backlog::Wakeup::State.default_path), "wakeup-summary.md") eligible.each do |project| issues = list_issues(project[:tracker_kind], project[:repo_slug]) if issues.nil? note "#{project[:repo_slug]}: issue listing failed (is #{project[:tracker_kind] == 'forgejo' ? 'tea' : 'gh'} logged in?) — skipping" next end last_seen = state.last_seen(project[:repo_slug]) if last_seen.nil? top = Backlog::Wakeup.highest_index(issues) note "#{project[:repo_slug]}: first scan — baselining at ##{top}, waking nothing" state.record(project[:repo_slug], top) if spawn_mode next end fresh = Backlog::Wakeup.new_issues(issues, last_seen) note "#{project[:repo_slug]}: #{fresh.size} new issue(s) since ##{last_seen}" if fresh.any? fresh.each do |issue| tier = Backlog::Wakeup.tier(issue["labels"]) if tier == :notify line = "- #{Time.now.strftime('%Y-%m-%d')} #{project[:repo_slug]}##{issue['index']} (#{tier}): #{issue['title']}" if spawn_mode File.open(summary_path, "a") { |f| f.puts(line) } note "notify-only -> appended to #{summary_path}" else note "would append to #{summary_path}: #{line}" end next end prompt = Backlog::Wakeup.session_prompt( issue: issue, tier: tier, url: issue["url"].empty? ? "#{project[:tracker_kind]}:#{project[:repo_slug]}##{issue['index']}" : issue["url"] ) cmd = Backlog::Wakeup.tmux_command( project: project[:name], path: project[:path], issue_index: issue["index"], prompt: prompt ) if spawn_mode ok = system(*cmd) note ok ? "spawned #{cmd[4]}" : "tmux spawn failed for #{cmd[4]} (is tmux installed?)" else note "would run: #{cmd.shelljoin}" end end state.record(project[:repo_slug], Backlog::Wakeup.highest_index(issues, last_seen)) if spawn_mode end note "dry-run complete — no state written, no sessions spawned (use --spawn)" unless spawn_mode