diff --git a/.github/workflows/github-review-queue.yml b/.github/workflows/github-review-queue.yml new file mode 100644 index 0000000..d5df705 --- /dev/null +++ b/.github/workflows/github-review-queue.yml @@ -0,0 +1,48 @@ +name: Build GitHub review queue + +on: + workflow_dispatch: + inputs: + created_after: + description: Earliest issue creation date (YYYY-MM-DD) + required: false + default: "2026-05-01" + min_score: + description: Minimum qualification score + required: false + default: "55" + schedule: + - cron: "17 5 * * 1,4" + +permissions: + contents: read + +jobs: + discover: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Discover and score relevant issues + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CREATED_AFTER: ${{ inputs.created_after || '2026-05-01' }} + MIN_SCORE: ${{ inputs.min_score || '55' }} + run: | + python scripts/discover_github_issues.py \ + --created-after "$CREATED_AFTER" \ + --min-score "$MIN_SCORE" + + - name: Upload review queue + uses: actions/upload-artifact@v4 + with: + name: lce-github-review-queue + path: | + artifacts/github-review-queue.json + artifacts/github-review-queue.md + if-no-files-found: error + retention-days: 14 diff --git a/README.md b/README.md index d019bef..9f16412 100644 --- a/README.md +++ b/README.md @@ -66,4 +66,14 @@ Project deletion is an explicit destructive operator action. The HTTP service is Classification is deterministic and intentionally conservative. A context pack is a source-linked retrieval artifact, not a completeness guarantee, autonomous memory, or decision-maker. +## GitHub inspection pipeline + +The repository includes a human-reviewed discovery pipeline for finding live GitHub issues that match LCE's implemented capabilities: + +```bash +python scripts/discover_github_issues.py +``` + +It produces ranked JSON and Markdown review queues. It never posts public comments automatically. See [`docs/GITHUB_REVIEW_PIPELINE.md`](docs/GITHUB_REVIEW_PIPELINE.md) for the qualification model, response gate, inspection-readiness checks, and weekly operating target. + See [`docs/INTEGRATION.md`](docs/INTEGRATION.md), [`docs/API.md`](docs/API.md), and [`DEPLOYABILITY_DISTRIBUTION.md`](DEPLOYABILITY_DISTRIBUTION.md). diff --git a/config/github_issue_queries.json b/config/github_issue_queries.json new file mode 100644 index 0000000..c517278 --- /dev/null +++ b/config/github_issue_queries.json @@ -0,0 +1,52 @@ +{ + "queries": [ + { + "id": "context-loss-and-resume", + "terms": [ + "losing context", + "resume work", + "context handoff", + "stale context", + "project memory" + ], + "exclude_labels": ["duplicate", "invalid", "good first issue"], + "help_angle": "Show a source-linked handoff/resume pattern using deterministic extraction and a compact context pack. Mention LCE only if the issue needs durable project memory rather than chat memory." + }, + { + "id": "decision-provenance", + "terms": [ + "decision log", + "decision provenance", + "audit trail", + "source line", + "traceable decisions" + ], + "exclude_labels": ["duplicate", "invalid", "good first issue"], + "help_angle": "Offer a minimal decision-record schema preserving source path, line, timestamp, and content hash; explain how this prevents unsupported summaries." + }, + { + "id": "operational-state-drift", + "terms": [ + "state drift", + "project status stale", + "blocked by missing context", + "handoff failure", + "context synchronization" + ], + "exclude_labels": ["duplicate", "invalid", "good first issue"], + "help_angle": "Identify the missing state boundary, propose deterministic re-ingestion and replacement semantics, and distinguish searchable evidence from inferred state." + }, + { + "id": "local-first-operational-memory", + "terms": [ + "local first memory", + "offline project memory", + "sqlite knowledge base", + "local context store", + "no cloud knowledge base" + ], + "exclude_labels": ["duplicate", "invalid", "good first issue"], + "help_angle": "Provide a local SQLite plus CLI/API architecture and the smallest ingestion/query loop that addresses the issue without requiring a hosted service." + } + ] +} diff --git a/docs/GITHUB_REVIEW_PIPELINE.md b/docs/GITHUB_REVIEW_PIPELINE.md new file mode 100644 index 0000000..86f73b0 --- /dev/null +++ b/docs/GITHUB_REVIEW_PIPELINE.md @@ -0,0 +1,114 @@ +# GitHub inspection and review pipeline + +The objective is not to broadcast Living Context Engine into unrelated threads. The objective is to find active engineering problems where LCE's implemented mechanisms are directly relevant, contribute a useful answer, and create a legitimate reason for an engineer to inspect the repository. + +## Conversion path + +```text +LCE capability + -> precise pain language + -> live open GitHub issue + -> deterministic qualification + -> human technical review + -> value-first response + -> optional contextual repository reference + -> repository inspection + -> useful feedback, issue, PR, or star +``` + +A star is a downstream signal, not the primary action. The controllable target is a qualified repository inspection by a relevant engineer. + +## Run locally + +```bash +python scripts/discover_github_issues.py +``` + +Optional controls: + +```bash +python scripts/discover_github_issues.py \ + --created-after 2026-05-01 \ + --per-query 30 \ + --min-score 60 +``` + +Outputs: + +- `artifacts/github-review-queue.json` for processing and measurement; +- `artifacts/github-review-queue.md` for human review. + +The scheduled GitHub Action runs twice weekly and uploads both files as a workflow artifact. It does not post comments. + +## Qualification model + +The score is deliberately simple and inspectable: + +| Component | Maximum | What it represents | +|---|---:|---| +| Relevance | 40 | Exact LCE capability language found in the issue | +| Intent | 25 | The author is actively seeking a fix, workaround, or architecture | +| Recency | 15 | The problem is current enough to engage | +| Validation | 10 | Other humans have engaged with the issue | +| Spam penalty | -20 | Bot, stale, duplicate, invalid, or beginner-task noise | + +The score is a queueing heuristic, not evidence that LCE solves the issue. + +## Mandatory human gate + +Before responding to any issue: + +1. Read the complete issue and all comments. +2. Confirm the problem remains unresolved. +3. Inspect linked code, logs, reproductions, and maintainer guidance. +4. Write the useful part first: diagnosis, workaround, schema, or runnable example. +5. Remove the LCE reference if it is not necessary to the answer. +6. Never repeat the same promotional wording across repositories. + +## Value-first response structure + +Use this order: + +1. **Observed failure:** restate the concrete mechanism, not the product category. +2. **Immediate help:** provide a specific fix, data shape, command sequence, or diagnostic. +3. **Boundary:** state what the proposed fix does not guarantee. +4. **Optional reference:** mention LCE only when its implemented behavior directly covers the recurring pattern. +5. **Verification request:** ask whether the proposed mechanism matches the maintainer's constraints. + +Example skeleton: + +```text +The failure looks less like "missing AI memory" and more like replacement without source provenance: the resumed state cannot show which file/line produced each claim. + +A minimal fix is to persist records as {project, source_path, source_line, observed_at, content_hash, kind, text}, replace records only within the same project/source boundary, and build the handoff from those records rather than from a free-form summary. + +That gives traceability, but it does not prove the context pack is complete or make decisions autonomously. + +I maintain Living Context Engine, which implements this exact local-first pattern with SQLite, source-linked records, CLI queries, context packs, and a read-only API. The schema/approach above is usable independently of the project. +``` + +## Inspection readiness gate + +Do not increase outreach volume until the repository passes these checks: + +- the README communicates the mechanism and evidence boundary in under two minutes; +- activation works from a clean Python environment; +- examples produce inspectable output; +- tests pass on the default branch; +- repository topics and description use the same pain language as the query matrix; +- issue templates invite reproducible failures and integration requests; +- the first contribution path is explicit; +- generated outreach can be traced to a query, issue, reviewer decision, and outcome. + +## Weekly operating target + +Start with a small controlled batch: + +- review the top 10 queue entries; +- select at most 3 where direct technical help is possible; +- post no more than 3 fully individualized responses; +- ask 2 relevant engineers for blunt repository inspection; +- record inspections, substantive replies, issues opened, PRs, and stars; +- change query terms only from observed false positives and real conversations. + +This prevents optimizing for low-quality impressions while the repository and message are still being calibrated. diff --git a/scripts/discover_github_issues.py b/scripts/discover_github_issues.py new file mode 100644 index 0000000..e93afe8 --- /dev/null +++ b/scripts/discover_github_issues.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +"""Discover and score GitHub issues that match Living Context Engine capabilities. + +Uses only the Python standard library. Public searches work without a token at a +lower rate limit; set GITHUB_TOKEN for authenticated requests. + +This script never posts comments. It produces a review queue for a human operator. +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +import urllib.error +import urllib.parse +import urllib.request +from dataclasses import asdict, dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +API_URL = "https://api.github.com/search/issues" +USER_AGENT = "living-context-engine-review-pipeline/1.0" + + +@dataclass(frozen=True) +class Candidate: + repository: str + number: int + title: str + url: str + created_at: str + updated_at: str + comments: int + author: str + query_id: str + matched_terms: list[str] + relevance: int + intent: int + recency: int + validation: int + spam_penalty: int + score: int + reason: str + draft_angle: str + + +def load_config(path: Path) -> dict[str, Any]: + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise SystemExit(f"Could not load config {path}: {exc}") from exc + if not isinstance(data.get("queries"), list): + raise SystemExit("Config must contain a 'queries' list") + return data + + +def github_get(params: dict[str, str], token: str | None) -> dict[str, Any]: + url = f"{API_URL}?{urllib.parse.urlencode(params)}" + headers = { + "Accept": "application/vnd.github+json", + "User-Agent": USER_AGENT, + "X-GitHub-Api-Version": "2022-11-28", + } + if token: + headers["Authorization"] = f"Bearer {token}" + request = urllib.request.Request(url, headers=headers) + try: + with urllib.request.urlopen(request, timeout=30) as response: + return json.load(response) + except urllib.error.HTTPError as exc: + body = exc.read().decode("utf-8", errors="replace") + raise RuntimeError(f"GitHub API returned {exc.code}: {body}") from exc + except urllib.error.URLError as exc: + raise RuntimeError(f"GitHub API request failed: {exc.reason}") from exc + + +def age_score(created_at: str) -> int: + created = datetime.fromisoformat(created_at.replace("Z", "+00:00")) + days = max(0, (datetime.now(timezone.utc) - created).days) + if days <= 14: + return 15 + if days <= 45: + return 12 + if days <= 90: + return 8 + return 3 + + +def score_item(item: dict[str, Any], query: dict[str, Any]) -> Candidate: + title = str(item.get("title", "")) + body = str(item.get("body") or "") + text = f"{title}\n{body}".lower() + terms = [str(term).lower() for term in query.get("terms", [])] + matched = sorted({term for term in terms if term in text}) + + relevance = min(40, 12 + 7 * len(matched)) + intent_markers = ( + "how do i", "how can i", "need a way", "looking for", "workaround", + "blocked", "losing context", "resume", "handoff", "provenance", + "source line", "audit trail", "stale context", "decision log", + ) + intent = min(25, 7 + 4 * sum(marker in text for marker in intent_markers)) + recency = age_score(str(item.get("created_at"))) + comments = int(item.get("comments", 0)) + validation = 10 if comments >= 3 else 7 if comments >= 1 else 2 + + login = str((item.get("user") or {}).get("login", "")) + labels = " ".join(str(label.get("name", "")) for label in item.get("labels", [])) + spam_markers = ("dependabot", "renovate", "stale", "duplicate", "invalid", "good first issue") + spam_penalty = 20 if any(marker in f"{login} {labels}".lower() for marker in spam_markers) else 0 + + score = max(0, min(100, relevance + intent + recency + validation - spam_penalty)) + repository_url = str(item.get("repository_url", "")) + repository = repository_url.removeprefix("https://api.github.com/repos/") + angle = str(query.get("help_angle", "Explain the mechanism, offer a concrete workaround, then mention LCE only when directly relevant.")) + reason = ( + f"Matched {len(matched)} capability terms; {comments} comments; " + f"intent={intent}, recency={recency}, penalty={spam_penalty}." + ) + return Candidate( + repository=repository, + number=int(item["number"]), + title=title, + url=str(item["html_url"]), + created_at=str(item["created_at"]), + updated_at=str(item["updated_at"]), + comments=comments, + author=login, + query_id=str(query["id"]), + matched_terms=matched, + relevance=relevance, + intent=intent, + recency=recency, + validation=validation, + spam_penalty=spam_penalty, + score=score, + reason=reason, + draft_angle=angle, + ) + + +def build_query(query: dict[str, Any], created_after: str) -> str: + phrases = " OR ".join(f'"{term}"' for term in query.get("terms", [])) + exclusions = " ".join(f'-label:"{label}"' for label in query.get("exclude_labels", [])) + return ( + f"is:issue is:open created:>{created_after} ({phrases}) " + f"{exclusions} archived:false" + ).strip() + + +def render_markdown(candidates: list[Candidate]) -> str: + lines = [ + "# LCE GitHub review queue", + "", + f"Generated: {datetime.now(timezone.utc).isoformat()}", + "", + "> Human review required. Do not automate public comments.", + "", + ] + for index, candidate in enumerate(candidates, start=1): + lines.extend([ + f"## {index}. [{candidate.repository} #{candidate.number}]({candidate.url}) — {candidate.score}/100", + "", + f"**{candidate.title}**", + "", + f"- Query: `{candidate.query_id}`", + f"- Author: `{candidate.author}`; comments: {candidate.comments}", + f"- Matched terms: {', '.join(candidate.matched_terms) or 'none'}", + f"- Qualification: {candidate.reason}", + f"- Help angle: {candidate.draft_angle}", + "- Review gate: verify the issue is unresolved, reproduce or inspect linked code, provide value before mentioning LCE.", + "", + ]) + return "\n".join(lines) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--config", type=Path, default=Path("config/github_issue_queries.json")) + parser.add_argument("--created-after", default="2026-05-01") + parser.add_argument("--per-query", type=int, default=20) + parser.add_argument("--min-score", type=int, default=55) + parser.add_argument("--output", type=Path, default=Path("artifacts/github-review-queue.json")) + parser.add_argument("--markdown", type=Path, default=Path("artifacts/github-review-queue.md")) + args = parser.parse_args() + + config = load_config(args.config) + token = os.getenv("GITHUB_TOKEN") + candidates: dict[str, Candidate] = {} + + for query in config["queries"]: + search = build_query(query, args.created_after) + payload = github_get({"q": search, "sort": "updated", "order": "desc", "per_page": str(args.per_query)}, token) + for item in payload.get("items", []): + if "pull_request" in item: + continue + candidate = score_item(item, query) + if candidate.score < args.min_score: + continue + current = candidates.get(candidate.url) + if current is None or candidate.score > current.score: + candidates[candidate.url] = candidate + + ranked = sorted(candidates.values(), key=lambda item: (-item.score, -item.comments, item.updated_at)) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps([asdict(item) for item in ranked], indent=2) + "\n", encoding="utf-8") + args.markdown.write_text(render_markdown(ranked), encoding="utf-8") + print(f"Wrote {len(ranked)} qualified candidates to {args.output} and {args.markdown}") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except RuntimeError as exc: + print(str(exc), file=sys.stderr) + raise SystemExit(1) from exc diff --git a/tests/test_github_issue_discovery.py b/tests/test_github_issue_discovery.py new file mode 100644 index 0000000..99bdd3c --- /dev/null +++ b/tests/test_github_issue_discovery.py @@ -0,0 +1,75 @@ +import sys +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path + + +SCRIPT = Path(__file__).parents[1] / "scripts" / "discover_github_issues.py" +SPEC = spec_from_file_location("discover_github_issues", SCRIPT) +assert SPEC and SPEC.loader +MODULE = module_from_spec(SPEC) +sys.modules[SPEC.name] = MODULE +SPEC.loader.exec_module(MODULE) + + +def test_build_query_preserves_issue_and_safety_filters(): + query = { + "id": "test", + "terms": ["losing context", "decision log"], + "exclude_labels": ["duplicate", "invalid"], + } + + rendered = MODULE.build_query(query, "2026-05-01") + + assert "is:issue is:open" in rendered + assert '"losing context" OR "decision log"' in rendered + assert 'created:>2026-05-01' in rendered + assert '-label:"duplicate"' in rendered + assert "archived:false" in rendered + + +def test_score_prefers_specific_active_human_pain(): + item = { + "number": 42, + "title": "How can I stop losing context during project handoffs?", + "body": "We need a way to resume work with a source line audit trail and decision log.", + "html_url": "https://github.com/example/repo/issues/42", + "repository_url": "https://api.github.com/repos/example/repo", + "created_at": "2026-08-01T00:00:00Z", + "updated_at": "2026-08-02T00:00:00Z", + "comments": 4, + "user": {"login": "human-author"}, + "labels": [], + } + query = { + "id": "context-loss", + "terms": ["losing context", "resume work", "source line", "decision log"], + "help_angle": "Offer a source-linked context pack.", + } + + candidate = MODULE.score_item(item, query) + + assert candidate.repository == "example/repo" + assert candidate.score >= 75 + assert candidate.spam_penalty == 0 + assert "decision log" in candidate.matched_terms + + +def test_score_penalizes_bot_or_stale_noise(): + item = { + "number": 7, + "title": "Decision log dependency update", + "body": "Automated update.", + "html_url": "https://github.com/example/repo/issues/7", + "repository_url": "https://api.github.com/repos/example/repo", + "created_at": "2026-08-01T00:00:00Z", + "updated_at": "2026-08-01T00:00:00Z", + "comments": 0, + "user": {"login": "dependabot[bot]"}, + "labels": [{"name": "stale"}], + } + query = {"id": "decision", "terms": ["decision log"]} + + candidate = MODULE.score_item(item, query) + + assert candidate.spam_penalty == 20 + assert candidate.score < 55