cc-os/plugins/os-backlog/bin/wakeup-poll

132 lines
4.7 KiB
Plaintext
Raw Normal View History

#!/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