{"id":51778330,"url":"https://github.com/bborbe/github-dark-factory-watcher","last_synced_at":"2026-07-20T08:32:14.587Z","repository":{"id":371687529,"uuid":"1299647351","full_name":"bborbe/github-dark-factory-watcher","owner":"bborbe","description":"Watches draft PRs with approved dark-factory specs in the PR diff and emits dark-factory-implement tasks to Kafka for github-dark-factory-agent","archived":false,"fork":false,"pushed_at":"2026-07-16T08:31:16.000Z","size":104,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-07-20T08:32:13.218Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-2-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bborbe.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-07-13T19:07:35.000Z","updated_at":"2026-07-16T08:31:39.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bborbe/github-dark-factory-watcher","commit_stats":null,"previous_names":["bborbe/github-dark-factory-watcher"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/bborbe/github-dark-factory-watcher","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bborbe%2Fgithub-dark-factory-watcher","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bborbe%2Fgithub-dark-factory-watcher/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bborbe%2Fgithub-dark-factory-watcher/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bborbe%2Fgithub-dark-factory-watcher/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bborbe","download_url":"https://codeload.github.com/bborbe/github-dark-factory-watcher/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bborbe%2Fgithub-dark-factory-watcher/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35681006,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"ssl_error","status_checked_at":"2026-07-20T02:08:09.736Z","response_time":111,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-20T08:32:13.990Z","updated_at":"2026-07-20T08:32:14.575Z","avatar_url":"https://github.com/bborbe.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# github-dark-factory-watcher\n\nPolls the GitHub Search API for open **DRAFT** pull requests that carry an\n**approved-not-completed dark-factory spec in the PR diff** and publishes a\n`CreateTaskCommand` to Kafka for each new or force-pushed candidate PR, so the\n[`github-dark-factory-agent`](https://github.com/bborbe/github-dark-factory-agent)\npicks it up and drives the dark-factory implement lifecycle automatically.\n\n## How It Works\n\nThe watcher runs a `user:\u003cscope\u003e` GitHub Search query on a configurable interval\nand, for every open PR since the cursor watermark, evaluates a network-backed\ncandidate predicate. A PR is kept only when ALL hold at its head SHA:\n\n1. the PR is **open** and a **draft** (re-confirmed via `pulls/N` to dodge search-index lag);\n2. `.dark-factory.yaml` is present at the head ref;\n3. at least one spec under `specs/in-progress/*.md` is **touched by the PR diff**\n   AND is approved-but-not-completed (frontmatter `approved:` set, `completed:` unset).\n   The diff-scoping is essential — specs persist in `specs/in-progress/` across their\n   whole lifecycle, so an existence-only check would false-trigger every unrelated draft PR;\n4. no `prompts/in-progress/*.md` remain at the head ref (self-trigger suppression —\n   the agent's own in-flight commits keep the spec approved-not-completed while it works).\n\nFor each kept PR it publishes a `dark-factory-implement` task keyed by a\ndeterministic UUID5 `task_identifier` derived from `(owner, repo, number, head_sha)`.\nThe cursor at `/data/cursor.json` (last-seen update time + `task_id → head_sha` map)\nis persisted between polls so a restart does not re-trigger every known PR, and a\nforce-push (new head SHA) re-submits a fresh task.\n\n## Emitted Task Contract\n\nFrontmatter (per the Agent Task File Contract):\n\n```\ntask_type: dark-factory-implement\nassignee: github-dark-factory-agent\nphase: planning\nstatus: in_progress\nstage: \u003cdev|prod\u003e\ntask_identifier: \u003cUUID5 of owner/repo#number@head_sha\u003e\ntitle: Implement \u003cowner\u003e/\u003crepo\u003e PR #\u003cn\u003e at \u003csha[:7]\u003e\nrepo: \u003cowner\u003e/\u003crepo\u003e\nclone_url: https://github.com/\u003cowner\u003e/\u003crepo\u003e.git\nref: \u003cfull head SHA\u003e\npr_number: \u003cn\u003e\nbranch: \u003chead ref name\u003e\n```\n\nThe body is an operator-readable header only (PR URL, branch, HEAD, approved-spec\ncount). The agent's data source is the clone of the draft PR branch, not the body.\n\n## Environment Variables\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `APP_ID` / `INSTALLATION_ID` / `PEM_KEY` | yes | — | GitHub App credentials (all three or none) |\n| `KAFKA_BROKERS` | yes | — | Comma-separated Kafka broker list |\n| `STAGE` | yes | — | Deployment stage (`dev` or `prod`) |\n| `LISTEN` | no | `:9090` | HTTP listen address (`/healthz`, `/readiness`, `/metrics`, `/check`) |\n| `POLL_INTERVAL` | no | `5m` | Poll interval (Go duration string) |\n| `REPO_SCOPE` | no | `bborbe` | GitHub user or org to search for PRs |\n| `REPO_ALLOWLIST` | no | — | Comma-separated host-qualified repo allowlist (`host/owner/repo`); empty means allow-all |\n| `BACKFILL_DURATION` | no | `720h` (30d) | On cold start, backdate the initial cursor by this; empty disables |\n| `TASK_SUFFIX` | no | — | Optional ` - \u003csuffix\u003e` appended to task filenames (per-stage disambiguator) |\n| `TARGET_VAULT` | no | — | Vault slug stamped on every command (matched against the controller's `VAULT_NAME`) |\n| `TOPIC_PREFIX` | no | — | Kafka topic prefix for CQRS topic construction (`develop`/`master`) |\n| `MAX_SLUG_LEN` | no | `80` | Max length of the slugified PR-title segment in vault filenames |\n| `MAX_TITLE_LEN` | no | `200` | Max length of the whole vault task filename (safety cap) |\n| `SENTRY_DSN` / `SENTRY_PROXY` | no | — | Sentry error tracking |\n\n### `REPO_ALLOWLIST` syntax\n\nEntries are comma-separated. A leading `!` marks an exclusion. A target is allowed iff\n`(includes is empty OR any include matches) AND (no exclude matches)`; excludes always\noverride includes. Wildcards (`github.com/bborbe/*`) are supported.\n\n## HTTP Endpoints\n\n| Path | Method | Purpose |\n|---|---|---|\n| `/healthz` | GET | Liveness probe |\n| `/readiness` | GET | Readiness probe |\n| `/metrics` | GET | Prometheus metrics |\n| `/check` | POST | Run a poll cycle in the background; returns 200 immediately |\n| `/setloglevel/{level}` | GET | Temporarily raise the glog verbosity |\n\n## Metrics\n\n| Metric | Labels | Purpose |\n|---|---|---|\n| `github_dark_factory_watcher_poll_cycles_total` | `result` (success / github_error / rate_limited) | poll health |\n| `github_dark_factory_watcher_published_total` | `status` (create / skipped / error) | task emission rate |\n| `github_dark_factory_watcher_repos_scanned_total` | — | sanity check on the scope filter |\n| `github_dark_factory_watcher_filter_skipped_total` | `reason` | per-reason skip visibility |\n\n## Development\n\n```bash\nmake test          # run unit tests\nmake generate      # regenerate counterfeiter mocks\nmake precommit     # format + lint + test + security checks\n```\n\n## License\n\nBSD 2-Clause License. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbborbe%2Fgithub-dark-factory-watcher","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbborbe%2Fgithub-dark-factory-watcher","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbborbe%2Fgithub-dark-factory-watcher/lists"}