132 lines
4.7 KiB
Plaintext
132 lines
4.7 KiB
Plaintext
|
|
#!/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
|