diff --git a/.github/workflows/launchpad-pr-check.yml b/.github/workflows/launchpad-pr-check.yml index 739078e1eab..2885f774e76 100644 --- a/.github/workflows/launchpad-pr-check.yml +++ b/.github/workflows/launchpad-pr-check.yml @@ -75,3 +75,34 @@ jobs: LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }} CLOSING_REFS: ${{ steps.closing.outputs.refs }} run: python3 launchpad/scripts/pr_body_check.py + + # The controls for launchpad/scripts/. A suite that runs only on the author's + # machine is a claim, not a gate: the first commit to break it does so silently. + # + # These need NO token and NO network. Every read in the pre-flight goes through + # an injected runner and every fixture is recorded, so the suite passes under + # `env -i` with no HOME and no GH_TOKEN. That is why this does not wait on the + # scoped token in #119 — that credential is for RUNNING the pre-flight against a + # live PR, which this job does not do. + scripts: + runs-on: ubuntu-latest + steps: + # persist-credentials: false for the same reason the job above gives, and + # more sharply here: the next step runs mutation_harness.py, which rewrites + # and executes repository source by design, from the fork's own commit. The + # token is read-only on a public repo either way — leaving it in the git + # config would still be a credential this job never needs. + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Controls + run: python3 -m unittest discover -s launchpad/scripts -t launchpad/scripts -v + + # Proves the controls can FAIL. It rewrites the modules in place, one at a + # time, and restores them — verified byte-for-byte before it exits. On a + # throwaway CI checkout there is nothing to lose if it is interrupted, and a + # green suite whose controls were never observed failing is what this whole + # stage refuses to accept anywhere else. + - name: Mutation harness — every control must be shown to fail + run: python3 launchpad/scripts/mutation_harness.py diff --git a/launchpad/plans/2026-08-12-issue-116-pr-review-preflight.md b/launchpad/plans/2026-08-12-issue-116-pr-review-preflight.md new file mode 100644 index 00000000000..1466660fe28 --- /dev/null +++ b/launchpad/plans/2026-08-12-issue-116-pr-review-preflight.md @@ -0,0 +1,376 @@ +Issue #116 — task: deterministic pre-flight for the PR review agent +Stated size: none given — the task template has no Size field -> cap: 12 steps + +Sized by asking, not guessing. Six done-criteria, one of which is a control suite +whose controls must each fail when their check is deleted. Answered: more than an +hour, so the cap is 12. Language answered: Python 3. + +Larger than an hour is flagged, not refused. These would have been better issues, +each observable on its own — splitting is the reader's call, not this plan's: + + (a) emit the PR facts record — title, body, labels, closing keyword, diff + (b) resolve required checks, which has an unanswered config question behind it + (c) resolve the nearest AGENTS.md/CLAUDE.md per changed path — no network needed + (d) the SKIP taxonomy and the mutation-checked control suite + +Planned as written below. + +ALREADY TRUE (verified against git and the GitHub API, not notes) + Branch feat/review-agent-preflight is at d897a06e8 — `git rev-list --left-right + --count origin/launchpad...HEAD` reports 0 0, so nothing of #116 is built. + No pre-flight script exists: `git ls-files | grep -iE 'preflight|pre-flight'` + returns nothing. launchpad/ holds AGENTS.md, AGENT_PR_TEMPLATE.md, README.md, + labels.yml, sync-labels.sh, decisions/ — and no scripts/ directory. + Root scripts/ and root docs/ are upstream's trees. launchpad/AGENTS.md §3 puts + cohort files under launchpad/, so both are off-limits for this work. + Toolchain present: python3 3.12.3, gh 2.93.0. + `gh pr view --json statusCheckRollup` does NOT expose isRequired — verified on + PR 86, whose keys are only __typename, completedAt, conclusion, detailsUrl, + name, startedAt, status, workflowName. isRequired comes from GraphQL only, as + isRequired(pullRequestNumber:). + PR 86 carries 24 checks and two of them are both named "check" from the same + workflow "launchpad — PR body check". Check names are NOT unique. + CORRECTED 2026-08-13, one day later: the live API and the recorded fixture both + report 47 contexts with THREE named "check" and 23 names duplicated overall. + Every "24" below is stale in the same way. The property each criterion tests — + names collide, so the record must carry a list — holds more strongly, not less, + and the controls assert against the recorded fixture rather than against these + numbers. + No branch protection is readable on launchpad: /rulesets returns [], + /branches/launchpad/protection 404s, and /rules/branches/launchpad returns []. + Every check on PR 86 reports isRequired false. This contradicts + launchpad/AGENTS.md §6 ("The launchpad branch is protected"), and the token + cannot see org-level rulesets — /orgs/launchpad-26/rulesets needs admin:org and + the token holds gist, project, read:org, repo, workflow. + No verify gate is installed in this checkout — .claude/settings.json and + settings.local.json are both absent, so review skills are manual invocations. + Upstream precedent for a testable script is a core module beside its test: + scripts/check-file-sizes-core.mjs and scripts/check-file-sizes-core.test.mjs. + The closing-keyword regex already exists in .github/workflows/launchpad-pr-check.yml + line 39 — `\b(Closes|Fixes|Resolves)\s+#\d+` — applied only after HTML comments + are stripped (line 28). + There is no `mergeBaseCommit` field on the GraphQL PullRequest type. Verified by + introspecting `__type(name:"PullRequest"){fields{name}}`: the merge- and base- + named fields are autoMergeRequest, baseRef, baseRefName, baseRefOid, + baseRepository, canBeRebased, mergeCommit, mergeStateStatus, mergeable, merged, + mergedAt, mergedBy, potentialMergeCommit and the viewer/queue variants. The + merge base is REST-only: /repos/{o}/{r}/compare/{base}...{head} -> + .merge_base_commit.sha. + PR 86 cannot exercise merge-base correctness: its baseRefOid, the current tip + of launchpad, and its merge_base_commit.sha are all d897a06e8. Every open PR in + this fork is in that state. A divergent case exists upstream — block/buzz#5625, + baseRefOid 4b3570671 against merge base 5bf78671f. + The git trees API reports partial results as HTTP 200 plus `truncated: true` + rather than an error. At d897a06e8 this repo returns 4332 entries with + `truncated: false`, well inside the ~100,000-entry limit. + PR 14 in this fork adds AGENTS.md (+22) and launchpad/AGENTS.md (+244), so the + rules-file-ADD fixture has a real source. No PR in this fork's seven-PR history + deletes a nearest rules file, and none was found upstream, so the DELETE + fixture has to be manufactured. + ast.walk finds imports nested in function bodies where tree.body does not, and + neither sees importlib.import_module — both verified by parsing a sample module + and comparing the collected name sets. + +STEP 1 Record real gh/GraphQL responses as fixtures under [independent] + launchpad/scripts/testdata/ — seven of them, recorded from the live API + and never hand-written: + (i) PR 86 — 24 checks, two sharing the name "check" + [recorded 2026-08-13: 47, three sharing it] + (ii) a PR carrying a closing keyword + (iii) a PR whose only `Closes #n` sits inside an HTML comment + (iv) a 404 PR number + (v) the empty /rules/branches/launchpad response + (vi) a DIVERGENT-base PR, where the base branch tip is ahead of the + commit the head branch actually forked from. Every open PR in + this fork currently has base tip == merge base, so this fixture + must come from upstream: block/buzz#5625 has baseRefOid + 4b3570671 against merge base 5bf78671f. Without it nothing in + this plan can tell a three-dot diff from a two-dot one. + (vii) two PRs, not one, because the add and delete directions have very + different provenance. ADD is real and already in this fork: + PR 14 adds AGENTS.md (+22) and launchpad/AGENTS.md (+244) among + 18 files. DELETE has no real source — no PR in this fork's + history, and none found upstream, deletes a nearest rules file. + Open a throwaway PR in the fork that deletes one, record the + response, close it unmerged. Say so in the PR body; do not let + the add case stand in for both by assertion. + Fixture (vi)'s SHAs are current as of 2026-08-12 on a still-open upstream + PR. Re-confirm at recording time that baseRefOid and merge_base_commit.sha + still differ, rather than trusting the SHAs quoted here — if that PR has + merged, any actively-developed repo will have another PR with the same + property. + done when: each fixture file parses under `python3 -c 'import json,sys; + json.load(open(sys.argv[1]))'`; the PR-86 checks fixture contains 24 + entries of which at least two share the name "check"; fixture (vi)'s + recorded baseRefOid and merge_base_commit.sha are different strings; and + (vii) is two recorded PRs, one adding and one deleting a rules file. + +STEP 2 launchpad/scripts/preflight_core.py — pure functions over [needs 1] + already-fetched data, no subprocess and no network in this module. The + record's `checks` field is a LIST, never a name-keyed map, because PR 86 + proves names collide and a map would silently drop an entry. + The record's fields are enumerated HERE, so that later steps have a fixed + list to be checked against rather than an open-ended "every field": + pr number, title, body, labels, base_ref, head_sha + closing_issue present, keyword, issue_number + diff merge_base_sha, head_sha, files[path, added, removed, + status] + checks [name, workflow, status, conclusion, required, + details_url] + required_gate configured, source_endpoint + nearest_rules per changed path, the resolved AGENTS.md and CLAUDE.md + skips [field, reason] + Adding a field to the record means adding it to this list in the same + commit. Whether the record also carries a schema version is OPEN below. + done when: `python3 -m py_compile launchpad/scripts/preflight_core.py` + succeeds; building the record from the PR-86 fixture yields exactly as + many check entries as the fixture holds — 24 when this was written, 47 as + recorded — and the record's top-level keys equal the seven names above. + +STEP 3 launchpad/scripts/pr-preflight.py — the CLI shell. Takes a [needs 2] <- RUNS HERE + PR number, prints the record as JSON on stdout, and takes its command + runner as an injected argument defaulting to real `gh`, so every later + control can feed fixtures without a network call. + done when: `python3 launchpad/scripts/pr-preflight.py 86` prints JSON + whose title matches `gh pr view 86 --json title -q .title` and whose + checks array has as many entries as the fixture holds (24 when written, + 47 as recorded), exiting 0; and a nonexistent PR number exits non-zero + rather than printing a record. + +STEP 4 Title, body, labels, and closing-keyword detection — reusing [needs 3] + launchpad-pr-check.yml's regex and its comment-stripping order, so a + `Closes #n` that appears only inside `` does not count as present. + done when: on the closing-keyword fixture the record names the keyword + and the issue number; on the commented-out fixture `closing_keyword` is + absent with a recorded reason, and the run does not report it as present. + +STEP 5 The merge-base diff — per-file path with added/removed counts, [needs 3] + plus the merge-base and head SHAs, so the record is pinned to the commit + pair it read. The merge base comes from REST: + `GET /repos/{o}/{r}/compare/{base}...{head}` -> `.merge_base_commit.sha`. + There is NO `mergeBaseCommit` field on the GraphQL PullRequest type — + introspection lists baseRef, baseRefName, baseRefOid, mergeCommit, + potentialMergeCommit and nothing resembling a merge base. Do not reach + for GraphQL here. + The trap this step exists to avoid: `baseRefOid` is the CURRENT TIP of + the base branch, not the commit the head forked from. Diffing against it + two-dot attributes every commit landed on launchpad since the fork to + this PR's author, in reverse. + done when: on PR 86 the record's sorted file paths equal `gh pr diff 86 + --repo launchpad-26/buzz --name-only | sort`; AND on the divergent + fixture (vi) the recorded base SHA equals its merge_base_commit.sha and + is NOT equal to its baseRefOid — which a two-dot implementation fails, + where on PR 86 alone it would pass, because PR 86's base tip and merge + base are the same commit (d897a06e8). + +STEP 6 Required checks, from both sources. Per-context isRequired via [needs 3] + GraphQL, plus a probe of /repos/{o}/{r}/rules/branches/{base} and + /rulesets, so an empty required set is reported as + `required_checks_configured: false` with the endpoint that answered — + never as a silent zero, which is indistinguishable from a scope failure. + done when: on PR 86 the record shows every check (24 when written, 47 as + recorded) each with required false, + names which endpoint supplied that, and — because the token lacks + admin:org — carries a SKIP for org-level rulesets rather than asserting + that none exist. + +STEP 7 Nearest-rules resolution: for each changed path, walk to the [needs 5] + repository root collecting the nearest AGENTS.md AND the nearest + CLAUDE.md. Both, not first-wins — launchpad/ has only AGENTS.md while + the root has both, so first-wins would hide the root CLAUDE.md from + every launchpad/ path. Resolved against the PR's head tree, not the + local worktree, or the answer is wrong for any PR not checked out. + done when: a changed `launchpad/AGENTS.md` resolves to launchpad/AGENTS.md + plus root CLAUDE.md; a changed `desktop/src/main.tsx` resolves to root + AGENTS.md plus root CLAUDE.md; a control passes with no local checkout of + the head branch present; and on fixture (vii) — a PR that deletes or adds + the nearest rules file for a path it also touches — resolution reflects + the head tree, so a deleted `launchpad/AGENTS.md` falls back to the root + one and a newly added one is picked up. Rules files under launchpad/ are + actively edited, so this is a case this repo will actually produce. + +STEP 8 The SKIP taxonomy and the exit contract — one enumerated reason [needs 4, 5, 6, 7] + per unreadable input, distinguishing absent (404), forbidden (403 or + missing scope), malformed, and empty-but-readable. + "Required input" must be defined, because the DoD's exit-code rule is + meaningless without it and "SKIP everything, always exit 0" would + otherwise satisfy every other criterion. This plan classifies: + REQUIRED, and a failure to read exits non-zero — the PR itself, its + metadata (title, body, labels), the compare/diff response, the check + list, and the head-tree listing STEP 7 resolves against. + SKIP-ONLY, absence is a legitimate reportable state and exit stays 0 — + org-level ruleset visibility (unreadable without admin:org), and a + changed path having no ancestor AGENTS.md or CLAUDE.md at all. + One trap makes those two categories bleed into each other, and it must be + closed explicitly: the git trees API does NOT fail loudly when it cannot + return a whole tree. It answers HTTP 200 with `truncated: true` and a + partial list once a tree passes roughly 100,000 entries or 7MB. A path + whose rules file sits beyond the truncation boundary then looks exactly + like the SKIP-ONLY "no ancestor file" case while the real cause is an + incomplete REQUIRED read. This fork is nowhere near the limit today — + 4332 entries at d897a06e8, `truncated: false` — but this is a CLI that + takes any PR number, so the guard is required, not hypothetical: + `truncated: true` is a head-tree-listing failure and exits non-zero. + That classification is a judgement, not a fact, and a reviewer may move + any line of it. + done when: the enumerated reasons and the required/SKIP-only split are + listed in the module's docstring and in `--help`; every reason is + reachable from at least one fixture; for EACH of the five required + inputs, a control that makes just that one call fail asserts the process + exits non-zero — not only the nonexistent-PR case STEP 3 already covers; + and a control feeding a `truncated: true` trees response asserts a + non-zero exit rather than an empty nearest_rules result. + +STEP 9 launchpad/scripts/test_preflight_core.py — a control per [needs 8] + done-criterion plus one per SKIP reason, all fixture-driven with the + runner injected. This suite, not any grep, is the mechanical proof that + an absence never reads as a value: for each of the seven record fields + enumerated in STEP 2, a control feeds an empty, malformed and erroring + response in turn and asserts the field comes back as a SKIP carrying a + reason, never as a value and never as PASS. Seven is a list someone can + count; "every field" was not, which is why STEP 2 now fixes it. + done when: `python3 -m unittest discover -s launchpad/scripts -t .` + passes; the suite runs with no network — asserted by injecting a runner + that raises if invoked with the real `gh` binary; and the empty, + malformed and erroring variants above each have a named test. + +STEP 10 Prove each control can fail. For every check function, neuter it [needs 9] + to a constant and confirm the suite goes red — the done-criterion is that + each control fails if its check is deleted, and a control never observed + failing has not been shown to test anything. + done when: a recorded run shows, for each check function, the suite + failing while that function is neutered and passing when restored, with + that raw output pasted into the PR body. + +STEP 11 Assert the no-model property, by AST and not by grep. A control [needs 9] + parses preflight_core.py with `ast`, collects every imported module name, + and asserts that set is a subset of a written allowlist — so any new + import fails the control by default rather than only the handful anyone + thought to name in a regex. A grep cannot do this: it reports every + occurrence and leaves a human to sort legitimate hits from real ones, + which is not a check that can fail. + Two ways an import evades that scan, both closed here: + Traverse with `ast.walk(tree)`, NOT `tree.body`. Verified: for a module + whose only top-level import is json and which does `import requests` + inside a function, tree.body yields {json} while ast.walk yields + {json, requests}. A top-level-only scan would satisfy a done-when that + did not say where the import went. + Collect `Call` nodes too, and forbid `importlib.import_module` and + `__import__` by name. `importlib.import_module("openai")` is a Call + node, never an Import node — verified: an import-name scan of a module + containing that line reports it as absent. One line evades an + import-only check while breaking the issue's no-model rule outright. + done when: adding `import urllib.request`, `requests`, `httpx`, `openai` + or `anthropic` to preflight_core.py each turn the control red, verified + by doing it and pasting the output; the same holds when the import is + placed inside a function body rather than at module level; a line reading + `importlib.import_module("requests")` also turns it red; the allowlist is + written in the test rather than inferred; and a control asserts `gh` is + the only binary the injected runner is ever asked to spawn. + +STEP 12 Open the PR against launchpad — AGENT_PR_TEMPLATE.md filled, [needs 10, 11] + `Closes #116`, `by:agent`, raw output pasted, and Escalations naming + both deferrals: no workflow file until #110 decides where this runs, and + the required-check contradiction found in ALREADY TRUE. + SUPERSEDED 2026-08-13: a workflow job IS added, running this stage's + controls and its mutation harness on every PR. That is not the invocation + #110 gates — it needs no token and no network — and the deferral of the + live pre-flight run to #119 stands. + done when: the PR is open against launchpad with the by:agent label, the + "launchpad — PR body check" check is green, and the body names #110 as + the reason no .github/workflows file INVOKES THE PRE-FLIGHT. A workflow + job running the controls was added on 2026-08-13; it needs no credential, + so #110 never gated it. + +PARALLEL Nothing here should be fanned out as written. Steps 4, 5 and 6 are + logically independent and all tagged [needs 3], but all three edit + preflight_core.py, and two steps editing one file are sequential regardless of + how unrelated they look. There is one way to earn the fan-out: if step 2 lands + one module per check (checks/body.py, checks/diff.py, checks/required.py) then + 4, 5 and 6 touch disjoint files and can run as three parallel subagents, with + step 7 still waiting on 5. That is a real design choice with a real payoff, and + it belongs to whoever executes this. Steps 8 through 12 are strictly + sequential — each reads the whole surface the previous one produced. Step 1 is + the only step that is independent as written. Nothing is dispatched here. + +GATES No verify gate is installed in this checkout — .claude/settings.json is + absent — so every gate below is a manual invocation and none of them will fire + on their own. Run serina:review-code and serina:review-tests after step 10, + then serina:review-adjudicate, then serina:review-final — all before the push + in step 12, because a review posted after the push only documents what already + shipped. + serina:review-plan has run TWICE on this file, both times before step 1, and + this revision is the result of both passes. Twelve findings total, all applied, + none disputed. + First pass — six findings, three Blockers: a GraphQL merge-base field that does + not exist (STEP 5), a fixture set that could not tell a three-dot diff from a + two-dot one (STEP 1, STEP 5), an exit contract titled but never defined or + tested (STEP 8), a grep that could not fail standing in for a control (STEP 9, + STEP 11), an untested rules-file-churn case (STEP 7), a prose/command mismatch + over urllib (STEP 11). + Second pass — six findings, no Blockers, all against the first pass's own + fixes: a truncated trees response masquerading as a legitimate no-rules-file + SKIP (STEP 8), "every field of the record" being unenumerable so STEP 2 now + fixes the seven field names (STEP 2, STEP 9), ast.walk versus tree.body + traversal depth (STEP 11), importlib.import_module evading an import-node scan + entirely (STEP 11), fixture (vii)'s delete half having no real source (STEP 1), + and fixture (vi)'s SHAs being pinned to a live upstream PR (STEP 1). + Both passes independently re-verified the API claims in ALREADY TRUE against + the live API. Everything held except the merge-base field. The second pass has + not re-reviewed the fixes applied for its own findings, so this revision is + once-reviewed at the margin. A clean check-plan.sh run remains mechanical only + and judges nothing about whether the steps are right. + +BUDGET Steps 8 and 9 eat the budget, and step 3 decides how badly. + The controls need every gh and GraphQL response fakeable. If step 3 hardcodes + `subprocess.run(["gh", ...])` at each call site instead of taking the runner as + an injected argument, step 9 stops being test-writing and becomes a rewrite of + steps 3 through 7. Step 10 is the second risk: proving twelve-odd controls each + go red under neutering is mechanical but slow, and it is the criterion most + likely to be quietly downgraded to "the suite passes", which is not what the + issue asked for. + +OPEN Not for a builder to decide. + Where this runs was #110's call. RESOLVED after this plan was written: #110 + decided GitHub Actions for Phase 1 at 2026-08-12T07:43Z — 45 minutes after this + file was committed — and its decision comment names #116's .github/workflows + invocation among what it unblocks. SUPERSEDED 2026-08-13, the same way STEP 12 above: a + workflow job IS added by this work — it runs this stage's controls and its + mutation harness on every pull request, needs no token and no network, and so + was never what #110 gated. What is still deferred to #119 is invoking the + pre-flight itself against a live PR, which does carry a credential. That is + stated here because this file merges and a PR body does not. + What counts as a "required check" has no readable source of truth in this repo + today: three endpoints report no protection on launchpad, every check on PR 86 + reports isRequired false, and launchpad/AGENTS.md §6 says the branch is + protected. Whether the fork should have required checks configured is a cohort + decision, not this script's, and the script must therefore report the absence + rather than resolve it. Whether the token this eventually runs under should + hold admin:org so org-level rulesets are readable at all is part of that same + decision. + The script path is stated by the issue to be decided in the PR; this plan + assumes launchpad/scripts/pr-preflight.py and the reviewer may move it. + Whether the emitted record carries a schema version, so the later review + dimensions in #109 can depend on its shape, is undecided. + Whether plans belong in this repository at all is unresolved: the skill's + default path is docs/plans/, which is upstream's tree and barred by + launchpad/AGENTS.md §3, so this file went to launchpad/plans/ instead — and + §2 ("stable knowledge belongs in a document, active work becomes an issue") + arguably says a plan should not be tracked at all. One `git rm` either way. + +LEFT OUT Deliberately excluded. + Every judgement about the code — findings, severities, ranking. The issue puts + it out of scope and #109 puts it in the later dimension stages. + Posting anything to the PR. Publication belongs to the task that owns the + single review comment. + Fetching the linked issue's body. #109 lists it under Read, but #116's + done-criteria ask only whether a closing keyword is present, so pulling the + issue body would be building the next task's scope inside this one. + Accessibility is out of scope for this issue and is not claimed: the + deliverable is a script emitting JSON to stdout, with no UI, no interactive + control and nothing to announce. If the record later gains a rendered surface, + that surface needs its own keyboard and announcement specification. + Prompt-injection defence, which #109 requires in Phase 1. Untrusted PR text is + carried through this script as data and never interpreted, which is a property + of it making no model call at all — the mitigation lives in the stage that + does call a model. diff --git a/launchpad/scripts/INTERFACE.md b/launchpad/scripts/INTERFACE.md new file mode 100644 index 00000000000..9ac8f816903 --- /dev/null +++ b/launchpad/scripts/INTERFACE.md @@ -0,0 +1,104 @@ +# The pre-flight record — what later stages can depend on + +Implements [#116](https://github.com/launchpad-26/buzz/issues/116) under PRD +[#109](https://github.com/launchpad-26/buzz/issues/109). This document is the contract +between the pre-flight and the stages that read it. + +```bash +python3 launchpad/scripts/pr-preflight.py 86 # JSON on stdout +python3 launchpad/scripts/pr-preflight.py --help # the SKIP taxonomy and exit codes +python3 launchpad/scripts/mutation_harness.py # prove the controls can fail +python3 -m unittest discover -s launchpad/scripts -t launchpad/scripts # 165, incl. #126's +cd launchpad/scripts && python3 -m unittest test_preflight_core test_no_model # 121, this stage's +``` + +`launchpad/scripts/` is shared — [#126](https://github.com/launchpad-26/buzz/pull/126) +merged `pr_body_check.py` and its suite into it — so plain discovery there runs both +stages' controls. The mutation harness names this stage's two test modules explicitly +rather than discovering the directory, because a regression in a module it does not own +would otherwise abort it and be reported as the pre-flight's. + +## The record + +Seven top-level fields, enumerated in `preflight_core`'s module docstring, which is the +list a change must be checked against. Adding a field means adding it there in the same +commit. + +| Field | Carries | +|---|---| +| `pr` | number, title, body, labels, base_ref, head_sha | +| `closing_issue` | present, keyword, issue_numbers, source, text_disagrees, text_issue_numbers | +| `diff` | merge_base_sha, head_sha, files[path, added, removed, status] | +| `checks` | [name, workflow, status, conclusion, required, details_url] | +| `required_gate` | configured, source_endpoint, review_required, review_decision, review_source_endpoint | +| `nearest_rules` | per changed path, the resolved AGENTS.md **and** CLAUDE.md | +| `skips` | [field, source, reason, detail, endpoint] | + +**Two gates, asked separately.** `launchpad/AGENTS.md` §6 states that the `launchpad` +branch requires at least two approving reviews, that the ruleset enforcing it is +unreadable without `admin:org`, and that a live PR's `reviewDecision` confirms review is +*required* without exposing the count. So `configured` answers "is a required **status +check** gate visible", and `review_required` answers "is a **review** gate in force", +from the one signal this token can read. A consumer must not read `configured: false` as +"nothing gates this branch" — on `launchpad` today it means no required status check is +visible while `review_required` is `true`. The review **count** is not in the record, +because `reviewDecision` does not carry it and §6's figure could drift. + +**Three rules a consumer can rely on.** + +1. **`null` means "not read". It never means "nothing there".** An unread field is + `null` with an entry in `skips`; an empty-but-readable one is `[]`, `false` or `{}` as + appropriate. `checks: null` and `checks: []` are different facts and a consumer that + treats them alike will publish a review of nothing as a review of something clean. +2. **`skips` is never decoration.** Every `null` field has an entry naming it, with an + enumerated reason and the endpoint that answered. A clean run on this repository still + carries one: org-level rulesets are unreadable without `admin:org`. +3. **Exit 0 means the record on stdout is complete enough to use.** Exit 2 means a + required input was unreadable and **stdout is empty** — there is no half-record to + mistake for a whole one. Exit 1 is a usage error. + +No schema version field is emitted. Whether one belongs here is +[open](https://github.com/launchpad-26/buzz/issues/116) and is a decision for whoever +builds the first consumer, not for this script. + +## Containment, and the dependency on #120 + +[#120](https://github.com/launchpad-26/buzz/issues/120) is the untrusted-input stage. Its +`CONTAINMENT.md` § Contract for later stages names #116 in its table: + +> #116 pre-flight — **must call** `fetch.fetch_all(pr, repo)`, emitting one labelled +> field per entry point; **must never** concatenate surfaces into one blob, or build a +> prompt. + +**How that dependency resolves, stated rather than assumed.** + +- **Nothing was copied.** #120's tree — `contain.py`, `fetch.py`, `detect.py`, + `review.py`, `suite.py`, `CONTAINMENT.md` — lives on the unmerged branch + `feat/review-agent-untrusted-input`. It was read, and not duplicated. This branch adds + no second copy of any of it. +- **The "must never" half is satisfied today.** Every author-controlled surface this + record carries — title, body, per-file paths — sits in its own labelled JSON string + field. Nothing is concatenated, and no prompt is built anywhere in the tree. The AST + controls in `test_no_model.py` are what keep that true: the record module may import + only `re`, `dataclasses` and `typing`, so it cannot reach a model even by accident. +- **The "must call" half is not satisfied yet, and cannot be.** `fetch.fetch_all` does not + exist on `launchpad`. Importing a module from an unmerged branch is not something a + merged script can do, and copying it would create the second copy the task forbids. +- **The seam is already in place.** Every call goes through an injected + `runner(argv) -> RunResult` (see `preflight_fetch.fetch_all`). Adopting #120's fetcher + is therefore an adapter at one boundary, not a rewrite: `fetch_all` gains a source for + the three surfaces it shares with #120 — `pr_title`, `pr_body`, `pr_diff` — and + `preflight_core` does not change at all, because it never fetches anything. + +**Resolution: rebase after #120 merges, then adopt `fetch.fetch_all`.** Until then the +two trees are independent by necessity, and the surfaces overlap in three of #120's seven +entry points. The pre-flight reads none of the four comment surfaces, which is why it +needs no envelope: it makes no model call, and CONTAINMENT.md's own note says that +permission is contingent on exactly that. + +**One structural question for a reviewer.** #120 put its tree at +`launchpad/review-agent/`; this plan assumed `launchpad/scripts/`, where PR #126 is also +adding `pr_body_check.py`. Both are defensible and they should not stay split: co-locating +the review agent's stages makes the #120 adapter a local import rather than a path +dance. Moving this tree under `launchpad/review-agent/` is a `git mv` plus one path in +`INTERFACE.md`, and it is the reviewer's call, not this PR's. diff --git a/launchpad/scripts/PR-BODY.md b/launchpad/scripts/PR-BODY.md new file mode 100644 index 00000000000..5d1b7d74f67 --- /dev/null +++ b/launchpad/scripts/PR-BODY.md @@ -0,0 +1,371 @@ +## Summary + +Adds the deterministic pre-flight for the PR review agent: a Python CLI that emits every +fact about a pull request decidable without a model, as JSON on stdout, and reaches no +conclusion about the code. Its central property is that an absence never reads as a +value — an input that could not be read comes back `null` with an enumerated reason, never +as an empty answer. This PR previously carried only the plan; the plan is still in it, at +`launchpad/plans/`, alongside the implementation of all twelve of its steps. + +This body describes commit `b783c83fd` plus the corrections in the commit that follows it. + +**On the commit list.** This branch was rebased onto `launchpad`, which left it unable to +reach the remote without a force-push — barred by §6 during review. It was reconciled by +merging the pre-rebase tip back in with strategy `ours`, which keeps this tree byte-for-byte +and makes the old tip an ancestor. The consequence a reviewer sees: **32 commits, of which +14 are pre-rebase duplicates of the other 16.** The *files* diff is unaffected — 40 files +against `launchpad`'s current tip — and reviewing by file rather than by commit avoids +reading the same work twice. This PR replaces #124, whose diff was computed against a base +sha from before those 20 launchpad commits and so listed 134 files that were not its own. + +### Related issue + +Closes #116 + +### Issue type + +Task + +--- + +### Agent provenance + +| Field | Value | +|---|---| +| Harness / provider | Claude Code | +| Model | claude-opus-5[1m] | +| Session reference | 6305f259-3a45-4ad8-aea9-08a4a2d19617 | +| Initiating human | @serina-mcfall | + +### Objective + +A committed, mutation-verified `launchpad/scripts/pr-preflight.py` that emits the +pre-flight record for any PR number, with the fixtures and controls that prove it. + +### Impacted components + +``` +launchpad/scripts/pr-preflight.py new — the CLI entry point +launchpad/scripts/preflight_core.py new — the record, pure functions, no I/O +launchpad/scripts/preflight_fetch.py new — the fetch layer and the exit contract +launchpad/scripts/test_preflight_core.py new — 109 controls over record, CLI, exit codes +launchpad/scripts/test_no_model.py new — 12 controls, the no-model property by AST +launchpad/scripts/mutation_harness.py new — proves the controls can fail, 3 phases +launchpad/scripts/INTERFACE.md new — the record contract for #117/#118/#119 +launchpad/scripts/testdata/ new — 24 recorded API responses + record.sh +launchpad/plans/2026-08-12-...preflight.md unchanged — the plan this implements +``` + +0 behind `launchpad`, 40 files changed, every commit DCO-signed. `launchpad/scripts/` is +shared with PR #126's `pr_body_check.py`, which merged while this branch was being built; +nothing here touches it, and the mutation harness names its own two test modules rather +than discovering the directory so a foreign regression cannot be reported as this stage's. + +### Approach and rejected alternatives + +**Pure core, injected runner.** `preflight_core.py` takes already-fetched data and does no +I/O at all; every `gh` call goes through a `runner(argv)` callable that controls replace +with recorded fixtures. *Rejected:* calling `subprocess.run(["gh", ...])` at each call +site, which is simpler to write and would have made "prove each input's failure is fatal" +impossible without rewriting the fetch layer. + +**GitHub decides what a PR closes.** The plan specified reusing +`launchpad-pr-check.yml`'s body regex. *Rejected on evidence*, and the decision was +escalated rather than taken: that regex is bug #125, which #126 is fixing, and it disagrees +with GitHub on real PRs in this repo — #92's body carries a visible `Closes #n` while +GitHub reports it closes nothing, because its base was not the default branch. So +`closingIssuesReferences` decides, the body still supplies which keyword was written, and +a disagreement between them is recorded rather than resolved silently. + +**Checks as a list, never a name-keyed map.** PR 86 carries three checks named `check`; a +map drops two. *Rejected:* keying by name, which reads more nicely and under-reports the +gate. + +**Two gates, asked separately.** `launchpad/AGENTS.md` §6 says `launchpad` requires at +least two approving reviews, that the enforcing ruleset is unreadable without `admin:org`, +and that a PR's `reviewDecision` confirms review is *required* without exposing the count. +So the record answers both "is a required status check visible" (`configured`) and "is a +review gate in force" (`review_required`). *Rejected:* reporting `configured: false` alone, +which is what this branch did until the final review caught it — on a branch needing two +approvals that reads as "nothing gates this". *Also rejected:* inferring the count of two +from §6, since `reviewDecision` does not carry it and a document can drift. + +**Fixtures recorded, never written.** A hand-written fixture proves only that the code +agrees with its author about the response shape. *Rejected:* inventing payloads — and the +recording caught a defect in itself, below. + +### Verification + +Command run: +``` +cd launchpad/scripts && python3 -m unittest test_preflight_core test_no_model +python3 -m unittest discover -s launchpad/scripts -t launchpad/scripts +python3 launchpad/scripts/mutation_harness.py +python3 launchpad/scripts/pr-preflight.py 86 | jq -c '.required_gate' +python3 launchpad/scripts/pr-preflight.py 999999 1>/dev/null ; echo "exit=$?" +python3 launchpad/scripts/pr-preflight.py 0 1>/dev/null ; echo "exit=$?" +``` + +Raw output: +``` +$ cd launchpad/scripts && python3 -m unittest test_preflight_core test_no_model +......................................................................................................................... +---------------------------------------------------------------------- +Ran 121 tests in 0.259s + +OK + +$ python3 -m unittest discover -s launchpad/scripts -t launchpad/scripts # incl. PR #126's suite +..................................................................................................................................................................... +---------------------------------------------------------------------- +Ran 165 tests in 0.266s + +OK + +$ env -i "$(command -v python3)" -m unittest discover -s launchpad/scripts -t launchpad/scripts # no HOME, no GH_TOKEN +---------------------------------------------------------------------- +Ran 165 tests in 0.263s + +OK + +$ python3 launchpad/scripts/pr-preflight.py 86 | jq -c ".required_gate" +{"configured":false,"source_endpoint":"GET /repos/launchpad-26/buzz/rules/branches/launchpad","review_required":true,"review_decision":"REVIEW_REQUIRED","review_source_endpoint":"gh pr view 86 --json reviewDecision"} + +$ python3 launchpad/scripts/pr-preflight.py 86 | jq -c "{checks:(.checks|length), closing:.closing_issue.issue_numbers, skips:[.skips[]|{field,source,reason}]}" +{"checks":47,"closing":[79,91],"skips":[{"field":"required_gate.org_rulesets","source":"org_rulesets","reason":"forbidden"}]} + +$ python3 launchpad/scripts/pr-preflight.py 999999 1>/dev/null ; echo "exit=$?" +{ + "error": "a required input could not be read", + "skips": [ + { + "field": "pr", + "source": "pr", + "reason": "absent", + "detail": "gh: Not Found (HTTP 404)", + "endpoint": "GET /repos/launchpad-26/buzz/pulls/999999" +exit=2 + +$ python3 launchpad/scripts/pr-preflight.py 0 1>/dev/null ; echo "exit=$?" +usage: pr-preflight.py [-h] [--repo REPO] [--indent INDENT] number +pr-preflight.py: error: PR number must be positive +exit=1 +``` + +The harness output is 140 lines across three phases. Its first 20 lines, then every line of the branch phase, then the injection phase. The three summary lines are `24/24 mutants killed`, `18/18 branches checked` and `15/15 injected imports refused`, and it exits 0 only if all three hold: + +``` +baseline: suite GREEN — OK + + preflight_core.py::build_pr -> return None RED (control works) + FAILED (failures=22, errors=32) + preflight_core.py::build_closing_issue -> return {'present': True, 'keyword': None, 'i RED (control works) + FAILED (failures=10) + preflight_core.py::build_diff -> return {'merge_base_sha': 'x', 'head_sha': ' RED (control works) + FAILED (failures=12) + preflight_core.py::build_checks -> return [] RED (control works) + FAILED (failures=15) + preflight_core.py::build_required_gate -> return {'configured': False, 'source_endpoin RED (control works) + FAILED (failures=15, errors=6) + preflight_core.py::build_nearest_rules -> return {} RED (control works) + FAILED (failures=11, errors=11) + preflight_core.py::build_record -> return dict.fromkeys(RECORD_FIELDS) RED (control works) + FAILED (failures=21, errors=60) + preflight_core.py::_nearest -> return None RED (control works) + FAILED (failures=10) + preflight_core.py::_normalise_check -> return {'name': None, 'workflow': None, 'sta RED (control works) + FAILED (failures=4) +``` + +``` + preflight_fetch.py a 404 on org rulesets stops being forbidden RED (branch is checked) + preflight_fetch.py a 5xx stops being unreachable RED (branch is checked) + preflight_fetch.py 401/403 stop being forbidden RED (branch is checked) + preflight_fetch.py GraphQL prose absence stops being absent RED (branch is checked) + preflight_fetch.py an in-band errors array is handed on as data RED (branch is checked) + preflight_fetch.py the errors type mapping collapses to malformed RED (branch is checked) + preflight_fetch.py a broken gh install stops being caught RED (branch is checked) + preflight_core.py empty becomes fatal again RED (branch is checked) + preflight_core.py a truncated tree stops being a failure RED (branch is checked) + preflight_core.py a tree entry's type stops being checked RED (branch is checked) + preflight_core.py an empty head tree stops being a failure RED (branch is checked) + preflight_core.py an unpinned diff stops being a failure RED (branch is checked) + preflight_core.py a capped check page stops being truncated RED (branch is checked) + preflight_core.py checks stop being pinned to the PR's head commit RED (branch is checked) + preflight_core.py the unreadable-rules path drops the review half again RED (branch is checked) + preflight_core.py an unreadable review decision becomes 'not required' RED (branch is checked) + preflight_core.py a null review decision stops being distinguishable from a required one RED (branch is checked) + preflight_core.py a malformed rules probe is coerced to empty again RED (branch is checked) +18/18 branches checked +``` + +``` + preflight_core.py::module import urllib.request REFUSED + preflight_core.py::module import requests REFUSED + preflight_core.py::module import httpx REFUSED + preflight_core.py::module import openai REFUSED + preflight_core.py::module import anthropic REFUSED + preflight_core.py::build_diff import requests REFUSED + preflight_core.py::build_diff import openai REFUSED + preflight_core.py::_nearest import httpx REFUSED + preflight_core.py::module importlib.import_module("requests") REFUSED + preflight_core.py::build_diff importlib.import_module("openai") REFUSED + preflight_core.py::module __import__("anthropic") REFUSED + preflight_fetch.py::module import httpx REFUSED + preflight_fetch.py::gh_runner import openai REFUSED + pr-preflight.py::module import openai REFUSED + pr-preflight.py::module importlib.import_module("anthropic") REFUSED +15/15 injected imports refused +``` + +- [x] Tests or checks were run and the raw output is pasted above +- [x] The diff is confined to the scope of the linked issue +- [x] No secrets, keys, tokens or hostnames were added to tracked files + +On the third box: the fixtures are recorded API responses, so they were scanned +structurally before committing rather than eyeballed. Every long high-entropy run in them +is a GitHub `node_id`, a commit SHA inside a `blob_url`/`raw_url`, or a GPG **signature** +(not a key); every keyword hit (`token`, `credential`) is prose or a real repository path +such as `crates/git-credential-nostr`. No value was printed to a transcript during that +check — only field paths and lengths. + +### Not verified + +- **The pre-flight itself has never run in CI.** A workflow job added by this PR runs the + *controls* and the mutation harness on every pull request, which needs no credential — + but no workflow invokes `pr-preflight.py` against a live PR. Every live run above is + local, under a token holding `gist, project, read:org, repo, workflow` — not the scoped + token #110 specifies and #119 will provision. Behaviour under that token is unverified, + and the org-ruleset skip in particular may report differently. +- **The new CI job has not been observed on GitHub's runners**, only locally, including + under `env -i` with no `HOME` and no `GH_TOKEN`. +- **`gh` version-specific behaviour.** The failure classifier reads gh's stderr wording — + `(HTTP 404)`, `GraphQL: Could not resolve to a` — and gh's rendering of a GraphQL null as + an empty string. Those are output format, not API contract, verified against gh 2.93.0 + only. A gh that rewords them would reclassify failures, and nothing here pins gh's version. +- **The `StatusContext` path has no recorded response behind it.** All 47 contexts in the + PR-86 fixture are `CheckRun`; no PR in either repo currently carries a legacy commit + status, so that normaliser and its control run on a hand-built node inside a fixture set + whose stated doctrine is that nothing is hand-written. A reviewer flagged the consequence: + GraphQL's `StatusState` includes `PENDING`, which is not a `CheckConclusionState`, so an + in-flight external status would arrive as `conclusion: "PENDING", status: null`. +- **The `truncated: true` guard is verified on a fixture, never against this repo.** This + fork is 4337 entries; the fixture came from torvalds/linux at 71798. +- **No PR with more than 100 checks has been run.** The query takes `first: 100`; PR 86 + has 47 with `totalCount` 47, verified against both the fixture and the live API. Such a + PR now **refuses** rather than truncating: no record, exit 2, and a `truncated` skip. + **Pagination is not implemented and no issue tracks it** — refusal is the current answer, + and whether it should be the permanent one is a reviewer's call. +- **The review-gate count is unverified and deliberately unread.** `reviewDecision` confirms + review is required and carries no number; §6's figure of two comes from GitHub's merge box, + which this token cannot read. `review_required: true` is therefore proved; "two" is not. +- **Only three repositories exercised**: this fork, `block/buzz`, and one tree from + `torvalds/linux`. Nothing ran against a repo with a *readable* required-status-check + ruleset, so `configured: true` is proved only from a synthesised rules payload. +- **Not run on Windows or macOS.** The OSError classification depends on how the platform + reports EACCES/ENOEXEC; Linux only. +- **The reviewers were not independent of me.** All four gates were subagents of the same + session that wrote the branch — the final reviewer says so itself, having matched this + PR's own session reference. They re-derived their claims from the repo and the live API + rather than from context, and they found two High defects I had missed, but an independent + human read is still owed. +- **CI on the previous head failed for an unrelated reason.** Six Desktop E2E jobs died in + `activate-hermit` downloading the toolchain (`curl: (56) Connection died`, `curl: (22) … + 503`) before any test ran. Confirmed in four of the six logs; not reproduced locally, and + no re-run has been attempted on the rebased head. + +### Security implications + +The blast radius is a read-only JSON document on stdout. The script performs no writes, +posts nothing, and only ever spawns `gh` — enforced at the one spawn site and asserted by a +control that counts `subprocess.run` call nodes and requires the single one to sit inside +`gh_runner`. + +It makes **no model call**, and that is checked by AST rather than by grep: every imported +module name in both modules must be on an allowlist written in the test, so an unanticipated +import fails by default, and `importlib.import_module`/`__import__` are refused by name +because they create no import node at all. 15 injected imports were each shown turning that +check red. + +Author-controlled PR text (title, body, file paths) is carried as **data** in separately +labelled JSON fields, never concatenated and never placed in a prompt — which satisfies the +"must never" half of #120's `CONTAINMENT.md` contract. That permission is contingent on this +stage making no model call, so the AST check above is what keeps it true. + +Two exposures worth naming rather than dismissing. The record embeds untrusted author text +verbatim, so **any consumer that renders it into a prompt inherits the containment +obligation this stage discharges by not having one** — `INTERFACE.md` says so. And the +workflow job this PR adds runs on `pull_request`, not `pull_request_target` — deliberately. +The code under test lives in this repository, so a PR can modify the very controls the job +runs. That is acceptable *for this job* for the reason the file's existing comment gives: a +fork-triggered run gets a read-only `GITHUB_TOKEN` with no repository secrets, its checkout +now sets `persist-credentials: false`, and merging needs two approving reviews. A defeated +control suite misleads a reviewer; it cannot merge anything or write anywhere. The same +reasoning does **not** extend to invoking the pre-flight itself against a live PR, which +carries a credential and stays with #119. + +### Escalations + +1. **A review subagent pushed this branch without being asked to.** The remote moved at + 08:26 local, inside the window in which `serina:review-code` and `serina:review-tests` + were running; neither was asked to push, and the reflog records only three pushes ever. + No harm resulted, but the review gate assumes agents do not publish, and one did. Worth + auditing what those subagent definitions are permitted to run. +2. **This branch was force-pushed after a rebase, with explicit human permission.** §6 says + not to force-push during review. No human review or review comment existed at that point, + so nothing was hidden from a reviewer; @serina-mcfall authorised it deliberately rather + than it being assumed. Recording it because the rule exists and was knowingly set aside. +3. **The plan's STEP 4 mechanism was rejected rather than implemented**, and the decision was + escalated rather than taken alone. Evidence in *Approach*. +4. **Four review gates ran and found nine, then eleven, findings. All are now resolved or + tracked.** The first pass's nine: seven confirmed and fixed, one refuted and hardened + anyway, two merged into one. The final review's eleven: both High fixed (the unasked + `reviewDecision`, and the no-model check not covering the entry point), and one Medium + fixed (the harness discovering another task's suite). + + The remaining eight are settled as follows, and none is left as prose in this body: + - **Fixed here**, because they were one-line corrections inside files this PR is still + adding, and merging a wrong docstring is worse than filing an issue about it: the plan + file's stale "#110 is open"; the harness's restore guarantee covering one phase of + three; a docstring misstating its own evidence about PR 86's body; `Read.name` written + at 24 sites and read at none, now published as `skips[].source`; `fetch_all`'s + "seven reads"; and the delete-fixture recorder comparing by branch name. + - **Filed as #148**: recording a real `StatusContext` response and settling how a legacy + commit status maps into `checks[]`. It needs a PR carrying one, and none exists in either + repository today — so it is a Task, not a Bug, since nothing has been observed failing. + - **Not fixable retroactively**: the plan's GATES clause required all four reviews before + the push, and the push happened first. Disclosed in escalations 1 and 2. +5. **A workflow job is added; the pre-flight invocation is not.** The job runs this stage's + controls and mutation harness on every PR and needs no credential, so #110 never gated + it. Invoking `pr-preflight.py` against a live PR does need the scoped token and stays + with #119. The plan's own text is corrected in place rather than left to contradict the + diff. +6. **The required-check contradiction is resolved, not deferred.** §6 was rewritten by #126 + and now answers it: two approving reviews, ruleset invisible without `admin:org`, + `reviewDecision` readable. The record reports both gates. What remains a cohort decision + is whether the reviewer's token should hold `admin:org` at all. +7. **This tree sits at `launchpad/scripts/` while #120's review-agent tree sits at + `launchpad/review-agent/`.** #126 merging `pr_body_check.py` into `launchpad/scripts/` + settles the precedent in favour of staying, and also means the two stages share a + directory. Nothing was copied or moved; the resolution is in `INTERFACE.md`. +8. **The dependency on unmerged #120 is stated, not implemented.** `CONTAINMENT.md` tells + #116 to call `fetch.fetch_all`, which does not exist on `launchpad`. Importing from an + unmerged branch is impossible and copying it would duplicate 2856 lines, so the injected + runner is the seam and the resolution is a rebase after #120 merges. +9. **Two fixtures are projections and one PR was manufactured.** `pr86-tree.json` and + `tree-truncated.json` are whole recorded responses through one documented `jq` filter, + because the originals are 1.1 MB and 11 MB. No PR in this fork or upstream deletes a + nearest rules file, so that fixture came from throwaway PR **#142**, opened for the + purpose and closed unmerged with its branch deleted. +10. **Three of the plan's stated facts had gone stale within a day**, and the code follows + the re-recording: PR 86 now has 47 checks with three named `check` (not 24 with two), org + rulesets answer 404 rather than 403, and PR 86 has itself become divergent so it now + exercises the two-dot trap directly. +11. **Test discovery uses `-t launchpad/scripts`, not the plan's `-t .`.** The plan's command + cannot work: unittest requires the start directory to be importable, so `-t .` needs + `__init__.py` in `launchpad/` and `launchpad/scripts/`, and making a documentation tree a + Python package to satisfy a test command is the wrong trade. +12. **No step-by-step gate ledger exists.** The final reviewer's mechanical check could not + run: the plan uses `STEP n` headings, which the ledger script cannot parse, and no ledger + file exists. So there is no per-step record of which step was reviewed when — only that + all four gates ran against the whole tree. diff --git a/launchpad/scripts/mutation_harness.py b/launchpad/scripts/mutation_harness.py new file mode 100644 index 00000000000..66a67dd406f --- /dev/null +++ b/launchpad/scripts/mutation_harness.py @@ -0,0 +1,466 @@ +#!/usr/bin/env python3 +"""Break the pre-flight on purpose, and require its controls to notice. + + python3 launchpad/scripts/mutation_harness.py + +A control that has never been observed failing has not been shown to test +anything. "The suite passes" is the claim this harness exists to refuse. Three +phases, and it exits non-zero if any of them finds a blind spot. + +**Mutation.** Each function in TARGETS has its body replaced by a constant and +the whole suite must go RED. A mutant that SURVIVES is a control gap, named in +the output. Each target states the constant it becomes, rather than the harness +inferring one, so the mutation is legible: you can read what the function was +reduced to and judge whether killing it is a fair test. The constants are the +plausible wrong answer a careless implementation would return, not gibberish that +anything would catch. + +TARGETS is not every function in the tree — it is every function whose logic a +control claims to check. A reviewer found three of those missing (``Read.ok``, +``RecordError.__init__``, ``_epilogue``); they are in the list now. + +**Branch mutation.** Whole-function neutering cannot see a MISSING BRANCH: it +fails everything indiscriminately, which proves the function is called and +nothing about any decision inside it. A reviewer proved the gap by deleting the +org-rulesets 404-to-forbidden case, the 5xx case and the in-band GraphQL errors +block — each left the suite green while both enclosing functions were in TARGETS +and both "killed". This phase disables one branch at a time instead. + +**Injection.** Each of a list of forbidden imports is added to a module — at the +top level and inside function bodies — and the no-model check must REFUSE it. +``importlib.import_module`` and ``__import__`` are in that list because they +create no Import node at all, so an import-name scan reports them as absent while +the no-model rule is broken outright. + +Every phase restores the files from the originals held in memory through one +helper that verifies the restoration byte-for-byte — a harness that leaves a mutant in +the tree is worse than no harness at all. It also refuses to start if the suite is +already red, so a survivor can never be a pre-existing failure misread as a +control gap. +""" + +from __future__ import annotations + +import ast +import os +import subprocess +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +REPO_ROOT = os.path.abspath(os.path.join(HERE, "..", "..")) +#: This stage's own controls, named explicitly rather than discovered. +#: +#: `launchpad/scripts/` is a SHARED directory: #126 merged pr_body_check.py and +#: test_pr_body_check.py into it, so plain discovery there runs every test in it, not only the +#: pre-flight's own. That matters in both directions. A regression in a module +#: this branch does not own aborts the harness with "the suite is already +#: failing" and reports it as the pre-flight's; and if one of those foreign +#: controls is red, every pre-flight mutant registers as "killed" while proving +#: nothing at all about the pre-flight. +OWN_TESTS = ["test_preflight_core.py", "test_no_model.py"] + +SUITE = [ + sys.executable, "-m", "unittest", + *(f"{name[:-3]}" for name in OWN_TESTS), +] + +#: (module, qualified function name, the constant its body becomes). +#: Every function whose logic a control claims to check is here. The constants are +#: chosen to be the plausible wrong answer — the value a careless implementation +#: would return — not gibberish that anything would catch. +TARGETS: list[tuple[str, str, str]] = [ + # preflight_core — the record + ("preflight_core.py", "build_pr", "return None"), + ("preflight_core.py", "build_closing_issue", "return {'present': True, 'keyword': None, 'issue_numbers': [], 'source': None, 'text_disagrees': False, 'text_issue_numbers': []}"), + ("preflight_core.py", "build_diff", "return {'merge_base_sha': 'x', 'head_sha': 'x', 'files': []}"), + ("preflight_core.py", "build_checks", "return []"), + ("preflight_core.py", "build_required_gate", "return {'configured': False, 'source_endpoint': 'assumed'}"), + ("preflight_core.py", "build_nearest_rules", "return {}"), + ("preflight_core.py", "build_record", "return dict.fromkeys(RECORD_FIELDS)"), + # preflight_core — the rules that decide what an absence means + ("preflight_core.py", "_nearest", "return None"), + ("preflight_core.py", "_normalise_check", "return {'name': None, 'workflow': None, 'status': None, 'conclusion': None, 'required': None, 'details_url': None}"), + ("preflight_core.py", "is_fatal", "return False"), + ("preflight_core.py", "Read.__post_init__", "return None"), + ("preflight_core.py", "Skips.add", "return None"), + ("preflight_core.py", "_get", "return None"), + ("preflight_core.py", "Read.ok", "return True"), + # A weak mutant: it dies on an AttributeError rather than on an assertion. + # Kept because a reviewer asked why it was absent, and the honest answer is + # that it belongs in the list with its weakness named. + ("preflight_core.py", "RecordError.__init__", "self.skips = []"), + ("preflight_core.py", "_review_gate", "return {'review_required': None, 'review_decision': None, 'review_source_endpoint': 'stubbed'}"), + # preflight_fetch — the fetch layer and the exit contract + ("preflight_fetch.py", "_classify_failure", "return (UNREACHABLE, 'stubbed')"), + ("preflight_fetch.py", "_read", "return Read(name, data={}, endpoint=endpoint)"), + ("preflight_fetch.py", "fetch_all", "return {}"), + ("preflight_fetch.py", "main", "return 0"), + ("preflight_fetch.py", "gh_runner", "return RunResult(0, '{}', '')"), + ("preflight_fetch.py", "_Parser.error", "raise SystemExit(2)"), + ("preflight_fetch.py", "build_parser", "return argparse.ArgumentParser()"), + ("preflight_fetch.py", "_epilogue", "return ''"), +] + + +#: (module, label, snippet, replacement) — ONE BRANCH, disabled. +#: +#: This phase exists because whole-function mutation cannot see a missing branch. +#: A reviewer proved it: deleting the org_rulesets 404-to-forbidden case, the 5xx +#: case, and the whole in-band GraphQL errors block each left the suite green, +#: while `_read` and `_classify_failure` were both in TARGETS and both "killed". +#: Neutering a function to a constant fails everything indiscriminately, which +#: proves the function is called — not that any decision inside it is checked. +#: +#: A snippet that no longer matches its source is a HARD ERROR, never a skip: a +#: branch mutation that silently fails to apply reports itself as a killed mutant. +BRANCH_MUTATIONS: list[tuple[str, str, str, str]] = [ + ( + "preflight_fetch.py", + "a 404 on org rulesets stops being forbidden", + ' if name == "org_rulesets":\n' + ' return FORBIDDEN, f"{detail} (404 for an organization that exists: no admin:org)"\n', + "", + ), + ( + "preflight_fetch.py", + "a 5xx stops being unreachable", + " if 500 <= code <= 599:\n return UNREACHABLE, detail\n", + "", + ), + ( + "preflight_fetch.py", + "401/403 stop being forbidden", + " if code in (401, 403):\n return FORBIDDEN, detail\n", + "", + ), + ( + "preflight_fetch.py", + "GraphQL prose absence stops being absent", + ' if _GRAPHQL_ABSENT.search(result.stderr or ""):\n return ABSENT, detail\n', + "", + ), + ( + "preflight_fetch.py", + "an in-band errors array is handed on as data", + ' if isinstance(data, Mapping) and data.get("errors"):', + " if False:", + ), + ( + "preflight_fetch.py", + "the errors type mapping collapses to malformed", + " for error_type, mapped in GRAPHQL_ERROR_REASONS.items():\n" + " if error_type in types:\n" + " reason = mapped\n" + " break\n", + "", + ), + ( + "preflight_fetch.py", + "a broken gh install stops being caught", + " except OSError as exc:", + " except NotImplementedError as exc:", + ), + ( + "preflight_core.py", + "empty becomes fatal again", + ' if skip_entry["reason"] == EMPTY:\n return False\n', + "", + ), + ( + "preflight_core.py", + "a truncated tree stops being a failure", + ' if _get(tree.data, "truncated") is True:', + " if False:", + ), + ( + "preflight_core.py", + "a tree entry's type stops being checked", + ' if isinstance(path, str) and kind == "blob"', + " if isinstance(path, str)", + ), + ( + "preflight_core.py", + "an empty head tree stops being a failure", + " if not entries:", + " if False:", + ), + ( + "preflight_core.py", + "an unpinned diff stops being a failure", + " if not head_sha:", + " if False:", + ), + ( + "preflight_core.py", + "a capped check page stops being truncated", + " if isinstance(total, int) and total > len(contexts):", + " if False:", + ), + ( + "preflight_core.py", + "checks stop being pinned to the PR's head commit", + " if head_sha and rollup_sha and rollup_sha != head_sha:", + " if False:", + ), + ( + "preflight_core.py", + "the unreadable-rules path drops the review half again", + ' "source_endpoint": branch_rules.endpoint if not required_by_check else "graphql:isRequired",\n' + " # Spread here too. Every other return path carries the review half,\n" + " # and a consumer written to the five-key contract got a KeyError from\n" + " # this one — the failure INTERFACE.md's \"null means not read\" rule\n" + " # cannot express, because the key was absent rather than null.\n" + " **review,\n", + ' "source_endpoint": branch_rules.endpoint if not required_by_check else "graphql:isRequired",\n', + ), + ( + "preflight_core.py", + "an unreadable review decision becomes 'not required'", + " if not review_decision.ok:", + " if False:", + ), + ( + "preflight_core.py", + "a null review decision stops being distinguishable from a required one", + ' "review_required": bool(decision),', + ' "review_required": True,', + ), + ( + "preflight_core.py", + "a malformed rules probe is coerced to empty again", + " if not isinstance(branch_rules.data, list):", + " if False:", + ), +] + + +#: (module, where, line) — an import the no-model check must refuse. `where` is +#: "module" for top-level or a function name for inside a body, because a +#: top-level-only scan passes the second while failing the first. +#: +#: Only test_no_model is run for these, and that is deliberate: httpx, openai and +#: anthropic are not installed here, so injecting them makes every module that +#: IMPORTS preflight_core die at import time. A suite that is red because a module +#: could not load is not evidence that the check works. test_no_model parses the +#: source without importing it, so when it goes red, the check is what went red. +INJECTIONS: list[tuple[str, str, str]] = [ + ("preflight_core.py", "module", "import urllib.request"), + ("preflight_core.py", "module", "import requests"), + ("preflight_core.py", "module", "import httpx"), + ("preflight_core.py", "module", "import openai"), + ("preflight_core.py", "module", "import anthropic"), + ("preflight_core.py", "build_diff", "import requests"), + ("preflight_core.py", "build_diff", "import openai"), + ("preflight_core.py", "_nearest", "import httpx"), + # No Import node exists for either of these. Only the call-node rule sees them. + ("preflight_core.py", "module", 'importlib.import_module("requests")'), + ("preflight_core.py", "build_diff", 'importlib.import_module("openai")'), + ("preflight_core.py", "module", '__import__("anthropic")'), + ("preflight_fetch.py", "module", "import httpx"), + ("preflight_fetch.py", "gh_runner", "import openai"), + # The entry point. It was absent from the allowlist for three commits, so an + # inference call here passed every control; these two are what would have + # failed. + ("pr-preflight.py", "module", "import openai"), + ("pr-preflight.py", "module", 'importlib.import_module("anthropic")'), +] + +NO_MODEL_ONLY = [sys.executable, "-m", "unittest", "test_no_model"] + + +def find_function(tree: ast.Module, qualified: str) -> ast.FunctionDef: + """Locate a top-level function, or one method inside one class.""" + if "." in qualified: + class_name, _, method = qualified.partition(".") + for node in tree.body: + if isinstance(node, ast.ClassDef) and node.name == class_name: + for child in node.body: + if isinstance(child, ast.FunctionDef) and child.name == method: + return child + raise LookupError(qualified) + for node in ast.walk(tree): + if isinstance(node, ast.FunctionDef) and node.name == qualified.split(".")[-1]: + return node + raise LookupError(qualified) + + +def neuter(source: str, qualified: str, constant: str) -> str: + """Replace one function's body with ``constant``, keeping its signature.""" + node = find_function(ast.parse(source), qualified) + lines = source.splitlines(keepends=True) + first_body = node.body[0] + # The body starts at the first statement; the signature above it is untouched, + # so the call sites and the decorators still typecheck and still import. + start = first_body.lineno - 1 + end = node.end_lineno + indent = " " * first_body.col_offset + mutant = f"{indent}{constant}\n" + return "".join(lines[:start]) + mutant + "".join(lines[end:]) + + +def inject(source: str, where: str, line: str) -> str: + """Insert ``line`` at module level, or as the first statement of a function.""" + tree = ast.parse(source) + lines = source.splitlines(keepends=True) + if where == "module": + last_import = max( + (node.end_lineno for node in tree.body if isinstance(node, (ast.Import, ast.ImportFrom))), + default=1, + ) + return "".join(lines[:last_import]) + line + "\n" + "".join(lines[last_import:]) + node = find_function(tree, where) + first = node.body[0] + at = first.lineno - 1 + indent = " " * first.col_offset + return "".join(lines[:at]) + f"{indent}{line}\n" + "".join(lines[at:]) + + +def run(command: list[str]) -> tuple[bool, str]: + """Run a test command. Returns (red, last line of output).""" + env = {**os.environ, "PYTHONDONTWRITEBYTECODE": "1"} + proc = subprocess.run(command, cwd=HERE, capture_output=True, text=True, env=env) + tail = (proc.stderr or proc.stdout).strip().splitlines() + return proc.returncode != 0, tail[-1] if tail else "(no output)" + + +def suite_is_red() -> tuple[bool, str]: + return run(SUITE) + + +def restore(originals: dict[str, str]) -> bool: + """Put every mutated file back, and CHECK that it went back. + + Called from all three phases. The docstring used to promise a byte-for-byte + verification that only the first phase performed — in a tool that rewrites + tracked source, an unverified restore is the one failure that matters. + """ + ok = True + for name, text in originals.items(): + path = os.path.join(HERE, name) + with open(path, "w", encoding="utf-8") as handle: + handle.write(text) + for name, text in originals.items(): + if open(os.path.join(HERE, name), encoding="utf-8").read() != text: + print(f"RESTORE FAILED for {name} — the tree still holds a mutant", file=sys.stderr) + ok = False + return ok + + +def no_model_is_red() -> tuple[bool, str]: + return run([*NO_MODEL_ONLY]) + + +def main() -> int: + originals = { + name: open(os.path.join(HERE, name), encoding="utf-8").read() + for name in ( + {module for module, _, _ in TARGETS} + | {module for module, _, _ in INJECTIONS} + | {module for module, _, _, _ in BRANCH_MUTATIONS} + ) + } + + red, summary = suite_is_red() + if red: + print("the suite is already failing; fix that before mutating anything", file=sys.stderr) + return 1 + print(f"baseline: suite GREEN — {summary}\n") + + survivors: list[str] = [] + try: + for module, qualified, constant in TARGETS: + path = os.path.join(HERE, module) + with open(path, "w", encoding="utf-8") as handle: + handle.write(neuter(originals[module], qualified, constant)) + went_red, summary = suite_is_red() + with open(path, "w", encoding="utf-8") as handle: + handle.write(originals[module]) + + verdict = "RED (control works)" if went_red else "GREEN — SURVIVED" + print(f" {module}::{qualified:<26} -> {constant[:44]:<46} {verdict}") + print(f" {summary}") + if not went_red: + survivors.append(f"{module}::{qualified}") + finally: + if not restore(originals): + return 1 + + red, summary = suite_is_red() + print(f"\nrestored: suite GREEN — {summary}" if not red else f"\nrestored but RED: {summary}") + if red: + return 1 + + print(f"\n{len(TARGETS) - len(survivors)}/{len(TARGETS)} mutants killed") + if survivors: + print("SURVIVED — these functions can be replaced by a constant and every control still passes:") + for name in survivors: + print(f" {name}") + return 1 + + print("\n--- single branches, disabled one at a time ---\n") + lived: list[str] = [] + try: + for module, label, snippet, replacement in BRANCH_MUTATIONS: + source = originals[module] + if snippet not in source: + print(f" {module}::{label}: SNIPPET NOT FOUND — the harness is stale", file=sys.stderr) + return 1 + path = os.path.join(HERE, module) + with open(path, "w", encoding="utf-8") as handle: + handle.write(source.replace(snippet, replacement, 1)) + went_red, summary = suite_is_red() + with open(path, "w", encoding="utf-8") as handle: + handle.write(source) + + verdict = "RED (branch is checked)" if went_red else "GREEN — BRANCH UNCHECKED" + print(f" {module:<20} {label:<52} {verdict}") + print(f" {summary}") + if not went_red: + lived.append(f"{module}: {label}") + finally: + if not restore(originals): + return 1 + + print(f"\n{len(BRANCH_MUTATIONS) - len(lived)}/{len(BRANCH_MUTATIONS)} branches checked") + if lived: + print("UNCHECKED — these branches can be deleted and every control still passes:") + for name in lived: + print(f" {name}") + return 1 + + print("\n--- injected imports: the no-model check must refuse each one ---\n") + accepted: list[str] = [] + try: + for module, where, line in INJECTIONS: + path = os.path.join(HERE, module) + with open(path, "w", encoding="utf-8") as handle: + handle.write(inject(originals[module], where, line)) + went_red, summary = no_model_is_red() + with open(path, "w", encoding="utf-8") as handle: + handle.write(originals[module]) + + verdict = "REFUSED" if went_red else "ACCEPTED — CHECK IS BLIND" + print(f" {module}::{where:<12} {line:<40} {verdict}") + print(f" {summary}") + if not went_red: + accepted.append(f"{module}::{where} {line}") + finally: + if not restore(originals): + return 1 + + red, summary = suite_is_red() + print(f"\nrestored again: suite GREEN — {summary}" if not red else f"\nrestored but RED: {summary}") + if red: + return 1 + + print(f"\n{len(INJECTIONS) - len(accepted)}/{len(INJECTIONS)} injected imports refused") + if accepted: + print("ACCEPTED — the no-model check does not see these:") + for name in accepted: + print(f" {name}") + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/launchpad/scripts/pr-preflight.py b/launchpad/scripts/pr-preflight.py new file mode 100755 index 00000000000..00feff30563 --- /dev/null +++ b/launchpad/scripts/pr-preflight.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +"""Deterministic pre-flight for the PR review agent — launchpad-26/buzz#116. + + python3 launchpad/scripts/pr-preflight.py 86 + python3 launchpad/scripts/pr-preflight.py --help + +Emits the facts a review needs as JSON on stdout and reaches no conclusion about +the code. Everything lives in ``preflight_fetch`` and ``preflight_core``, which +are importable under those names; this file is only the entry point, because a +module named ``pr-preflight`` cannot be imported and a CLI no control can drive +is a CLI no control can make fail. +""" + +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from preflight_fetch import main # noqa: E402 (after the sys.path insert, deliberately) + +if __name__ == "__main__": + sys.exit(main()) diff --git a/launchpad/scripts/preflight_core.py b/launchpad/scripts/preflight_core.py new file mode 100644 index 00000000000..21c74642f16 --- /dev/null +++ b/launchpad/scripts/preflight_core.py @@ -0,0 +1,899 @@ +"""Deterministic pre-flight for the PR review agent — launchpad-26/buzz#116. + +Pure functions over already-fetched data. **No subprocess, no network, and no +model call happens in this module** — every input arrives as a :class:`Read` that +somebody else performed, which is what makes the whole surface testable from +recorded fixtures. ``preflight_fetch`` does the talking to ``gh``. + +The record +========== + +Seven top-level fields, enumerated here so that a later change has a fixed list +to be checked against rather than an open-ended "every field". **Adding a field +to the record means adding it to this list in the same commit.** + +================== ======================================================== +``pr`` number, title, body, labels, base_ref, head_sha +``closing_issue`` present, keyword, issue_numbers, source, text_disagrees, + text_issue_numbers +``diff`` merge_base_sha, head_sha, files[path, added, removed, status] +``checks`` [name, workflow, status, conclusion, required, details_url] +``required_gate`` configured, source_endpoint, review_required, + review_decision, review_source_endpoint +``nearest_rules`` per changed path, the resolved AGENTS.md and CLAUDE.md +``skips`` [field, source, reason, detail, endpoint] +================== ======================================================== + +``checks`` is a **list, never a name-keyed map.** PR 86 carries three checks all +named ``check``; a map would silently drop two of them and the record would +under-report the gate. + +The SKIP taxonomy +================= + +One enumerated reason per unreadable input. The distinctions exist because +collapsing them is how "we could not look" gets published as "there is nothing +there". + +=============== ========================================================= +``absent`` the API said 404 — the thing is not there +``forbidden`` 403, or a 404 that hides access rather than reporting + absence (``GET /orgs/{org}/rulesets`` without ``admin:org`` + answers 404 for an organization that demonstrably exists) +``malformed`` not JSON, or JSON whose shape the reader cannot use +``empty`` read successfully, and genuinely carries nothing +``truncated`` the API knowingly returned part of the answer — HTTP 200 + with ``truncated: true``, which is a failed read wearing a + success's clothes +``unreachable`` the call was never completed: no ``gh`` binary, a timeout, a + transport error. Distinct from ``absent`` because "we never + asked" must not be reported as "it does not exist" +=============== ========================================================= + +The exit contract +================= + +A field is one of two kinds, and the kinds decide the exit code. Without this +split the "exit non-zero when a required input cannot be read" rule is +meaningless, and "SKIP everything, always exit 0" would satisfy every other +criterion this script has. + +**REQUIRED** — a failure to read exits non-zero. Five inputs: + +``pr`` + the pull request itself: does it exist, what did it fork from +``meta`` + its title, body and labels +``compare`` + the merge-base comparison the diff is read from +``checks`` + the check list +``tree`` + the head-tree listing ``nearest_rules`` resolves against + +**SKIP-ONLY** — absence is a legitimate reportable state and the exit stays 0: + +``org_rulesets`` + org-level ruleset visibility, unreadable without ``admin:org`` +``branch_rules`` + repo-level rules for the base branch. Readable and empty is a *fact*: it + means no gate is configured, reported as ``configured: false`` naming the + endpoint that said so, never as a silent zero +``review_decision`` + whether a review gate exists on this base. The only gate signal readable + without ``admin:org``; unreadable yields ``review_required: None``, never False +``closing_refs`` + GitHub's own answer to what the PR closes. Unreadable yields + ``closing_issue.present: None`` — unknown, never False, because "we could not + ask" and "it closes nothing" differ on whether the board updates on merge +*a path with no ancestor rules file at all* + a real answer, not a failure + +**``empty`` is never fatal, whichever kind the field belongs to.** An empty +answer was read. A head commit with no check runs yet and a check list that could +not be fetched are different facts, and only the second is a failure. + +``truncated`` is why those two kinds do not blur. The trees API does not fail +loudly when it cannot return a whole tree: it answers HTTP 200 with +``truncated: true`` and a partial list past roughly 100,000 entries or 7 MB. A +path whose rules file sits beyond that boundary then looks exactly like the +SKIP-ONLY "no ancestor file" case while the real cause is an incomplete REQUIRED +read. So a truncated tree is a **tree-listing failure** and exits non-zero. This +fork is nowhere near the limit — 4337 entries at the time of writing — but this +is a CLI that takes any PR number, so the guard is required, not hypothetical. + +That classification is a judgement, not a fact, and a reviewer may move any line +of it. + +Containment +=========== + +Author-controlled text (title, body, per-file paths) is carried through as +**data** and never interpreted: it lands in labelled JSON string fields, which +CONTAINMENT.md § Contract for later stages permits for a stage that builds no +prompt. That permission is contingent on this module making no model call, so if +that ever changes, containment moves in with it. See ``INTERFACE.md``. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from typing import Any, Iterable, Mapping + +# --------------------------------------------------------------------------- # +# The taxonomy, as values. Written once so the docstring, --help and the +# controls all read the same list rather than three copies that drift. +# --------------------------------------------------------------------------- # + +ABSENT = "absent" +FORBIDDEN = "forbidden" +MALFORMED = "malformed" +EMPTY = "empty" +TRUNCATED = "truncated" +UNREACHABLE = "unreachable" + +SKIP_REASONS: dict[str, str] = { + ABSENT: "the API said 404 — the thing is not there", + FORBIDDEN: "403, or a 404 that hides access rather than reporting absence", + MALFORMED: "not JSON, or JSON whose shape the reader cannot use", + EMPTY: "read successfully, and genuinely carries nothing", + TRUNCATED: "the API knowingly returned part of the answer (HTTP 200 + truncated: true)", + UNREACHABLE: "the call was never completed — no gh binary, a timeout, a transport error", +} + +#: Inputs whose failure is fatal. See § The exit contract. +REQUIRED_INPUTS = ("pr", "meta", "compare", "checks", "tree") + +#: Inputs whose absence is a reportable state, not a failure. +SKIP_ONLY_INPUTS = ("branch_rules", "org_rulesets", "closing_refs", "review_decision") + +#: The record's top-level fields, in order. STEP 2 of the plan fixes this list. +RECORD_FIELDS = ( + "pr", + "closing_issue", + "diff", + "checks", + "required_gate", + "nearest_rules", + "skips", +) + +#: Filenames that count as rules files. Compared as whole basenames, never with +#: endswith: `VISION_REMOTE_AGENTS.md` ends with "AGENTS.md" and is not one. +RULES_FILENAMES = ("AGENTS.md", "CLAUDE.md") + +#: The launchpad PR-body check's own pattern and comment-stripping order, so the +#: pre-flight and the check cannot disagree about what a closing keyword is. +CLOSING_KEYWORD = re.compile(r"\b(Closes|Fixes|Resolves)\s+#(\d+)", re.I) +HTML_COMMENT = re.compile(r"", re.S) + + +@dataclass(frozen=True) +class Read: + """One attempt to read one input. + + ``skip`` is None exactly when the read succeeded. A ``Read`` with data *and* + a skip reason is a contradiction and :func:`build_record` refuses it. + """ + + name: str + data: Any = None + skip: str | None = None + detail: str = "" + endpoint: str = "" + + def __post_init__(self) -> None: + if self.skip is not None and self.skip not in SKIP_REASONS: + raise ValueError(f"unenumerated skip reason: {self.skip!r}") + + @property + def ok(self) -> bool: + return self.skip is None + + +@dataclass +class Skips: + """The register of everything that could not be read, and why.""" + + entries: list[dict[str, str]] = field(default_factory=list) + + def add(self, field_name: str, read: Read) -> None: + self.entries.append( + { + "field": field_name, + # WHICH READ failed, which is not always the field that lost a + # value: build_pr reports under "pr" when the `meta` read is what + # broke. `Read.name` carried that and nothing published it, so the + # entry silently dropped the one fact a debugger starts from. + "source": read.name, + "reason": read.skip or MALFORMED, + "detail": read.detail or SKIP_REASONS.get(read.skip or "", ""), + "endpoint": read.endpoint, + } + ) + +class RecordError(Exception): + """A REQUIRED input could not be read. The caller must exit non-zero.""" + + def __init__(self, skips: list[dict[str, str]]) -> None: + super().__init__("; ".join(f"{s['field']}: {s['reason']}" for s in skips)) + self.skips = skips + + +def _get(mapping: Any, *path: str, default: Any = None) -> Any: + """Walk nested mappings without raising on a shape that is not there.""" + current = mapping + for key in path: + if not isinstance(current, Mapping) or key not in current: + return default + current = current[key] + return current if current is not None else default + + +# --------------------------------------------------------------------------- # +# Section builders. Each takes the reads it needs and either returns a value or +# records a skip and returns None. None means "not read"; it never means "empty". +# --------------------------------------------------------------------------- # + + +def build_pr(pr: Read, meta: Read, skips: Skips) -> dict[str, Any] | None: + if not pr.ok: + skips.add("pr", pr) + return None + if not meta.ok: + skips.add("pr", meta) + return None + + number = _get(pr.data, "number") + base_ref = _get(pr.data, "base", "ref") + head_sha = _get(pr.data, "head", "sha") + if number is None or not base_ref or not head_sha: + skips.add("pr", Read("pr", skip=MALFORMED, detail="no number, base.ref or head.sha", endpoint=pr.endpoint)) + return None + + labels = _get(meta.data, "labels", default=[]) + if not isinstance(labels, list): + skips.add("pr", Read("meta", skip=MALFORMED, detail="labels is not a list", endpoint=meta.endpoint)) + return None + + title = _get(meta.data, "title") + if title is None: + skips.add("pr", Read("meta", skip=MALFORMED, detail="no title field", endpoint=meta.endpoint)) + return None + + return { + "number": number, + "title": title, + # An absent body is "" from GitHub and None from a hand-built payload. + # Both mean the author wrote nothing; neither is a failed read. + "body": _get(meta.data, "body", default="") or "", + "labels": [lab.get("name") if isinstance(lab, Mapping) else lab for lab in labels], + "base_ref": base_ref, + "head_sha": head_sha, + } + + +def build_closing_issue(meta: Read, closing_refs: Read, skips: Skips) -> dict[str, Any] | None: + """Whether merging this PR would close an issue — asked of GitHub, not of a regex. + + **GitHub decides.** ``closingIssuesReferences`` is the only reliable answer, + and a pattern over the body disagrees with it in both directions on real pull + requests in this repository: + + - PR 86 closes two issues and its body says so on consecutive lines. A + first-match ``re.search`` — which is what the plan specified — reports only + #79, so it under-reports what the merge will do. ``finditer`` sees both, and + a control reproduces the single search to pin the difference. + - PR 92's body writes a visible ``Closes #n`` and GitHub reports **nothing**, + because the PR's base was not the default branch — merging it would close + no issue at all. + - A reference written inside code is ignored by GitHub and matched by a + regex. CommonMark has more code forms than a pattern enumerates; four of + them defeated the launchpad PR-body check (launchpad-26/buzz#125). + + The body is still read, for two things a reference list cannot give: **which + keyword** the author wrote, and whether the text and GitHub **disagree**. A + disagreement is reportable signal — it is exactly the shape of #125 — so it + is recorded rather than resolved silently. + + HTML comments are stripped before the text is scanned, in the PR-body check's + order, so an unfilled ```` placeholder is not read as a + reference by the text half either. + + An unreadable ``closingIssuesReferences`` yields ``present: None`` — unknown. + Never False. "We could not ask" and "it closes nothing" are different facts, + and only one of them means the board will not update. + """ + if not meta.ok: + skips.add("closing_issue", meta) + return None + body = _get(meta.data, "body", default="") + if body is None: + body = "" + if not isinstance(body, str): + skips.add( + "closing_issue", + Read("meta", skip=MALFORMED, detail="body is not a string", endpoint=meta.endpoint), + ) + return None + + visible = HTML_COMMENT.sub("", body) + matches = list(CLOSING_KEYWORD.finditer(visible)) + keyword = matches[0].group(1) if matches else None + text_numbers = sorted({int(m.group(2)) for m in matches}) + + section: dict[str, Any] = { + "present": None, + "keyword": keyword, + "issue_numbers": None, + "source": None, + "text_disagrees": None, + "text_issue_numbers": text_numbers, + } + + if not closing_refs.ok: + skips.add("closing_issue.closing_refs", closing_refs) + section["source"] = "unresolved: GitHub's own answer could not be read" + return section + + nodes = _get( + closing_refs.data, + "data", "repository", "pullRequest", "closingIssuesReferences", "nodes", + ) + if not isinstance(nodes, list): + skips.add( + "closing_issue.closing_refs", + Read( + "closing_refs", + skip=MALFORMED, + detail="closingIssuesReferences.nodes is not a list", + endpoint=closing_refs.endpoint, + ), + ) + section["source"] = "unresolved: GitHub's own answer could not be read" + return section + + numbers = sorted({n["number"] for n in nodes if isinstance(n, Mapping) and "number" in n}) + section.update( + { + "present": bool(numbers), + "issue_numbers": numbers, + "source": "graphql:closingIssuesReferences", + "text_disagrees": set(numbers) != set(text_numbers), + } + ) + return section + + +def build_diff(compare: Read, head_sha: str | None, skips: Skips) -> dict[str, Any] | None: + """Per-file counts against the **merge base**, not the base branch tip. + + ``baseRefOid`` is the current tip of the base branch, not the commit the head + branch forked from. Diffing against it attributes every commit landed on the + base since the fork to this PR's author, in reverse. The merge base is + REST-only: ``GET /repos/{o}/{r}/compare/{base}...{head}`` -> + ``.merge_base_commit.sha``. There is no ``mergeBaseCommit`` field on the + GraphQL ``PullRequest`` type. + """ + if not compare.ok: + skips.add("diff", compare) + return None + + if not head_sha: + # An unpinned diff is not a trustworthy diff: head_sha is what ties the + # file list to the commit pair it was read at. Taking it as a plain + # argument, rather than reaching into an optional pr section, means this + # invariant is enforced here instead of by every caller's discipline. + skips.add( + "diff", + Read( + "pr", + skip=MALFORMED, + detail="no head sha to pin the comparison to", + endpoint=compare.endpoint, + ), + ) + return None + + merge_base = _get(compare.data, "merge_base_commit", "sha") + if not merge_base: + skips.add( + "diff", + Read("compare", skip=MALFORMED, detail="no merge_base_commit.sha", endpoint=compare.endpoint), + ) + return None + + files = _get(compare.data, "files") + if not isinstance(files, list): + skips.add( + "diff", + Read("compare", skip=MALFORMED, detail="files is not a list", endpoint=compare.endpoint), + ) + return None + + entries = [] + for item in files: + path = _get(item, "filename") + if not path: + skips.add( + "diff", + Read("compare", skip=MALFORMED, detail="a file entry has no filename", endpoint=compare.endpoint), + ) + return None + entries.append( + { + "path": path, + "added": _get(item, "additions", default=0), + "removed": _get(item, "deletions", default=0), + "status": _get(item, "status", default="unknown"), + } + ) + + return { + "merge_base_sha": merge_base, + # The head sha the comparison was actually taken at, so the record is + # pinned to the commit pair it read rather than to whatever HEAD is now. + "head_sha": head_sha, + "files": entries, + } + + +def _normalise_check(node: Mapping[str, Any]) -> dict[str, Any]: + """One rollup context, flattened. + + Two shapes arrive. A ``CheckRun`` has a name, a status and a conclusion. A + ``StatusContext`` — the older commit-status API — has a context and a single + state and no workflow at all; its state is mapped onto ``conclusion`` because + that is what it means, and ``status`` stays None rather than being invented. + """ + if node.get("__typename") == "StatusContext": + return { + "name": node.get("context"), + "workflow": None, + "status": None, + "conclusion": node.get("state"), + "required": node.get("isRequired"), + "details_url": node.get("targetUrl"), + } + return { + "name": node.get("name"), + "workflow": _get(node, "checkSuite", "workflowRun", "workflow", "name"), + "status": node.get("status"), + "conclusion": node.get("conclusion"), + "required": node.get("isRequired"), + "details_url": node.get("detailsUrl"), + } + + +def build_checks(checks: Read, head_sha: str | None, skips: Skips) -> list[dict[str, Any]] | None: + """The check list, pinned to a commit and refused when it is partial. + + Two guards that exist because the value proving each was already being + fetched and thrown away: + + **The page cap.** The query asks for ``contexts(first: 100)`` and the API + answers with ``totalCount`` beside the nodes. A PR with more than 100 + contexts would otherwise publish the first hundred as if they were all of + them — a partial read wearing a complete answer's clothes, which is the exact + shape the ``truncated`` reason exists for on the trees API. This one is + rarer, not different: PR 86 has 47. It fails closed. + + **The commit.** The rollup is reached through ``commits(last: 1)``, resolved + server-side at its own moment, while ``diff`` is pinned to the head sha read + from the PR payload. If the PR is pushed to between the two calls, the record + would carry a diff for commit A and checks for commit B with nothing able to + say so — while every other section states which commit it describes. + """ + if not checks.ok: + skips.add("checks", checks) + return None + + nodes = _get( + checks.data, + "data", "repository", "pullRequest", "commits", "nodes", + ) + if not isinstance(nodes, list) or not nodes: + skips.add( + "checks", + Read("checks", skip=MALFORMED, detail="no commit nodes in the rollup", endpoint=checks.endpoint), + ) + return None + + rollup = _get(nodes[0], "commit", "statusCheckRollup") + if rollup is None: + # GitHub returns null here when the head commit has no checks at all. + # That is readable and empty, not malformed. + skips.add( + "checks", + Read("checks", skip=EMPTY, detail="the head commit has no check rollup", endpoint=checks.endpoint), + ) + return None + + contexts = _get(rollup, "contexts", "nodes") + if not isinstance(contexts, list): + skips.add( + "checks", + Read("checks", skip=MALFORMED, detail="contexts.nodes is not a list", endpoint=checks.endpoint), + ) + return None + if not contexts: + skips.add( + "checks", + Read("checks", skip=EMPTY, detail="the rollup lists no contexts", endpoint=checks.endpoint), + ) + return None + + total = _get(rollup, "contexts", "totalCount") + if isinstance(total, int) and total > len(contexts): + skips.add( + "checks", + Read( + "checks", + skip=TRUNCATED, + detail=( + f"the rollup has {total} contexts and the query returned {len(contexts)}; " + "publishing the page as the whole list would under-report the gate" + ), + endpoint=checks.endpoint, + ), + ) + return None + + rollup_sha = _get(nodes[0], "commit", "oid") + if head_sha and rollup_sha and rollup_sha != head_sha: + skips.add( + "checks", + Read( + "checks", + skip=MALFORMED, + detail=( + f"the rollup describes commit {rollup_sha[:9]} but the PR's head is " + f"{head_sha[:9]} — the branch moved between reads, so these checks do " + "not belong to the diff in this record" + ), + endpoint=checks.endpoint, + ), + ) + return None + + # A list, in the order the API gave them. Names collide; position does not. + return [_normalise_check(node) for node in contexts] + + +def build_required_gate( + branch_rules: Read, + org_rulesets: Read, + review_decision: Read, + checks_section: Iterable[Mapping[str, Any]] | None, + skips: Skips, +) -> dict[str, Any]: + """Whether any check is required, and which endpoint said so. + + An empty required set is reported as ``configured: false`` **naming the + endpoint that answered**, never as a silent zero — a zero is + indistinguishable from a scope failure. + + **Two different gates, asked separately, because only one is invisible.** + ``launchpad/AGENTS.md`` §6 states the position and it is not a contradiction + to report: the ``launchpad`` branch requires *at least two approving reviews*, + the ruleset enforcing it is unreadable without ``admin:org`` — three endpoints + report nothing — but **a live PR's ``reviewDecision`` confirms that review is + required**, without exposing the count. + + So this record asks for both: + + ``configured`` / ``source_endpoint`` + whether a required *status check* gate is visible, and what answered. + ``review_required`` / ``review_decision`` / ``review_source_endpoint`` + whether a *review* gate exists, from the one signal this token can read. + + Reporting `configured: false` while never asking `reviewDecision` would have + told a consumer that nothing gates this branch, on a branch that needs two + approvals — an absence published as an answer while the evidence was one call + away. ``reviewDecision`` carries no count, so the count stays unknown rather + than being inferred. + + **What ``configured: false`` does and does not mean.** It means nothing + *visible to this token* requires a check. Two blind spots survive, and both + are why the org-ruleset skip must be published alongside the false rather + than instead of it: + + - An org-level ruleset is unreadable without ``admin:org``. Its effect leaks + through per-context ``isRequired``, which is why that is consulted too. + - A required context that **never ran** is absent from the rollup entirely, + so ``isRequired`` cannot report it. A ruleset requiring a check that never + started is therefore invisible from here. + + So ``configured: false`` is a statement about what could be seen. The skip + entry beside it is what stops that being read as a statement about what is. + """ + if not org_rulesets.ok: + # SKIP-ONLY. Never "no org rulesets exist" — this token cannot tell. + skips.add("required_gate.org_rulesets", org_rulesets) + + review = _review_gate(review_decision, skips) + + required_by_check = [ + c.get("name") for c in (checks_section or []) if c.get("required") is True + ] + + if not branch_rules.ok: + skips.add("required_gate.branch_rules", branch_rules) + return { + "configured": None if not required_by_check else True, + "source_endpoint": branch_rules.endpoint if not required_by_check else "graphql:isRequired", + # Spread here too. Every other return path carries the review half, + # and a consumer written to the five-key contract got a KeyError from + # this one — the failure INTERFACE.md's "null means not read" rule + # cannot express, because the key was absent rather than null. + **review, + } + + if not isinstance(branch_rules.data, list): + # A shape the reader cannot use is NOT an empty rules list. Coercing it to + # [] here would publish `configured: false` — a definite answer — from a + # response nobody could read. + skips.add( + "required_gate.branch_rules", + Read( + "branch_rules", + skip=MALFORMED, + detail=f"the rules probe returned {type(branch_rules.data).__name__}, not a list", + endpoint=branch_rules.endpoint, + ), + ) + return { + "configured": True if required_by_check else None, + "source_endpoint": "graphql:isRequired" if required_by_check else branch_rules.endpoint, + **review, + } + + rules = branch_rules.data + rule_types = {_get(r, "type") for r in rules if isinstance(r, Mapping)} + by_rules = "required_status_checks" in rule_types + + if by_rules: + return {"configured": True, "source_endpoint": branch_rules.endpoint, **review} + if required_by_check: + # The rules probe saw nothing but GraphQL marks contexts required, which + # is what an org-level ruleset looks like from underneath. + return {"configured": True, "source_endpoint": "graphql:isRequired", **review} + return {"configured": False, "source_endpoint": branch_rules.endpoint, **review} + + +def _review_gate(review_decision: Read, skips: Skips) -> dict[str, Any]: + """The review half of the gate, from the one signal this token can read. + + ``gh`` renders a GraphQL null as an EMPTY STRING for a string field, so ``""`` + and None both mean "GitHub reports no review requirement for this base" — + verified: PR 86, whose base is ``launchpad``, answers ``REVIEW_REQUIRED`` + while PR 92, whose base was a topic branch, answers ``""``. Treating ``""`` as + an unrecognised value would turn "not required" into a malformed read. + + The COUNT is deliberately absent. §6 says two approving reviews, and + ``reviewDecision`` does not carry the number, so the number is not inferred + here from a document that could drift. + """ + unknown = { + "review_required": None, + "review_decision": None, + "review_source_endpoint": review_decision.endpoint, + } + if not review_decision.ok: + skips.add("required_gate.review_decision", review_decision) + return unknown + + decision = _get(review_decision.data, "reviewDecision", default="") + if not isinstance(decision, str): + skips.add( + "required_gate.review_decision", + Read( + "review_decision", + skip=MALFORMED, + detail=f"reviewDecision is {type(decision).__name__}, not a string", + endpoint=review_decision.endpoint, + ), + ) + return unknown + + return { + "review_required": bool(decision), + "review_decision": decision or None, + "review_source_endpoint": review_decision.endpoint, + } + + +def _nearest(path: str, filename: str, tree_paths: frozenset[str]) -> str | None: + """The nearest ``filename`` at or above ``path``'s directory, or None.""" + parts = path.split("/")[:-1] + while True: + candidate = "/".join([*parts, filename]) + if candidate in tree_paths: + return candidate + if not parts: + return None + parts.pop() + + +def build_nearest_rules( + tree: Read, + diff_section: Mapping[str, Any] | None, + skips: Skips, +) -> dict[str, dict[str, str | None]] | None: + """For each changed path, the nearest AGENTS.md **and** the nearest CLAUDE.md. + + Both, not first-wins. ``launchpad/`` holds only an AGENTS.md while the root + holds both, so first-wins would hide the root CLAUDE.md from every path under + ``launchpad/``. + + Resolved against the **PR's head tree**, never the local worktree — the + answer is wrong for any PR that is not checked out, and it is wrong in the + direction that matters when the PR is the one adding or deleting the rules + file. + """ + if not tree.ok: + skips.add("nearest_rules", tree) + return None + if diff_section is None: + skips.add( + "nearest_rules", + Read("compare", skip=MALFORMED, detail="no diff to resolve paths for", endpoint=tree.endpoint), + ) + return None + + if _get(tree.data, "truncated") is True: + # HTTP 200 with a partial answer. Reporting "no rules file" from a tree + # we only half read is the exact confusion this guard exists to prevent. + skips.add( + "nearest_rules", + Read( + "tree", + skip=TRUNCATED, + detail="the head tree came back truncated, so an absent rules file cannot be distinguished from an unread one", + endpoint=tree.endpoint, + ), + ) + return None + + entries = _get(tree.data, "tree") + if not isinstance(entries, list): + skips.add( + "nearest_rules", + Read("tree", skip=MALFORMED, detail="tree is not a list", endpoint=tree.endpoint), + ) + return None + if not entries: + # A head tree listing nothing cannot be reconciled with a diff that + # changed files. Resolving every path to "no rules file" against it would + # be an empty read publishing itself as an answer. + skips.add( + "nearest_rules", + Read("tree", skip=EMPTY, detail="the head tree listed no entries at all", endpoint=tree.endpoint), + ) + return None + + # Blobs only. A git *directory* named AGENTS.md is legal, and so is a + # submodule (type "commit"); either would satisfy a path-only match and be + # reported as a resolved rules file that cannot be read — while also + # suppressing the SKIP-ONLY "no ancestor rules file" entry that is the truth. + # Entries with no type recorded are kept, because a projection that dropped + # the field should not silently drop the file with it. + tree_paths = frozenset( + path + for path, kind in ( + (_get(e, "path"), _get(e, "type", default="blob")) for e in entries + ) + if isinstance(path, str) and kind == "blob" + ) + + resolved: dict[str, dict[str, str | None]] = {} + for entry in diff_section["files"]: + path = entry["path"] + found = {name: _nearest(path, name, tree_paths) for name in RULES_FILENAMES} + resolved[path] = found + if not any(found.values()): + # SKIP-ONLY: a path with no ancestor rules file is a real answer. + skips.add( + f"nearest_rules[{path}]", + Read("tree", skip=EMPTY, detail="no ancestor AGENTS.md or CLAUDE.md", endpoint=tree.endpoint), + ) + return resolved + + +# --------------------------------------------------------------------------- # +# The record +# --------------------------------------------------------------------------- # + + +def build_record(reads: Mapping[str, Read]) -> dict[str, Any]: + """Assemble the record from already-fetched reads. + + Raises :class:`RecordError` when a REQUIRED input could not be read, so that + the caller exits non-zero instead of publishing a record with holes in it. + """ + missing = set(REQUIRED_INPUTS) - set(reads) + if missing: + raise KeyError(f"no read supplied for required inputs: {sorted(missing)}") + + skips = Skips() + pr_section = build_pr(reads["pr"], reads["meta"], skips) + closing = build_closing_issue( + reads["meta"], + reads.get("closing_refs", Read("closing_refs", skip=UNREACHABLE, detail="not attempted")), + skips, + ) + diff_section = build_diff( + reads["compare"], (pr_section or {}).get("head_sha"), skips + ) + checks_section = build_checks( + reads["checks"], (pr_section or {}).get("head_sha"), skips + ) + gate = build_required_gate( + reads.get("branch_rules", Read("branch_rules", skip=UNREACHABLE, detail="not attempted")), + reads.get("org_rulesets", Read("org_rulesets", skip=UNREACHABLE, detail="not attempted")), + reads.get("review_decision", Read("review_decision", skip=UNREACHABLE, detail="not attempted")), + checks_section, + skips, + ) + rules = build_nearest_rules(reads["tree"], diff_section, skips) + + record = { + "pr": pr_section, + "closing_issue": closing, + "diff": diff_section, + "checks": checks_section, + "required_gate": gate, + "nearest_rules": rules, + "skips": skips.entries, + } + assert tuple(record) == RECORD_FIELDS, "the record grew a field without the docstring" + + fatal = [entry for entry in skips.entries if is_fatal(entry)] + if fatal: + raise RecordError(fatal) + return record + + +#: Reasons that mean the input was NOT read. `empty` is deliberately absent: it +#: means the input was read and carries nothing, which is an answer. +FATAL_REASONS = (ABSENT, FORBIDDEN, MALFORMED, TRUNCATED, UNREACHABLE) + +#: Record fields whose unreadability is fatal — the five REQUIRED inputs, seen +#: from the record's side rather than the fetch layer's. +FATAL_FIELDS = ("pr", "closing_issue", "diff", "checks", "nearest_rules") + +#: Named explicitly rather than matched by prefix, so a new field cannot become +#: non-fatal by accident of what it is called. +#: Every sub-gate skip uses the dotted form, so one selector finds them all: +#: a consumer filtering `field.startswith("required_gate.")` used to miss the +#: branch-rules entry, which was filed bare. +NEVER_FATAL_FIELDS = ( + "required_gate", + "required_gate.branch_rules", + "required_gate.org_rulesets", + "required_gate.review_decision", + "closing_issue.closing_refs", +) + + +def is_fatal(skip_entry: Mapping[str, str]) -> bool: + """Whether one skip entry means the process must exit non-zero. + + Two rules, and the first outranks the second: + + 1. **``empty`` is never fatal, for any field.** An empty answer was *read*. + A pull request whose head commit has no check runs yet, and a pull request + whose check list could not be fetched, are different facts — and treating + the first as a failure is the same conflation, pointed the other way, as + treating the second as "no checks are required". + 2. Otherwise a field is fatal when it is one of the five REQUIRED inputs. + """ + if skip_entry["reason"] == EMPTY: + return False + field_name = skip_entry["field"] + if field_name in NEVER_FATAL_FIELDS: + return False + if field_name.startswith("nearest_rules["): + return False + return field_name in FATAL_FIELDS diff --git a/launchpad/scripts/preflight_fetch.py b/launchpad/scripts/preflight_fetch.py new file mode 100644 index 00000000000..c58773fe042 --- /dev/null +++ b/launchpad/scripts/preflight_fetch.py @@ -0,0 +1,423 @@ +"""The pre-flight's fetch layer and CLI — launchpad-26/buzz#116. + +This module is where the talking to ``gh`` happens, and the only place in the +tree that spawns a process. It exists as a separate module from the CLI script so +that controls can ``import`` it: ``pr-preflight.py`` cannot be imported under +that name, and a fetch layer no control can drive is a fetch layer no control can +make fail. + +**The runner is injected.** Every call goes through a ``runner(argv) -> +RunResult`` callable that defaults to :func:`gh_runner`. Controls pass a fake that +answers from recorded fixtures, so the whole surface — including each way a fetch +can fail — is exercisable with no network. Hardcoding ``subprocess.run(["gh", +...])` at each call site would make that impossible without a rewrite. + +Nine reads, five of them required +================================= + +================ ====================================================== ========= +name call kind +================ ====================================================== ========= +pr ``GET /repos/{o}/{r}/pulls/{n}`` required +meta ``gh pr view {n} --json title,body,labels`` required +compare ``GET /repos/{o}/{r}/compare/{base.sha}...{head.sha}`` required +checks GraphQL ``statusCheckRollup`` with ``isRequired`` required +tree ``GET /repos/{o}/{r}/git/trees/{head}?recursive=1`` required +branch_rules ``GET /repos/{o}/{r}/rules/branches/{base}`` skip-only +org_rulesets ``GET /orgs/{o}/rulesets`` skip-only +closing_refs GraphQL ``closingIssuesReferences`` skip-only +review_decision ``gh pr view {n} --json reviewDecision`` skip-only +================ ====================================================== ========= + +``review_decision`` is the only gate signal this token can read: §6's ruleset is +invisible without ``admin:org``, and ``reviewDecision`` confirms review is +*required* without exposing the count. + +``compare`` is taken **by SHA**, ``base.sha...head.sha``, never by base branch +name: a name resolves to the tip today, and for a merged PR that tip already +contains the head, so the API answers ``200 OK`` with zero files — a real PR +rendered as one that changed nothing. + +Exit codes +========== + +=== ===================================================================== +0 a record was emitted on stdout. It may carry SKIP-only entries +1 usage error — a bad argument, nothing was fetched +2 a REQUIRED input could not be read. Nothing is printed on stdout +=== ===================================================================== + +Exit 2 prints the skip register on **stderr** and nothing on stdout, so a caller +that pipes stdout into a reviewer never receives a record with holes in it. +""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys +from dataclasses import dataclass +from typing import Callable, Mapping + +from preflight_core import ( + ABSENT, + FORBIDDEN, + MALFORMED, + REQUIRED_INPUTS, + SKIP_ONLY_INPUTS, + SKIP_REASONS, + UNREACHABLE, + Read, + RecordError, + build_record, +) + +DEFAULT_REPO = "launchpad-26/buzz" + +#: The one binary this tool is allowed to spawn. +BINARY = "gh" + +_HTTP_STATUS = re.compile(r"\(HTTP (\d{3})\)") + +#: GraphQL says a thing does not exist in prose, with no HTTP status attached: +#: `gh pr view 999999` fails with "GraphQL: Could not resolve to a PullRequest +#: with the number of 999999." Without this, a definitive "it is not there" is +#: classified as unreachable — the exact confusion between "we could not ask" and +#: "we asked and it is absent" that the taxonomy exists to prevent. +_GRAPHQL_ABSENT = re.compile( + r"could not resolve to a|no pull requests found|could not find any pull request", + re.I, +) + +#: GraphQL error `type` -> the reason it means. Checked in this order, because a +#: response can carry several errors and the most specific failure should win. +#: Mapping everything but FORBIDDEN onto `malformed` would report a definite +#: "this does not exist" as a response nobody could parse. +GRAPHQL_ERROR_REASONS = { + "FORBIDDEN": FORBIDDEN, + "NOT_FOUND": ABSENT, + "RATE_LIMITED": UNREACHABLE, + "SERVICE_UNAVAILABLE": UNREACHABLE, +} + + +@dataclass(frozen=True) +class RunResult: + returncode: int + stdout: str + stderr: str + + +def gh_runner(argv: list[str]) -> RunResult: + """Run ``gh``. The only process spawn in the pre-flight. + + Refuses any other binary rather than trusting its caller: this function is + the seam where an injected runner could otherwise be talked into spawning + something else. + """ + if not argv or argv[0] != BINARY: + raise ValueError(f"the pre-flight may only spawn {BINARY!r}, not {argv[:1]!r}") + try: + proc = subprocess.run(argv, capture_output=True, timeout=60, check=False) + except subprocess.TimeoutExpired: + return RunResult(124, "", f"{BINARY} timed out after 60s") + except OSError as exc: + # Every OSError, not only FileNotFoundError. An uncaught one exits 1 with + # a traceback, and 1 is this tool's "usage error" code — so a broken gh + # install would tell a caller its arguments were bad. Three reproduced + # triggers, none of them a missing binary: + # EACCES a gh on PATH that is not executable + # EACCES a *directory* named gh on PATH + # ENOEXEC a gh that is executable but not a runnable binary — a + # truncated download or the wrong architecture, which is the + # plausible one in practice + # A non-executable gh with a working gh later on PATH is harmless: + # CPython keeps walking PATH and only reports the saved EACCES if no + # entry succeeds. + return RunResult(127, "", f"cannot run {BINARY}: {type(exc).__name__}: {exc}") + return RunResult( + proc.returncode, + proc.stdout.decode("utf-8", "replace"), + proc.stderr.decode("utf-8", "replace"), + ) + + +Runner = Callable[[list[str]], RunResult] + + +def _classify_failure(name: str, result: RunResult) -> tuple[str, str]: + """Map a failed ``gh`` call onto one enumerated reason and a detail.""" + status = _HTTP_STATUS.search(result.stderr or "") + detail = (result.stderr or "").strip().splitlines() + detail = detail[-1] if detail else f"{BINARY} exited {result.returncode}" + + if result.returncode in (124, 127): + return UNREACHABLE, detail + if status: + code = int(status.group(1)) + if code in (401, 403): + return FORBIDDEN, detail + if code == 404: + # A 404 on org rulesets hides access; it does not report absence. + # launchpad-26 is an Organization, verified, so "not found" here can + # only mean this token cannot see it — reporting that as absent would + # let a scope failure read as "this org configures no rulesets". + if name == "org_rulesets": + return FORBIDDEN, f"{detail} (404 for an organization that exists: no admin:org)" + return ABSENT, detail + if 500 <= code <= 599: + return UNREACHABLE, detail + return MALFORMED, detail + if _GRAPHQL_ABSENT.search(result.stderr or ""): + return ABSENT, detail + return UNREACHABLE, detail + + +def _read(name: str, endpoint: str, argv: list[str], runner: Runner) -> Read: + """Perform one read and classify its outcome. Never raises on failure.""" + try: + result = runner(argv) + except FileNotFoundError as exc: # a runner that cannot start the binary + return Read(name, skip=UNREACHABLE, detail=str(exc), endpoint=endpoint) + + if result.returncode != 0: + reason, detail = _classify_failure(name, result) + return Read(name, skip=reason, detail=detail, endpoint=endpoint) + + try: + data = json.loads(result.stdout) + except json.JSONDecodeError as exc: + return Read(name, skip=MALFORMED, detail=f"not JSON: {exc}", endpoint=endpoint) + + # An errors array arriving with a SUCCESSFUL exit code — a failure wearing a + # success's clothes. + # + # `gh` never does this: it exits non-zero whenever the response carries + # errors, including the partial case where some aliases resolved and others + # did not, so through the real binary this branch is unreachable and a + # nonexistent PR is classified `absent` from gh's prose instead. Verified + # against gh 2.93.0. + # + # It is kept for the INJECTED runner, which is the documented extension point + # (see INTERFACE.md on adopting #120's fetch.fetch_all): a runner that speaks + # to GitHub directly rather than through gh returns HTTP 200 with an errors + # body and a zero status, and without this the errors array would be handed + # on as if it were data. Both halves — the guard and the type mapping — are + # driven by controls through a fake runner, because nothing else can drive + # them. + if isinstance(data, Mapping) and data.get("errors"): + types = {e.get("type") for e in data["errors"] if isinstance(e, Mapping)} + messages = "; ".join( + str(e.get("message", "")) for e in data["errors"] if isinstance(e, Mapping) + ) + reason = MALFORMED + for error_type, mapped in GRAPHQL_ERROR_REASONS.items(): + if error_type in types: + reason = mapped + break + return Read(name, skip=reason, detail=f"graphql errors: {messages}", endpoint=endpoint) + + return Read(name, data=data, endpoint=endpoint) + + +#: GitHub's own answer to what a PR closes. The only reliable one: a pattern over +#: the body reports a reference written inside code, misses second and later +#: references, and says a PR closes an issue when its base is not the default +#: branch and merging it will close nothing. +CLOSING_QUERY = """ +query($owner:String!,$repo:String!,$pr:Int!){ + repository(owner:$owner,name:$repo){ + pullRequest(number:$pr){closingIssuesReferences(first:50){nodes{number}}}}} +""" + +CHECKS_QUERY = """ +query($owner:String!,$repo:String!,$pr:Int!){ + repository(owner:$owner,name:$repo){ + pullRequest(number:$pr){ + commits(last:1){nodes{commit{oid statusCheckRollup{state contexts(first:100){totalCount nodes{ + __typename + ... on CheckRun{name status conclusion detailsUrl isRequired(pullRequestNumber:$pr) checkSuite{workflowRun{workflow{name}}}} + ... on StatusContext{context state targetUrl isRequired(pullRequestNumber:$pr)} + }}}}}} + } + } +} +""" + + +def fetch_all(number: int, repo: str, runner: Runner) -> dict[str, Read]: + """Perform all nine reads. A failure in one never aborts the others.""" + owner, _, name = repo.partition("/") + reads: dict[str, Read] = {} + + reads["pr"] = _read( + "pr", + f"GET /repos/{repo}/pulls/{number}", + [BINARY, "api", f"repos/{repo}/pulls/{number}"], + runner, + ) + reads["meta"] = _read( + "meta", + f"gh pr view {number} --json title,body,labels", + [BINARY, "pr", "view", str(number), "--repo", repo, "--json", "title,body,labels"], + runner, + ) + reads["checks"] = _read( + "checks", + "graphql:statusCheckRollup", + [ + BINARY, "api", "graphql", + "-f", f"owner={owner}", + "-f", f"repo={name}", + "-F", f"pr={number}", + "-f", f"query={CHECKS_QUERY}", + ], + runner, + ) + + # The one gate signal readable without admin:org. launchpad/AGENTS.md §6: the + # ruleset requiring two approving reviews is invisible to this token, but a + # live PR's reviewDecision confirms review IS required — and it differs by + # base, so it cannot be assumed from the repository alone. + reads["review_decision"] = _read( + "review_decision", + f"gh pr view {number} --json reviewDecision", + [BINARY, "pr", "view", str(number), "--repo", repo, "--json", "reviewDecision"], + runner, + ) + reads["closing_refs"] = _read( + "closing_refs", + "graphql:closingIssuesReferences", + [ + BINARY, "api", "graphql", + "-f", f"owner={owner}", + "-f", f"repo={name}", + "-F", f"pr={number}", + "-f", f"query={CLOSING_QUERY}", + ], + runner, + ) + + # compare and tree need the PR's own base and head SHAs. Without them the + # calls cannot be made at all, and "not attempted" is a different fact from + # "answered 404" — inventing a base ref to fetch anyway would be worse. + base_sha = head_sha = base_ref = None + if reads["pr"].ok and isinstance(reads["pr"].data, Mapping): + base_sha = (reads["pr"].data.get("base") or {}).get("sha") + base_ref = (reads["pr"].data.get("base") or {}).get("ref") + head_sha = (reads["pr"].data.get("head") or {}).get("sha") + + not_attempted = "not attempted: the PR read did not yield base.sha and head.sha" + if base_sha and head_sha: + reads["compare"] = _read( + "compare", + f"GET /repos/{repo}/compare/{base_sha}...{head_sha}", + [BINARY, "api", f"repos/{repo}/compare/{base_sha}...{head_sha}"], + runner, + ) + reads["tree"] = _read( + "tree", + f"GET /repos/{repo}/git/trees/{head_sha}?recursive=1", + [BINARY, "api", f"repos/{repo}/git/trees/{head_sha}?recursive=1"], + runner, + ) + else: + reads["compare"] = Read("compare", skip=UNREACHABLE, detail=not_attempted) + reads["tree"] = Read("tree", skip=UNREACHABLE, detail=not_attempted) + + if base_ref: + reads["branch_rules"] = _read( + "branch_rules", + f"GET /repos/{repo}/rules/branches/{base_ref}", + [BINARY, "api", f"repos/{repo}/rules/branches/{base_ref}"], + runner, + ) + else: + reads["branch_rules"] = Read("branch_rules", skip=UNREACHABLE, detail=not_attempted) + + reads["org_rulesets"] = _read( + "org_rulesets", + f"GET /orgs/{owner}/rulesets", + [BINARY, "api", f"orgs/{owner}/rulesets"], + runner, + ) + return reads + + +def _epilogue() -> str: + """The taxonomy and the exit contract, in --help as well as the docstring.""" + reasons = "\n".join(f" {name:<13} {why}" for name, why in SKIP_REASONS.items()) + return ( + "SKIP reasons, one per unreadable input:\n" + f"{reasons}\n\n" + "Required inputs — a failure to read any of these exits 2, and no record\n" + "is printed:\n" + f" {', '.join(REQUIRED_INPUTS)}\n\n" + "Skip-only inputs — absence is a reportable state and the exit stays 0:\n" + f" {', '.join(SKIP_ONLY_INPUTS)}\n" + " a changed path with no ancestor AGENTS.md or CLAUDE.md\n\n" + "A truncated head tree is a required-input FAILURE, not an empty result:\n" + "the trees API answers HTTP 200 with truncated: true and a partial list,\n" + "so a path whose rules file sits past the boundary is indistinguishable\n" + "from a path that has none.\n\n" + "Exit codes: 0 record emitted, 1 usage error, 2 a required input was unreadable.\n" + ) + + +class _Parser(argparse.ArgumentParser): + """argparse exits 2 on a usage error, which is this tool's "unreadable + required input" code. Two very different failures sharing an exit code makes + the contract untestable, so usage errors are moved to 1 and 2 keeps its + meaning.""" + + def error(self, message: str): # noqa: D102 - argparse's own contract + self.print_usage(sys.stderr) + sys.stderr.write(f"{self.prog}: error: {message}\n") + raise SystemExit(1) + + +def build_parser() -> argparse.ArgumentParser: + parser = _Parser( + prog="pr-preflight.py", + description=( + "Emit the deterministic facts a PR review needs, as JSON on stdout. " + "Makes no model call and reaches no conclusion about the code: " + "author-controlled text is carried through as labelled data." + ), + epilog=_epilogue(), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("number", type=int, help="the pull request number") + parser.add_argument("--repo", default=DEFAULT_REPO, help=f"owner/name (default {DEFAULT_REPO})") + parser.add_argument("--indent", type=int, default=2, help="JSON indent; 0 for one line") + return parser + + +def main(argv: list[str] | None = None, runner: Runner | None = None) -> int: + """Fetch, build, print. Returns the process exit code.""" + parser = build_parser() + args = parser.parse_args(argv) + if args.number <= 0: + parser.error("PR number must be positive") + + reads = fetch_all(args.number, args.repo, runner or gh_runner) + try: + record = build_record(reads) + except RecordError as failure: + json.dump( + {"error": "a required input could not be read", "skips": failure.skips}, + sys.stderr, + indent=2, + ) + sys.stderr.write("\n") + return 2 + + json.dump(record, sys.stdout, indent=args.indent or None) + sys.stdout.write("\n") + return 0 + + +if __name__ == "__main__": # pragma: no cover - exercised via pr-preflight.py + sys.exit(main()) diff --git a/launchpad/scripts/test_no_model.py b/launchpad/scripts/test_no_model.py new file mode 100644 index 00000000000..f326d1bc9f0 --- /dev/null +++ b/launchpad/scripts/test_no_model.py @@ -0,0 +1,309 @@ +"""The no-model property, asserted by AST — launchpad-26/buzz#116. + +The issue requires that the pre-flight make no model call. This module proves it +by parsing the source and comparing every imported module name against an +allowlist **written here**, so any new import fails by default rather than only +the handful someone thought to name in a pattern. + +Why this is not a grep +====================== + +A grep reports every occurrence and leaves a human to sort the legitimate hits +from the real ones. That is not a check that can fail. An allowlist inverts the +burden: the check goes red on an import nobody anticipated. + +Three ways an import evades a naive scan, all closed here +========================================================= + +**Traverse with ``ast.walk``, not ``tree.body``.** ``tree.body`` sees only +top-level statements, so an ``import requests`` inside a function body is +invisible to it. :meth:`Traversal.test_a_top_level_scan_misses_a_nested_import` +demonstrates the difference on a sample rather than asserting it. + +**Collect ``Call`` nodes too.** ``importlib.import_module("openai")`` is a Call, +never an Import — an import-name scan reports it as absent. One line, and the +no-model rule is broken outright while every import-based check stays green. + +**``__import__("openai")`` needs no import statement at all**, so only the +call-node rule can see it. + +This module deliberately **does not import** the modules it checks. It reads +their source and parses it. An import-hygiene check that imports its subject +cannot report on a subject whose new import is not installed: it dies at import +time, and a suite that is red because a module could not load is not evidence +that this check works. Keeping it source-only is what lets +``mutation_harness.py`` inject ``import openai`` and show *this* control turning +red. +""" + +from __future__ import annotations + +import ast +import os +import unittest + +HERE = os.path.dirname(os.path.abspath(__file__)) + +#: Written here, not inferred. Anything else fails the check by default. +ALLOWLIST: dict[str, frozenset[str]] = { + # The record. No I/O of any kind: no subprocess, no filesystem, no network. + "preflight_core.py": frozenset({"__future__", "re", "dataclasses", "typing"}), + # The fetch layer. subprocess is here and nowhere else, and it may only spawn gh. + "preflight_fetch.py": frozenset( + { + "__future__", + "argparse", + "json", + "re", + "subprocess", + "sys", + "dataclasses", + "typing", + "preflight_core", + } + ), + # The entry point — the file the issue calls "the script", and the first one a + # reader opens. It was missing from this list for three commits: an inference + # call placed here left every control green, on precisely the file anyone + # checking the no-model claim would look at. + "pr-preflight.py": frozenset({"__future__", "os", "sys", "preflight_fetch"}), +} + + +#: Files in this directory that belong to another task, named one by one with the +#: reason. Everything else is ours and must be on the allowlist. +NOT_OURS = { + "pr_body_check.py": "the PR-body check from #126; its imports are that task's business", + "mutation_harness.py": "the evidence generator — it rewrites source by design, and " + "its own imports are not part of the no-model claim", +} + + +def preflight_sources() -> set[str]: + """Every file this stage owns, by EXCLUSION rather than by name shape. + + The allowlist above is deliberately written by hand — that is what makes an + unanticipated import fail — but *which files it must cover* cannot also be + hand-written, or a new module simply never gets checked. That is how + ``pr-preflight.py`` went unguarded. + + An earlier version of this function matched ``name.startswith("preflight")``, + which is a hand-written list wearing a filename prefix: a module this stage + owns called ``gate_reader.py`` would not be discovered, would not be on the + allowlist, and the control asserting full coverage would stay green while its + imports went unchecked — the same failure, one level up. So the rule is + inverted. A new file is ours until someone says otherwise **here**, with a + reason, and an unlisted new module fails closed whatever it is called. + """ + return { + name + for name in os.listdir(HERE) + if name.endswith(".py") and not name.startswith("test_") and name not in NOT_OURS + } + +#: Calls that load code by name, bypassing every import statement. +FORBIDDEN_CALLS = frozenset({"importlib.import_module", "__import__", "eval", "exec"}) + + +def source_of(name: str) -> str: + with open(os.path.join(HERE, name), encoding="utf-8") as handle: + return handle.read() + + +def dotted_name(node: ast.AST) -> str: + """`a.b.c` for an Attribute/Name chain, so a call can be named in full.""" + parts: list[str] = [] + current = node + while isinstance(current, ast.Attribute): + parts.append(current.attr) + current = current.value + if isinstance(current, ast.Name): + parts.append(current.id) + return ".".join(reversed(parts)) + + +def imported_modules(source: str) -> set[str]: + """Every module name imported anywhere in ``source``, nesting included.""" + found: set[str] = set() + for node in ast.walk(ast.parse(source)): # walk, never tree.body + if isinstance(node, ast.Import): + for alias in node.names: + found.add(alias.name.split(".")[0]) + elif isinstance(node, ast.ImportFrom): + if node.level: # a relative import names no external module + continue + if node.module: + found.add(node.module.split(".")[0]) + return found + + +def loader_calls(source: str) -> set[str]: + """Every call that loads code by name rather than by import statement.""" + found: set[str] = set() + for node in ast.walk(ast.parse(source)): + if isinstance(node, ast.Call): + name = dotted_name(node.func) + if name in FORBIDDEN_CALLS or name.split(".")[-1] in {"import_module"}: + found.add(name) + return found + + +class NoModelCall(unittest.TestCase): + def test_every_file_this_stage_owns_is_on_the_allowlist(self): + """A new pre-flight module must fail this, not silently skip the check. + + The entry point was absent from ALLOWLIST for three commits while the + suite reported the no-model property as proved. Enumerating the directory + is what makes a missing entry a failure instead of a blind spot. + """ + found = preflight_sources() + self.assertIn("pr-preflight.py", found, "the entry point must be discovered") + self.assertEqual( + found, + set(ALLOWLIST), + f"unchecked: {sorted(found - set(ALLOWLIST))}; " + f"listed but absent from disk: {sorted(set(ALLOWLIST) - found)}", + ) + + def test_the_scope_excludes_another_task_s_script(self): + """#126's pr_body_check.py shares this directory and is not ours to police.""" + self.assertTrue( + os.path.exists(os.path.join(HERE, "pr_body_check.py")), + "this control is only meaningful while that file is really here", + ) + self.assertNotIn("pr_body_check.py", preflight_sources()) + for name, reason in NOT_OURS.items(): + with self.subTest(excluded=name): + self.assertTrue(reason, "an exclusion without a stated reason is a hole") + + def test_a_new_module_fails_closed_whatever_it_is_called(self): + """The prefix filter this replaced would have missed `gate_reader.py`. + + Discovery is by exclusion now, so a file nobody anticipated is ours by + default and its absence from ALLOWLIST is a failure, not a silence. + """ + invented = os.path.join(HERE, "gate_reader.py") + self.assertFalse(os.path.exists(invented), "pick a name that is not real") + try: + with open(invented, "w", encoding="utf-8") as handle: + handle.write("import openai\n") + self.assertIn("gate_reader.py", preflight_sources(), "a new module must be discovered") + self.assertNotIn("gate_reader.py", ALLOWLIST, "and it is not on the allowlist") + with self.assertRaises(AssertionError): + NoModelCall("test_every_file_this_stage_owns_is_on_the_allowlist").test_every_file_this_stage_owns_is_on_the_allowlist() + finally: + os.remove(invented) + self.assertNotIn("gate_reader.py", preflight_sources()) + + def test_every_import_is_on_the_allowlist(self): + for name, allowed in ALLOWLIST.items(): + with self.subTest(module=name): + found = imported_modules(source_of(name)) + self.assertLessEqual( + found, + allowed, + f"{name} imports {sorted(found - allowed)}, which is not on the allowlist. " + "An import this check did not anticipate fails it by default — that is the point. " + "If the import is legitimate, add it here and say why in the commit.", + ) + + def test_the_entry_point_only_wires_the_cli_up(self): + """It may touch sys.path; it may not fetch, decide, or call a model.""" + source = source_of("pr-preflight.py") + found = imported_modules(source) + self.assertEqual(found, ALLOWLIST["pr-preflight.py"] & found) + for banned in ("subprocess", "socket", "http", "urllib", "requests", "httpx", "openai"): + self.assertNotIn(banned, found) + self.assertEqual(loader_calls(source), set()) + + def test_the_record_module_performs_no_io_at_all(self): + """No subprocess, no socket, no filesystem: the record is pure.""" + found = imported_modules(source_of("preflight_core.py")) + for banned in ("subprocess", "socket", "os", "pathlib", "http", "urllib", "requests", "httpx"): + self.assertNotIn(banned, found) + + def test_no_module_loads_code_by_name(self): + for name in ALLOWLIST: + with self.subTest(module=name): + self.assertEqual( + loader_calls(source_of(name)), + set(), + "importlib.import_module or __import__ bypasses every import-name scan", + ) + + def test_no_model_sdk_is_reachable_from_either_module(self): + for name in ALLOWLIST: + with self.subTest(module=name): + found = imported_modules(source_of(name)) + for sdk in ("openai", "anthropic", "google", "cohere", "mistralai", "litellm", "ollama"): + self.assertNotIn(sdk, found) + + def test_only_the_fetch_layer_may_spawn_anything(self): + """One spawn site, inside gh_runner, and it refuses any binary but gh. + + Counted as call nodes rather than as text. A string count over the source + reads this module's own prose about ``subprocess.run`` as a second spawn + site — which is the grep-shaped failure this whole file argues against, + and it happened here first. + """ + self.assertIn("subprocess", ALLOWLIST["preflight_fetch.py"]) + self.assertNotIn("subprocess", ALLOWLIST["preflight_core.py"]) + + source = source_of("preflight_fetch.py") + tree = ast.parse(source) + spawns = [ + node + for node in ast.walk(tree) + if isinstance(node, ast.Call) and dotted_name(node.func) == "subprocess.run" + ] + self.assertEqual(len(spawns), 1, "exactly one place in the tree starts a process") + + runner = next( + node + for node in ast.walk(tree) + if isinstance(node, ast.FunctionDef) and node.name == "gh_runner" + ) + self.assertTrue( + runner.lineno <= spawns[0].lineno <= runner.end_lineno, + "the spawn must be inside gh_runner, which is the seam controls replace", + ) + self.assertIn('BINARY = "gh"', source) + self.assertIn("argv[0] != BINARY", source, "the spawn site must refuse any other binary") + + +class Traversal(unittest.TestCase): + """The scan's own blind spots, demonstrated rather than asserted.""" + + NESTED = ( + "import json\n" + "def fetch():\n" + " import requests\n" + " return requests\n" + ) + BY_NAME = 'value = importlib.import_module("openai")\n' + DUNDER = 'value = __import__("openai")\n' + + def test_a_top_level_scan_misses_a_nested_import(self): + top_level = { + alias.name + for node in ast.parse(self.NESTED).body + if isinstance(node, ast.Import) + for alias in node.names + } + self.assertEqual(top_level, {"json"}, "tree.body sees only the top level") + self.assertEqual(imported_modules(self.NESTED), {"json", "requests"}, "ast.walk sees both") + + def test_an_import_name_scan_misses_a_call(self): + self.assertEqual(imported_modules(self.BY_NAME), set(), "no Import node exists to find") + self.assertEqual(loader_calls(self.BY_NAME), {"importlib.import_module"}) + + def test_dunder_import_needs_no_import_statement_at_all(self): + self.assertEqual(imported_modules(self.DUNDER), set()) + self.assertEqual(loader_calls(self.DUNDER), {"__import__"}) + + def test_a_relative_import_is_not_mistaken_for_a_module(self): + self.assertEqual(imported_modules("from . import sibling\n"), set()) + + +if __name__ == "__main__": + unittest.main() diff --git a/launchpad/scripts/test_preflight_core.py b/launchpad/scripts/test_preflight_core.py new file mode 100644 index 00000000000..4fc24573d15 --- /dev/null +++ b/launchpad/scripts/test_preflight_core.py @@ -0,0 +1,1428 @@ +"""Controls for the pre-flight — launchpad-26/buzz#116. + +Run from the repository root: + + python3 -m unittest discover -s launchpad/scripts -t launchpad/scripts + +No control here touches the network. Fixtures under ``testdata/`` were recorded +from the live API by ``testdata/record.sh``; see ``testdata/README.md``. +""" + +from __future__ import annotations + +import contextlib +import io +import json +import os +import sys +import unittest +from collections import Counter + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import preflight_core as core # noqa: E402 (after the sys.path insert, deliberately) +import preflight_fetch as fetch # noqa: E402 + +TESTDATA = os.path.join(os.path.dirname(os.path.abspath(__file__)), "testdata") + + +def fixture(name: str): + with open(os.path.join(TESTDATA, name), encoding="utf-8") as handle: + return json.load(handle) + + +#: The endpoint each read would have come from, so a skip can name its source. +ENDPOINTS = { + "pr": "GET /repos/{o}/{r}/pulls/{n}", + "meta": "gh pr view {n} --json title,body,labels", + "compare": "GET /repos/{o}/{r}/compare/{base}...{head}", + "checks": "graphql:statusCheckRollup", + "tree": "GET /repos/{o}/{r}/git/trees/{head}?recursive=1", + "branch_rules": "GET /repos/{o}/{r}/rules/branches/{base}", + "org_rulesets": "GET /orgs/{o}/rulesets", + "closing_refs": "graphql:closingIssuesReferences", + "review_decision": "gh pr view {n} --json reviewDecision", +} + +#: PR 86, everything readable. The baseline every degradation is measured against. +PR86_FIXTURES = { + "pr": "pr86-pr.json", + "meta": "pr86-meta.json", + "compare": "pr86-compare.json", + "checks": "pr86-checks.json", + "tree": "pr86-tree.json", + "branch_rules": "rules-branches-launchpad.json", + "closing_refs": "pr86-closing-refs.json", + "review_decision": "pr86-review-decision.json", +} + + +def reads(**overrides: core.Read) -> dict[str, core.Read]: + """PR 86's reads, with any of them replaced. + + Org rulesets default to the forbidden state, because that is the state this + token is actually in — a control that assumed it readable would be testing a + world we do not run in. + """ + built = { + name: core.Read(name, data=fixture(path), endpoint=ENDPOINTS[name]) + for name, path in PR86_FIXTURES.items() + } + built["org_rulesets"] = core.Read( + "org_rulesets", + skip=core.FORBIDDEN, + detail="404 for an organization that exists — this token lacks admin:org", + endpoint=ENDPOINTS["org_rulesets"], + ) + built.update(overrides) + return built + + +def unreadable(name: str, reason: str) -> core.Read: + return core.Read(name, skip=reason, detail=f"forced {reason}", endpoint=ENDPOINTS[name]) + + +# --------------------------------------------------------------------------- # +# The fake runner. Every control that drives the CLI goes through this, so no +# control needs the network and each individual call can be made to fail. +# --------------------------------------------------------------------------- # + + +class FakeGh: + """Answer ``gh`` calls from recorded fixtures, and remember every call made. + + ``fail`` forces one read to a given (returncode, stdout, stderr) so a control + can break exactly one call and leave the other six working — which is the + only way to show that *this* input is the one whose failure is fatal. + """ + + def __init__(self, fail: dict[str, fetch.RunResult] | None = None, payloads: dict[str, object] | None = None): + self.fail = fail or {} + self.payloads = payloads or {} + self.calls: list[list[str]] = [] + self.binaries: list[str] = [] + + #: fixture per read for a fully-readable PR 86 run + FIXTURES = { + "pr": "pr86-pr.json", + "meta": "pr86-meta.json", + "checks": "pr86-checks.json", + "compare": "pr86-compare.json", + "tree": "pr86-tree.json", + "branch_rules": "rules-branches-launchpad.json", + "closing_refs": "pr86-closing-refs.json", + "review_decision": "pr86-review-decision.json", + } + + @staticmethod + def classify(argv: list[str]) -> str: + if argv[1:3] == ["pr", "view"]: + # Two `gh pr view` calls now; the --json fields tell them apart. + return "review_decision" if "reviewDecision" in argv else "meta" + target = argv[2] if len(argv) > 2 else "" + if target == "graphql": + query = " ".join(argv) + if "closingIssuesReferences" in query: + return "closing_refs" + if "statusCheckRollup" in query: + return "checks" + raise AssertionError(f"unrecognised graphql query: {query[:120]}") + if "/compare/" in target: + return "compare" + if "/git/trees/" in target: + return "tree" + if "/rules/branches/" in target: + return "branch_rules" + if target.startswith("orgs/"): + return "org_rulesets" + if "/pulls/" in target: + return "pr" + raise AssertionError(f"the fake runner does not know this call: {argv}") + + def __call__(self, argv: list[str]) -> fetch.RunResult: + self.calls.append(argv) + self.binaries.append(argv[0]) + if argv[0] != "gh": + # Not raising: a control asserts on `binaries`, and a runner that + # threw here would hide the argv from the assertion. + return fetch.RunResult(127, "", f"refusing to spawn {argv[0]!r}") + name = self.classify(argv) + if name in self.fail: + return self.fail[name] + if name in self.payloads: + return fetch.RunResult(0, json.dumps(self.payloads[name]), "") + if name == "org_rulesets": + # The state this token is really in: a 404 that hides access. + return fetch.RunResult(1, "", "gh: Not Found (HTTP 404)") + return fetch.RunResult(0, json.dumps(fixture(self.FIXTURES[name])), "") + + +def run_cli(argv: list[str], runner) -> tuple[int, str, str]: + """Drive the CLI in-process and capture its streams and exit code.""" + out, err = io.StringIO(), io.StringIO() + with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err): + code = fetch.main(argv, runner=runner) + return code, out.getvalue(), err.getvalue() + + +NOT_FOUND = fetch.RunResult(1, "", "gh: Not Found (HTTP 404)") +FORBIDDEN_RESULT = fetch.RunResult(1, "", "gh: Resource not accessible (HTTP 403)") +GARBAGE = fetch.RunResult(0, "502 upstream", "") + + +def rollup_contexts(recorded: dict) -> list[dict]: + """The check contexts inside a recorded GraphQL statusCheckRollup response.""" + commit = recorded["data"]["repository"]["pullRequest"]["commits"]["nodes"][0]["commit"] + return commit["statusCheckRollup"]["contexts"]["nodes"] + + +class FixtureIntegrity(unittest.TestCase): + """STEP 1's done-when, as controls rather than as a command someone ran once. + + These are the properties the later controls lean on. If a re-recording makes + one false, the fixture set has drifted out from under the suite and these + fail first, where the cause is legible. + """ + + def test_every_fixture_parses_as_json(self): + names = sorted(n for n in os.listdir(TESTDATA) if n.endswith(".json")) + self.assertGreaterEqual(len(names), 17, "fixtures are missing; re-run record.sh") + for name in names: + with self.subTest(fixture=name): + fixture(name) + + def test_check_names_collide_so_a_map_would_drop_entries(self): + """The reason the record carries checks as a list, not a name-keyed map.""" + names = [c.get("name") or c.get("context") for c in rollup_contexts(fixture("pr86-checks.json"))] + self.assertGreater(len(names), 1) + duplicated = {n: c for n, c in Counter(names).items() if c > 1} + self.assertIn("check", duplicated, "PR 86's colliding 'check' entries are the point of this fixture") + self.assertGreaterEqual(duplicated["check"], 2) + self.assertLess( + len(set(names)), + len(names), + "a name-keyed map would silently drop the collisions", + ) + + def test_divergent_fixture_base_tip_is_not_the_merge_base(self): + """Without this, no control can tell a three-dot diff from a two-dot one.""" + base_tip = fixture("upstream-divergent-pr.json")["base"]["sha"] + merge_base = fixture("upstream-divergent-compare.json")["merge_base_commit"]["sha"] + self.assertNotEqual( + base_tip, + merge_base, + "the divergent fixture has converged — re-run record.sh to find a live one", + ) + + def test_rules_file_fixtures_cover_both_directions(self): + added = [ + f["filename"] + for f in fixture("pr14-compare.json")["files"] + if f["status"] == "added" and f["filename"].rsplit("/", 1)[-1] in ("AGENTS.md", "CLAUDE.md") + ] + removed = [ + f["filename"] + for f in fixture("prdelete-compare.json")["files"] + if f["status"] == "removed" and f["filename"].rsplit("/", 1)[-1] in ("AGENTS.md", "CLAUDE.md") + ] + self.assertEqual(added, ["launchpad/AGENTS.md"]) + self.assertEqual(removed, ["launchpad/AGENTS.md"]) + + def test_deleted_rules_file_is_absent_from_the_head_tree(self): + """A resolver reading the local worktree passes the add case and fails this.""" + paths = {e["path"] for e in fixture("prdelete-tree.json")["tree"]} + self.assertNotIn("launchpad/AGENTS.md", paths) + self.assertIn("AGENTS.md", paths, "the root file it must fall back to") + + def test_pr86_compare_is_not_an_empty_success(self): + """Recorded by branch name once, which answered 200 OK with zero files.""" + recorded = fixture("pr86-compare.json") + self.assertGreater(len(recorded["files"]), 0, "re-record: compare by base.sha, not branch name") + self.assertNotEqual( + fixture("pr86-pr.json")["base"]["sha"], + recorded["merge_base_commit"]["sha"], + "PR 86's base tip and merge base have reconverged; the two-dot trap needs the upstream fixture", + ) + + def test_truncated_tree_fixture_reports_success(self): + """The trap: a partial tree arrives as HTTP 200 with truncated: true.""" + recorded = fixture("tree-truncated.json") + self.assertTrue(recorded["truncated"]) + + def test_unreadable_fixtures_are_not_empty_successes(self): + self.assertEqual(fixture("pr-notfound.json")["message"], "Not Found") + self.assertEqual(fixture("rules-branches-launchpad.json"), []) + self.assertIn("message", fixture("orgs-rulesets-forbidden.json")) + + def test_markdown_tree_fixture_keeps_a_lookalike_rules_path(self): + """`endswith("AGENTS.md")` is the wrong test, and this fixture proves it.""" + paths = {e["path"] for e in fixture("pr86-tree.json")["tree"]} + self.assertIn("VISION_REMOTE_AGENTS.md", paths) + self.assertIn("AGENTS.md", paths) + + +class RecordShape(unittest.TestCase): + """STEP 2 — the record's fields are a fixed list, and checks stay a list.""" + + def test_top_level_keys_are_exactly_the_seven_enumerated_fields(self): + record = core.build_record(reads()) + self.assertEqual(tuple(record), core.RECORD_FIELDS) + self.assertEqual( + core.RECORD_FIELDS, + ("pr", "closing_issue", "diff", "checks", "required_gate", "nearest_rules", "skips"), + "the enumerated list changed — the module docstring must change with it", + ) + + def test_every_recorded_check_survives_into_the_record(self): + """No de-duplication, no name-keying: 47 in, 47 out, three named `check`.""" + recorded = rollup_contexts(fixture("pr86-checks.json")) + record = core.build_record(reads()) + self.assertEqual(len(record["checks"]), len(recorded)) + self.assertIsInstance(record["checks"], list) + names = [c["name"] for c in record["checks"]] + self.assertEqual(names.count("check"), 3) + self.assertEqual( + len({(c["name"], c["details_url"]) for c in record["checks"]}), + len(recorded), + "checks must stay distinguishable once names collide", + ) + + def test_each_check_carries_the_six_enumerated_keys(self): + for check in core.build_record(reads())["checks"]: + self.assertEqual( + sorted(check), + ["conclusion", "details_url", "name", "required", "status", "workflow"], + ) + + def test_pr_section_carries_the_six_enumerated_keys(self): + pr = core.build_record(reads())["pr"] + self.assertEqual( + sorted(pr), ["base_ref", "body", "head_sha", "labels", "number", "title"] + ) + self.assertEqual(pr["number"], 86) + self.assertEqual(pr["base_ref"], "launchpad") + + def test_status_context_shape_does_not_invent_a_status(self): + """The old commit-status API has one state and no workflow. Say so.""" + node = {"__typename": "StatusContext", "context": "ci/legacy", "state": "SUCCESS", + "targetUrl": "https://example.invalid/1", "isRequired": True} + self.assertEqual( + core._normalise_check(node), + {"name": "ci/legacy", "workflow": None, "status": None, + "conclusion": "SUCCESS", "required": True, + "details_url": "https://example.invalid/1"}, + ) + + def test_an_unenumerated_skip_reason_is_refused(self): + with self.assertRaises(ValueError): + core.Read("pr", skip="probably-fine") + + +class ClosingIssue(unittest.TestCase): + """STEP 4 — GitHub decides what a PR closes; the text supplies the keyword. + + Every control here is driven by a recorded response, and each of the three + closing-refs fixtures exists because the text and GitHub disagree in a + different direction. + """ + + def closing(self, meta: str, refs: str | None) -> dict: + overrides = {"meta": core.Read("meta", data=fixture(meta), endpoint=ENDPOINTS["meta"])} + if refs is not None: + overrides["closing_refs"] = core.Read( + "closing_refs", data=fixture(refs), endpoint="graphql:closingIssuesReferences" + ) + return core.build_record(reads(**overrides))["closing_issue"] + + def test_a_visible_keyword_is_reported_with_the_keyword_used(self): + section = self.closing("pr86-meta.json", "pr86-closing-refs.json") + self.assertTrue(section["present"]) + self.assertEqual(section["keyword"], "Closes") + self.assertEqual(section["source"], "graphql:closingIssuesReferences") + + def test_every_reference_is_collected_not_only_the_first(self): + """PR 86 closes two issues, and the plan's `re.search` would report one. + + This is the control that pins the implementation off first-match: it + reproduces what a single search returns and requires the record to hold + more than that. + """ + section = self.closing("pr86-meta.json", "pr86-closing-refs.json") + self.assertEqual(section["issue_numbers"], [79, 91], "GitHub's answer") + self.assertEqual(section["text_issue_numbers"], [79, 91], "and every keyword in the body") + + body = core.HTML_COMMENT.sub("", fixture("pr86-meta.json")["body"]) + first_only = [int(core.CLOSING_KEYWORD.search(body).group(2))] + self.assertEqual(first_only, [79]) + self.assertNotEqual( + section["text_issue_numbers"], + first_only, + "a first-match regex reports one of the two issues this PR closes", + ) + self.assertFalse(section["text_disagrees"], "on PR 86 the body and GitHub do agree") + + def test_a_keyword_that_closes_nothing_is_not_reported_as_present(self): + """PR 92's base was not the default branch, so merging closes no issue.""" + section = self.closing("pr92-meta.json", "pr92-closing-refs.json") + self.assertFalse(section["present"]) + self.assertEqual(section["issue_numbers"], []) + self.assertTrue(section["text_issue_numbers"], "the body really does carry a keyword") + self.assertTrue(section["text_disagrees"]) + + def test_a_keyword_only_inside_an_html_comment_is_not_present(self): + """An unfilled placeholder closes nothing.""" + section = self.closing("upstream5695-meta.json", "upstream5695-closing-refs.json") + self.assertFalse(section["present"]) + self.assertEqual(section["issue_numbers"], []) + self.assertEqual( + section["text_issue_numbers"], [], "comments are stripped before the text is scanned" + ) + self.assertFalse(section["text_disagrees"], "both halves agree it closes nothing") + self.assertIsNone(section["keyword"]) + + def test_the_commented_out_keyword_is_really_in_the_fixture(self): + """Otherwise the control above passes for the wrong reason.""" + body = fixture("upstream5695-meta.json")["body"] + self.assertRegex(body, r"(?is)` | an unfilled template placeholder is not a closing reference | +| `pr86-review-decision.json` | `reviewDecision` on a PR based on `launchpad` | `REVIEW_REQUIRED` — a review gate exists, readable without `admin:org` | +| `pr92-review-decision.json` | the same field on a PR based on a topic branch | `""` — gh renders GraphQL null as an empty string, and review is not required there | +| `pr-notfound.json` | `GET /pulls/999999` | a 404 body; the exit code is in `pr-notfound.exit` | +| `rules-branches-launchpad.json` | `GET /repos/{o}/{r}/rules/branches/launchpad` | `[]` — readable **and** empty, which is a fact, not a failure | +| `orgs-rulesets-forbidden.json` | `GET /orgs/launchpad-26/rulesets` | this token cannot see org rulesets | +| `upstream-divergent-pr.json` | an upstream PR whose base tip is **ahead** of its fork point | `baseRefOid` is not the merge base | +| `upstream-divergent-compare.json` | its compare response | the merge base a three-dot diff needs | +| `pr14-compare.json`, `pr14-tree.json` | PR 14 — **adds** `launchpad/AGENTS.md` | the rules-file ADD direction, real and merged | +| `prdelete-compare.json`, `prdelete-tree.json` | throwaway PR #142 — **deletes** `launchpad/AGENTS.md` | the DELETE direction (see below) | +| `tree-truncated.json` | a tree past the API's size limit | `truncated: true` arrives with **HTTP 200** | + +## Four things worth knowing + +**Every compare is recorded by SHA, `base.sha...head.sha`, never by branch name.** +A branch name resolves to its tip *today*, and for a merged PR the tip already contains +the head — so `compare/launchpad...{head}` answers `200 OK` with **zero files**: a real +six-file PR rendered as a PR that changed nothing. These fixtures were recorded that way +once, and the empty file list is what caught it. Same class of error as the two-dot trap, +one layer down. + +**The check count moved.** The plan for #116 recorded 24 checks on PR 86 with two named +`check`. On 2026-08-13 both `gh pr view --json statusCheckRollup` and GraphQL report +**47**, with **three** named `check` and 23 names duplicated overall. The property the +fixture exists for is stronger than before, not weaker, and the controls assert against +the recorded fixture rather than a number copied out of the plan. + +**PR 86 has become divergent, which makes it a better fixture than the plan expected.** +Its recorded `base.sha` is `b3db9afbc` while its merge base is `d897a06e8` — `launchpad` +moved 19 commits under it, so the compare reports `status: diverged`, `behind_by: 6`. The +plan for #116 said PR 86 could not exercise merge-base correctness because its base tip +and merge base were the same commit. That is no longer true, so a two-dot implementation +now fails on PR 86 as well as on the deliberately-divergent upstream fixture. + +**The forbidden case is a 404, not a 403.** `GET /orgs/launchpad-26/rulesets` answers +`404 Not Found` for a token without `admin:org` — `launchpad-26` is an Organization +(verified), so the 404 hides access, it does not report absence. A 404 here therefore +cannot be read as "this org has no rulesets", which is exactly why org-level ruleset +visibility is a reported SKIP rather than a silent zero. + +**PR #142 was manufactured, and closed unmerged.** No pull request in this fork's +history and none found upstream deletes a nearest rules file, so the DELETE direction +was recorded from a throwaway PR opened by +[`record-delete-fixture.sh`](record-delete-fixture.sh) and closed minutes later with its +branch deleted. It was never merged and never reviewed. The add case is real (PR 14); +the delete case had to be made, and saying so here is the point — a resolver that reads +the local worktree instead of the PR's head tree passes the add case and fails this one. + +## Two fixtures are projections + +Both are whole recorded responses passed through one documented `jq` filter, because the +whole thing is megabytes of entries nothing reads. Every key of every retained entry is +untouched — only irrelevant entries were dropped, and the filter is in `record.sh`: + +- `pr86-tree.json` — 4337 entries → the 120 `*.md` entries (1.1 MB → 30 KB) +- `tree-truncated.json` — 71798 entries → the first 20 (11 MB → 4 KB); it exists for its + `truncated: true` flag + +`endswith(".md")` is deliberately wider than the rules files themselves: it keeps 116 +markdown paths that are *not* rules files, so a resolver matching on the wrong suffix has +something to trip over — `VISION_REMOTE_AGENTS.md` ends with `AGENTS.md` and is not one. diff --git a/launchpad/scripts/testdata/delete-pr.number b/launchpad/scripts/testdata/delete-pr.number new file mode 100644 index 00000000000..aaacbe66291 --- /dev/null +++ b/launchpad/scripts/testdata/delete-pr.number @@ -0,0 +1 @@ +142 diff --git a/launchpad/scripts/testdata/orgs-rulesets-forbidden.exit b/launchpad/scripts/testdata/orgs-rulesets-forbidden.exit new file mode 100644 index 00000000000..d00491fd7e5 --- /dev/null +++ b/launchpad/scripts/testdata/orgs-rulesets-forbidden.exit @@ -0,0 +1 @@ +1 diff --git a/launchpad/scripts/testdata/orgs-rulesets-forbidden.json b/launchpad/scripts/testdata/orgs-rulesets-forbidden.json new file mode 100644 index 00000000000..19967205192 --- /dev/null +++ b/launchpad/scripts/testdata/orgs-rulesets-forbidden.json @@ -0,0 +1 @@ +{"message":"Not Found","documentation_url":"https://docs.github.com/rest/orgs/rules#get-all-organization-repository-rulesets","status":"404"} \ No newline at end of file diff --git a/launchpad/scripts/testdata/orgs-rulesets-forbidden.stderr b/launchpad/scripts/testdata/orgs-rulesets-forbidden.stderr new file mode 100644 index 00000000000..abe03ebf00a --- /dev/null +++ b/launchpad/scripts/testdata/orgs-rulesets-forbidden.stderr @@ -0,0 +1,2 @@ +gh: Not Found (HTTP 404) +gh: This API operation needs the "admin:org" scope. To request it, run: gh auth refresh -h github.com -s admin:org diff --git a/launchpad/scripts/testdata/pr-notfound.exit b/launchpad/scripts/testdata/pr-notfound.exit new file mode 100644 index 00000000000..d00491fd7e5 --- /dev/null +++ b/launchpad/scripts/testdata/pr-notfound.exit @@ -0,0 +1 @@ +1 diff --git a/launchpad/scripts/testdata/pr-notfound.json b/launchpad/scripts/testdata/pr-notfound.json new file mode 100644 index 00000000000..e0dfe72c10b --- /dev/null +++ b/launchpad/scripts/testdata/pr-notfound.json @@ -0,0 +1 @@ +{"message":"Not Found","documentation_url":"https://docs.github.com/rest/pulls/pulls#get-a-pull-request","status":"404"} \ No newline at end of file diff --git a/launchpad/scripts/testdata/pr-notfound.stderr b/launchpad/scripts/testdata/pr-notfound.stderr new file mode 100644 index 00000000000..124b889f2fb --- /dev/null +++ b/launchpad/scripts/testdata/pr-notfound.stderr @@ -0,0 +1 @@ +gh: Not Found (HTTP 404) diff --git a/launchpad/scripts/testdata/pr14-compare.json b/launchpad/scripts/testdata/pr14-compare.json new file mode 100644 index 00000000000..fef73e70f2b --- /dev/null +++ b/launchpad/scripts/testdata/pr14-compare.json @@ -0,0 +1,121 @@ +{ + "base_commit": { + "sha": "bbcb677a7b37b433dd005447cea7a2f9fe07274c" + }, + "merge_base_commit": { + "sha": "bbcb677a7b37b433dd005447cea7a2f9fe07274c" + }, + "status": "ahead", + "ahead_by": 4, + "behind_by": 0, + "files": [ + { + "filename": ".github/ISSUE_TEMPLATE/01-prd.yml", + "status": "added", + "additions": 78, + "deletions": 0 + }, + { + "filename": ".github/ISSUE_TEMPLATE/02-task.yml", + "status": "added", + "additions": 70, + "deletions": 0 + }, + { + "filename": ".github/ISSUE_TEMPLATE/03-enhancement.yml", + "status": "added", + "additions": 71, + "deletions": 0 + }, + { + "filename": ".github/ISSUE_TEMPLATE/04-bug.yml", + "status": "added", + "additions": 82, + "deletions": 0 + }, + { + "filename": ".github/ISSUE_TEMPLATE/05-adr.yml", + "status": "added", + "additions": 85, + "deletions": 0 + }, + { + "filename": ".github/ISSUE_TEMPLATE/06-agent-workflow.yml", + "status": "added", + "additions": 110, + "deletions": 0 + }, + { + "filename": ".github/ISSUE_TEMPLATE/bug-report.md", + "status": "removed", + "additions": 0, + "deletions": 24 + }, + { + "filename": ".github/ISSUE_TEMPLATE/config.yml", + "status": "modified", + "additions": 11, + "deletions": 1 + }, + { + "filename": ".github/ISSUE_TEMPLATE/feature-request.md", + "status": "removed", + "additions": 0, + "deletions": 21 + }, + { + "filename": ".github/PULL_REQUEST_TEMPLATE.md", + "status": "modified", + "additions": 11, + "deletions": 1 + }, + { + "filename": ".github/workflows/launchpad-issue-check.yml", + "status": "added", + "additions": 89, + "deletions": 0 + }, + { + "filename": ".github/workflows/launchpad-pr-check.yml", + "status": "added", + "additions": 80, + "deletions": 0 + }, + { + "filename": "AGENTS.md", + "status": "modified", + "additions": 22, + "deletions": 0 + }, + { + "filename": "launchpad/AGENTS.md", + "status": "added", + "additions": 244, + "deletions": 0 + }, + { + "filename": "launchpad/AGENT_PR_TEMPLATE.md", + "status": "added", + "additions": 83, + "deletions": 0 + }, + { + "filename": "launchpad/README.md", + "status": "added", + "additions": 154, + "deletions": 0 + }, + { + "filename": "launchpad/labels.yml", + "status": "added", + "additions": 55, + "deletions": 0 + }, + { + "filename": "launchpad/sync-labels.sh", + "status": "added", + "additions": 44, + "deletions": 0 + } + ] +} diff --git a/launchpad/scripts/testdata/pr14-pr.json b/launchpad/scripts/testdata/pr14-pr.json new file mode 100644 index 00000000000..2bbef76a416 --- /dev/null +++ b/launchpad/scripts/testdata/pr14-pr.json @@ -0,0 +1 @@ +{"url":"https://api.github.com/repos/launchpad-26/buzz/pulls/14","id":4243334795,"node_id":"PR_kwDOTv8O78787CaL","html_url":"https://github.com/launchpad-26/buzz/pull/14","diff_url":"https://github.com/launchpad-26/buzz/pull/14.diff","patch_url":"https://github.com/launchpad-26/buzz/pull/14.patch","issue_url":"https://api.github.com/repos/launchpad-26/buzz/issues/14","number":14,"state":"closed","locked":false,"title":"chore(process): issue and PR conventions for human and agent contributors","user":{"login":"tucktuck101","id":70870926,"node_id":"MDQ6VXNlcjcwODcwOTI2","avatar_url":"https://avatars.githubusercontent.com/u/70870926?v=4","gravatar_id":"","url":"https://api.github.com/users/tucktuck101","html_url":"https://github.com/tucktuck101","followers_url":"https://api.github.com/users/tucktuck101/followers","following_url":"https://api.github.com/users/tucktuck101/following{/other_user}","gists_url":"https://api.github.com/users/tucktuck101/gists{/gist_id}","starred_url":"https://api.github.com/users/tucktuck101/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tucktuck101/subscriptions","organizations_url":"https://api.github.com/users/tucktuck101/orgs","repos_url":"https://api.github.com/users/tucktuck101/repos","events_url":"https://api.github.com/users/tucktuck101/events{/privacy}","received_events_url":"https://api.github.com/users/tucktuck101/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"## Summary\n\nReplaces the inherited `block/buzz` issue templates with five forms matched to how this\nfork actually works, and adds a PR path for agent-authored changes that records\nprovenance. Two CI checks validate issue and PR bodies identically whether they came from\nthe web UI or the CLI, so structure holds for humans and agents alike.\n\n### Related issue\n\nCloses #13\n\n### Issue type\n\nPRD\n\n---\n\n### Agent provenance\n\n| Field | Value |\n|---|---|\n| Harness / provider | Claude Code |\n| Model | claude-opus-5[1m] |\n| Session reference | N/A — not exposed by this harness |\n| Initiating human | @tucktuck101 |\n\n### Objective\n\nA complete issue-and-PR convention for `launchpad-26/buzz` that both humans and AI\nagents can follow, with CI enforcing the contract on both paths.\n\n### Impacted components\n\n```\n.github/ISSUE_TEMPLATE/01-prd.yml .. 05-adr.yml five new forms\n.github/ISSUE_TEMPLATE/config.yml blank issues off, advisory + guidance links\n.github/ISSUE_TEMPLATE/bug-report.md removed (pointed at block/buzz)\n.github/ISSUE_TEMPLATE/feature-request.md removed (pointed at block/buzz)\n.github/PULL_REQUEST_TEMPLATE.md one added section\n.github/workflows/launchpad-issue-check.yml new\n.github/workflows/launchpad-pr-check.yml new\nlaunchpad/README.md human entry point\nlaunchpad/AGENTS.md normative spec\nlaunchpad/AGENT_PR_TEMPLATE.md agent PR schema\nlaunchpad/labels.yml label source of truth\nlaunchpad/sync-labels.sh label sync (already run — 14 labels created)\nAGENTS.md root fork-pointer block (CLAUDE.md symlinks here)\n```\n\n### Approach and rejected alternatives\n\n**Rejected — moving the app into `buzz-app/`.** Upstream is ~3,800 files. Git carries\nupstream *edits* through a rename, but every upstream-*added* file lands back at the old\nroot, so every sync needs manual repair. It also breaks root-anchored tooling and strands\n~630 existing fork branches. Chose an additive `launchpad/` namespace instead.\n\n**Rejected — a PR template per issue type.** That needs a `.github/PULL_REQUEST_TEMPLATE/`\ndirectory, which GitHub gives no chooser for; ordinary PRs would get no template at all.\nSplit by **author** instead — agent PRs need provenance, human PRs don't — with the agent\ntemplate living outside `.github/` entirely so nothing is lost on the human path.\n\n**Rejected — seven issue types** (mirroring `tucktuck101/agent-trust-platform`). Its data\nshows 2 of 7 templates unused across 105 issues, and 16 issues carrying multiple type\nlabels. Cut to five, with Epic mapped to a GitHub Milestone.\n\nFull reasoning, with the evidence tables, is in #13.\n\n### Verification\n\nCommand run:\n\n```\nfor f in .github/ISSUE_TEMPLATE/*.yml .github/workflows/launchpad-*.yml launchpad/labels.yml; do\n ruby -ryaml -e \"YAML.safe_load(File.read(ARGV[0]), aliases: true)\" \"$f\" && echo \"OK $f\"\ndone\n```\n\nRaw output:\n\n```\nOK .github/ISSUE_TEMPLATE/01-prd.yml\nOK .github/ISSUE_TEMPLATE/02-task.yml\nOK .github/ISSUE_TEMPLATE/03-enhancement.yml\nOK .github/ISSUE_TEMPLATE/04-bug.yml\nOK .github/ISSUE_TEMPLATE/05-adr.yml\nOK .github/ISSUE_TEMPLATE/config.yml\nOK .github/workflows/launchpad-issue-check.yml\nOK .github/workflows/launchpad-pr-check.yml\nOK launchpad/labels.yml\n```\n\nBoth CI validators were then exercised against crafted good and bad bodies:\n\n```\n=== PR check, bad agent body ===\n - No closing keyword found. Add 'Closes #' so the board updates on merge.\n - by:agent PR missing provenance value for 'Harness / provider'.\n - by:agent PR missing provenance value for 'Model'.\n - by:agent PR missing provenance value for 'Initiating human'.\n - 'Not verified' must name something specific.\n - by:agent PR must paste raw command output in a fenced code block.\nexit=1\n\n=== PR check, good agent body ===\nPR body check passed. (agent)\nexit=0\n\n=== issue check, two type labels ===\n2 type labels (type:task, type:adr). Exactly one is allowed.\n\n=== issue check, task missing sections ===\nMissing section: **Parent PRD**\nMissing section: **Definition of done**\nMissing section: **Impacted components**\nMissing section: **Out of scope**\n\n=== issue check, complete task ===\n(no output — passes)\n\n=== issue check, bug with no raw output ===\nBug reports must paste raw output in a fenced code block, not a paraphrase.\n```\n\nFull local pre-push gate also ran green on this exact tree:\n\n```\n✔️ desktop-check 31.10s\n✔️ desktop-test 421.55s\n✔️ rust-tests 481.50s\n✔️ desktop-tauri-checks 675.58s\n```\n\n- [x] Tests or checks were run and the raw output is pasted above\n- [x] The diff is confined to the scope of the linked issue\n- [x] No secrets, keys, tokens or hostnames were added to tracked files\n\n### Not verified\n\n- **GitHub has never parsed these issue forms.** Ruby's YAML parser accepts them, but\n GitHub's issue-form schema is stricter than generic YAML. If a field type or attribute\n is wrong, GitHub reports it only after merge to the default branch.\n- **Neither CI workflow has executed on GitHub.** The Python inside them was extracted and\n run locally; the surrounding Actions YAML, event triggers and permissions are untested.\n `launchpad-pr-check` should run on this PR — that is its first real execution.\n- **`mobile-test` hook result unknown.** Its output was truncated by my own command; not\n confirmed green.\n- **`branch-skew` guard was bypassed** with `--no-verify`. See Escalations.\n- Templates have not been used by a second person or a different agent harness, so the\n instruction blocks are unproven against anything but this session.\n\n### Security implications\n\nNet reduction. The issue chooser now routes vulnerabilities to a **private security\nadvisory**, and the Bug form requires an explicit non-vulnerability assertion — previously\nblank issues were enabled with no routing at all. The agent PR template requires\nconfirming no secrets or hostnames were added to tracked files, which matters because this\nrepository is public.\n\nBoth workflows declare `contents: read`; the issue check additionally needs `issues: write`\nto apply `needs-triage` and comment. Neither reads secrets, so fork PRs gain nothing by\ntriggering them.\n\nOut of scope and still unresolved: the repository's Actions permissions, fork-PR approval\npolicy, and self-hosted runner inventory cannot be read without admin. That needs\nverifying before prd-03 wires deployment credentials.\n\n### Escalations\n\n1. **`scripts/check-branch-skew.sh` blocked this push and was bypassed with\n `--no-verify`.** The script hardcodes `origin/main` as the PR base. In this fork\n `origin` is a personal fork and the PR base is `launchpad` — against its real base the\n branch is 0 behind, 2 ahead. The flagged overlap was `AGENTS.md`, where upstream\n changed 9 lines mid-file and this branch appends 22 at the end. A human decided the\n bypass; it was not an agent's call. Follow-up filed separately.\n\n2. **Two upstream-owned files are modified, knowingly.** `.github/ISSUE_TEMPLATE/` and one\n section of `.github/PULL_REQUEST_TEMPLATE.md`. These will conflict on future upstream\n merges. Documented in `launchpad/AGENTS.md` §3.\n\n3. **Labels were created before this PR was approved** (`sync-labels.sh`, 14 labels).\n Label creation is a direct write with no PR path, and #13 needed `type:prd` to exist.\n If this PR is rejected the labels are orphaned and need removing by hand.\n","created_at":"2026-08-10T09:17:29Z","updated_at":"2026-08-11T03:37:13Z","closed_at":"2026-08-11T03:37:13Z","merged_at":"2026-08-11T03:37:13Z","merge_commit_sha":"e550184d598ac66d302180ca43309e0259491b2b","assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":11789196488,"node_id":"LA_kwDOTv8O788AAAACvrDcyA","url":"https://api.github.com/repos/launchpad-26/buzz/labels/area:ci","name":"area:ci","color":"bfd4f2","default":false,"description":"GitHub Actions, checks, automation"},{"id":11789196654,"node_id":"LA_kwDOTv8O788AAAACvrDdbg","url":"https://api.github.com/repos/launchpad-26/buzz/labels/area:process","name":"area:process","color":"e4e669","default":false,"description":"How the cohort works: templates, labels, conventions"},{"id":11789196953,"node_id":"LA_kwDOTv8O788AAAACvrDemQ","url":"https://api.github.com/repos/launchpad-26/buzz/labels/by:agent","name":"by:agent","color":"5319e7","default":false,"description":"Filed or authored by an AI agent, not a human"}],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/launchpad-26/buzz/pulls/14/commits","review_comments_url":"https://api.github.com/repos/launchpad-26/buzz/pulls/14/comments","review_comment_url":"https://api.github.com/repos/launchpad-26/buzz/pulls/comments{/number}","comments_url":"https://api.github.com/repos/launchpad-26/buzz/issues/14/comments","statuses_url":"https://api.github.com/repos/launchpad-26/buzz/statuses/f266e81204dff5e1111ff7f4ff58ef431906ab2c","head":{"label":"launchpad-26:launchpad-issue-pr-scaffold","ref":"launchpad-issue-pr-scaffold","sha":"f266e81204dff5e1111ff7f4ff58ef431906ab2c","user":{"login":"launchpad-26","id":309398704,"node_id":"O_kgDOEnEMsA","avatar_url":"https://avatars.githubusercontent.com/u/309398704?v=4","gravatar_id":"","url":"https://api.github.com/users/launchpad-26","html_url":"https://github.com/launchpad-26","followers_url":"https://api.github.com/users/launchpad-26/followers","following_url":"https://api.github.com/users/launchpad-26/following{/other_user}","gists_url":"https://api.github.com/users/launchpad-26/gists{/gist_id}","starred_url":"https://api.github.com/users/launchpad-26/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/launchpad-26/subscriptions","organizations_url":"https://api.github.com/users/launchpad-26/orgs","repos_url":"https://api.github.com/users/launchpad-26/repos","events_url":"https://api.github.com/users/launchpad-26/events{/privacy}","received_events_url":"https://api.github.com/users/launchpad-26/received_events","type":"Organization","user_view_type":"public","site_admin":false},"repo":{"id":1325338351,"node_id":"R_kgDOTv8O7w","name":"buzz","full_name":"launchpad-26/buzz","private":false,"owner":{"login":"launchpad-26","id":309398704,"node_id":"O_kgDOEnEMsA","avatar_url":"https://avatars.githubusercontent.com/u/309398704?v=4","gravatar_id":"","url":"https://api.github.com/users/launchpad-26","html_url":"https://github.com/launchpad-26","followers_url":"https://api.github.com/users/launchpad-26/followers","following_url":"https://api.github.com/users/launchpad-26/following{/other_user}","gists_url":"https://api.github.com/users/launchpad-26/gists{/gist_id}","starred_url":"https://api.github.com/users/launchpad-26/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/launchpad-26/subscriptions","organizations_url":"https://api.github.com/users/launchpad-26/orgs","repos_url":"https://api.github.com/users/launchpad-26/repos","events_url":"https://api.github.com/users/launchpad-26/events{/privacy}","received_events_url":"https://api.github.com/users/launchpad-26/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/launchpad-26/buzz","description":"A hive mind communication platform","fork":true,"url":"https://api.github.com/repos/launchpad-26/buzz","forks_url":"https://api.github.com/repos/launchpad-26/buzz/forks","keys_url":"https://api.github.com/repos/launchpad-26/buzz/keys{/key_id}","collaborators_url":"https://api.github.com/repos/launchpad-26/buzz/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/launchpad-26/buzz/teams","hooks_url":"https://api.github.com/repos/launchpad-26/buzz/hooks","issue_events_url":"https://api.github.com/repos/launchpad-26/buzz/issues/events{/number}","events_url":"https://api.github.com/repos/launchpad-26/buzz/events","assignees_url":"https://api.github.com/repos/launchpad-26/buzz/assignees{/user}","branches_url":"https://api.github.com/repos/launchpad-26/buzz/branches{/branch}","tags_url":"https://api.github.com/repos/launchpad-26/buzz/tags","blobs_url":"https://api.github.com/repos/launchpad-26/buzz/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/launchpad-26/buzz/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/launchpad-26/buzz/git/refs{/sha}","trees_url":"https://api.github.com/repos/launchpad-26/buzz/git/trees{/sha}","statuses_url":"https://api.github.com/repos/launchpad-26/buzz/statuses/{sha}","languages_url":"https://api.github.com/repos/launchpad-26/buzz/languages","stargazers_url":"https://api.github.com/repos/launchpad-26/buzz/stargazers","contributors_url":"https://api.github.com/repos/launchpad-26/buzz/contributors","subscribers_url":"https://api.github.com/repos/launchpad-26/buzz/subscribers","subscription_url":"https://api.github.com/repos/launchpad-26/buzz/subscription","commits_url":"https://api.github.com/repos/launchpad-26/buzz/commits{/sha}","git_commits_url":"https://api.github.com/repos/launchpad-26/buzz/git/commits{/sha}","comments_url":"https://api.github.com/repos/launchpad-26/buzz/comments{/number}","issue_comment_url":"https://api.github.com/repos/launchpad-26/buzz/issues/comments{/number}","contents_url":"https://api.github.com/repos/launchpad-26/buzz/contents/{+path}","compare_url":"https://api.github.com/repos/launchpad-26/buzz/compare/{base}...{head}","merges_url":"https://api.github.com/repos/launchpad-26/buzz/merges","archive_url":"https://api.github.com/repos/launchpad-26/buzz/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/launchpad-26/buzz/downloads","issues_url":"https://api.github.com/repos/launchpad-26/buzz/issues{/number}","pulls_url":"https://api.github.com/repos/launchpad-26/buzz/pulls{/number}","milestones_url":"https://api.github.com/repos/launchpad-26/buzz/milestones{/number}","notifications_url":"https://api.github.com/repos/launchpad-26/buzz/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/launchpad-26/buzz/labels{/name}","releases_url":"https://api.github.com/repos/launchpad-26/buzz/releases{/id}","deployments_url":"https://api.github.com/repos/launchpad-26/buzz/deployments","created_at":"2026-08-06T14:20:40Z","updated_at":"2026-08-12T20:14:46Z","pushed_at":"2026-08-12T20:56:02Z","git_url":"git://github.com/launchpad-26/buzz.git","ssh_url":"git@github.com:launchpad-26/buzz.git","clone_url":"https://github.com/launchpad-26/buzz.git","svn_url":"https://github.com/launchpad-26/buzz","homepage":"","size":473018,"stargazers_count":0,"watchers_count":0,"language":"Rust","has_issues":true,"has_projects":true,"has_downloads":false,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":96,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":[],"visibility":"public","forks":1,"open_issues":96,"watchers":0,"default_branch":"launchpad"}},"base":{"label":"launchpad-26:launchpad","ref":"launchpad","sha":"bbcb677a7b37b433dd005447cea7a2f9fe07274c","user":{"login":"launchpad-26","id":309398704,"node_id":"O_kgDOEnEMsA","avatar_url":"https://avatars.githubusercontent.com/u/309398704?v=4","gravatar_id":"","url":"https://api.github.com/users/launchpad-26","html_url":"https://github.com/launchpad-26","followers_url":"https://api.github.com/users/launchpad-26/followers","following_url":"https://api.github.com/users/launchpad-26/following{/other_user}","gists_url":"https://api.github.com/users/launchpad-26/gists{/gist_id}","starred_url":"https://api.github.com/users/launchpad-26/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/launchpad-26/subscriptions","organizations_url":"https://api.github.com/users/launchpad-26/orgs","repos_url":"https://api.github.com/users/launchpad-26/repos","events_url":"https://api.github.com/users/launchpad-26/events{/privacy}","received_events_url":"https://api.github.com/users/launchpad-26/received_events","type":"Organization","user_view_type":"public","site_admin":false},"repo":{"id":1325338351,"node_id":"R_kgDOTv8O7w","name":"buzz","full_name":"launchpad-26/buzz","private":false,"owner":{"login":"launchpad-26","id":309398704,"node_id":"O_kgDOEnEMsA","avatar_url":"https://avatars.githubusercontent.com/u/309398704?v=4","gravatar_id":"","url":"https://api.github.com/users/launchpad-26","html_url":"https://github.com/launchpad-26","followers_url":"https://api.github.com/users/launchpad-26/followers","following_url":"https://api.github.com/users/launchpad-26/following{/other_user}","gists_url":"https://api.github.com/users/launchpad-26/gists{/gist_id}","starred_url":"https://api.github.com/users/launchpad-26/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/launchpad-26/subscriptions","organizations_url":"https://api.github.com/users/launchpad-26/orgs","repos_url":"https://api.github.com/users/launchpad-26/repos","events_url":"https://api.github.com/users/launchpad-26/events{/privacy}","received_events_url":"https://api.github.com/users/launchpad-26/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/launchpad-26/buzz","description":"A hive mind communication platform","fork":true,"url":"https://api.github.com/repos/launchpad-26/buzz","forks_url":"https://api.github.com/repos/launchpad-26/buzz/forks","keys_url":"https://api.github.com/repos/launchpad-26/buzz/keys{/key_id}","collaborators_url":"https://api.github.com/repos/launchpad-26/buzz/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/launchpad-26/buzz/teams","hooks_url":"https://api.github.com/repos/launchpad-26/buzz/hooks","issue_events_url":"https://api.github.com/repos/launchpad-26/buzz/issues/events{/number}","events_url":"https://api.github.com/repos/launchpad-26/buzz/events","assignees_url":"https://api.github.com/repos/launchpad-26/buzz/assignees{/user}","branches_url":"https://api.github.com/repos/launchpad-26/buzz/branches{/branch}","tags_url":"https://api.github.com/repos/launchpad-26/buzz/tags","blobs_url":"https://api.github.com/repos/launchpad-26/buzz/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/launchpad-26/buzz/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/launchpad-26/buzz/git/refs{/sha}","trees_url":"https://api.github.com/repos/launchpad-26/buzz/git/trees{/sha}","statuses_url":"https://api.github.com/repos/launchpad-26/buzz/statuses/{sha}","languages_url":"https://api.github.com/repos/launchpad-26/buzz/languages","stargazers_url":"https://api.github.com/repos/launchpad-26/buzz/stargazers","contributors_url":"https://api.github.com/repos/launchpad-26/buzz/contributors","subscribers_url":"https://api.github.com/repos/launchpad-26/buzz/subscribers","subscription_url":"https://api.github.com/repos/launchpad-26/buzz/subscription","commits_url":"https://api.github.com/repos/launchpad-26/buzz/commits{/sha}","git_commits_url":"https://api.github.com/repos/launchpad-26/buzz/git/commits{/sha}","comments_url":"https://api.github.com/repos/launchpad-26/buzz/comments{/number}","issue_comment_url":"https://api.github.com/repos/launchpad-26/buzz/issues/comments{/number}","contents_url":"https://api.github.com/repos/launchpad-26/buzz/contents/{+path}","compare_url":"https://api.github.com/repos/launchpad-26/buzz/compare/{base}...{head}","merges_url":"https://api.github.com/repos/launchpad-26/buzz/merges","archive_url":"https://api.github.com/repos/launchpad-26/buzz/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/launchpad-26/buzz/downloads","issues_url":"https://api.github.com/repos/launchpad-26/buzz/issues{/number}","pulls_url":"https://api.github.com/repos/launchpad-26/buzz/pulls{/number}","milestones_url":"https://api.github.com/repos/launchpad-26/buzz/milestones{/number}","notifications_url":"https://api.github.com/repos/launchpad-26/buzz/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/launchpad-26/buzz/labels{/name}","releases_url":"https://api.github.com/repos/launchpad-26/buzz/releases{/id}","deployments_url":"https://api.github.com/repos/launchpad-26/buzz/deployments","created_at":"2026-08-06T14:20:40Z","updated_at":"2026-08-12T20:14:46Z","pushed_at":"2026-08-12T20:56:02Z","git_url":"git://github.com/launchpad-26/buzz.git","ssh_url":"git@github.com:launchpad-26/buzz.git","clone_url":"https://github.com/launchpad-26/buzz.git","svn_url":"https://github.com/launchpad-26/buzz","homepage":"","size":473018,"stargazers_count":0,"watchers_count":0,"language":"Rust","has_issues":true,"has_projects":true,"has_downloads":false,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":96,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":[],"visibility":"public","forks":1,"open_issues":96,"watchers":0,"default_branch":"launchpad"}},"_links":{"self":{"href":"https://api.github.com/repos/launchpad-26/buzz/pulls/14"},"html":{"href":"https://github.com/launchpad-26/buzz/pull/14"},"issue":{"href":"https://api.github.com/repos/launchpad-26/buzz/issues/14"},"comments":{"href":"https://api.github.com/repos/launchpad-26/buzz/issues/14/comments"},"review_comments":{"href":"https://api.github.com/repos/launchpad-26/buzz/pulls/14/comments"},"review_comment":{"href":"https://api.github.com/repos/launchpad-26/buzz/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/launchpad-26/buzz/pulls/14/commits"},"statuses":{"href":"https://api.github.com/repos/launchpad-26/buzz/statuses/f266e81204dff5e1111ff7f4ff58ef431906ab2c"}},"author_association":"MEMBER","auto_merge":null,"assignee":null,"active_lock_reason":null,"merged":true,"mergeable":null,"rebaseable":null,"mergeable_state":"unknown","merged_by":{"login":"tucktuck101","id":70870926,"node_id":"MDQ6VXNlcjcwODcwOTI2","avatar_url":"https://avatars.githubusercontent.com/u/70870926?v=4","gravatar_id":"","url":"https://api.github.com/users/tucktuck101","html_url":"https://github.com/tucktuck101","followers_url":"https://api.github.com/users/tucktuck101/followers","following_url":"https://api.github.com/users/tucktuck101/following{/other_user}","gists_url":"https://api.github.com/users/tucktuck101/gists{/gist_id}","starred_url":"https://api.github.com/users/tucktuck101/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tucktuck101/subscriptions","organizations_url":"https://api.github.com/users/tucktuck101/orgs","repos_url":"https://api.github.com/users/tucktuck101/repos","events_url":"https://api.github.com/users/tucktuck101/events{/privacy}","received_events_url":"https://api.github.com/users/tucktuck101/received_events","type":"User","user_view_type":"public","site_admin":false},"comments":1,"review_comments":0,"maintainer_can_modify":false,"commits":4,"additions":1289,"deletions":47,"changed_files":18} \ No newline at end of file diff --git a/launchpad/scripts/testdata/pr14-tree.json b/launchpad/scripts/testdata/pr14-tree.json new file mode 100644 index 00000000000..2936a339581 --- /dev/null +++ b/launchpad/scripts/testdata/pr14-tree.json @@ -0,0 +1,879 @@ +{ + "sha": "f266e81204dff5e1111ff7f4ff58ef431906ab2c", + "url": "https://api.github.com/repos/launchpad-26/buzz/git/trees/f266e81204dff5e1111ff7f4ff58ef431906ab2c", + "truncated": false, + "tree": [ + { + "path": ".agents/skills/desktop-screenshot/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b", + "size": 65, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b" + }, + { + "path": ".agents/skills/sprout-cli/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "091795a0d44351523ca4e35eac0d74516ab88c25", + "size": 59, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/091795a0d44351523ca4e35eac0d74516ab88c25" + }, + { + "path": ".claude/skills/desktop-screenshot/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b", + "size": 65, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b" + }, + { + "path": ".claude/skills/sprout-cli/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "091795a0d44351523ca4e35eac0d74516ab88c25", + "size": 59, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/091795a0d44351523ca4e35eac0d74516ab88c25" + }, + { + "path": ".codex/skills/desktop-screenshot/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b", + "size": 65, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b" + }, + { + "path": ".codex/skills/sprout-cli/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "091795a0d44351523ca4e35eac0d74516ab88c25", + "size": 59, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/091795a0d44351523ca4e35eac0d74516ab88c25" + }, + { + "path": ".github/PULL_REQUEST_TEMPLATE.md", + "mode": "100644", + "type": "blob", + "sha": "b9adbdda312fa9640c7b16904d9f154fce0cd914", + "size": 578, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/b9adbdda312fa9640c7b16904d9f154fce0cd914" + }, + { + "path": ".goose/skills/desktop-screenshot/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b", + "size": 65, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b" + }, + { + "path": ".goose/skills/sprout-cli/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "091795a0d44351523ca4e35eac0d74516ab88c25", + "size": 59, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/091795a0d44351523ca4e35eac0d74516ab88c25" + }, + { + "path": "AGENTS.md", + "mode": "100644", + "type": "blob", + "sha": "06a3620478920b8f0ec18e67180534a314145962", + "size": 29879, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/06a3620478920b8f0ec18e67180534a314145962" + }, + { + "path": "ARCHITECTURE.md", + "mode": "100644", + "type": "blob", + "sha": "892082d96c6da9bed8a1b7305ea7c7034e5315db", + "size": 45578, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/892082d96c6da9bed8a1b7305ea7c7034e5315db" + }, + { + "path": "CHANGELOG.md", + "mode": "100644", + "type": "blob", + "sha": "171f260d3e9edb12b991f102f6d874cc6cace722", + "size": 245184, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/171f260d3e9edb12b991f102f6d874cc6cace722" + }, + { + "path": "CLAUDE.md", + "mode": "120000", + "type": "blob", + "sha": "47dc3e3d863cfb5727b87d785d09abf9743c0a72", + "size": 9, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/47dc3e3d863cfb5727b87d785d09abf9743c0a72" + }, + { + "path": "CODE_OF_CONDUCT.md", + "mode": "100644", + "type": "blob", + "sha": "a2bc369a3cf426b62e24666c73e3d7c70eab62f3", + "size": 5496, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/a2bc369a3cf426b62e24666c73e3d7c70eab62f3" + }, + { + "path": "CONTRIBUTING.md", + "mode": "100644", + "type": "blob", + "sha": "db0aea637fea3a6fda23165d44d57fad350fe517", + "size": 19053, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/db0aea637fea3a6fda23165d44d57fad350fe517" + }, + { + "path": "GOVERNANCE.md", + "mode": "100644", + "type": "blob", + "sha": "4af3958eedafeccc3669161f3cd8bbe443dfa0f6", + "size": 127, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/4af3958eedafeccc3669161f3cd8bbe443dfa0f6" + }, + { + "path": "NOSTR.md", + "mode": "100644", + "type": "blob", + "sha": "cce70f2f77f8aa0ebf77783a5d3edf9a1f6ea6a9", + "size": 19578, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/cce70f2f77f8aa0ebf77783a5d3edf9a1f6ea6a9" + }, + { + "path": "README.md", + "mode": "100644", + "type": "blob", + "sha": "56439f00bc1565ce7aca273a886eecdc12df5c1d", + "size": 15945, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/56439f00bc1565ce7aca273a886eecdc12df5c1d" + }, + { + "path": "RELEASING.md", + "mode": "100644", + "type": "blob", + "sha": "53d580556191ad40f2197e3797cfe536087b1e34", + "size": 15023, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/53d580556191ad40f2197e3797cfe536087b1e34" + }, + { + "path": "SECURITY.md", + "mode": "100644", + "type": "blob", + "sha": "09ea73022b311594060dabde5d291c09865ffbf3", + "size": 5316, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/09ea73022b311594060dabde5d291c09865ffbf3" + }, + { + "path": "TESTING.md", + "mode": "100644", + "type": "blob", + "sha": "764b86d408c3edc0c3f04e0277ac5f4f225b407f", + "size": 16473, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/764b86d408c3edc0c3f04e0277ac5f4f225b407f" + }, + { + "path": "VISION.md", + "mode": "100644", + "type": "blob", + "sha": "66a106bdebc5e751ee39babb56056d0d7572cf1a", + "size": 12934, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/66a106bdebc5e751ee39babb56056d0d7572cf1a" + }, + { + "path": "VISION_ACTIVITY.md", + "mode": "100644", + "type": "blob", + "sha": "d1d1e0ac5c22b5a990bb18e15d433ea4553fc8ef", + "size": 5208, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/d1d1e0ac5c22b5a990bb18e15d433ea4553fc8ef" + }, + { + "path": "VISION_AGENT.md", + "mode": "100644", + "type": "blob", + "sha": "2b2597c8d2b542241f19e6e7cfff110c97188935", + "size": 4501, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/2b2597c8d2b542241f19e6e7cfff110c97188935" + }, + { + "path": "VISION_MESH.md", + "mode": "100644", + "type": "blob", + "sha": "d1354d50fee00cf7de0ea981c58973c406ec69f7", + "size": 4607, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/d1354d50fee00cf7de0ea981c58973c406ec69f7" + }, + { + "path": "VISION_MODERATION.md", + "mode": "100644", + "type": "blob", + "sha": "45d3ab86fd03c2f3dfd8d89374a663a47e00d45b", + "size": 7378, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/45d3ab86fd03c2f3dfd8d89374a663a47e00d45b" + }, + { + "path": "VISION_PROJECTS.md", + "mode": "100644", + "type": "blob", + "sha": "8601b87829eb3743671c1ed7cbebba42902a8d86", + "size": 15808, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/8601b87829eb3743671c1ed7cbebba42902a8d86" + }, + { + "path": "VISION_REMOTE_AGENTS.md", + "mode": "100644", + "type": "blob", + "sha": "4b187f355a5dd65ab7b7b1b598c62bead7a7e804", + "size": 8922, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/4b187f355a5dd65ab7b7b1b598c62bead7a7e804" + }, + { + "path": "VISION_SOVEREIGN.md", + "mode": "100644", + "type": "blob", + "sha": "77362b3f7e2f4cf9cc8b61837c5125c55e797244", + "size": 13541, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/77362b3f7e2f4cf9cc8b61837c5125c55e797244" + }, + { + "path": "benchmarks/harbor-buzz-orchestra/README.md", + "mode": "100644", + "type": "blob", + "sha": "0358c954e7d7b78c9a7bc05bec43c436059a6df0", + "size": 6342, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/0358c954e7d7b78c9a7bc05bec43c436059a6df0" + }, + { + "path": "benchmarks/harbor-buzz-orchestra/personas/orchestrator-m1.md", + "mode": "100644", + "type": "blob", + "sha": "d248806e4ec3414df6aa07fb0d2a8479f0560a7c", + "size": 1936, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/d248806e4ec3414df6aa07fb0d2a8479f0560a7c" + }, + { + "path": "benchmarks/harbor-buzz-orchestra/personas/orchestrator-tb.md", + "mode": "100644", + "type": "blob", + "sha": "c1dbd7b79578f2b0cabafc610f8b6bddbd8aedc2", + "size": 2424, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/c1dbd7b79578f2b0cabafc610f8b6bddbd8aedc2" + }, + { + "path": "benchmarks/harbor-buzz-orchestra/personas/worker-m1.md", + "mode": "100644", + "type": "blob", + "sha": "442d7b874d0f293ebe5b6f73813793b26fee4a6b", + "size": 1777, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/442d7b874d0f293ebe5b6f73813793b26fee4a6b" + }, + { + "path": "benchmarks/harbor-buzz-orchestra/personas/worker-tb.md", + "mode": "100644", + "type": "blob", + "sha": "49a9125a17bb9885ce31c0e3edb9e4766fe5b838", + "size": 1684, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/49a9125a17bb9885ce31c0e3edb9e4766fe5b838" + }, + { + "path": "benchmarks/harbor-buzz-orchestra/testbed/endpoints/README.md", + "mode": "100644", + "type": "blob", + "sha": "f8d2a560bf275232d3c81e722563001ff5bd6865", + "size": 1190, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/f8d2a560bf275232d3c81e722563001ff5bd6865" + }, + { + "path": "bin/README.hermit.md", + "mode": "100644", + "type": "blob", + "sha": "e889550ba4cbc92a720527042e5f6e7a303bfcbc", + "size": 270, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/e889550ba4cbc92a720527042e5f6e7a303bfcbc" + }, + { + "path": "crates/buzz-acp/README.md", + "mode": "100644", + "type": "blob", + "sha": "e6164b02dd3ef1e0d9d63571b7f863fb7ade07d9", + "size": 18083, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/e6164b02dd3ef1e0d9d63571b7f863fb7ade07d9" + }, + { + "path": "crates/buzz-acp/src/base_prompt.md", + "mode": "100644", + "type": "blob", + "sha": "1d85221f113e41f8a098fa13efaad413b8788512", + "size": 15007, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1d85221f113e41f8a098fa13efaad413b8788512" + }, + { + "path": "crates/buzz-agent/README.md", + "mode": "100644", + "type": "blob", + "sha": "5d942777d5e5b0b38a767d56ae7ec7b2bb505aad", + "size": 22677, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/5d942777d5e5b0b38a767d56ae7ec7b2bb505aad" + }, + { + "path": "crates/buzz-backend-kubernetes/tests/fixtures/provider-wire/README.md", + "mode": "100644", + "type": "blob", + "sha": "56a972f89420cebfcb74cfd73598d9ff90c9a7f1", + "size": 2323, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/56a972f89420cebfcb74cfd73598d9ff90c9a7f1" + }, + { + "path": "crates/buzz-cli/README.md", + "mode": "100644", + "type": "blob", + "sha": "a2dcdce6d2104b1ba6ddef3bcb1ccac467fc8e3f", + "size": 6553, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/a2dcdce6d2104b1ba6ddef3bcb1ccac467fc8e3f" + }, + { + "path": "crates/buzz-cli/TESTING.md", + "mode": "100644", + "type": "blob", + "sha": "77234b7faab6966bdf6429da9f17fa05a05463a9", + "size": 20746, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/77234b7faab6966bdf6429da9f17fa05a05463a9" + }, + { + "path": "crates/buzz-conformance/LIMITS.md", + "mode": "100644", + "type": "blob", + "sha": "37f0f28d3ee728f5ff633784bed6bbbe791ad6ec", + "size": 5767, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/37f0f28d3ee728f5ff633784bed6bbbe791ad6ec" + }, + { + "path": "crates/buzz-conformance/TRACE_SCHEMA.md", + "mode": "100644", + "type": "blob", + "sha": "c1dca674d559e5daf58576cb305ef9b9251b6fde", + "size": 6969, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/c1dca674d559e5daf58576cb305ef9b9251b6fde" + }, + { + "path": "crates/buzz-core/src/pairing/NIP-AB.md", + "mode": "100644", + "type": "blob", + "sha": "fc8f69245e80fb710e95694ea28dbe286047da6d", + "size": 54727, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/fc8f69245e80fb710e95694ea28dbe286047da6d" + }, + { + "path": "crates/buzz-media/tests/fixtures/android/README.md", + "mode": "100644", + "type": "blob", + "sha": "b0b06bb0607d985b5c0a9e7d6c4360b55131cf17", + "size": 3067, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/b0b06bb0607d985b5c0a9e7d6c4360b55131cf17" + }, + { + "path": "crates/buzz-media/tests/fixtures/ios/README.md", + "mode": "100644", + "type": "blob", + "sha": "2f6d5f5e256a3f0e9e40508c1668ae150a09e61e", + "size": 2209, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/2f6d5f5e256a3f0e9e40508c1668ae150a09e61e" + }, + { + "path": "crates/buzz-pairing-cli/README.md", + "mode": "100644", + "type": "blob", + "sha": "4f9fd1e530943ce732c22244725522bb210ab4b4", + "size": 5016, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/4f9fd1e530943ce732c22244725522bb210ab4b4" + }, + { + "path": "crates/buzz-persona/PERSONA_PACK_SPEC.md", + "mode": "100644", + "type": "blob", + "sha": "cb3a7d1c05a74495c929a677f6dc2125561cb409", + "size": 47257, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/cb3a7d1c05a74495c929a677f6dc2125561cb409" + }, + { + "path": "crates/buzz-relay/CHANGELOG.md", + "mode": "100644", + "type": "blob", + "sha": "c0d6ab9ccd56d8f3565c1604978cf36a6af799db", + "size": 7745, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/c0d6ab9ccd56d8f3565c1604978cf36a6af799db" + }, + { + "path": "crates/git-credential-nostr/README.md", + "mode": "100644", + "type": "blob", + "sha": "827f8d55a330d508cc870113d680b981acd1f696", + "size": 2482, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/827f8d55a330d508cc870113d680b981acd1f696" + }, + { + "path": "crates/git-sign-nostr/README.md", + "mode": "100644", + "type": "blob", + "sha": "908682fd7fe4e037e0f8e64812db8d234f86bbd8", + "size": 1345, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/908682fd7fe4e037e0f8e64812db8d234f86bbd8" + }, + { + "path": "deploy/charts/buzz/README.md", + "mode": "100644", + "type": "blob", + "sha": "b2778df28b57d11f3ec5a179a9b1b442079e7903", + "size": 12342, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/b2778df28b57d11f3ec5a179a9b1b442079e7903" + }, + { + "path": "deploy/compose/README.md", + "mode": "100644", + "type": "blob", + "sha": "bb0e63fe15d1c540fe0fe6fa76a25045df36952a", + "size": 2338, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/bb0e63fe15d1c540fe0fe6fa76a25045df36952a" + }, + { + "path": "desktop/README.md", + "mode": "100644", + "type": "blob", + "sha": "d53698a9d04fca2636b0475d126acb4f7c152730", + "size": 627, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/d53698a9d04fca2636b0475d126acb4f7c152730" + }, + { + "path": "desktop/public/harness-logos/CREDITS.md", + "mode": "100644", + "type": "blob", + "sha": "716c43e1ae32eca14f4ec19daf0b60c3751fa580", + "size": 3597, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/716c43e1ae32eca14f4ec19daf0b60c3751fa580" + }, + { + "path": "desktop/scripts/texture-card/README.md", + "mode": "100644", + "type": "blob", + "sha": "1de2addce99cf2a340013a83ca82e4a0a4511a54", + "size": 1014, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1de2addce99cf2a340013a83ca82e4a0a4511a54" + }, + { + "path": "desktop/src-tauri/resources/pocket-voices/NOTICE.md", + "mode": "100644", + "type": "blob", + "sha": "9cc515dea3c733f540a707c34f94d273261700dc", + "size": 2133, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/9cc515dea3c733f540a707c34f94d273261700dc" + }, + { + "path": "desktop/src-tauri/src/managed_agents/nest_agents.md", + "mode": "100644", + "type": "blob", + "sha": "7cb7489b85291e0d2cf9d72eb5b1a9fd878e12c7", + "size": 3620, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/7cb7489b85291e0d2cf9d72eb5b1a9fd878e12c7" + }, + { + "path": "desktop/src-tauri/src/managed_agents/nest_skill.md", + "mode": "100644", + "type": "blob", + "sha": "79a5ea301d467444fafe53bb137b2c5f86be58b3", + "size": 11014, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/79a5ea301d467444fafe53bb137b2c5f86be58b3" + }, + { + "path": "desktop/src-tauri/src/managed_agents/screenshot_skill.md", + "mode": "100644", + "type": "blob", + "sha": "361e747fe28b62e3ae80772cccf8e35eb4c46e69", + "size": 3726, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/361e747fe28b62e3ae80772cccf8e35eb4c46e69" + }, + { + "path": "desktop/src/features/agents/AGENTS.md", + "mode": "100644", + "type": "blob", + "sha": "b929dbb613190e384866b10ae16025cdd9eefbed", + "size": 13613, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/b929dbb613190e384866b10ae16025cdd9eefbed" + }, + { + "path": "docs/MCP_DRIVEN_HOOKS.md", + "mode": "100644", + "type": "blob", + "sha": "6812a5b9a6c47d536b0d315d90eb81579991dac8", + "size": 4775, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/6812a5b9a6c47d536b0d315d90eb81579991dac8" + }, + { + "path": "docs/admin/README.md", + "mode": "100644", + "type": "blob", + "sha": "e51566fb292ee430f61010065b4c6e04a6f851db", + "size": 3174, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/e51566fb292ee430f61010065b4c6e04a6f851db" + }, + { + "path": "docs/bridge-channel-window.md", + "mode": "100644", + "type": "blob", + "sha": "42f1d82835e176e7e438a5b75c1020c2ce67751b", + "size": 6250, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/42f1d82835e176e7e438a5b75c1020c2ce67751b" + }, + { + "path": "docs/buzz-entity-links.md", + "mode": "100644", + "type": "blob", + "sha": "df32037898e93c608d7e474f6eb925ca2c81d4ab", + "size": 12939, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/df32037898e93c608d7e474f6eb925ca2c81d4ab" + }, + { + "path": "docs/buzz-shared-compute-dev.md", + "mode": "100644", + "type": "blob", + "sha": "3712bb7965d9f4cdbcb18b6016888508a463eafb", + "size": 6842, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/3712bb7965d9f4cdbcb18b6016888508a463eafb" + }, + { + "path": "docs/formal/STATEFUL_GATEWAY.md", + "mode": "100644", + "type": "blob", + "sha": "77b472619a59d053b9d3318264eba8fa7f43f9de", + "size": 1268, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/77b472619a59d053b9d3318264eba8fa7f43f9de" + }, + { + "path": "docs/formal/nip-pl/NOTE.md", + "mode": "100644", + "type": "blob", + "sha": "0cb70a54ad91d31d64c49bdb57b9d2ee464123db", + "size": 2196, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/0cb70a54ad91d31d64c49bdb57b9d2ee464123db" + }, + { + "path": "docs/formal/nip-rs-unread/NOTE.md", + "mode": "100644", + "type": "blob", + "sha": "7472c3dca3a93c33793e98b01266173723ba9383", + "size": 39649, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/7472c3dca3a93c33793e98b01266173723ba9383" + }, + { + "path": "docs/git-on-object-storage.md", + "mode": "100644", + "type": "blob", + "sha": "1d87720edc7fed73de31151fe6cf36141c163999", + "size": 36714, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1d87720edc7fed73de31151fe6cf36141c163999" + }, + { + "path": "docs/linux-rendering-troubleshooting.md", + "mode": "100644", + "type": "blob", + "sha": "1e09ef1aca519e92fa6f0e0972be5d66f482f459", + "size": 6775, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1e09ef1aca519e92fa6f0e0972be5d66f482f459" + }, + { + "path": "docs/multi-tenant-conformance.md", + "mode": "100644", + "type": "blob", + "sha": "3cd56066eb6d260ea307c5eae8ad94a5bdfaba8f", + "size": 16743, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/3cd56066eb6d260ea307c5eae8ad94a5bdfaba8f" + }, + { + "path": "docs/multi-tenant-relay.md", + "mode": "100644", + "type": "blob", + "sha": "85de728b12c63df1f8b1aa00fa8cf4bf282c2bea", + "size": 74264, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/85de728b12c63df1f8b1aa00fa8cf4bf282c2bea" + }, + { + "path": "docs/nips/NIP-AA.md", + "mode": "100644", + "type": "blob", + "sha": "920ee7851087e716058a44eda97d2060c00522a9", + "size": 22346, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/920ee7851087e716058a44eda97d2060c00522a9" + }, + { + "path": "docs/nips/NIP-AE.md", + "mode": "100644", + "type": "blob", + "sha": "c24078d8a45672a282d9e2f7f0069f6fa4356d8d", + "size": 21677, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/c24078d8a45672a282d9e2f7f0069f6fa4356d8d" + }, + { + "path": "docs/nips/NIP-AM.md", + "mode": "100644", + "type": "blob", + "sha": "794a6722d54550a0786921e0580f70b3b3e6e14c", + "size": 13973, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/794a6722d54550a0786921e0580f70b3b3e6e14c" + }, + { + "path": "docs/nips/NIP-AO.md", + "mode": "100644", + "type": "blob", + "sha": "36adea04871c830e38717c1badf149150b7b781e", + "size": 9972, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/36adea04871c830e38717c1badf149150b7b781e" + }, + { + "path": "docs/nips/NIP-AP.md", + "mode": "100644", + "type": "blob", + "sha": "ad23d92282dbba3ba9ee4d60e4d8ebf6e1150c9d", + "size": 25710, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/ad23d92282dbba3ba9ee4d60e4d8ebf6e1150c9d" + }, + { + "path": "docs/nips/NIP-CW.md", + "mode": "100644", + "type": "blob", + "sha": "f21b63405b2d240677207a6696813bd5a0844364", + "size": 21151, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/f21b63405b2d240677207a6696813bd5a0844364" + }, + { + "path": "docs/nips/NIP-DV.md", + "mode": "100644", + "type": "blob", + "sha": "2efba0ba659c14c7ef43590b9e79bae79513e43d", + "size": 11390, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/2efba0ba659c14c7ef43590b9e79bae79513e43d" + }, + { + "path": "docs/nips/NIP-ER.md", + "mode": "100644", + "type": "blob", + "sha": "f384ff7ef6c9c01611a86937aad341ed328f3d07", + "size": 19511, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/f384ff7ef6c9c01611a86937aad341ed328f3d07" + }, + { + "path": "docs/nips/NIP-GS.md", + "mode": "100644", + "type": "blob", + "sha": "7ef37bcc276f3566a994db5861178b16db641ba7", + "size": 38515, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/7ef37bcc276f3566a994db5861178b16db641ba7" + }, + { + "path": "docs/nips/NIP-IA.md", + "mode": "100644", + "type": "blob", + "sha": "28b2cfa52d479f4f28465a9fbe0b2f5db9d6b390", + "size": 39959, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/28b2cfa52d479f4f28465a9fbe0b2f5db9d6b390" + }, + { + "path": "docs/nips/NIP-MP.md", + "mode": "100644", + "type": "blob", + "sha": "14936d2a7cd38affdc8002e3b92785048ca8c967", + "size": 41745, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/14936d2a7cd38affdc8002e3b92785048ca8c967" + }, + { + "path": "docs/nips/NIP-OA.md", + "mode": "100644", + "type": "blob", + "sha": "d148841440effe97533deab1471f9cfb5dfdbae9", + "size": 9096, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/d148841440effe97533deab1471f9cfb5dfdbae9" + }, + { + "path": "docs/nips/NIP-PL.md", + "mode": "100644", + "type": "blob", + "sha": "08fa6ca34d9b504b8a19819d489fe0446c5ff85d", + "size": 47748, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/08fa6ca34d9b504b8a19819d489fe0446c5ff85d" + }, + { + "path": "docs/nips/NIP-PMA.md", + "mode": "100644", + "type": "blob", + "sha": "82592b1eca059755cfddefc49ef7bf8b161737e0", + "size": 5870, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/82592b1eca059755cfddefc49ef7bf8b161737e0" + }, + { + "path": "docs/nips/NIP-RS.md", + "mode": "100644", + "type": "blob", + "sha": "6a095df60ab672f09df192ab3dcbc69c6b69df1e", + "size": 73928, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/6a095df60ab672f09df192ab3dcbc69c6b69df1e" + }, + { + "path": "docs/nips/NIP-WP.md", + "mode": "100644", + "type": "blob", + "sha": "ebaf612de9d2ffe703468593c55854304993e8fb", + "size": 6741, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/ebaf612de9d2ffe703468593c55854304993e8fb" + }, + { + "path": "docs/push-gateway-deployment.md", + "mode": "100644", + "type": "blob", + "sha": "63c63355a1140ba0d1a50dafb70f36380f2246ad", + "size": 13855, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/63c63355a1140ba0d1a50dafb70f36380f2246ad" + }, + { + "path": "docs/remote-agents.md", + "mode": "100644", + "type": "blob", + "sha": "45289ef910af7379a20b66f91ece0e49c3ed4aaa", + "size": 114647, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/45289ef910af7379a20b66f91ece0e49c3ed4aaa" + }, + { + "path": "docs/welcome-kickoff-silent-failures.md", + "mode": "100644", + "type": "blob", + "sha": "58a4a5fa9580dc5cb1453edc9ee35d295e5c5fe7", + "size": 27131, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/58a4a5fa9580dc5cb1453edc9ee35d295e5c5fe7" + }, + { + "path": "examples/README.md", + "mode": "100644", + "type": "blob", + "sha": "8649ba360ddad538a5940499ebf153c17db6ac3d", + "size": 912, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/8649ba360ddad538a5940499ebf153c17db6ac3d" + }, + { + "path": "examples/countdown-bot/README.md", + "mode": "100644", + "type": "blob", + "sha": "09b3c135200a12b6f8330c483f4773bb943722b0", + "size": 4025, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/09b3c135200a12b6f8330c483f4773bb943722b0" + }, + { + "path": "examples/meadow-core/README.md", + "mode": "100644", + "type": "blob", + "sha": "3eedd1391eace6b6ac32224ef60a66ece9f104cc", + "size": 1767, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/3eedd1391eace6b6ac32224ef60a66ece9f104cc" + }, + { + "path": "examples/meadow-core/agents/bana.persona.md", + "mode": "100644", + "type": "blob", + "sha": "19b8b975ac7794e8fd72968112aa5a7f51e53e73", + "size": 1557, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/19b8b975ac7794e8fd72968112aa5a7f51e53e73" + }, + { + "path": "examples/meadow-core/agents/lev.persona.md", + "mode": "100644", + "type": "blob", + "sha": "5963eedd563d56763ab648f0d3c8fde411959916", + "size": 1509, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/5963eedd563d56763ab648f0d3c8fde411959916" + }, + { + "path": "examples/meadow-core/agents/skip.persona.md", + "mode": "100644", + "type": "blob", + "sha": "a3897991a4548afa1dae190866974e2883b1f262", + "size": 1458, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/a3897991a4548afa1dae190866974e2883b1f262" + }, + { + "path": "examples/meadow-core/instructions.md", + "mode": "100644", + "type": "blob", + "sha": "4517467c0cfe4a27701d315f0189dd5f19db1e7f", + "size": 538, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/4517467c0cfe4a27701d315f0189dd5f19db1e7f" + }, + { + "path": "examples/meadow-core/skills/github-research/SKILL.md", + "mode": "100644", + "type": "blob", + "sha": "b940e55a890d9ee3b5de9bb95f81d16e70717b65", + "size": 1445, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/b940e55a890d9ee3b5de9bb95f81d16e70717b65" + }, + { + "path": "launchpad/AGENTS.md", + "mode": "100644", + "type": "blob", + "sha": "f2183732930e627cbe33823eb849a23aee694771", + "size": 10253, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/f2183732930e627cbe33823eb849a23aee694771" + }, + { + "path": "launchpad/AGENT_PR_TEMPLATE.md", + "mode": "100644", + "type": "blob", + "sha": "501f37ef40fa427294e8269870c7f31451c85c06", + "size": 3258, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/501f37ef40fa427294e8269870c7f31451c85c06" + }, + { + "path": "launchpad/README.md", + "mode": "100644", + "type": "blob", + "sha": "f46e384c75001dea5ec9035d4e57ac506d800a3f", + "size": 6384, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/f46e384c75001dea5ec9035d4e57ac506d800a3f" + }, + { + "path": "mobile/CHANGELOG.md", + "mode": "100644", + "type": "blob", + "sha": "08421f4d9d009fe4ba2ca3a74c3e991b5d7db723", + "size": 6785, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/08421f4d9d009fe4ba2ca3a74c3e991b5d7db723" + }, + { + "path": "mobile/README.md", + "mode": "100644", + "type": "blob", + "sha": "ada0568e28a21626ba1da6c40f8ab3749627820f", + "size": 3663, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/ada0568e28a21626ba1da6c40f8ab3749627820f" + }, + { + "path": "mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md", + "mode": "100644", + "type": "blob", + "sha": "89c2725b70f1882be97f5214fafe22d27a0ec01e", + "size": 336, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/89c2725b70f1882be97f5214fafe22d27a0ec01e" + }, + { + "path": "perf/RELAY_BUS_SCALING.md", + "mode": "100644", + "type": "blob", + "sha": "835ab4fe5e9eef35486a0591551413eab08d3055", + "size": 3043, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/835ab4fe5e9eef35486a0591551413eab08d3055" + }, + { + "path": "scripts/cutover/README.md", + "mode": "100644", + "type": "blob", + "sha": "27388bdaa2b95418ac93b7baf130ea178eaa4f48", + "size": 3836, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/27388bdaa2b95418ac93b7baf130ea178eaa4f48" + } + ] +} diff --git a/launchpad/scripts/testdata/pr86-checks.json b/launchpad/scripts/testdata/pr86-checks.json new file mode 100644 index 00000000000..7dd2c267f90 --- /dev/null +++ b/launchpad/scripts/testdata/pr86-checks.json @@ -0,0 +1 @@ +{"data":{"repository":{"pullRequest":{"commits":{"nodes":[{"commit":{"oid":"4434519667afac207f8eeb3950cc5c5de726addd","statusCheckRollup":{"state":"SUCCESS","contexts":{"totalCount":47,"nodes":[{"__typename":"CheckRun","name":"Detect Changed Paths","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93960883611","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Detect Changed Paths","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94106260706","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"check","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778356/job/93960882777","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"launchpad — PR body check"}}}},{"__typename":"CheckRun","name":"check","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546827551/job/93961029417","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"launchpad — PR body check"}}}},{"__typename":"CheckRun","name":"check","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358289/job/94106260649","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"launchpad — PR body check"}}}},{"__typename":"CheckRun","name":"Dead Token Reference Guard","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93960883632","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Dead Token Reference Guard","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94106260798","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Rust Lint","status":"COMPLETED","conclusion":"SKIPPED","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93960992873","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Rust Lint","status":"COMPLETED","conclusion":"SKIPPED","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94106374983","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Unit Tests","status":"COMPLETED","conclusion":"SKIPPED","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93960992438","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Unit Tests","status":"COMPLETED","conclusion":"SKIPPED","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94106375289","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop Core","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93960991714","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop Core","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94106373879","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop Smoke E2E (1)","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93960991744","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop Smoke E2E (1)","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94106373996","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop Smoke E2E (2)","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93960991770","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop Smoke E2E (2)","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94106373897","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop Smoke E2E (3)","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93960991780","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop Smoke E2E (3)","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94106374020","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop Smoke E2E (4)","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93960991788","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop Smoke E2E (4)","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94106373911","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop E2E Relay","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93960991704","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop E2E Relay","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94106373956","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Web","status":"COMPLETED","conclusion":"SKIPPED","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93960992473","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Web","status":"COMPLETED","conclusion":"SKIPPED","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94106375323","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Mobile","status":"COMPLETED","conclusion":"SKIPPED","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93960992451","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Mobile","status":"COMPLETED","conclusion":"SKIPPED","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94106374664","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Security","status":"COMPLETED","conclusion":"SKIPPED","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93960992555","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Security","status":"COMPLETED","conclusion":"SKIPPED","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94106375247","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Server Cross-Compile","status":"COMPLETED","conclusion":"SKIPPED","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93960992727","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Server Cross-Compile","status":"COMPLETED","conclusion":"SKIPPED","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94106375406","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Windows Rust (x86_64-pc-windows-msvc)","status":"COMPLETED","conclusion":"SKIPPED","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93960992531","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Windows Rust (x86_64-pc-windows-msvc)","status":"COMPLETED","conclusion":"SKIPPED","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94106374677","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop Build (macOS)","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93960991795","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop Build (macOS)","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94106373982","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93965676204","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94112668059","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop E2E Integration (1/2)","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93961062041","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop E2E Integration (1/2)","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94106454367","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop E2E Integration (2/2)","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93961062037","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop E2E Integration (2/2)","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94106454251","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Backend Integration (relay e2e)","status":"COMPLETED","conclusion":"SKIPPED","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93961062699","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Backend Integration (relay e2e)","status":"COMPLETED","conclusion":"SKIPPED","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94106455324","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Relay E2E","status":"COMPLETED","conclusion":"SKIPPED","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93961063123","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Relay E2E","status":"COMPLETED","conclusion":"SKIPPED","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94106455311","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop E2E Integration","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31546778365/job/93962365660","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}},{"__typename":"CheckRun","name":"Desktop E2E Integration","status":"COMPLETED","conclusion":"SUCCESS","detailsUrl":"https://github.com/launchpad-26/buzz/actions/runs/31594358290/job/94108260171","isRequired":false,"checkSuite":{"workflowRun":{"workflow":{"name":"CI"}}}}]}}}}]}}}}} \ No newline at end of file diff --git a/launchpad/scripts/testdata/pr86-closing-refs.json b/launchpad/scripts/testdata/pr86-closing-refs.json new file mode 100644 index 00000000000..aeddda1f2eb --- /dev/null +++ b/launchpad/scripts/testdata/pr86-closing-refs.json @@ -0,0 +1 @@ +{"data":{"repository":{"pullRequest":{"closingIssuesReferences":{"nodes":[{"number":79},{"number":91}]}}}}} \ No newline at end of file diff --git a/launchpad/scripts/testdata/pr86-compare.json b/launchpad/scripts/testdata/pr86-compare.json new file mode 100644 index 00000000000..b4a44f3601c --- /dev/null +++ b/launchpad/scripts/testdata/pr86-compare.json @@ -0,0 +1 @@ +{"url":"https://api.github.com/repos/launchpad-26/buzz/compare/b3db9afbc8054693f6412ad1872c23b394ac12b5...4434519667afac207f8eeb3950cc5c5de726addd","html_url":"https://github.com/launchpad-26/buzz/compare/b3db9afbc8054693f6412ad1872c23b394ac12b5...4434519667afac207f8eeb3950cc5c5de726addd","permalink_url":"https://github.com/launchpad-26/buzz/compare/launchpad-26:b3db9af...launchpad-26:4434519","diff_url":"https://github.com/launchpad-26/buzz/compare/b3db9afbc8054693f6412ad1872c23b394ac12b5...4434519667afac207f8eeb3950cc5c5de726addd.diff","patch_url":"https://github.com/launchpad-26/buzz/compare/b3db9afbc8054693f6412ad1872c23b394ac12b5...4434519667afac207f8eeb3950cc5c5de726addd.patch","base_commit":{"sha":"b3db9afbc8054693f6412ad1872c23b394ac12b5","node_id":"C_kwDOTv8O79oAKGIzZGI5YWZiYzgwNTQ2OTNmNjQxMmFkMTg3MmMyM2IzOTRhYzEyYjU","commit":{"author":{"name":"tucktuck101","email":"jeffreytaylorrobertson@gmail.com","date":"2026-08-12T09:21:23Z"},"committer":{"name":"tucktuck101","email":"70870926+tucktuck101@users.noreply.github.com","date":"2026-08-12T11:57:04Z"},"message":"docs(deploy): archive failed VPS deployment method\n\nSigned-off-by: tucktuck101 ","tree":{"sha":"27ee64ba4a6fde875bb72ac7e0e5bb268acb1799","url":"https://api.github.com/repos/launchpad-26/buzz/git/trees/27ee64ba4a6fde875bb72ac7e0e5bb268acb1799"},"url":"https://api.github.com/repos/launchpad-26/buzz/git/commits/b3db9afbc8054693f6412ad1872c23b394ac12b5","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null,"verified_at":null}},"url":"https://api.github.com/repos/launchpad-26/buzz/commits/b3db9afbc8054693f6412ad1872c23b394ac12b5","html_url":"https://github.com/launchpad-26/buzz/commit/b3db9afbc8054693f6412ad1872c23b394ac12b5","comments_url":"https://api.github.com/repos/launchpad-26/buzz/commits/b3db9afbc8054693f6412ad1872c23b394ac12b5/comments","author":{"login":"tucktuck101","id":70870926,"node_id":"MDQ6VXNlcjcwODcwOTI2","avatar_url":"https://avatars.githubusercontent.com/u/70870926?v=4","gravatar_id":"","url":"https://api.github.com/users/tucktuck101","html_url":"https://github.com/tucktuck101","followers_url":"https://api.github.com/users/tucktuck101/followers","following_url":"https://api.github.com/users/tucktuck101/following{/other_user}","gists_url":"https://api.github.com/users/tucktuck101/gists{/gist_id}","starred_url":"https://api.github.com/users/tucktuck101/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tucktuck101/subscriptions","organizations_url":"https://api.github.com/users/tucktuck101/orgs","repos_url":"https://api.github.com/users/tucktuck101/repos","events_url":"https://api.github.com/users/tucktuck101/events{/privacy}","received_events_url":"https://api.github.com/users/tucktuck101/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"tucktuck101","id":70870926,"node_id":"MDQ6VXNlcjcwODcwOTI2","avatar_url":"https://avatars.githubusercontent.com/u/70870926?v=4","gravatar_id":"","url":"https://api.github.com/users/tucktuck101","html_url":"https://github.com/tucktuck101","followers_url":"https://api.github.com/users/tucktuck101/followers","following_url":"https://api.github.com/users/tucktuck101/following{/other_user}","gists_url":"https://api.github.com/users/tucktuck101/gists{/gist_id}","starred_url":"https://api.github.com/users/tucktuck101/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tucktuck101/subscriptions","organizations_url":"https://api.github.com/users/tucktuck101/orgs","repos_url":"https://api.github.com/users/tucktuck101/repos","events_url":"https://api.github.com/users/tucktuck101/events{/privacy}","received_events_url":"https://api.github.com/users/tucktuck101/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"2dea5214bbbe36bed002de88385faaa15853e4e8","url":"https://api.github.com/repos/launchpad-26/buzz/commits/2dea5214bbbe36bed002de88385faaa15853e4e8","html_url":"https://github.com/launchpad-26/buzz/commit/2dea5214bbbe36bed002de88385faaa15853e4e8"}]},"merge_base_commit":{"sha":"d897a06e8ef5b72f63480f4a7044d086a3c4199e","node_id":"C_kwDOTv8O79oAKGQ4OTdhMDZlOGVmNWI3MmY2MzQ4MGY0YTcwNDRkMDg2YTNjNDE5OWU","commit":{"author":{"name":"tucktuck101","email":"jeffreytaylorrobertson@gmail.com","date":"2026-08-10T13:36:33Z"},"committer":{"name":"tucktuck101","email":"70870926+tucktuck101@users.noreply.github.com","date":"2026-08-11T04:52:47Z"},"message":"docs(process): record the four prd-02 decisions already taken in sub-issues\n\nFour of prd-02's (#4) seven open questions were answered inside its sub-issues months\nbefore any ADR issue existed for them, so the decisions were discoverable only by\nreading a task body. This writes them down.\n\nADR-0001 handbook repo location + publication target (decided in #6) Closes #54\nADR-0002 source repository scope (decided in #10) Closes #55\nADR-0003 page provenance contract (decided in #7) Closes #56\nADR-0004 staleness detection mechanism (decided in #11) Closes #57\n\nThese ratify decisions humans already made; nothing here is a new choice. Each record\nnames where the decision was actually taken. The remaining ADR issues stay open with\ntheir outcome blank.\n\nAlso adds launchpad/decisions/README.md, since the folder had no stated format or\nnumbering rule and the first four records would otherwise set it by accident.\n\nDoes not touch .github/ISSUE_TEMPLATE/ or any file in PR #14, so the two do not\nconflict.\n\nSigned-off-by: tucktuck101 ","tree":{"sha":"4050eb665c3f60988645dbb414a6ee120075b90e","url":"https://api.github.com/repos/launchpad-26/buzz/git/trees/4050eb665c3f60988645dbb414a6ee120075b90e"},"url":"https://api.github.com/repos/launchpad-26/buzz/git/commits/d897a06e8ef5b72f63480f4a7044d086a3c4199e","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null,"verified_at":null}},"url":"https://api.github.com/repos/launchpad-26/buzz/commits/d897a06e8ef5b72f63480f4a7044d086a3c4199e","html_url":"https://github.com/launchpad-26/buzz/commit/d897a06e8ef5b72f63480f4a7044d086a3c4199e","comments_url":"https://api.github.com/repos/launchpad-26/buzz/commits/d897a06e8ef5b72f63480f4a7044d086a3c4199e/comments","author":{"login":"tucktuck101","id":70870926,"node_id":"MDQ6VXNlcjcwODcwOTI2","avatar_url":"https://avatars.githubusercontent.com/u/70870926?v=4","gravatar_id":"","url":"https://api.github.com/users/tucktuck101","html_url":"https://github.com/tucktuck101","followers_url":"https://api.github.com/users/tucktuck101/followers","following_url":"https://api.github.com/users/tucktuck101/following{/other_user}","gists_url":"https://api.github.com/users/tucktuck101/gists{/gist_id}","starred_url":"https://api.github.com/users/tucktuck101/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tucktuck101/subscriptions","organizations_url":"https://api.github.com/users/tucktuck101/orgs","repos_url":"https://api.github.com/users/tucktuck101/repos","events_url":"https://api.github.com/users/tucktuck101/events{/privacy}","received_events_url":"https://api.github.com/users/tucktuck101/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"tucktuck101","id":70870926,"node_id":"MDQ6VXNlcjcwODcwOTI2","avatar_url":"https://avatars.githubusercontent.com/u/70870926?v=4","gravatar_id":"","url":"https://api.github.com/users/tucktuck101","html_url":"https://github.com/tucktuck101","followers_url":"https://api.github.com/users/tucktuck101/followers","following_url":"https://api.github.com/users/tucktuck101/following{/other_user}","gists_url":"https://api.github.com/users/tucktuck101/gists{/gist_id}","starred_url":"https://api.github.com/users/tucktuck101/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tucktuck101/subscriptions","organizations_url":"https://api.github.com/users/tucktuck101/orgs","repos_url":"https://api.github.com/users/tucktuck101/repos","events_url":"https://api.github.com/users/tucktuck101/events{/privacy}","received_events_url":"https://api.github.com/users/tucktuck101/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"a629c630a78cf7b1522c4170aab7491bafd9c025","url":"https://api.github.com/repos/launchpad-26/buzz/commits/a629c630a78cf7b1522c4170aab7491bafd9c025","html_url":"https://github.com/launchpad-26/buzz/commit/a629c630a78cf7b1522c4170aab7491bafd9c025"}]},"status":"diverged","ahead_by":12,"behind_by":6,"total_commits":12,"commits":[{"sha":"d598d3d55f0147e33cfbd3b7bf0d8df1a5d7ab8d","node_id":"C_kwDOTv8O79oAKGQ1OThkM2Q1NWYwMTQ3ZTMzY2ZiZDNiN2JmMGQ4ZGYxYTVkN2FiOGQ","commit":{"author":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T07:16:13Z"},"committer":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T07:16:13Z"},"message":"docs(launchpad): add the cohort vision and mission\n\nSigned-off-by: Serina Mcfall ","tree":{"sha":"b722654db20d196d015d85be15909a375089494c","url":"https://api.github.com/repos/launchpad-26/buzz/git/trees/b722654db20d196d015d85be15909a375089494c"},"url":"https://api.github.com/repos/launchpad-26/buzz/git/commits/d598d3d55f0147e33cfbd3b7bf0d8df1a5d7ab8d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null,"verified_at":null}},"url":"https://api.github.com/repos/launchpad-26/buzz/commits/d598d3d55f0147e33cfbd3b7bf0d8df1a5d7ab8d","html_url":"https://github.com/launchpad-26/buzz/commit/d598d3d55f0147e33cfbd3b7bf0d8df1a5d7ab8d","comments_url":"https://api.github.com/repos/launchpad-26/buzz/commits/d598d3d55f0147e33cfbd3b7bf0d8df1a5d7ab8d/comments","author":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"d897a06e8ef5b72f63480f4a7044d086a3c4199e","url":"https://api.github.com/repos/launchpad-26/buzz/commits/d897a06e8ef5b72f63480f4a7044d086a3c4199e","html_url":"https://github.com/launchpad-26/buzz/commit/d897a06e8ef5b72f63480f4a7044d086a3c4199e"}]},{"sha":"4e967bbf7e07b598fbe642e0e9d4d2b889f1465b","node_id":"C_kwDOTv8O79oAKDRlOTY3YmJmN2UwN2I1OThmYmU2NDJlMGU5ZDRkMmI4ODlmMTQ2NWI","commit":{"author":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T07:27:01Z"},"committer":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T07:27:01Z"},"message":"docs(launchpad): mark unratified vision claims as proposed\n\nSigned-off-by: Serina Mcfall ","tree":{"sha":"ffd5e1213cccf4c2d7b7b524d465ccce37e46d8e","url":"https://api.github.com/repos/launchpad-26/buzz/git/trees/ffd5e1213cccf4c2d7b7b524d465ccce37e46d8e"},"url":"https://api.github.com/repos/launchpad-26/buzz/git/commits/4e967bbf7e07b598fbe642e0e9d4d2b889f1465b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null,"verified_at":null}},"url":"https://api.github.com/repos/launchpad-26/buzz/commits/4e967bbf7e07b598fbe642e0e9d4d2b889f1465b","html_url":"https://github.com/launchpad-26/buzz/commit/4e967bbf7e07b598fbe642e0e9d4d2b889f1465b","comments_url":"https://api.github.com/repos/launchpad-26/buzz/commits/4e967bbf7e07b598fbe642e0e9d4d2b889f1465b/comments","author":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"d598d3d55f0147e33cfbd3b7bf0d8df1a5d7ab8d","url":"https://api.github.com/repos/launchpad-26/buzz/commits/d598d3d55f0147e33cfbd3b7bf0d8df1a5d7ab8d","html_url":"https://github.com/launchpad-26/buzz/commit/d598d3d55f0147e33cfbd3b7bf0d8df1a5d7ab8d"}]},{"sha":"33c6123735252fb3ff1ae02f13add009a7a88e09","node_id":"C_kwDOTv8O79oAKDMzYzYxMjM3MzUyNTJmYjNmZjFhZTAyZjEzYWRkMDA5YTdhODhlMDk","commit":{"author":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T07:37:55Z"},"committer":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T07:37:55Z"},"message":"docs(launchpad): add the target architecture\n\nSigned-off-by: Serina Mcfall ","tree":{"sha":"c456d17752172f9337d799260befbc37e5bc66ca","url":"https://api.github.com/repos/launchpad-26/buzz/git/trees/c456d17752172f9337d799260befbc37e5bc66ca"},"url":"https://api.github.com/repos/launchpad-26/buzz/git/commits/33c6123735252fb3ff1ae02f13add009a7a88e09","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null,"verified_at":null}},"url":"https://api.github.com/repos/launchpad-26/buzz/commits/33c6123735252fb3ff1ae02f13add009a7a88e09","html_url":"https://github.com/launchpad-26/buzz/commit/33c6123735252fb3ff1ae02f13add009a7a88e09","comments_url":"https://api.github.com/repos/launchpad-26/buzz/commits/33c6123735252fb3ff1ae02f13add009a7a88e09/comments","author":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"4e967bbf7e07b598fbe642e0e9d4d2b889f1465b","url":"https://api.github.com/repos/launchpad-26/buzz/commits/4e967bbf7e07b598fbe642e0e9d4d2b889f1465b","html_url":"https://github.com/launchpad-26/buzz/commit/4e967bbf7e07b598fbe642e0e9d4d2b889f1465b"}]},{"sha":"75e204a54f271df3ce631e4f1032c836f7bb9e6c","node_id":"C_kwDOTv8O79oAKDc1ZTIwNGE1NGYyNzFkZjNjZTYzMWU0ZjEwMzJjODM2ZjdiYjllNmM","commit":{"author":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T07:50:00Z"},"committer":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T07:50:00Z"},"message":"docs(launchpad): add the environments document\n\nSigned-off-by: Serina Mcfall ","tree":{"sha":"0aa54c34b5346b42e9d97d4d321c412fc1797378","url":"https://api.github.com/repos/launchpad-26/buzz/git/trees/0aa54c34b5346b42e9d97d4d321c412fc1797378"},"url":"https://api.github.com/repos/launchpad-26/buzz/git/commits/75e204a54f271df3ce631e4f1032c836f7bb9e6c","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null,"verified_at":null}},"url":"https://api.github.com/repos/launchpad-26/buzz/commits/75e204a54f271df3ce631e4f1032c836f7bb9e6c","html_url":"https://github.com/launchpad-26/buzz/commit/75e204a54f271df3ce631e4f1032c836f7bb9e6c","comments_url":"https://api.github.com/repos/launchpad-26/buzz/commits/75e204a54f271df3ce631e4f1032c836f7bb9e6c/comments","author":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"33c6123735252fb3ff1ae02f13add009a7a88e09","url":"https://api.github.com/repos/launchpad-26/buzz/commits/33c6123735252fb3ff1ae02f13add009a7a88e09","html_url":"https://github.com/launchpad-26/buzz/commit/33c6123735252fb3ff1ae02f13add009a7a88e09"}]},{"sha":"d5da6a9d17aa166fb05bfa7b065827ec347ce532","node_id":"C_kwDOTv8O79oAKGQ1ZGE2YTlkMTdhYTE2NmZiMDViZmE3YjA2NTgyN2VjMzQ3Y2U1MzI","commit":{"author":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T08:22:04Z"},"committer":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T08:22:04Z"},"message":"docs(launchpad): add the security posture and its known gaps\n\nSigned-off-by: Serina Mcfall ","tree":{"sha":"ce0293f3d114e8eae42dfa0221a504c03fa01b46","url":"https://api.github.com/repos/launchpad-26/buzz/git/trees/ce0293f3d114e8eae42dfa0221a504c03fa01b46"},"url":"https://api.github.com/repos/launchpad-26/buzz/git/commits/d5da6a9d17aa166fb05bfa7b065827ec347ce532","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null,"verified_at":null}},"url":"https://api.github.com/repos/launchpad-26/buzz/commits/d5da6a9d17aa166fb05bfa7b065827ec347ce532","html_url":"https://github.com/launchpad-26/buzz/commit/d5da6a9d17aa166fb05bfa7b065827ec347ce532","comments_url":"https://api.github.com/repos/launchpad-26/buzz/commits/d5da6a9d17aa166fb05bfa7b065827ec347ce532/comments","author":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"75e204a54f271df3ce631e4f1032c836f7bb9e6c","url":"https://api.github.com/repos/launchpad-26/buzz/commits/75e204a54f271df3ce631e4f1032c836f7bb9e6c","html_url":"https://github.com/launchpad-26/buzz/commit/75e204a54f271df3ce631e4f1032c836f7bb9e6c"}]},{"sha":"ee41bc846735beef8d087c7185459b55cf993b2d","node_id":"C_kwDOTv8O79oAKGVlNDFiYzg0NjczNWJlZWY4ZDA4N2M3MTg1NDU5YjU1Y2Y5OTNiMmQ","commit":{"author":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T08:32:56Z"},"committer":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T08:32:56Z"},"message":"docs(launchpad): correct overstated implemented markers\n\nSigned-off-by: Serina Mcfall ","tree":{"sha":"96359b4042fb1468341f2c9a892d35b63c170f7a","url":"https://api.github.com/repos/launchpad-26/buzz/git/trees/96359b4042fb1468341f2c9a892d35b63c170f7a"},"url":"https://api.github.com/repos/launchpad-26/buzz/git/commits/ee41bc846735beef8d087c7185459b55cf993b2d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null,"verified_at":null}},"url":"https://api.github.com/repos/launchpad-26/buzz/commits/ee41bc846735beef8d087c7185459b55cf993b2d","html_url":"https://github.com/launchpad-26/buzz/commit/ee41bc846735beef8d087c7185459b55cf993b2d","comments_url":"https://api.github.com/repos/launchpad-26/buzz/commits/ee41bc846735beef8d087c7185459b55cf993b2d/comments","author":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"d5da6a9d17aa166fb05bfa7b065827ec347ce532","url":"https://api.github.com/repos/launchpad-26/buzz/commits/d5da6a9d17aa166fb05bfa7b065827ec347ce532","html_url":"https://github.com/launchpad-26/buzz/commit/d5da6a9d17aa166fb05bfa7b065827ec347ce532"}]},{"sha":"38027eedba1641a1afb0ec3dde3857ff77cc52b0","node_id":"C_kwDOTv8O79oAKDM4MDI3ZWVkYmExNjQxYTFhZmIwZWMzZGRlMzg1N2ZmNzdjYzUyYjA","commit":{"author":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T08:41:26Z"},"committer":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T08:41:26Z"},"message":"docs(launchpad): align the agents-md rule marker with the others\n\nSigned-off-by: Serina Mcfall ","tree":{"sha":"bdb3c141109fdfa9237b0d7f7cd8666b22b4f802","url":"https://api.github.com/repos/launchpad-26/buzz/git/trees/bdb3c141109fdfa9237b0d7f7cd8666b22b4f802"},"url":"https://api.github.com/repos/launchpad-26/buzz/git/commits/38027eedba1641a1afb0ec3dde3857ff77cc52b0","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null,"verified_at":null}},"url":"https://api.github.com/repos/launchpad-26/buzz/commits/38027eedba1641a1afb0ec3dde3857ff77cc52b0","html_url":"https://github.com/launchpad-26/buzz/commit/38027eedba1641a1afb0ec3dde3857ff77cc52b0","comments_url":"https://api.github.com/repos/launchpad-26/buzz/commits/38027eedba1641a1afb0ec3dde3857ff77cc52b0/comments","author":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"ee41bc846735beef8d087c7185459b55cf993b2d","url":"https://api.github.com/repos/launchpad-26/buzz/commits/ee41bc846735beef8d087c7185459b55cf993b2d","html_url":"https://github.com/launchpad-26/buzz/commit/ee41bc846735beef8d087c7185459b55cf993b2d"}]},{"sha":"04db0c81465b927d54dc22a70c8d75082872b592","node_id":"C_kwDOTv8O79oAKDA0ZGIwYzgxNDY1YjkyN2Q1NGRjMjJhNzBjOGQ3NTA4Mjg3MmI1OTI","commit":{"author":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T08:49:15Z"},"committer":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T08:49:15Z"},"message":"docs(launchpad): correct a misquotation of issue 62\n\nSigned-off-by: Serina Mcfall ","tree":{"sha":"b3514b378f3faf86845455bb350187a609205872","url":"https://api.github.com/repos/launchpad-26/buzz/git/trees/b3514b378f3faf86845455bb350187a609205872"},"url":"https://api.github.com/repos/launchpad-26/buzz/git/commits/04db0c81465b927d54dc22a70c8d75082872b592","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null,"verified_at":null}},"url":"https://api.github.com/repos/launchpad-26/buzz/commits/04db0c81465b927d54dc22a70c8d75082872b592","html_url":"https://github.com/launchpad-26/buzz/commit/04db0c81465b927d54dc22a70c8d75082872b592","comments_url":"https://api.github.com/repos/launchpad-26/buzz/commits/04db0c81465b927d54dc22a70c8d75082872b592/comments","author":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"38027eedba1641a1afb0ec3dde3857ff77cc52b0","url":"https://api.github.com/repos/launchpad-26/buzz/commits/38027eedba1641a1afb0ec3dde3857ff77cc52b0","html_url":"https://github.com/launchpad-26/buzz/commit/38027eedba1641a1afb0ec3dde3857ff77cc52b0"}]},{"sha":"4010165da553292d753a387a1a8d8bbf064ea0dc","node_id":"C_kwDOTv8O79oAKDQwMTAxNjVkYTU1MzI5MmQ3NTNhMzg3YTFhOGQ4YmJmMDY0ZWEwZGM","commit":{"author":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T09:16:07Z"},"committer":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T09:16:07Z"},"message":"docs(launchpad): list the newly raised observability and testing decisions\n\nSigned-off-by: Serina Mcfall ","tree":{"sha":"efee4c8c018284a161ba49f854e6b165bccac6c8","url":"https://api.github.com/repos/launchpad-26/buzz/git/trees/efee4c8c018284a161ba49f854e6b165bccac6c8"},"url":"https://api.github.com/repos/launchpad-26/buzz/git/commits/4010165da553292d753a387a1a8d8bbf064ea0dc","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null,"verified_at":null}},"url":"https://api.github.com/repos/launchpad-26/buzz/commits/4010165da553292d753a387a1a8d8bbf064ea0dc","html_url":"https://github.com/launchpad-26/buzz/commit/4010165da553292d753a387a1a8d8bbf064ea0dc","comments_url":"https://api.github.com/repos/launchpad-26/buzz/commits/4010165da553292d753a387a1a8d8bbf064ea0dc/comments","author":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"04db0c81465b927d54dc22a70c8d75082872b592","url":"https://api.github.com/repos/launchpad-26/buzz/commits/04db0c81465b927d54dc22a70c8d75082872b592","html_url":"https://github.com/launchpad-26/buzz/commit/04db0c81465b927d54dc22a70c8d75082872b592"}]},{"sha":"493531a7f931cea88f986b36f59d9728467a7b22","node_id":"C_kwDOTv8O79oAKDQ5MzUzMWE3ZjkzMWNlYTg4Zjk4NmIzNmY1OWQ5NzI4NDY3YTdiMjI","commit":{"author":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T09:21:49Z"},"committer":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T09:21:49Z"},"message":"docs(launchpad): quote issue 43 verbatim in the rhizomorph section\n\nSigned-off-by: Serina Mcfall ","tree":{"sha":"f9368fcbbc122a3a3c05a999aadba6ebb0c440bd","url":"https://api.github.com/repos/launchpad-26/buzz/git/trees/f9368fcbbc122a3a3c05a999aadba6ebb0c440bd"},"url":"https://api.github.com/repos/launchpad-26/buzz/git/commits/493531a7f931cea88f986b36f59d9728467a7b22","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null,"verified_at":null}},"url":"https://api.github.com/repos/launchpad-26/buzz/commits/493531a7f931cea88f986b36f59d9728467a7b22","html_url":"https://github.com/launchpad-26/buzz/commit/493531a7f931cea88f986b36f59d9728467a7b22","comments_url":"https://api.github.com/repos/launchpad-26/buzz/commits/493531a7f931cea88f986b36f59d9728467a7b22/comments","author":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"4010165da553292d753a387a1a8d8bbf064ea0dc","url":"https://api.github.com/repos/launchpad-26/buzz/commits/4010165da553292d753a387a1a8d8bbf064ea0dc","html_url":"https://github.com/launchpad-26/buzz/commit/4010165da553292d753a387a1a8d8bbf064ea0dc"}]},{"sha":"30eecbc4050a4114c09ed9511c712abe7f0c2810","node_id":"C_kwDOTv8O79oAKDMwZWVjYmM0MDUwYTQxMTRjMDllZDk1MTFjNzEyYWJlN2YwYzI4MTA","commit":{"author":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T09:43:07Z"},"committer":{"name":"Serina Mcfall","email":"serina.mcfall@gmail.com","date":"2026-08-11T09:43:07Z"},"message":"docs(launchpad): make citations followable and reachable from the README\n\nVISION.md defined the rule that a citation must be a link a reader can\nfollow, then cited milestones and AGENTS.md as bare text. Its milestone and\nAGENTS.md citations are now links, matching the other documents in the set.\n\nThe constraint that no secrets, keys, tokens, private hostnames or member\nrosters appear in tracked files read as a verified property of the\nrepository. Nothing verifies it. The row now marks the rule's binding force\nand links SECURITY-POSTURE.md, which owns the bound.\n\nThe four documents were unreachable from launchpad/README.md, so nothing\nlinked in to them; they are now rows in its directory table.\n\nThe agent toolset row in ARCHITECTURE.md dropped 'todo' and folded the buzz\nCLI into the toolset. Corrected against its cited source.\n\nARCHITECTURE.md drew no datastore, while ENVIRONMENTS.md makes the\ncommunities table load-bearing in every environment. Added a cohort-owned\ndatastore row, marked OPEN, with component names taken from #5's Evidence\nsection.\n\nSigned-off-by: Serina Mcfall ","tree":{"sha":"efa929483186225f2f595259d222d808a91aa3d8","url":"https://api.github.com/repos/launchpad-26/buzz/git/trees/efa929483186225f2f595259d222d808a91aa3d8"},"url":"https://api.github.com/repos/launchpad-26/buzz/git/commits/30eecbc4050a4114c09ed9511c712abe7f0c2810","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null,"verified_at":null}},"url":"https://api.github.com/repos/launchpad-26/buzz/commits/30eecbc4050a4114c09ed9511c712abe7f0c2810","html_url":"https://github.com/launchpad-26/buzz/commit/30eecbc4050a4114c09ed9511c712abe7f0c2810","comments_url":"https://api.github.com/repos/launchpad-26/buzz/commits/30eecbc4050a4114c09ed9511c712abe7f0c2810/comments","author":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"493531a7f931cea88f986b36f59d9728467a7b22","url":"https://api.github.com/repos/launchpad-26/buzz/commits/493531a7f931cea88f986b36f59d9728467a7b22","html_url":"https://github.com/launchpad-26/buzz/commit/493531a7f931cea88f986b36f59d9728467a7b22"}]},{"sha":"4434519667afac207f8eeb3950cc5c5de726addd","node_id":"C_kwDOTv8O79oAKDQ0MzQ1MTk2NjdhZmFjMjA3ZjhlZWIzOTUwY2M1YzVkZTcyNmFkZGQ","commit":{"author":{"name":"Serina McFall","email":"serina.mcfall@gmail.com","date":"2026-08-11T23:31:04Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2026-08-11T23:31:04Z"},"message":"docs(launchpad): add the cohort business and non-functional requirements (#92)\n\nBusiness and non-functional requirements as identified, prioritised, traceable\nand verifiable objects. Structured following ISO/IEC/IEEE 29148:2018 without\nclaiming conformance.\n\nCloses #91\n\nSigned-off-by: Serina Mcfall ","tree":{"sha":"8e0214a3fb8fad59956ef93642cc60a279dfe5be","url":"https://api.github.com/repos/launchpad-26/buzz/git/trees/8e0214a3fb8fad59956ef93642cc60a279dfe5be"},"url":"https://api.github.com/repos/launchpad-26/buzz/git/commits/4434519667afac207f8eeb3950cc5c5de726addd","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJqe7C4CRC1aQ7uu5UhlAAAjz0QAGFliW7lvakNw0KOJYXeXwB3\n52gD5+cN3WwKiJNn0yXqNGm8CPKi3M6kV+EviaU6PVBzx5fAOGlB4XXq8FI80GAu\nSH/qxco50CCUJ1QTKMR59Ds7Zot/FX9EbA/yatqA4KzvnmMwN6LRcFrWAiklCHW7\nMcqXafEmUZ167vQMG4wq/flzoEcWl2LiTHBvKzT2pkLPsXgQip4hdlWLDrq/3T2z\npvs5kLvhFRaFWeVgG7mJa7/bF8W9nQOA5JG9zuXPzOTZjU+DN6VrJUm+tDUovrnp\neQR/oEqUhHCwI+OzR0rJhOpHVZneyjw9EO8PL70Pr0OL8ts0KvNjajgG76HyU5Yk\nV/Vm9ih22cKO00Qcow2UvseR/32bTiR9XX2smGRDx179KbmCEg28550gXFpAyExB\nFqtK5iwWkVDzfI3qJrE/Z7tJO8JKdCJzOKPAu2mYF5mB/jtO1020K1bGytrXD6bV\nz0rrDQnFy6KXp+NbVSy1BwIf7537mEEWdVs2DRLBzxYrqe7u+Dx+GEGP+FtZbel+\nMMaQYwJROTJqmxqL9lx/5NKPf7rIlRQKWxZsrUJVqBdHqAZpjIEoP9Q4YFSYy2ME\nPQ9ZdKkSfnJZwFifOy6krPtTWOGB1kmQtpPC7tUkqyJhmgSp7afERRbboqeeL49x\n8MnZRgslyihoE+2ebhvn\n=EW1z\n-----END PGP SIGNATURE-----\n","payload":"tree 8e0214a3fb8fad59956ef93642cc60a279dfe5be\nparent 30eecbc4050a4114c09ed9511c712abe7f0c2810\nauthor Serina McFall 1786491064 +1200\ncommitter GitHub 1786491064 +1200\n\ndocs(launchpad): add the cohort business and non-functional requirements (#92)\n\nBusiness and non-functional requirements as identified, prioritised, traceable\nand verifiable objects. Structured following ISO/IEC/IEEE 29148:2018 without\nclaiming conformance.\n\nCloses #91\n\nSigned-off-by: Serina Mcfall ","verified_at":"2026-08-12T06:22:34Z"}},"url":"https://api.github.com/repos/launchpad-26/buzz/commits/4434519667afac207f8eeb3950cc5c5de726addd","html_url":"https://github.com/launchpad-26/buzz/commit/4434519667afac207f8eeb3950cc5c5de726addd","comments_url":"https://api.github.com/repos/launchpad-26/buzz/commits/4434519667afac207f8eeb3950cc5c5de726addd/comments","author":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"30eecbc4050a4114c09ed9511c712abe7f0c2810","url":"https://api.github.com/repos/launchpad-26/buzz/commits/30eecbc4050a4114c09ed9511c712abe7f0c2810","html_url":"https://github.com/launchpad-26/buzz/commit/30eecbc4050a4114c09ed9511c712abe7f0c2810"}]}],"files":[{"sha":"fb1f8f3a1accbcc8668f03e02ea766aa26aedbf0","filename":"launchpad/ARCHITECTURE.md","status":"added","additions":199,"deletions":0,"changes":199,"blob_url":"https://github.com/launchpad-26/buzz/blob/4434519667afac207f8eeb3950cc5c5de726addd/launchpad%2FARCHITECTURE.md","raw_url":"https://github.com/launchpad-26/buzz/raw/4434519667afac207f8eeb3950cc5c5de726addd/launchpad%2FARCHITECTURE.md","contents_url":"https://api.github.com/repos/launchpad-26/buzz/contents/launchpad%2FARCHITECTURE.md?ref=4434519667afac207f8eeb3950cc5c5de726addd","patch":"@@ -0,0 +1,199 @@\n+# Target architecture\n+\n+The intended end state of the launchpad-26 cohort's Buzz environment: what runs where, who\n+owns which part, and which parts do not exist yet.\n+\n+This document records a target and an ownership split. It does not build any of it, and it\n+does not settle anything an open decision owns — where a choice is open it says so and\n+links the issue.\n+\n+---\n+\n+## How to read this\n+\n+Every claim below carries exactly one status marker, read as defined in\n+[VISION.md § How to read this](VISION.md#how-to-read-this). The legend lives there and is\n+not restated here.\n+\n+---\n+\n+## The intended end state\n+\n+`PROPOSED` — no part of the diagram below is deployed. The direction it draws is recorded\n+in [#42](https://github.com/launchpad-26/buzz/issues/42); the containment question it\n+raises is `OPEN` — [#43](https://github.com/launchpad-26/buzz/issues/43).\n+\n+```\n+ contributors' machines\n+ | ^\n+ (1) connect | | (3) agent executes here\n+ v |\n+ +-------------------------+\n+ | Buzz relay (the VPS) | <- (2) agent initiated here\n+ +-------------------------+\n+ |\n+ work targets\n+ launchpad-26/rhizomorph\n+```\n+\n+Reading the three arrows:\n+\n+1. Contributors connect their Buzz clients to a single cohort relay, which replaces\n+ Discord as the coordination surface.\n+2. An agent is initiated on the relay — the relay is where the work is asked for.\n+3. The agent executes on a contributor's own machine, not on the relay, and the work it\n+ does is directed at `launchpad-26/rhizomorph`.\n+\n+The diagram is a topology, not a deployment. What is actually running on 2026-08-11 is in\n+[What exists today](#what-exists-today).\n+\n+---\n+\n+## Components and who owns them\n+\n+| Component | Owner | Status |\n+|---|---|---|\n+| Buzz relay (`crates/buzz-relay`) | upstream `block/buzz` | `IMPLEMENTED` upstream; not deployed by the cohort |\n+| Desktop, mobile and web clients | upstream `block/buzz` | `IMPLEMENTED` upstream |\n+| Agent harness `buzz-acp` → `buzz-agent` → `buzz-dev-mcp` | upstream `block/buzz` | `IMPLEMENTED` upstream |\n+| Relay deployment and host configuration | cohort | `OPEN` — [#5](https://github.com/launchpad-26/buzz/issues/5), [#22](https://github.com/launchpad-26/buzz/issues/22), [#24](https://github.com/launchpad-26/buzz/issues/24) |\n+| Relay datastores — PostgreSQL, Redis, and object storage such as MinIO | cohort | `OPEN` — [#5](https://github.com/launchpad-26/buzz/issues/5) |\n+| Containment at the agent execution boundary | cohort | `OPEN` — [#43](https://github.com/launchpad-26/buzz/issues/43) |\n+| Upstream intelligence | cohort | `OPEN` — [#3](https://github.com/launchpad-26/buzz/issues/3) |\n+| Knowledge layer | cohort | `OPEN` — [#4](https://github.com/launchpad-26/buzz/issues/4) |\n+\n+Each row carries its own marker, and those markers cover only the rows present when this\n+section was written. A component added later does not inherit any of them — give a new row\n+its own status marker and its own link to evidence.\n+\n+The upstream components above are directories in this repository:\n+[`crates/buzz-relay`](../crates/buzz-relay), [`desktop`](../desktop),\n+[`mobile`](../mobile), [`web`](../web), [`crates/buzz-acp`](../crates/buzz-acp),\n+[`crates/buzz-agent`](../crates/buzz-agent) and\n+[`crates/buzz-dev-mcp`](../crates/buzz-dev-mcp). \"Owner: upstream `block/buzz`\" means the\n+cohort operates them and does not change them — see\n+[`AGENTS.md`](AGENTS.md) §1.\n+\n+The datastore row is drawn because the relay's state is load-bearing rather than\n+incidental: the `communities` row each environment depends on is a Postgres table created\n+by [`migrations/0001_initial_schema.sql`](../migrations/0001_initial_schema.sql), and the\n+consequence of it being absent is in\n+[ENVIRONMENTS.md § What must never differ between them](ENVIRONMENTS.md#what-must-never-differ-between-them).\n+The component names are those in\n+[#5](https://github.com/launchpad-26/buzz/issues/5)'s Evidence section; the marker is\n+`OPEN` because the cohort has deployed none of them.\n+\n+The split of concerns between this repository and `launchpad-26/rhizomorph` is recorded in\n+[VISION.md § Relationship to rhizomorph](VISION.md#relationship-to-rhizomorph) and is not\n+restated here.\n+\n+---\n+\n+## How an agent is initiated and where it executes\n+\n+The upstream capability and the cohort's use of it are two different claims with two\n+different statuses, and are kept apart here on purpose.\n+\n+| Claim | Status |\n+|---|---|\n+| Buzz can initiate an agent and run it as the `buzz-acp` → `buzz-agent` → `buzz-dev-mcp` process tree, carrying the production MCP toolset — shell, file tools, todo — with the `buzz` CLI on the shell's `PATH` | `IMPLEMENTED` upstream — [`benchmarks/harbor-buzz-orchestra/README.md`](../benchmarks/harbor-buzz-orchestra/README.md), [`crates/buzz-acp`](../crates/buzz-acp), [`crates/buzz-agent`](../crates/buzz-agent), [`crates/buzz-dev-mcp`](../crates/buzz-dev-mcp) |\n+| The cohort initiating agents on its own relay and executing them on contributors' machines | `OPEN` — [#43](https://github.com/launchpad-26/buzz/issues/43) |\n+\n+Those markers cover only the rows present when this section was written; a claim added\n+later carries its own marker.\n+\n+The consequence of the first row is what the next section is about: the toolset an agent\n+starts with already includes a shell on the machine it executes on. That is upstream's\n+default, not something the cohort would have to add.\n+\n+---\n+\n+## The execution boundary\n+\n+The point where a relay-initiated agent executes is the control that matters most in this\n+architecture, and it is undecided.\n+\n+[#43](https://github.com/launchpad-26/buzz/issues/43) states the exposure plainly: \"the\n+execution targets here are contributors' daily-driver laptops, holding their SSH keys,\n+credentials and unrelated work\". On the toolset described above, the same issue says:\n+\"Shell access on a contributor's machine is the default starting position, not an\n+escalation.\"\n+\n+The containment mechanism at that boundary is `OPEN` —\n+[#43](https://github.com/launchpad-26/buzz/issues/43). That issue lists candidate options\n+and deliberately leaves its Decision outcome blank, citing [`AGENTS.md`](AGENTS.md) §5\n+rule 1: draft everything, approve nothing.\n+\n+This section names the risk and stops. It does not narrow the options, because\n+[#42](https://github.com/launchpad-26/buzz/issues/42)'s non-goals reserve the agent\n+execution security model to that ADR — and a proposal presented as a decision is worse\n+than an absent document.\n+\n+---\n+\n+## What exists today\n+\n+Nothing in the architecture above is deployed. As of 2026-08-11 the cohort runs Buzz on\n+`localhost` only, and both of the issues that would change that are open.\n+\n+| Work that would change this | Status |\n+|---|---|\n+| Deploy the relay to the VPS with real DNS and TLS | `OPEN` — [#22](https://github.com/launchpad-26/buzz/issues/22), on [M0 — Buzz MVP](https://github.com/launchpad-26/buzz/milestone/1) |\n+| Verify two people on two machines can talk on the deployed relay | `OPEN` — [#23](https://github.com/launchpad-26/buzz/issues/23), on [M0 — Buzz MVP](https://github.com/launchpad-26/buzz/milestone/1) |\n+\n+Both states were read from the live issues with `gh issue view` on 2026-08-11. If either\n+has since closed, this section is stale — correct it in the same pull request that changes\n+what is running.\n+\n+The cohort-owned work beyond deployment sits on\n+[M1 — Cohort Intelligence & Knowledge](https://github.com/launchpad-26/buzz/milestone/2):\n+upstream intelligence ([#3](https://github.com/launchpad-26/buzz/issues/3)) and the\n+knowledge layer ([#4](https://github.com/launchpad-26/buzz/issues/4)). Neither has started.\n+\n+---\n+\n+## Open decisions\n+\n+Every decision this architecture depends on that has not been taken. Titles are the issue\n+titles as filed.\n+\n+| Decision | Status |\n+|---|---|\n+| adr: VPS specification for the cohort Buzz relay | `OPEN` — [#21](https://github.com/launchpad-26/buzz/issues/21) |\n+| adr: configuration-management tool, Ubuntu baseline and service runtime shape | `OPEN` — [#24](https://github.com/launchpad-26/buzz/issues/24) |\n+| adr: CI/CD deployment identity, privilege boundary and secret storage | `OPEN` — [#25](https://github.com/launchpad-26/buzz/issues/25) |\n+| adr: administrative access model after bootstrap | `OPEN` — [#26](https://github.com/launchpad-26/buzz/issues/26) |\n+| adr: security patch and reboot policy | `OPEN` — [#27](https://github.com/launchpad-26/buzz/issues/27) |\n+| adr: what must survive destruction of the host | `OPEN` — [#28](https://github.com/launchpad-26/buzz/issues/28) |\n+| adr: agent execution security model — containment for relay-initiated agents on contributor machines | `OPEN` — [#43](https://github.com/launchpad-26/buzz/issues/43) |\n+| adr: host firewall implementation for the deny-by-default policy | `OPEN` — [#44](https://github.com/launchpad-26/buzz/issues/44) |\n+| adr: AppArmor and service confinement for the Buzz runtime | `OPEN` — [#45](https://github.com/launchpad-26/buzz/issues/45) |\n+| adr: which security checks gate deployment in CI/CD | `OPEN` — [#46](https://github.com/launchpad-26/buzz/issues/46) |\n+| adr: minimum external security smoke test before a deployment is declared healthy | `OPEN` — [#47](https://github.com/launchpad-26/buzz/issues/47) |\n+| adr: observability strategy for the cohort Buzz environment | `OPEN` — [#83](https://github.com/launchpad-26/buzz/issues/83) |\n+\n+The last row is here and not in\n+[SECURITY-POSTURE.md § Open security decisions](SECURITY-POSTURE.md#open-security-decisions):\n+it decides what the cohort runs to watch itself — host logs, infrastructure metrics and\n+agent traces are three signals with three possible destinations — which is a shape\n+question for this architecture. It has a security consequence, stated in\n+[#83](https://github.com/launchpad-26/buzz/issues/83), but the posture's own dependency on\n+security-event visibility is already carried by\n+[#34](https://github.com/launchpad-26/buzz/issues/34) in\n+[SECURITY-POSTURE.md § What is true today](SECURITY-POSTURE.md#what-is-true-today), and\n+[#83](https://github.com/launchpad-26/buzz/issues/83) defines no control that the posture\n+asserts.\n+\n+Those markers cover only the rows present when this section was written, all confirmed\n+`OPEN` against the live issues on 2026-08-11. A decision added later carries its own\n+marker, and a decision that is taken becomes `DECIDED` with a link to its accepted ADR in\n+[`decisions/`](decisions) — see [`AGENTS.md`](AGENTS.md) §2.\n+\n+---\n+\n+## Adding to this document\n+\n+> Additions arrive by pull request against `launchpad`. Every new claim carries a status\n+> marker and a link to its evidence. Anything not yet true is an issue, not a line here —\n+> see [`AGENTS.md` §2](AGENTS.md). Append within a section rather than renumbering\n+> headings, so links from issues and the handbook keep resolving."},{"sha":"56463bea4cab23735cc53d979bc08185cf6a7a34","filename":"launchpad/ENVIRONMENTS.md","status":"added","additions":86,"deletions":0,"changes":86,"blob_url":"https://github.com/launchpad-26/buzz/blob/4434519667afac207f8eeb3950cc5c5de726addd/launchpad%2FENVIRONMENTS.md","raw_url":"https://github.com/launchpad-26/buzz/raw/4434519667afac207f8eeb3950cc5c5de726addd/launchpad%2FENVIRONMENTS.md","contents_url":"https://api.github.com/repos/launchpad-26/buzz/contents/launchpad%2FENVIRONMENTS.md?ref=4434519667afac207f8eeb3950cc5c5de726addd","patch":"@@ -0,0 +1,86 @@\n+# Environments\n+\n+Where Buzz runs for this cohort, what each place is for, and what must hold true no matter\n+which one you are looking at.\n+\n+This document records a target and a set of invariants. It does not build any of it, and\n+it does not settle anything an open decision owns — where a choice is open it says so and\n+links the issue.\n+\n+---\n+\n+## How to read this\n+\n+Every claim below carries exactly one status marker, read as defined in\n+[VISION.md § How to read this](VISION.md#how-to-read-this). The legend lives there and is\n+not restated here.\n+\n+---\n+\n+## The environments\n+\n+| Environment | Purpose | Status | Tracking |\n+|---|---|---|---|\n+| Local development (`localhost`) | Day-to-day work against a relay on the developer's own machine | `IMPLEMENTED` | — |\n+| Local VM harness | Rehearse Host-to-community binding and membership gating before touching the VPS | `OPEN` | [#17](https://github.com/launchpad-26/buzz/issues/17), [#19](https://github.com/launchpad-26/buzz/issues/19) |\n+| Cohort VPS | The shared internet-facing relay | `OPEN` | [#21](https://github.com/launchpad-26/buzz/issues/21), [#22](https://github.com/launchpad-26/buzz/issues/22) |\n+| Contributors' machines | Where relay-initiated agent work executes | `OPEN` | [#43](https://github.com/launchpad-26/buzz/issues/43) |\n+\n+No hostnames. [`AGENTS.md` §8](AGENTS.md) forbids private hostnames in tracked files, and\n+the VPS hostname is not decided anyway.\n+\n+The status markers above cover only the rows present when this section was written. A\n+new environment is a new row, and it carries its own status marker and its own tracking\n+issue rather than inheriting these.\n+\n+---\n+\n+## What each one proves\n+\n+| Environment | What it proves that the others cannot |\n+|---|---|\n+| Local development (`localhost`) | The relay and clients run at all, with no network, DNS or TLS variable in the way. `IMPLEMENTED` — this is how the cohort has worked so far. |\n+| Local VM harness | That Host-to-community binding and membership gating behave as expected, rehearsed somewhere an internet-facing host does not yet depend on the outcome. `OPEN` — [#19](https://github.com/launchpad-26/buzz/issues/19). |\n+| Cohort VPS | That the relay works as a shared service reachable by two different people on two different machines, rather than only a local one. `OPEN` — [#2](https://github.com/launchpad-26/buzz/issues/2). |\n+| Contributors' machines | That a relay-initiated agent can execute somewhere other than the relay itself, on infrastructure the cohort does not own. `OPEN` — [#43](https://github.com/launchpad-26/buzz/issues/43). |\n+\n+The section-level markers above cover only the rows present when this section was\n+written. A row added later does not inherit one — give it its own status marker and its\n+own link to evidence.\n+\n+---\n+\n+## What must never differ between them\n+\n+Two properties of the software today, sourced to\n+[#2](https://github.com/launchpad-26/buzz/issues/2)'s Evidence section. Both are marked\n+`IMPLEMENTED` — they are true of the code now, not decisions anyone is waiting on.\n+\n+**The relay derives its community from the request `Host` header, so the hostname\n+serving each environment must exist in the `communities` table or every connection is\n+rejected.** `IMPLEMENTED` — [#2](https://github.com/launchpad-26/buzz/issues/2): \"The\n+relay resolves its community from the request `Host` header. A deployed hostname absent\n+from the `communities` table rejects every connection.\" Every environment above needs its\n+own serving hostname seeded as a community before anyone can connect through it.\n+\n+**The desktop client assumes `wss://` for any scheme-less host, so a plain `ws://`\n+deployment is typed wrong by users.** `IMPLEMENTED` — [#2](https://github.com/launchpad-26/buzz/issues/2):\n+\"The desktop client assumes `wss://` for any scheme-less host, so a plain `ws://`\n+deployment will be typed wrong by users.\" Any environment served without TLS needs its\n+`ws://` scheme stated explicitly wherever the address is shared, or users will be misled\n+into an address the client will not reach.\n+\n+These two markers cover only the two properties stated when this section was written. A\n+property added later carries its own marker and its own link to evidence.\n+\n+---\n+\n+## Adding to this document\n+\n+> Additions arrive by pull request against `launchpad`. Every new claim carries a status\n+> marker and a link to its evidence. Anything not yet true is an issue, not a line here —\n+> see [`AGENTS.md` §2](AGENTS.md). Append within a section rather than renumbering\n+> headings, so links from issues and the handbook keep resolving.\n+\n+A new environment is a new row in [The environments](#the-environments), and it carries\n+its own status and tracking issue."},{"sha":"8b80be52119803674ed1cdc5fc14cdc47d81bf4f","filename":"launchpad/README.md","status":"modified","additions":5,"deletions":0,"changes":5,"blob_url":"https://github.com/launchpad-26/buzz/blob/4434519667afac207f8eeb3950cc5c5de726addd/launchpad%2FREADME.md","raw_url":"https://github.com/launchpad-26/buzz/raw/4434519667afac207f8eeb3950cc5c5de726addd/launchpad%2FREADME.md","contents_url":"https://api.github.com/repos/launchpad-26/buzz/contents/launchpad%2FREADME.md?ref=4434519667afac207f8eeb3950cc5c5de726addd","patch":"@@ -124,6 +124,11 @@ need one approving review — the branch is protected and you can't approve your\n | [`README.md`](README.md) | This file — start here |\n | [`AGENTS.md`](AGENTS.md) | Normative spec: types, rules, agent constraints, git workflow |\n | [`AGENT_PR_TEMPLATE.md`](AGENT_PR_TEMPLATE.md) | PR body schema for agent-authored PRs |\n+| [`VISION.md`](VISION.md) | What this cohort is building on Buzz, for whom, and what \"done\" looks like |\n+| [`ARCHITECTURE.md`](ARCHITECTURE.md) | Target architecture: what runs where, who owns which part, what does not exist yet |\n+| [`ENVIRONMENTS.md`](ENVIRONMENTS.md) | Where Buzz runs, what each place proves, and what must hold in all of them |\n+| [`SECURITY-POSTURE.md`](SECURITY-POSTURE.md) | Where security controls stand today, which risks are accepted, and the gap nothing owns |\n+| [`REQUIREMENTS.md`](REQUIREMENTS.md) | What the cohort's environment must do and must be, prioritised, traced to a source, and verifiable |\n | [`labels.yml`](labels.yml) | Label source of truth |\n | [`sync-labels.sh`](sync-labels.sh) | Applies `labels.yml` to the repo |\n | `decisions/` | ADRs, once accepted |"},{"sha":"58f3180ecacc33053a0760d7a6dfd5d3753509ec","filename":"launchpad/REQUIREMENTS.md","status":"added","additions":300,"deletions":0,"changes":300,"blob_url":"https://github.com/launchpad-26/buzz/blob/4434519667afac207f8eeb3950cc5c5de726addd/launchpad%2FREQUIREMENTS.md","raw_url":"https://github.com/launchpad-26/buzz/raw/4434519667afac207f8eeb3950cc5c5de726addd/launchpad%2FREQUIREMENTS.md","contents_url":"https://api.github.com/repos/launchpad-26/buzz/contents/launchpad%2FREQUIREMENTS.md?ref=4434519667afac207f8eeb3950cc5c5de726addd","patch":"@@ -0,0 +1,300 @@\n+# Requirements\n+\n+What the launchpad-26 cohort's Buzz environment must do and must be, as individually\n+identified requirements a reader can prioritise, trace to a source and check.\n+\n+This document records requirements. It builds none of them, and it does not settle\n+anything an open decision owns — where a choice is open it says so and links the issue.\n+\n+---\n+\n+## How to read this\n+\n+Every requirement below carries exactly one status marker, read as defined in\n+[VISION.md § How to read this](VISION.md#how-to-read-this). The legend lives there and is\n+not restated here.\n+\n+Every requirement also carries a MoSCoW priority — `Must`, `Should`, `Could` or `Won't`.\n+\n+**Priority and status are different axes, and conflating them is the obvious misreading.**\n+Priority says how important the requirement is to the cohort. Status says whether it is\n+satisfied. A `Must` that is `OPEN` is not a contradiction: it is the most important kind of\n+row in this document, because it is both required and unmet. Nothing here is `IMPLEMENTED`\n+merely for being a `Must`.\n+\n+No requirement below is prioritised `Won't`. What the cohort has decided *not* to do is\n+recorded as scope, in\n+[VISION.md § Scope boundaries](VISION.md#scope-boundaries), rather than as a requirement\n+with an emptied-out priority.\n+\n+---\n+\n+## How this document is structured\n+\n+The sections below follow the shape of the Business Requirements Specification described\n+by ISO/IEC/IEEE 29148:2018 — business purpose, business scope, business overview, business\n+environment, stakeholders, then the requirements themselves.\n+\n+**This document does not claim conformance to that standard.** The standard's full text is\n+paywalled, nobody in the cohort has read it in full, and nothing here has been checked\n+against its clauses. Only the outline was available, from secondary sources. Saying\n+\"structured following\" is the whole of the claim: an unverified conformance assertion in a\n+public repository is exactly the failure the status markers in this document set exist to\n+prevent.\n+\n+The requirement objects carry the metadata shape used by the reference corpus at\n+[`tucktuck101/ForgePilot` `docs/business-analysis/`](https://github.com/tucktuck101/ForgePilot/tree/main/docs/business-analysis)\n+— an identifier, a MoSCoW priority, a source, and a summary carrying one dominant\n+obligation — and take the idea of a stated verification method from that corpus's\n+[`requirements/quality-and-verification-baseline.md`](https://github.com/tucktuck101/ForgePilot/blob/main/docs/business-analysis/requirements/quality-and-verification-baseline.md),\n+which supplies an observable method for every non-functional requirement it holds.\n+\n+Where this document departs from that corpus, it does so deliberately. It states every\n+obligation with `shall`; the corpus does that for its business requirements but not\n+throughout, also using `must`, `should` and `may`, and its baseline treats `shall` and\n+`must` as mandatory and `should` as preferred. It also cites sources directly instead of\n+modelling stakeholder needs and evidence as separately addressable objects, which\n+[#91](https://github.com/launchpad-26/buzz/issues/91) records as a deliberate non-goal at\n+this cohort's size.\n+\n+That corpus makes no reference to ISO/IEC/IEEE 29148 and is not offered as evidence of\n+conformance to it. It was read on 2026-08-12, and the metadata claims above were checked\n+against every requirement object in its requirements collections rather than sampled.\n+\n+---\n+\n+## Business purpose\n+\n+Owned by [VISION.md § Mission](VISION.md#mission) and not restated here: the cohort\n+operates Buzz rather than developing it, and deploys and runs a shared Buzz platform it can\n+safely depend on and recreate.\n+\n+---\n+\n+## Business scope\n+\n+Owned by [VISION.md § Scope boundaries](VISION.md#scope-boundaries) and not restated here.\n+That table is what decides whether a candidate requirement belongs in this document at all.\n+\n+---\n+\n+## Business overview\n+\n+Owned by [ARCHITECTURE.md § The intended end state](ARCHITECTURE.md#the-intended-end-state)\n+and not restated here: contributors connect to a single cohort relay, agents are initiated\n+on that relay, and the agent work executes on contributors' own machines.\n+\n+---\n+\n+## Business environment\n+\n+Where these requirements have to hold is owned by\n+[ENVIRONMENTS.md § The environments](ENVIRONMENTS.md#the-environments), and the state of the\n+controls that would satisfy the security-shaped ones is owned by\n+[SECURITY-POSTURE.md § What is true today](SECURITY-POSTURE.md#what-is-true-today). Neither\n+is restated here.\n+\n+Environmental facts that bear directly on how these requirements are written, each recorded\n+elsewhere in this set:\n+\n+- The repository is public, so no requirement below names a hostname, a credential or a\n+ person — [SECURITY-POSTURE.md § The public-repository rule](SECURITY-POSTURE.md#the-public-repository-rule).\n+- The cohort is not yet running the environment these requirements describe, so most of\n+ them are unmet — [ARCHITECTURE.md § What exists today](ARCHITECTURE.md#what-exists-today).\n+\n+---\n+\n+## Stakeholders\n+\n+Owned by [VISION.md § Who this is for](VISION.md#who-this-is-for) and not restated here.\n+Each requirement below serves one or more of the audiences in that table.\n+\n+---\n+\n+## Why there are no functional requirements here\n+\n+**The cohort operates Buzz and does not develop it, so what Buzz does is not the cohort's\n+to require.** That boundary is [`AGENTS.md` §1](AGENTS.md): \"We operate Buzz. We do not\n+develop Buzz.\"\n+\n+A functional requirement for the product — what the relay accepts, how a client renders a\n+channel, which event kinds exist — would be a requirement written on\n+[`block/buzz`](https://github.com/block/buzz)'s roadmap by people with no standing to write\n+it. It would also rot silently, because upstream would change the behaviour without ever\n+reading this file. Genuine product defects go to\n+[`block/buzz` issues](https://github.com/block/buzz/issues).\n+\n+What the cohort *does* own is the environment around the product: that it exists, that it\n+is reachable by the right people, that it can be rebuilt, and that running it does not\n+endanger the machines and credentials of the people running it. Those are business\n+requirements and non-functional requirements, which is why this document holds only those\n+two classes.\n+\n+Properties of the shipped software that the cohort must design around, rather than require,\n+are recorded as invariants in\n+[ENVIRONMENTS.md § What must never differ between them](ENVIRONMENTS.md#what-must-never-differ-between-them).\n+\n+---\n+\n+## Business requirements\n+\n+Each row states one dominant obligation. `Source` is a link a reader can follow to the\n+milestone, issue or file the obligation comes from.\n+\n+| ID | Requirement | Priority | Status | Source |\n+|---|---|---|---|---|\n+| BR-001 | The cohort **shall** operate a shared internet-facing Buzz relay that multiple cohort members can connect to and communicate through. | `Must` | `DECIDED` | [milestone M0](https://github.com/launchpad-26/buzz/milestone/1); [#2](https://github.com/launchpad-26/buzz/issues/2) |\n+| BR-002 | The cohort **shall** move its own coordination onto that relay rather than continuing to coordinate on Discord. | `Should` | `PROPOSED` | [#42](https://github.com/launchpad-26/buzz/issues/42), via [VISION.md § What we are building](VISION.md#what-we-are-building) |\n+| BR-003 | The cohort **shall** be able to answer what is changing and how the system works from within Buzz, \"without first needing to know which repository, changelog, issue, PR or documentation file contains the answer\". | `Should` | `DECIDED` | [milestone M1](https://github.com/launchpad-26/buzz/milestone/2) |\n+| BR-004 | Agent work initiated on the relay **shall** be directed at assisting development of `launchpad-26/rhizomorph`. | `Should` | `PROPOSED` | [#42](https://github.com/launchpad-26/buzz/issues/42); conditional on NFR-012 |\n+| BR-005 | The cohort **shall** deploy and maintain the server through source-controlled automation rather than through manual administration of a running host. | `Must` | `DECIDED` | [milestone M0](https://github.com/launchpad-26/buzz/milestone/1); [#5](https://github.com/launchpad-26/buzz/issues/5) Ruling 1 |\n+| BR-006 | The cohort **shall** operate Buzz and not develop it, routing genuine product defects to `block/buzz`. | `Must` | `IMPLEMENTED` | [`AGENTS.md` §1](AGENTS.md) |\n+| BR-007 | The cohort **shall** hold its stable operating knowledge in this repository as documentation, rather than in the recollection of whoever configured something. | `Should` | `PROPOSED` | [#42](https://github.com/launchpad-26/buzz/issues/42); [`AGENTS.md` §2](AGENTS.md) |\n+\n+The markers above cover only the rows present when this section was written. A requirement\n+added later does not inherit one — it carries its own status marker, its own priority and\n+its own link to a source.\n+\n+`DECIDED` here means the obligation is agreed and recorded on the milestone or issue named,\n+and is not met yet, exactly as in\n+[VISION.md § What success looks like](VISION.md#what-success-looks-like).\n+\n+### How each business requirement is demonstrated\n+\n+| ID | What would demonstrate it holds |\n+|---|---|\n+| BR-001 | Two cohort members, on two different machines, each connect to the deployed relay with their own Nostr identity, see each other's messages in a shared channel, and exchange a direct message in both directions. |\n+| BR-002 | The coordination that previously happened on Discord happens in relay channels, and the cohort's dependence on Discord ends by a recorded decision rather than by drift. |\n+| BR-003 | A member and an agent each answer one question about a recent upstream change and one about how a part of the system works, starting from Buzz, without first being told which repository holds the answer. |\n+| BR-004 | An agent initiated on the relay produces reviewable work against `launchpad-26/rhizomorph`, and no such agent has executed on a contributor's machine before NFR-012 holds. |\n+| BR-005 | A documented command applied to a fresh supported Ubuntu host produces a working configuration with no manual step outside the controlled bootstrap that [#5](https://github.com/launchpad-26/buzz/issues/5) Ruling 2 defines as the deployment's input, and the security controls arrive from the same version-controlled automation as the rest of the host configuration. Ruling 1 requires that \"where practical\", so anything that cannot be expressed as automation is declared rather than left as an undocumented requirement of the running host. |\n+| BR-006 | Cohort changes land under `launchpad/`, `.github/workflows/launchpad-*` and cohort process files; defects in the product appear as issues on `block/buzz` rather than as patches to upstream directories in this fork. |\n+| BR-007 | A contributor joining later can state what the system is, and the access an agent workflow needs together with its blast radius, from the documents in `launchpad/` alone. |\n+\n+---\n+\n+## Non-functional requirements\n+\n+Standing properties the environment must hold. Each carries one dominant obligation, a\n+priority, a status and a source; the verification method for each is in the matrix that\n+follows.\n+\n+| ID | Property | Requirement | Priority | Status | Source |\n+|---|---|---|---|---|---|\n+| NFR-001 | Recoverability | The environment **shall** be rebuildable from a bare supported Ubuntu host to an equivalent functional and security state. | `Must` | `DECIDED` | [milestone M0](https://github.com/launchpad-26/buzz/milestone/1); [#5](https://github.com/launchpad-26/buzz/issues/5) Ruling 14 |\n+| NFR-002 | Idempotent convergence | Reapplying the deployment automation to an already-converged host **shall** report no changes. | `Must` | `DECIDED` | [#5](https://github.com/launchpad-26/buzz/issues/5) Ruling 11 |\n+| NFR-003 | Least privilege | No administrative, application or service identity **shall** hold access beyond its role. | `Must` | `DECIDED` | [milestone M0](https://github.com/launchpad-26/buzz/milestone/1); [#5](https://github.com/launchpad-26/buzz/issues/5) Rulings 4 and 9 |\n+| NFR-004 | Explicit trust | Access between people, automation identities and services **shall** rest on explicit identity and authorisation rather than on network location. | `Must` | `DECIDED` | [milestone M0](https://github.com/launchpad-26/buzz/milestone/1); [#5](https://github.com/launchpad-26/buzz/issues/5) Ruling 4 |\n+| NFR-005 | Minimal exposure | Only the services intended to be public **shall** be reachable from the internet. | `Must` | `DECIDED` | [milestone M0](https://github.com/launchpad-26/buzz/milestone/1); [#5](https://github.com/launchpad-26/buzz/issues/5) Ruling 5 |\n+| NFR-006 | Transport protection | Public Buzz traffic **shall** be TLS-protected. | `Must` | `DECIDED` | [#5](https://github.com/launchpad-26/buzz/issues/5); [#2](https://github.com/launchpad-26/buzz/issues/2) |\n+| NFR-007 | Credential handling | A routine deployment **shall** complete without a root credential being distributed to any cohort member. | `Must` | `DECIDED` | [milestone M0](https://github.com/launchpad-26/buzz/milestone/1); [#5](https://github.com/launchpad-26/buzz/issues/5) Rulings 6 and 7 |\n+| NFR-008 | Secret containment | Secret material **shall** be kept out of tracked files by a check rather than by attention. | `Must` | `OPEN` | [#67](https://github.com/launchpad-26/buzz/issues/67) for detection in diffs and history, [#68](https://github.com/launchpad-26/buzz/issues/68) for the ignore-pattern and tracked-file assertions, both under [#62](https://github.com/launchpad-26/buzz/issues/62); [`AGENTS.md` §8](AGENTS.md) |\n+| NFR-009 | Security maintenance | Supported Ubuntu security updates **shall** stay applied on the running host, with reboot-required state visible rather than assumed. | `Must` | `DECIDED` | [#5](https://github.com/launchpad-26/buzz/issues/5) Ruling 10 |\n+| NFR-010 | Observability of failure | Security-relevant failures **shall** remain observable to authorised operators after hardening. | `Should` | `OPEN` | [#34](https://github.com/launchpad-26/buzz/issues/34); [#5](https://github.com/launchpad-26/buzz/issues/5) Ruling 13 |\n+| NFR-011 | External verification | A deployment **shall** be declared healthy only after a machine-checkable suite verifies it from an untrusted client's position, not because the configuration run succeeded. | `Must` | `DECIDED` | [#5](https://github.com/launchpad-26/buzz/issues/5) Ruling 12; the minimum set is `OPEN` — [#47](https://github.com/launchpad-26/buzz/issues/47) |\n+| NFR-012 | Containment at the agent execution boundary | No agent initiated over the relay **shall** execute on a contributor's machine before containment at that boundary exists. | `Must` | `OPEN` | [#43](https://github.com/launchpad-26/buzz/issues/43) |\n+| NFR-013 | Workload measurability | The cost of a change to the running stack **shall** be measured against a recorded baseline before the change is adopted. | `Should` | `OPEN` | [#41](https://github.com/launchpad-26/buzz/issues/41) |\n+\n+The markers above cover only the rows present when this section was written. A property\n+added later does not inherit one — it carries its own status marker, its own priority, its\n+own source and its own row in the matrix below.\n+\n+NFR-008 is `OPEN` even though the rule it enforces binds today. The rule's existence is\n+`IMPLEMENTED` — [`AGENTS.md` §8](AGENTS.md) — and\n+[SECURITY-POSTURE.md § The public-repository rule](SECURITY-POSTURE.md#the-public-repository-rule)\n+draws the distinction this requirement turns on: \"A rule that binds is not a mechanism that\n+checks.\" The requirement is for the check, and nothing checks yet.\n+\n+NFR-012 states that containment is required and stops there. It does not name a mechanism,\n+rank the options or imply a direction, because\n+[#43](https://github.com/launchpad-26/buzz/issues/43) owns that decision and deliberately\n+left its outcome blank, citing [`AGENTS.md`](AGENTS.md) §5 rule 1: draft everything,\n+approve nothing. The exposure it bounds is described in\n+[ARCHITECTURE.md § The execution boundary](ARCHITECTURE.md#the-execution-boundary) and is\n+not restated here.\n+\n+### Verification matrix\n+\n+What someone could do to demonstrate each property holds. \"It was tested\" is not a\n+verification method; each row names an observation whose outcome a reader could check.\n+\n+| ID | Observable verification |\n+|---|---|\n+| NFR-001 | A replacement bare supported Ubuntu host, given only replacement infrastructure coordinates and controlled bootstrap credentials, is brought to a state where NFR-011's suite passes, with no step taken that is not in the automation or its documented bootstrap. |\n+| NFR-002 | The automation is applied twice in succession to the same host and the second run reports zero changed resources. Anything that cannot safely be idempotent is declared as an explicit exception in the automation rather than discovered during a run. |\n+| NFR-003 | The declared identities are inspected in version control: each administrative, application and service identity's permissions are readable there and map to a stated role, and the Buzz runtime and its supporting services run under identities constrained below root. |\n+| NFR-004 | For every access path between a person, an automation identity and a service, an identity and an authorisation can be named. No path is permitted on the basis of co-location, source address or `localhost` alone. |\n+| NFR-005 | An external port scan, run from a client the cohort does not control, reaches only the intentionally public services; the database, cache and object-storage administration interfaces do not answer. |\n+| NFR-006 | A plaintext connection attempt to the public endpoint fails; the `wss://` endpoint connects from outside the host; the `https://` endpoint returns NIP-11 JSON and `/health` returns 200. |\n+| NFR-007 | A routine deployment completes end to end through the dedicated CI/CD machine identity while no cohort member holds a root credential, and remote root access on the host matches the declared policy when checked from outside it. |\n+| NFR-008 | The audit flags deliberately planted secret-shaped fixtures in a pull-request diff and in git history, reporting file and line without echoing the matched value, and fails when a file matching an ignore-list pattern is tracked. A check nobody has watched fail is not known to work. |\n+| NFR-009 | On the running host, the update policy is active and evidenced, and a check reports pending-reboot state rather than an operator inferring currency from the fact that packages were installed. |\n+| NFR-010 | An authentication failure and a denied privileged action are each provoked deliberately and then located in the host's records by an authorised operator, with no secret appearing in what is retrieved. |\n+| NFR-011 | The suite runs against the deployed host from both an authorised user's and an untrusted internet client's position, passes, and is the thing whose result declares the deployment healthy. |\n+| NFR-012 | Before the first relay-initiated agent executes on a contributor's machine, the containment decision exists as an accepted ADR in [`decisions/`](decisions), and the containment it records can be demonstrated from outside the agent — per [#43](https://github.com/launchpad-26/buzz/issues/43): \"Any control that cannot be verified from outside the agent is an assumption, not a control.\" |\n+| NFR-013 | The harness drives human-authenticated and agent-authenticated connections against a target it was explicitly given, refuses to run without one, confirms events were accepted before reporting any figure, and reports each change as a delta against a recorded baseline that states the conditions it was measured under. |\n+\n+Every row above is a method, not a result. None of them has been run: the cohort is not\n+running the environment they would be run against —\n+[ARCHITECTURE.md § What exists today](ARCHITECTURE.md#what-exists-today).\n+\n+---\n+\n+## Traceability\n+\n+Requirement → the work that would satisfy it → the milestone that work sits on. Issue\n+titles are as filed.\n+\n+| ID | Satisfying work | Milestone |\n+|---|---|---|\n+| BR-001 | [#2](https://github.com/launchpad-26/buzz/issues/2) — prd 0 - Deploy the relay to a VPS and verify two people can log in | [M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| BR-002 | No work item owns the migration itself; the direction is recorded in [#42](https://github.com/launchpad-26/buzz/issues/42) | — |\n+| BR-003 | [#3](https://github.com/launchpad-26/buzz/issues/3) — prd-01 — upstream intelligence: Buzz keeps the cohort aware of upstream; [#4](https://github.com/launchpad-26/buzz/issues/4) — # prd 02— human + agent knowledge layer: one coherent surface for Buzz | [M1](https://github.com/launchpad-26/buzz/milestone/2) |\n+| BR-004 | No work item owns the agent workflows; the direction is recorded in [#42](https://github.com/launchpad-26/buzz/issues/42) and is blocked by [#43](https://github.com/launchpad-26/buzz/issues/43) | [M0](https://github.com/launchpad-26/buzz/milestone/1) for [#43](https://github.com/launchpad-26/buzz/issues/43) |\n+| BR-005 | [#5](https://github.com/launchpad-26/buzz/issues/5) — # prd-03 — reproducible and hardened Buzz deployment: rebuild the cohort server from a bare Ubuntu host | [M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| BR-006 | No work item — the boundary is in force as a rule, [`AGENTS.md` §1](AGENTS.md) | — |\n+| BR-007 | [#42](https://github.com/launchpad-26/buzz/issues/42) — prd-06 — capture the cohort's vision, architecture and operating documentation | [M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| NFR-001 | [#5](https://github.com/launchpad-26/buzz/issues/5) | [M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| NFR-002 | [#5](https://github.com/launchpad-26/buzz/issues/5) | [M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| NFR-003 | [#5](https://github.com/launchpad-26/buzz/issues/5) | [M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| NFR-004 | [#5](https://github.com/launchpad-26/buzz/issues/5) | [M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| NFR-005 | [#5](https://github.com/launchpad-26/buzz/issues/5) | [M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| NFR-006 | [#5](https://github.com/launchpad-26/buzz/issues/5); [#2](https://github.com/launchpad-26/buzz/issues/2) | [M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| NFR-007 | [#5](https://github.com/launchpad-26/buzz/issues/5) | [M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| NFR-008 | [#62](https://github.com/launchpad-26/buzz/issues/62) — prd-07 — repository security hygiene: an enforced audit sequence for a public, agent-heavy fork | [M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| NFR-009 | [#5](https://github.com/launchpad-26/buzz/issues/5) | [M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| NFR-010 | [#34](https://github.com/launchpad-26/buzz/issues/34) — task: keep security-relevant events observable after hardening | [M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| NFR-011 | [#5](https://github.com/launchpad-26/buzz/issues/5); [#47](https://github.com/launchpad-26/buzz/issues/47) — adr: minimum external security smoke test before a deployment is declared healthy | [M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| NFR-012 | [#43](https://github.com/launchpad-26/buzz/issues/43) — adr: agent execution security model — containment for relay-initiated agents on contributor machines | [M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| NFR-013 | [#41](https://github.com/launchpad-26/buzz/issues/41) — prd-05 — internal performance and agent-workload testing harness | — |\n+\n+A row carries no milestone where the work is not on one:\n+[#41](https://github.com/launchpad-26/buzz/issues/41) was filed without a milestone, and a\n+requirement with no work item has nothing to place on one. **Every requirement that nobody\n+currently owns says so in its `Satisfying work` cell, which begins \"No work item\".** Read\n+that column to find them all — a list here would leave a later addition out, and an\n+unowned obligation going unnoticed is the problem this document exists to prevent.\n+Every issue state and milestone in this table was read from the live issues with `gh` on\n+2026-08-12; if any has since closed or moved, this section is stale — correct it in the\n+same pull request that changes it. That reading covers only the rows present when this\n+section was written. A requirement added later brings its own row and its own reading.\n+\n+A requirement outlives the work that satisfies it. When an issue in this table closes, the\n+requirement does not close with it: its status marker changes and the row keeps pointing at\n+the work as the record of how it was met.\n+\n+---\n+\n+## Adding to this document\n+\n+> Additions arrive by pull request against `launchpad`. Every new claim carries a status\n+> marker and a link to its evidence. Anything not yet true is an issue, not a line here —\n+> see [`AGENTS.md` §2](AGENTS.md). Append within a section rather than renumbering\n+> headings, so links from issues and the handbook keep resolving.\n+\n+A new requirement additionally carries its own identifier taken from the next unused number\n+in its class, one dominant obligation stated with `shall`, a MoSCoW priority, a status\n+marker, and a source someone can follow to a milestone, issue or file. A non-functional\n+requirement also carries a row in the verification matrix naming an observation, not a\n+result. Retired identifiers are not reused, so a citation from an issue or a pull request\n+keeps meaning what it meant."},{"sha":"89a97fe6fb2a940b6f6fb643c02cb705e29e1121","filename":"launchpad/SECURITY-POSTURE.md","status":"added","additions":214,"deletions":0,"changes":214,"blob_url":"https://github.com/launchpad-26/buzz/blob/4434519667afac207f8eeb3950cc5c5de726addd/launchpad%2FSECURITY-POSTURE.md","raw_url":"https://github.com/launchpad-26/buzz/raw/4434519667afac207f8eeb3950cc5c5de726addd/launchpad%2FSECURITY-POSTURE.md","contents_url":"https://api.github.com/repos/launchpad-26/buzz/contents/launchpad%2FSECURITY-POSTURE.md?ref=4434519667afac207f8eeb3950cc5c5de726addd","patch":"@@ -0,0 +1,214 @@\n+# Security posture\n+\n+Where the launchpad-26 cohort's security controls actually stand today, which risks have\n+been accepted with a reason, and the one gap that nothing currently owns.\n+\n+This document records posture. It applies no control, and it does not settle anything an\n+open decision owns — where a choice is open it says so and links the issue.\n+\n+---\n+\n+## How to read this\n+\n+Every claim below carries exactly one status marker, read as defined in\n+[VISION.md § How to read this](VISION.md#how-to-read-this). The legend lives there and is\n+not restated here.\n+\n+Read this document as a description of posture, not of controls in force: a marker says\n+whether a control exists, is agreed, is proposed or is undecided, and only `IMPLEMENTED`\n+means something is protecting something right now.\n+\n+---\n+\n+## What is true today\n+\n+As of 2026-08-11 the cohort's security controls are **designed, not applied.**\n+\n+Nothing the hardening work describes is deployed — the cohort runs Buzz on `localhost`\n+only, per\n+[ARCHITECTURE.md § What exists today](ARCHITECTURE.md#what-exists-today). Every child\n+issue of [#5](https://github.com/launchpad-26/buzz/issues/5), the PRD that owns host\n+hardening, was open when this section was written, as was every child of\n+[#62](https://github.com/launchpad-26/buzz/issues/62), the PRD that owns repository\n+hygiene. Both sit on\n+[M0 — Buzz MVP](https://github.com/launchpad-26/buzz/milestone/1).\n+\n+| Control | Status |\n+|---|---|\n+| Deny-by-default host firewall exposing only the public Buzz service | `OPEN` — [#30](https://github.com/launchpad-26/buzz/issues/30), [#44](https://github.com/launchpad-26/buzz/issues/44) |\n+| Named administrative identities, with remote root access restricted after bootstrap | `OPEN` — [#29](https://github.com/launchpad-26/buzz/issues/29), [#26](https://github.com/launchpad-26/buzz/issues/26) |\n+| Buzz and supporting services constrained below root under declared identities | `OPEN` — [#31](https://github.com/launchpad-26/buzz/issues/31), [#45](https://github.com/launchpad-26/buzz/issues/45) |\n+| Security update policy applied, with reboot-required state visible | `OPEN` — [#32](https://github.com/launchpad-26/buzz/issues/32), [#27](https://github.com/launchpad-26/buzz/issues/27) |\n+| Routine deployment through a dedicated CI/CD machine identity | `OPEN` — [#37](https://github.com/launchpad-26/buzz/issues/37), [#25](https://github.com/launchpad-26/buzz/issues/25) |\n+| Security-relevant events still observable after hardening | `OPEN` — [#34](https://github.com/launchpad-26/buzz/issues/34) |\n+| External verification of the deployed host from an untrusted client | `OPEN` — [#35](https://github.com/launchpad-26/buzz/issues/35), [#47](https://github.com/launchpad-26/buzz/issues/47) |\n+| Detection of secret material in pull-request diffs and in git history | `OPEN` — [#67](https://github.com/launchpad-26/buzz/issues/67), [#63](https://github.com/launchpad-26/buzz/issues/63) |\n+| Ignore coverage, tracked files and the agent config surface asserted rather than assumed | `OPEN` — [#68](https://github.com/launchpad-26/buzz/issues/68) |\n+| A dependency alerting path that covers more than Cargo | `OPEN` — [#71](https://github.com/launchpad-26/buzz/issues/71), [#64](https://github.com/launchpad-26/buzz/issues/64) |\n+| `cargo-deny check` runs in CI and covers Rust advisories, and only those | `IMPLEMENTED` — [`.github/workflows/ci.yml`](../.github/workflows/ci.yml); scope stated in [#62](https://github.com/launchpad-26/buzz/issues/62) |\n+\n+The markers above cover only the rows present when this section was written. A control\n+added later does not inherit one — give the new row its own status marker and its own\n+link to evidence.\n+\n+Two repository-level facts were checked against the live repository on 2026-08-11 rather\n+than taken from an issue body. Dependabot alerts are disabled on this fork\n+(`gh api repos/launchpad-26/buzz/dependabot/alerts` returns\n+`Dependabot alerts are disabled for this repository.`, HTTP 403). That measurement carries\n+no status marker, and deliberately so: none of the four fits a measured *absence*, and\n+marking it `IMPLEMENTED` would say a control exists when what exists is the hole where one\n+would go. The control itself is `OPEN` —\n+[#71](https://github.com/launchpad-26/buzz/issues/71). Whether secret scanning and push\n+protection are enabled cannot be answered from a non-admin account at all, which\n+[#62](https://github.com/launchpad-26/buzz/issues/62) records as part of the problem:\n+\"nobody without admin can currently answer 'is push protection on?', and that question\n+should not require privilege to answer.\" Making that answer visible is\n+[#70](https://github.com/launchpad-26/buzz/issues/70); obtaining it is\n+[#72](https://github.com/launchpad-26/buzz/issues/72).\n+\n+One row above is a control that runs: `cargo-deny check`, which guards this repository's\n+Rust dependency surface in CI and nothing beyond it. Nothing above protects a running\n+system, because the cohort is not running one. A reader who finishes this section\n+believing otherwise has misread it, and this document has failed at the only job it has.\n+\n+---\n+\n+## Accepted risks\n+\n+Each row is a risk the cohort carries deliberately, with the reason written down and a\n+source that can be read. Nothing is listed here without one.\n+\n+A marker in this table describes the standing of the *acceptance*, never of a control:\n+`DECIDED` means the acceptance is agreed and recorded, which is what makes a risk accepted\n+rather than merely present. No row here protects anything.\n+\n+| Risk | Status | Why accepted | Source |\n+|---|---|---|---|\n+| This repository is public, so a written architecture is a map for an attacker as much as for a contributor | `DECIDED` | An undocumented architecture hides gaps — including the one below — from the people who could fix them | [#42](https://github.com/launchpad-26/buzz/issues/42) security implications; [`AGENTS.md` §8](AGENTS.md) |\n+| An initial privileged bootstrap step survives: the deployment's declared input is a bare Ubuntu host with root SSH | `DECIDED` | Something must establish the first identities. Ruling 6 confines root to bootstrap authority rather than an operational identity, and Ruling 7 keeps it from being handed round as a deployment mechanism | [#5](https://github.com/launchpad-26/buzz/issues/5) non-goals: \"No requirement to eliminate the initial privileged bootstrap step\" |\n+| Host hardening does not make the application itself invulnerable | `DECIDED` | The objective is \"a **reproducible, defensible internet-facing cohort server**, not a complete enterprise security platform\"; product defects go upstream under [`AGENTS.md` §1](AGENTS.md) | [#5](https://github.com/launchpad-26/buzz/issues/5) non-goals: \"No claim that host hardening makes the application itself invulnerable\" |\n+| No SOC, SIEM or enterprise security operations platform | `DECIDED` | Ruling 13 draws the line where the cohort can still diagnose a failure: \"A larger observability or SIEM platform is outside scope; establishing enough evidence to diagnose access and security failures is not.\" | [#5](https://github.com/launchpad-26/buzz/issues/5) non-goals and Ruling 13 |\n+| Zero trust is applied proportionately to one cohort server, not as enterprise zero-trust infrastructure | `DECIDED` | Ruling 4 requires the principle — explicit identity, least privilege, no permission granted by location — without requiring the infrastructure | [#5](https://github.com/launchpad-26/buzz/issues/5) Ruling 4 |\n+| Every repository-level control the cohort can build without admin will detect rather than prevent, and on a public repository detection lands after disclosure. None of that detection runs yet — it is `OPEN` in [What is true today](#what-is-true-today); what is accepted here is its shape | `DECIDED` | Push protection \"can only be *enabled* by a repository admin\", so detection is deliberately scoped to need none — \"closing it is a separate, human, privileged act\", requested under [#72](https://github.com/launchpad-26/buzz/issues/72) | [#62](https://github.com/launchpad-26/buzz/issues/62) security implications |\n+| Secret-shaped material already in git history is not removed by the hygiene work | `DECIDED` | \"remediation is a separate decision with its own blast radius — this PRD delivers the finding, not the force-push\" | [#62](https://github.com/launchpad-26/buzz/issues/62) non-goals |\n+\n+The markers above cover only the rows present when this section was written. A risk added\n+later does not inherit one — it carries its own status marker and its own written source.\n+\n+---\n+\n+## The gap this document exists to surface\n+\n+`OPEN` — [#43](https://github.com/launchpad-26/buzz/issues/43).\n+\n+Stated plainly: if agents are initiated on the relay and executed on contributors'\n+machines, **the relay stops being only a service to protect and becomes a distribution\n+point for code execution.**\n+\n+[#42](https://github.com/launchpad-26/buzz/issues/42)'s security-implications section is\n+where that consequence is written down. Of the hardening PRD it says the rulings \"all\n+address protecting the host; none addresses the host as an initiation point.\"\n+\n+[#43](https://github.com/launchpad-26/buzz/issues/43) reaches the same place from the\n+other side: \"#5 currently treats the host as an asset to protect, and this makes it a\n+distribution point whose compromise reaches every connected contributor. #5's scope\n+should be widened to reflect that, or the gap recorded explicitly.\" This section is that\n+explicit record.\n+\n+What happens at the far end — the exposure on contributors' own machines, and the toolset\n+an agent starts with there — is owned by\n+[ARCHITECTURE.md § The execution boundary](ARCHITECTURE.md#the-execution-boundary) and is\n+not restated here.\n+\n+This section names the gap and stops. It does not narrow the containment options, rank\n+them, or point at any of them, because\n+[#42](https://github.com/launchpad-26/buzz/issues/42)'s non-goals reserve that question\n+to its ADR: \"the agent execution security model in particular is a separate ADR and must\n+not be pre-empted here.\"\n+\n+---\n+\n+## Open security decisions\n+\n+Every security decision this posture depends on that has not been taken. Titles are the\n+issue titles as filed.\n+\n+| Decision | Status |\n+|---|---|\n+| adr: CI/CD deployment identity, privilege boundary and secret storage | `OPEN` — [#25](https://github.com/launchpad-26/buzz/issues/25) |\n+| adr: administrative access model after bootstrap | `OPEN` — [#26](https://github.com/launchpad-26/buzz/issues/26) |\n+| adr: security patch and reboot policy | `OPEN` — [#27](https://github.com/launchpad-26/buzz/issues/27) |\n+| adr: what must survive destruction of the host | `OPEN` — [#28](https://github.com/launchpad-26/buzz/issues/28) |\n+| adr: agent execution security model — containment for relay-initiated agents on contributor machines | `OPEN` — [#43](https://github.com/launchpad-26/buzz/issues/43) |\n+| adr: host firewall implementation for the deny-by-default policy | `OPEN` — [#44](https://github.com/launchpad-26/buzz/issues/44) |\n+| adr: AppArmor and service confinement for the Buzz runtime | `OPEN` — [#45](https://github.com/launchpad-26/buzz/issues/45) |\n+| adr: which security checks gate deployment in CI/CD | `OPEN` — [#46](https://github.com/launchpad-26/buzz/issues/46) |\n+| adr: minimum external security smoke test before a deployment is declared healthy | `OPEN` — [#47](https://github.com/launchpad-26/buzz/issues/47) |\n+| adr: secret-scanning engine for the fork, and where its allowlist lives | `OPEN` — [#63](https://github.com/launchpad-26/buzz/issues/63) |\n+| adr: dependency update path for this fork — Dependabot, Renovate, or neither | `OPEN` — [#64](https://github.com/launchpad-26/buzz/issues/64) |\n+| adr: what privilege the repository security audit runs with | `OPEN` — [#65](https://github.com/launchpad-26/buzz/issues/65) |\n+\n+The last three sit under [#62](https://github.com/launchpad-26/buzz/issues/62), the PRD\n+for repository security hygiene. The host-hardening decisions above them were raised\n+under [#5](https://github.com/launchpad-26/buzz/issues/5).\n+[#43](https://github.com/launchpad-26/buzz/issues/43) is filed standalone — no PRD raised\n+it — which is why it appears here and in\n+[ARCHITECTURE.md § Open decisions](ARCHITECTURE.md#open-decisions) without a parent.\n+\n+Most of the rows above also appear in\n+[ARCHITECTURE.md § Open decisions](ARCHITECTURE.md#open-decisions), which lists every\n+decision the target architecture depends on rather than only the security ones. Both\n+documents carry the shared rows on purpose, so a reader arriving at either sees a complete\n+list — the cost is that an accepted ADR has to be struck from both, and updating one alone\n+leaves the other asserting a decision that is closed.\n+\n+Those markers cover only the rows present when this section was written, all confirmed\n+`OPEN` against the live issues on 2026-08-11 with `gh issue view`. A decision added later\n+carries its own marker, and a decision that is taken becomes `DECIDED` with a link to its\n+accepted ADR in [`decisions/`](decisions) — see [`AGENTS.md`](AGENTS.md) §2.\n+\n+---\n+\n+## The public-repository rule\n+\n+The rule exists and binds — `IMPLEMENTED` — [`AGENTS.md` §8](AGENTS.md). Nothing enforces\n+it; the last paragraph of this section says what that marker does and does not cover.\n+Quoted in full:\n+\n+> - **Never open a public issue for a vulnerability.** Use the private advisory link on\n+> the issue chooser page.\n+> - **This repository is public.** Every file you commit is world-readable. Config is\n+> fine; credentials never are. Parameterise secrets out of files from the first commit.\n+> - Never add a secret, key, token, or private hostname to a tracked file.\n+\n+Those rules bind this document as tightly as they bind any deployment file, which is why\n+nothing here names a hostname, a credential or a person. A security posture is exactly\n+the kind of document that tempts an author to be concrete, and being concrete is how\n+operational detail reaches a world-readable file.\n+\n+A vulnerability in Buzz the product, rather than in this cohort's operation of it, routes\n+as [`SECURITY.md`](../SECURITY.md) directs — still never as a public issue here. That\n+split is recorded in [#62](https://github.com/launchpad-26/buzz/issues/62)'s non-goals.\n+\n+`IMPLEMENTED` above marks the rule's existence and its binding force, and nothing beyond\n+that. **A rule that binds is not a mechanism that checks.** Nothing detects a violation of\n+§8: detection of secret material in diffs and history is `OPEN`, and whether secret\n+scanning and push protection are even switched on cannot be answered without admin — both\n+in [What is true today](#what-is-true-today). Compliance therefore rests on the attention\n+of whoever is writing, which\n+[#62](https://github.com/launchpad-26/buzz/issues/62) already measured and declined to\n+treat as a control. Of the near-misses it records: \"All three were caught by human\n+attention alone.\" Its verdict on that: \"Three catches in one session is not evidence that\n+attention works; it is evidence of the rate at which this material is generated.\"\n+\n+---\n+\n+## Adding to this document\n+\n+> Additions arrive by pull request against `launchpad`. Every new claim carries a status\n+> marker and a link to its evidence. Anything not yet true is an issue, not a line here —\n+> see [`AGENTS.md` §2](AGENTS.md). Append within a section rather than renumbering\n+> headings, so links from issues and the handbook keep resolving.\n+\n+A new accepted risk additionally needs a `Why accepted` entry and a source that can be\n+read. Without one it is not an accepted risk — it is an undocumented one."},{"sha":"7f507cbd50be61b7ccc55e3673551d7305a40015","filename":"launchpad/VISION.md","status":"added","additions":203,"deletions":0,"changes":203,"blob_url":"https://github.com/launchpad-26/buzz/blob/4434519667afac207f8eeb3950cc5c5de726addd/launchpad%2FVISION.md","raw_url":"https://github.com/launchpad-26/buzz/raw/4434519667afac207f8eeb3950cc5c5de726addd/launchpad%2FVISION.md","contents_url":"https://api.github.com/repos/launchpad-26/buzz/contents/launchpad%2FVISION.md?ref=4434519667afac207f8eeb3950cc5c5de726addd","patch":"@@ -0,0 +1,203 @@\n+# Vision — the launchpad-26 cohort's Buzz environment\n+\n+What this cohort is building on top of [`block/buzz`](https://github.com/block/buzz),\n+for whom, and what \"done\" looks like beyond the current milestone.\n+\n+This document records direction. It does not implement any of it, and it does not settle\n+anything an open decision owns — where a choice is open it says so and links the issue.\n+\n+---\n+\n+## How to read this\n+\n+Every claim in this document carries exactly one status marker. The marker is the claim's\n+honesty, not decoration: a proposal presented as a decision is worse than no document at\n+all.\n+\n+| Marker | Means | Evidence it carries |\n+|---|---|---|\n+| `IMPLEMENTED` | True today | a link to the file or commit |\n+| `DECIDED` | Agreed, not built | a link to the accepted ADR |\n+| `PROPOSED` | Not yet agreed | — |\n+| `OPEN` | Undecided | a link to the ADR issue |\n+\n+Where a claim's evidence is a milestone or an issue rather than an ADR document, the\n+Source or Status cell names it instead — the requirement is a link a reader can follow,\n+not a particular kind of link.\n+\n+The other documents in this set link to this section rather than restating the legend.\n+\n+---\n+\n+## Mission\n+\n+`PROPOSED`\n+\n+The launchpad-26 cohort operates Buzz; it does not develop Buzz. Our mission is to deploy\n+and run a shared Buzz platform that the cohort can safely depend on and recreate, and to\n+use running it as the way we learn to operate real systems. Beyond that, the intent is for\n+Buzz to become an operational intelligence and knowledge platform for the cohort, rather\n+than only the software the cohort happens to be running.\n+\n+Synthesised from [`AGENTS.md` §1](AGENTS.md),\n+[milestone M0](https://github.com/launchpad-26/buzz/milestone/1) and\n+[milestone M1](https://github.com/launchpad-26/buzz/milestone/2). Marked\n+`PROPOSED` rather than `DECIDED` because nobody has written or confirmed these particular\n+sentences: [#42](https://github.com/launchpad-26/buzz/issues/42) requires the vision to be\n+written or confirmed by a human, and approving this document's pull request is what\n+promotes it.\n+\n+---\n+\n+## Why Buzz\n+\n+`PROPOSED` — a human confirmed these reasons on 2026-08-11, and no ADR issue was raised\n+for the question, so there is no accepted decision record to link. Approving this\n+document's pull request is what promotes them, exactly as for the Mission above.\n+\n+| Reason | Substance |\n+|---|---|\n+| Agents cannot live in Discord | Buzz treats agents as first-class members that can be initiated on the relay and executed elsewhere. Discord bots can converse; they cannot be given work this way. |\n+| Learning by operating it | The cohort learns DevOps, security and AgentOps by running a real internet-facing system. Operating it is the curriculum, not a side effect. |\n+| We own the infrastructure and data | Self-hosting means the cohort controls the relay, identities, data and membership, with no dependence on a third party's terms, availability or export limits. |\n+| One surface for humans, agents and knowledge | Answer \"what is changing?\" and \"how does this work?\" without first knowing which repository holds the answer. |\n+\n+The section-level marker above covers only the rows present when this section was\n+written. A row added later does not inherit it — give any new reason its own status\n+marker and its own link to evidence.\n+\n+The wording of \"One surface for humans, agents and knowledge\" comes from\n+[milestone M1](https://github.com/launchpad-26/buzz/milestone/2), whose completion\n+statement is the cohort being able to answer those two questions \"without first needing to\n+know which repository, changelog, issue, PR or documentation file contains the answer.\"\n+\n+---\n+\n+## Who this is for\n+\n+| Audience | What they need from this environment | Status |\n+|---|---|---|\n+| Cohort members | A shared Buzz platform they can depend on and recreate, taking over from Discord as the cohort's coordination surface. | `PROPOSED` — [#42](https://github.com/launchpad-26/buzz/issues/42) |\n+| Agents contributing to this repository | A written statement of what is being built and why, so work is not scoped against an unwritten target, alongside the rules for how work is filed. | `IMPLEMENTED` — this document and [`AGENTS.md`](AGENTS.md) |\n+| Developers of `launchpad-26/rhizomorph` | Agent work initiated on the relay and executed elsewhere, directed at assisting that project's development. | `PROPOSED` — [#42](https://github.com/launchpad-26/buzz/issues/42); the execution boundary is `OPEN` — [#43](https://github.com/launchpad-26/buzz/issues/43) |\n+| Contributors joining later | Enough context to propose an agent workflow, and to state the access it needs and its blast radius, without first asking someone what the system around it is. | `PROPOSED` — [#42](https://github.com/launchpad-26/buzz/issues/42) |\n+\n+---\n+\n+## What we are building\n+\n+The direction below is recorded in\n+[#42](https://github.com/launchpad-26/buzz/issues/42), and a human confirmed it on\n+2026-08-11. Nothing here is deployed, and #42 records the *requirement* for that\n+confirmation rather than the confirmation itself, so each row stays `PROPOSED` until\n+approving this document's pull request promotes it — the same promotion event the Mission\n+section names.\n+\n+| Direction | Status |\n+|---|---|\n+| Migrate the cohort off Discord onto Buzz | `PROPOSED` — [#42](https://github.com/launchpad-26/buzz/issues/42) |\n+| Initiate agents on the relay that execute on cohort members' own machines | `PROPOSED` — [#42](https://github.com/launchpad-26/buzz/issues/42); how execution is contained is `OPEN` — [#43](https://github.com/launchpad-26/buzz/issues/43) |\n+| Direct that agent work at assisting development of `launchpad-26/rhizomorph` | `PROPOSED` — [#42](https://github.com/launchpad-26/buzz/issues/42) |\n+\n+This changes what \"secure\" and \"correctly sized\" mean for the relay: it stops being only a\n+service to protect and becomes an initiation point for code execution on other people's\n+machines. That consequence is owned by\n+[#43](https://github.com/launchpad-26/buzz/issues/43) and is not settled here.\n+\n+---\n+\n+## Constraints we build within\n+\n+Every row below is sourced, and every status was checked against the live issue state on\n+2026-08-11.\n+\n+| Constraint | Status | Source |\n+|---|---|---|\n+| The repository is public; every tracked file is world-readable | `IMPLEMENTED` | [`launchpad/AGENTS.md` §8](AGENTS.md) |\n+| No secrets, keys, tokens, private hostnames or member rosters in tracked files | `IMPLEMENTED` as a binding rule, not as a checked property of the repository — [SECURITY-POSTURE.md § The public-repository rule](SECURITY-POSTURE.md#the-public-repository-rule) | [`launchpad/AGENTS.md` §8](AGENTS.md) and [#42](https://github.com/launchpad-26/buzz/issues/42) |\n+| The cohort operates Buzz and does not develop it; product bugs go upstream | `IMPLEMENTED` | [`launchpad/AGENTS.md` §1](AGENTS.md) |\n+| Upstream files are never moved or renamed; cohort files live under `launchpad/` | `IMPLEMENTED` | [`launchpad/AGENTS.md` §3](AGENTS.md) |\n+| Stable knowledge is a document; active work is an issue | `IMPLEMENTED` | [`launchpad/AGENTS.md` §2](AGENTS.md) |\n+| Agents draft everything and approve nothing | `IMPLEMENTED` | [`launchpad/AGENTS.md` §5 rule 1](AGENTS.md) |\n+| One VPS of cohort scale; whether the stack fits the proposed sizing is unmeasured | `OPEN` | [#18](https://github.com/launchpad-26/buzz/issues/18), [#21](https://github.com/launchpad-26/buzz/issues/21) |\n+| Routine deployment without distributing root credentials to cohort members | `OPEN` | [#5](https://github.com/launchpad-26/buzz/issues/5), [#25](https://github.com/launchpad-26/buzz/issues/25) |\n+\n+The host sizing is deliberately not stated as a settled number.\n+[#18](https://github.com/launchpad-26/buzz/issues/18) is a task to *measure* whether the\n+stack fits the proposed sizing, and [#21](https://github.com/launchpad-26/buzz/issues/21)\n+has not decided the specification. Writing an unmeasured hypothesis here as though it were\n+a constraint is exactly the failure this document is meant to avoid.\n+\n+The full rules for filing, reviewing and merging work are in\n+[`AGENTS.md`](AGENTS.md); this section lists only the constraints that shape *what* gets\n+built, not *how* work is filed.\n+\n+---\n+\n+## What success looks like\n+\n+Whole-programme measures, not the acceptance criteria of any single PRD. Those live on\n+their own issues.\n+\n+| Measure | Status | Source |\n+|---|---|---|\n+| Multiple cohort members can connect to a usable internet-facing Buzz server and communicate through it | `DECIDED` | [milestone M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| The server is hardened around least privilege, minimal exposure and explicit trust | `DECIDED` | [milestone M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| Running it does not require routine sharing of root credentials | `DECIDED` | [milestone M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| The server can be deployed and maintained through automation | `DECIDED` | [milestone M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| The server can be rebuilt from a bare supported Ubuntu host if the existing one is lost | `DECIDED` | [milestone M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| The outcome is a shared Buzz platform the cohort can safely depend on and recreate — not merely \"Buzz is running\" | `DECIDED` | [milestone M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| The cohort can answer \"What is changing?\" without first knowing which repository, changelog, issue, PR or documentation file holds the answer | `DECIDED` | [milestone M1](https://github.com/launchpad-26/buzz/milestone/2) |\n+| The cohort can answer \"How does this work?\" on the same terms | `DECIDED` | [milestone M1](https://github.com/launchpad-26/buzz/milestone/2) |\n+| Agents initiated on the relay do useful work assisting development of `launchpad-26/rhizomorph` | `PROPOSED` | beyond both current milestones; direction recorded in [#42](https://github.com/launchpad-26/buzz/issues/42) |\n+\n+`DECIDED` here means the measure is agreed and recorded on its milestone, and is not met\n+yet. None of these is `IMPLEMENTED`.\n+\n+---\n+\n+## Scope boundaries\n+\n+| Concern | In this cohort's scope? | Status |\n+|---|---|---|\n+| Deploying Buzz and configuring the host it runs on | Yes | `DECIDED` — [milestone M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| Operating the relay and the community running on it | Yes | `DECIDED` — [milestone M0](https://github.com/launchpad-26/buzz/milestone/1) |\n+| Documenting what the cohort is doing with Buzz and why | Yes | `IMPLEMENTED` for this document; the wider set is [#42](https://github.com/launchpad-26/buzz/issues/42) |\n+| Automating deployment, CI/CD and agent workflows around Buzz | Yes | `DECIDED` — [milestone M0](https://github.com/launchpad-26/buzz/milestone/1) and [milestone M1](https://github.com/launchpad-26/buzz/milestone/2) |\n+| Developing Buzz itself — Rust crates, desktop, mobile | No. Genuine product bugs go to [`block/buzz` issues](https://github.com/block/buzz/issues) | `IMPLEMENTED` — [`AGENTS.md` §1](AGENTS.md) |\n+| Publishing the knowledge layer — the MkDocs surface, page contract, provenance gates and synthesised content | No. Owned by [#4](https://github.com/launchpad-26/buzz/issues/4) on [milestone M1](https://github.com/launchpad-26/buzz/milestone/2), per [#42](https://github.com/launchpad-26/buzz/issues/42)'s non-goals | `OPEN` — [#4](https://github.com/launchpad-26/buzz/issues/4) |\n+\n+This document produces source material a later synthesis can draw from. If its content\n+ends up published, it is published by [#4](https://github.com/launchpad-26/buzz/issues/4)'s\n+machinery, not by anything here.\n+\n+---\n+\n+## Relationship to rhizomorph\n+\n+`launchpad-26/rhizomorph` is a public sibling project in the same organisation, described\n+as \"A live, replayable localhost dashboard for a git-worktree agent swarm — watches\n+worktrees, branches, tmux panes, and workmux state, read-only.\"\n+\n+[ADR-0002](decisions/ADR-0002-handbook-source-repository-scope.md) records its role for\n+the handbook as \"Agent observability tooling\", carrying the `[supporting]` origin prefix.\n+\n+[#43](https://github.com/launchpad-26/buzz/issues/43) records the asymmetry that matters\n+most between the two projects: \"Sibling project `launchpad-26/rhizomorph` is deliberately\n+read-only; agents deployed to assist its development are not, and that delta is exactly\n+what needs containing\".\n+\n+| Concern | Owner | Status |\n+|---|---|---|\n+| Coordination surface, agent initiation, relay operation | `launchpad-26/buzz` (this repository) | `OPEN` — nothing deployed |\n+| Observing a git-worktree agent swarm on a developer's machine | `launchpad-26/rhizomorph` | `IMPLEMENTED` (that project) |\n+| Containment at the execution boundary between them | undecided | `OPEN` — [#43](https://github.com/launchpad-26/buzz/issues/43) |\n+\n+---\n+\n+## Adding to this document\n+\n+> Additions arrive by pull request against `launchpad`. Every new claim carries a status\n+> marker and a link to its evidence. Anything not yet true is an issue, not a line here —\n+> see [`AGENTS.md` §2](AGENTS.md). Append within a section rather than renumbering\n+> headings, so links from issues and the handbook keep resolving."}]} \ No newline at end of file diff --git a/launchpad/scripts/testdata/pr86-meta.json b/launchpad/scripts/testdata/pr86-meta.json new file mode 100644 index 00000000000..2c7efd17fb4 --- /dev/null +++ b/launchpad/scripts/testdata/pr86-meta.json @@ -0,0 +1 @@ +{"body":"## Summary\n\nAdds five documents under `launchpad/` — `VISION.md`, `ARCHITECTURE.md`, `ENVIRONMENTS.md`,\n`SECURITY-POSTURE.md` and `REQUIREMENTS.md` — recording what the cohort is building, the\nintended end state, where Buzz runs, where the security controls actually stand, and what\nthe environment is required to do and to be. Every claim carries one of four status markers\nand a link a reader can follow, so a proposal is never readable as a decision. No upstream\nfile is touched and nothing is deployed by this change.\n\n**Scope widened after review.** `REQUIREMENTS.md` arrived via #92, which was reviewed\nseparately against #91 and squash-merged into this branch at @serina-mcfall's direction, so\nboth bodies of work land under a single approval rather than a two-stage merge. #92 carries\nits own review with its own verification output; this PR body covers the original four.\n\n### Related issue\n\nCloses #79\nCloses #91\n\n### Issue type\n\nTask\n\n---\n\n### Agent provenance\n\n| Field | Value |\n|---|---|\n| Harness / provider | Claude Code |\n| Model | `claude-opus-5[1m]` (Claude Opus 5, 1M-context variant) |\n| Session reference | N/A - the harness exposes no shareable run id |\n| Initiating human | @serina-mcfall (Serina Mcfall) |\n\n### Objective\n\nCreate the cohort's vision, target architecture, environments and security-posture\ndocuments as four reviewable files under `launchpad/`.\n\n### Impacted components\n\n```\nlaunchpad/VISION.md\nlaunchpad/ARCHITECTURE.md\nlaunchpad/ENVIRONMENTS.md\nlaunchpad/SECURITY-POSTURE.md\n```\n\n### Approach and rejected alternatives\n\nFour separate documents, each opening with a status legend or a link to the one in\n`VISION.md`, each ending with an \"Adding to this document\" block stating how the next\nperson extends it. Claims are tables of rows, and every row carries its own marker rather\nthan inheriting a section-level one, so appending a row cannot silently borrow a status it\nwas never given.\n\n**Rejected: one combined document.** It reads well once and then rots, because the four\nsubjects change on different clocks — the environment list changes when a host appears,\nthe posture changes when a control lands, the architecture changes when a decision is\ntaken. Splitting them means a pull request that changes one does not have to re-review the\nother three.\n\n**Rejected: stating a recommended containment mechanism at the agent execution boundary.**\n#42's non-goals reserve the agent execution security model to its own ADR (#43). Both\n`ARCHITECTURE.md § The execution boundary` and\n`SECURITY-POSTURE.md § The gap this document exists to surface` therefore name the risk\nand stop without ranking the options.\n\n**Rejected: writing a host sizing figure into `VISION.md § Constraints we build within`.**\n#18 is a task to *measure* whether the stack fits the proposed sizing and #21 has not\ndecided the specification, so the row is `OPEN` with both issues linked rather than\ncarrying an unmeasured number.\n\n### Verification\n\nCommand run:\n\n```\n# Step 1 — status markers, closed-set language, secrets, owner names\nfor f in launchpad/VISION.md launchpad/ARCHITECTURE.md launchpad/ENVIRONMENTS.md launchpad/SECURITY-POSTURE.md; do\n echo \"=== $f ===\"\n echo \"-- markers used:\"\n grep -oE '\\b(IMPLEMENTED|DECIDED|PROPOSED|OPEN)\\b' \"$f\" | sort | uniq -c\n echo \"-- closed-set language (want none):\"\n grep -niE 'the (two|three|four|five|six|seven) (reasons|constraints|environments|documents|goals)' \"$f\"\n echo \"-- secrets or rosters (want none):\"\n grep -niE 'password|token|secret|api[_-]?key|BEGIN [A-Z ]*PRIVATE KEY' \"$f\"\n echo \"-- owner names (want none):\"\n grep -nE '@[A-Za-z0-9_-]+' \"$f\"\ndone\n```\n\nRaw output:\n\n```\n=== launchpad/VISION.md ===\n-- markers used:\n 14 DECIDED\n 12 IMPLEMENTED\n 8 OPEN\n 12 PROPOSED\n-- closed-set language (want none):\n-- secrets or rosters (want none):\n115:| No secrets, keys, tokens, private hostnames or member rosters in tracked files | `IMPLEMENTED` | `launchpad/AGENTS.md` §8 and [#42](https://github.com/launchpad-26/buzz/issues/42) |\n-- owner names (want none):\n=== launchpad/ARCHITECTURE.md ===\n-- markers used:\n 1 DECIDED\n 4 IMPLEMENTED\n 22 OPEN\n 1 PROPOSED\n-- closed-set language (want none):\n-- secrets or rosters (want none):\n154:| adr: CI/CD deployment identity, privilege boundary and secret storage | `OPEN` — [#25](https://github.com/launchpad-26/buzz/issues/25) |\n-- owner names (want none):\n=== launchpad/ENVIRONMENTS.md ===\n-- markers used:\n 5 IMPLEMENTED\n 6 OPEN\n-- closed-set language (want none):\n-- secrets or rosters (want none):\n-- owner names (want none):\n=== launchpad/SECURITY-POSTURE.md ===\n-- markers used:\n 9 DECIDED\n 5 IMPLEMENTED\n 27 OPEN\n-- closed-set language (want none):\n-- secrets or rosters (want none):\n45:| Detection of secret material in pull-request diffs and in git history | `OPEN` — [#67](https://github.com/launchpad-26/buzz/issues/67), [#63](https://github.com/launchpad-26/buzz/issues/63) |\n61:[#71](https://github.com/launchpad-26/buzz/issues/71). Whether secret scanning and push\n93:| Secret-shaped material already in git history is not removed by the hygiene work | `DECIDED` | \"remediation is a separate decision with its own blast radius — this PRD delivers the finding, not the force-push\" | [#62](https://github.com/launchpad-26/buzz/issues/62) non-goals |\n138:| adr: CI/CD deployment identity, privilege boundary and secret storage | `OPEN` — [#25](https://github.com/launchpad-26/buzz/issues/25) |\n147:| adr: secret-scanning engine for the fork, and where its allowlist lives | `OPEN` — [#63](https://github.com/launchpad-26/buzz/issues/63) |\n181:> fine; credentials never are. Parameterise secrets out of files from the first commit.\n182:> - Never add a secret, key, token, or private hostname to a tracked file.\n195:§8: detection of secret material in diffs and history is `OPEN`, and whether secret\n-- owner names (want none):\n```\n\n**Reading that output rather than summarising it.** The closed-set-language check and the\nowner-name check printed nothing for all four files. The secrets check is a keyword scan,\nnot a credential detector, and it printed eleven lines across two files. Every one of them\nis the *word* `secret`, `secrets` or `tokens` appearing in prose about the policy — the\n`AGENTS.md` §8 quotation, the constraint row that forbids credentials in tracked files, and\nfive ADR titles that contain the word. None is a credential value. A reviewer should read\nthose eleven lines rather than take this paragraph's word for it.\n\nCommand run:\n\n```\n# Step 2 — every link target resolves\nfor f in launchpad/VISION.md launchpad/ARCHITECTURE.md launchpad/ENVIRONMENTS.md launchpad/SECURITY-POSTURE.md; do\n grep -oE '\\]\\(([^)]+)\\)' \"$f\" | sed -E 's/^\\]\\(//; s/\\)$//' | sort -u | while read -r t; do\n case \"$t\" in\n http*) code=$(curl -s -o /dev/null -w '%{http_code}' -L \"$t\"); [ \"$code\" = 200 ] || echo \"$f -> $t = $code\" ;;\n \\#*) anchor=$(printf '%s' \"$t\" | tr -d '#'); grep -qiE \"^#+ .*\" \"$f\" || echo \"$f -> $t no headings\" ;;\n *) base=${t%%#*}; [ -z \"$base\" ] && continue\n test -e \"launchpad/$base\" || test -e \"$base\" || echo \"BROKEN $f -> $base\" ;;\n esac\n done\ndone\necho \"(no output above means every target resolved: no BROKEN paths, no non-200 responses)\"\n```\n\nRaw output:\n\n```\n(no output above means every target resolved: no BROKEN paths, no non-200 responses)\n```\n\n65 unique link targets were checked. The loop printed nothing before the trailing echo, so\nno relative path was missing and no HTTP target returned anything other than 200 after\nredirects. GitHub did not rate-limit this run.\n\nCommand run:\n\n```\n# Anchor verification — every ](file.md#anchor) and ](#anchor) target against the\n# heading slugs GitHub derives (lowercase, punctuation stripped, spaces -> hyphens).\npython3 anchors.py\n```\n\nRaw output (heading listing elided, resolution results in full):\n\n```\n== anchor targets found in links, and whether they resolve ==\nOK launchpad/ARCHITECTURE.md:15 -> VISION.md#how-to-read-this\nOK launchpad/ARCHITECTURE.md:48 -> #what-exists-today\nOK launchpad/ARCHITECTURE.md:77 -> VISION.md#relationship-to-rhizomorph\nOK launchpad/ARCHITECTURE.md:166 -> SECURITY-POSTURE.md#open-security-decisions\nOK launchpad/ARCHITECTURE.md:173 -> SECURITY-POSTURE.md#what-is-true-today\nOK launchpad/ENVIRONMENTS.md:15 -> VISION.md#how-to-read-this\nOK launchpad/ENVIRONMENTS.md:85 -> #the-environments\nOK launchpad/SECURITY-POSTURE.md:14 -> VISION.md#how-to-read-this\nOK launchpad/SECURITY-POSTURE.md:29 -> ARCHITECTURE.md#what-exists-today\nOK launchpad/SECURITY-POSTURE.md:92 -> #what-is-true-today\nOK launchpad/SECURITY-POSTURE.md:120 -> ARCHITECTURE.md#the-execution-boundary\nOK launchpad/SECURITY-POSTURE.md:156 -> ARCHITECTURE.md#open-decisions\nOK launchpad/SECURITY-POSTURE.md:159 -> ARCHITECTURE.md#open-decisions\nOK launchpad/SECURITY-POSTURE.md:197 -> #what-is-true-today\n\nanchor targets checked: 14; failures: 0\n```\n\nStep 2's `\\#*` branch only checks that the file has headings at all — it cannot tell a live\nanchor from a dead one, and three earlier reviews left that as an unresolved ⚠️. The script\nabove closes it by deriving each target file's slugs from its `##`/`###` headings and\nmatching the link targets against them. `VISION.md` appears only as a target, never as a\nsource: it links to no anchors, which is why no line names it on the left.\n\nCommand run:\n\n```\n# Quotation check — every double-quoted span of 25+ characters in the four documents,\n# compared against the normalised bodies of the issues, milestones and ADR they cite.\n# Both sides are whitespace-collapsed first, because issue bodies hard-wrap.\npython3 quotes.py\n```\n\nRaw output:\n\n```\nFAIL launchpad/VISION.md: \"One surface for humans, agents and knowledge\"\nOK launchpad/VISION.md: \"A live, replayable localhost dashboard for a git-worktree agent swarm ...\"\nOK launchpad/VISION.md: \"Agent observability tooling\"\nOK launchpad/VISION.md: \"Sibling project `launchpad-26/rhizomorph` is deliberately\nread-only; a...\"\nFAIL launchpad/ARCHITECTURE.md: \"Owner: upstream `block/buzz`\"\nFAIL launchpad/ARCHITECTURE.md: \". On the toolset described above, the same issue says:\n\"\nOK launchpad/ENVIRONMENTS.md: \"The desktop client assumes `wss://` for any scheme-less host, so a pla...\"\nOK launchpad/SECURITY-POSTURE.md: \"nobody without admin can currently answer 'is push protection on?', an...\"\nOK launchpad/SECURITY-POSTURE.md: \"No requirement to eliminate the initial privileged bootstrap step\"\nOK launchpad/SECURITY-POSTURE.md: \"a **reproducible, defensible internet-facing cohort server**, not a co...\"\nOK launchpad/SECURITY-POSTURE.md: \"No claim that host hardening makes the application itself invulnerable\"\nOK launchpad/SECURITY-POSTURE.md: \"A larger observability or SIEM platform is outside scope; establishing...\"\nOK launchpad/SECURITY-POSTURE.md: \"can only be *enabled* by a repository admin\"\nOK launchpad/SECURITY-POSTURE.md: \"closing it is a separate, human, privileged act\"\nOK launchpad/SECURITY-POSTURE.md: \"remediation is a separate decision with its own blast radius — this PR...\"\nOK launchpad/SECURITY-POSTURE.md: \"#5 currently treats the host as an asset to protect, and this makes it...\"\nOK launchpad/SECURITY-POSTURE.md: \"the agent execution security model in particular is a separate ADR and...\"\nOK launchpad/SECURITY-POSTURE.md: \"All three were caught by human\nattention alone.\"\nOK launchpad/SECURITY-POSTURE.md: \"Three catches in one session is not evidence that\nattention works; it ...\"\n\nquoted spans checked: 19; not found in cited issue bodies: 3\n```\n\nThree `FAIL` lines, adjudicated rather than waved through:\n\n- `\"Owner: upstream `block/buzz`\"` — `ARCHITECTURE.md` quoting the value in its own\n Components table, not quoting an issue. Correct as written.\n- `\". On the toolset described above, the same issue says:\"` — a regex artefact. The\n matcher paired the closing quote of one quotation with the opening quote of the next,\n capturing the connecting prose between them. Both real quotations either side of it\n appear as `OK` lines. Not a defect in the document.\n- `\"One surface for humans, agents and knowledge\"` — a real observation, carried below in\n *Not verified*.\n\nThis check found one genuine defect, now fixed in `493531a7f`: `VISION.md` quoted #43 as\n*\"rhizomorph is deliberately read-only…\"* where the issue reads *\"Sibling project\n`launchpad-26/rhizomorph` is deliberately read-only…\"*, and added a full stop the source\nbullet does not have. The elision was silent, so the quotation is now given in full.\n\nCommand run:\n\n```\n# Step 4 — branch scope\ngit diff --stat origin/launchpad...HEAD\ngit status --short\n```\n\nRaw output:\n\n```\n launchpad/ARCHITECTURE.md | 189 +++++++++++++++++++++++++++++++++++++\n launchpad/ENVIRONMENTS.md | 86 +++++++++++++++++\n launchpad/SECURITY-POSTURE.md | 214 ++++++++++++++++++++++++++++++++++++++++++\n launchpad/VISION.md | 201 +++++++++++++++++++++++++++++++++++++++\n 4 files changed, 690 insertions(+)\n?? .claude/.edit-count\n?? .claude/.session-tracker\n?? .claude/.sid-a43b0276\n?? .claude/.sid-e9ab6c70\n?? .claude/.verified\n?? .claude/.worklog.lock\n?? .claude/session-journal.md\n?? .claude/worklog.md\n?? .repoql/\n```\n\nFour added files, all under `launchpad/`, no upstream file modified — `AGENTS.md` §3. The\nuntracked entries are local agent scratch state and are deliberately unstaged; none appears\nin the diff above.\n\n- [x] Tests or checks were run and the raw output is pasted above\n- [x] The diff is confined to the scope of the linked issue\n- [x] No secrets, keys, tokens or hostnames were added to tracked files\n\n### #42's success criteria, one line each\n\nThis PR closes #79, not #42. #42 is the PRD; this walkthrough says where each of its\neleven criteria now lives so the PRD can be judged on evidence rather than on this PR's\nmerge.\n\n1. **Vision document — what, for whom, what \"done\" looks like.** `VISION.md` §§ Mission,\n Who this is for, What we are building, What success looks like.\n2. **Target architecture, marking what exists and what does not.** `ARCHITECTURE.md`\n §§ The intended end state, How an agent is initiated and where it executes, The\n execution boundary, What exists today.\n3. **Relationship to `launchpad-26/rhizomorph`, including which project owns which\n concern.** `VISION.md § Relationship to rhizomorph` — an ownership table with a row for\n each concern, including the undecided one.\n4. **Environments document, at minimum the local VM harness and the cohort VPS.**\n `ENVIRONMENTS.md` §§ The environments (both are rows), What each one proves, What must\n never differ between them.\n5. **A development process document exists, or points at `launchpad/AGENTS.md` rather\n than restating it.** Satisfied by pointing, not by a new document: `VISION.md §\n Constraints we build within` closes by directing the reader to `AGENTS.md` for the\n filing, review and merge rules, and each document's \"Adding to this document\" block\n cites `AGENTS.md` §2.\n6. **Observability strategy recorded as a decision naming candidates.** **Carried by\n #83**, not by a document — #79's scope explicitly excludes it. #83 is now listed in\n `ARCHITECTURE.md § Open decisions` so the list is not stale on the day it merges.\n7. **Testing methodology recorded the same way.** **Carried by #84**, not by a document,\n and by design it appears in neither document's decision list — see *Not verified*.\n8. **Security posture and known gaps, accepted and open.** `SECURITY-POSTURE.md`\n §§ What is true today, Accepted risks, The gap this document exists to surface, Open\n security decisions.\n9. **Milestones documented with what each is for.** **Carried by #81**. Nothing in this PR\n delivers it; the four documents cite M0 and M1 as links but do not describe them.\n10. **Every document states honestly what is decided, proposed and unknown.** The marker\n convention, defined once in `VISION.md § How to read this` and linked from the other\n three. The Step 1 marker counts above are the evidence that all four use it; the\n twelve `PROPOSED` markers in `VISION.md` are the ones this review ratifies or does not.\n11. **Nothing restates something a linked issue or ADR already owns.** The two places it\n would be most tempting both stop deliberately: `ARCHITECTURE.md § The execution\n boundary` and `SECURITY-POSTURE.md § The gap this document exists to surface` name the\n risk and decline to rank the options, citing #42's non-goals; `VISION.md § Scope\n boundaries` leaves publishing to #4.\n\n### Not verified\n\n- **Whether the Mission wording reflects the group's intent.** `VISION.md § Mission`,\n § Why Buzz and § What we are building are marked `PROPOSED` precisely because nobody has\n agreed these particular sentences. **Approving this pull request is the act that promotes\n them.** Leaving them `PROPOSED` is a legitimate outcome — if the group has not settled\n the wording, say so in review rather than approving around it.\n- **`\"One surface for humans, agents and knowledge\"`.** `VISION.md § Why Buzz` says the\n wording of that row \"comes from milestone M1\". M1's text reads \"one coherent,\n provenance-preserving knowledge surface\" and \"humans and agents\"; the row is a\n condensation of those, not a verbatim string in the milestone. The two questions quoted\n in the same sentence *are* verbatim. A reviewer who reads \"comes from\" as \"is quoted\n from\" would be misled by a sentence that is otherwise accurate.\n- **Whether #83 also belongs in `SECURITY-POSTURE.md § Open security decisions`.** It was\n added to `ARCHITECTURE.md § Open decisions` only, with the reasoning stated in the\n document. That is a judgement call, not a verified fact, and a reviewer may overrule it.\n- **Whether #84 belongs in either list.** It was added to neither. A testing methodology\n is a process decision: the architecture's shape does not depend on it, and the posture\n asserts no control that it defines. #84 is a live `type:adr` issue on M0 and is carried\n there. If the group wants process decisions visible in this document set, that is a new\n section rather than a row in a list scoped to something else.\n- **No document was tested against a running system**, because there is none — the cohort\n runs Buzz on `localhost` only. Every `OPEN` and `PROPOSED` row is sourced to an issue or\n a milestone, never to an observation of a deployment.\n- **Issue and milestone states were read on 2026-08-11.** Any issue closed after that makes\n the row citing it stale; the check is a re-read, not a rerun of anything here.\n- **The `curl` loop confirms an HTTP 200, not that a page's content still says what the\n document claims.** A renamed issue would still return 200.\n- **No human has read these documents end to end yet.** The checks above are structural.\n Whether the prose is *right* is what this review is for.\n\n### Security implications\n\nThis repository is public, and a written architecture is a map for an attacker as much as\nfor a contributor — recorded as an accepted risk in `SECURITY-POSTURE.md § Accepted risks`\nwith its reasoning. Nothing here names a hostname, a credential, an IP address or a cohort\nmember; the Step 1 scan output above is the evidence, read honestly in the paragraph under\nit. No control changes, because these documents apply none — `SECURITY-POSTURE.md § What\nis true today` states plainly that the cohort's controls are designed, not applied, and\nthat exactly one row in it (`cargo-deny check` in CI) is a control that runs.\n\nThe change with the largest security consequence is one of visibility rather than\nexposure: `SECURITY-POSTURE.md § The gap this document exists to surface` writes down, in\na world-readable file, that a relay initiating agents which execute on contributors'\nmachines stops being only a service to protect and becomes a distribution point for code\nexecution. That gap is already public in #42 and #43. Recording it here makes it harder to\nlose track of, which is the trade #42 already accepted.\n\n### Escalations\n\n- **Placement of #83 and #84.** Both ADRs were filed after these documents were written,\n so both lists predated them. I added #83 to `ARCHITECTURE.md § Open decisions` and added\n #84 to nothing, with reasoning stated in the document and in *Not verified*. Neither is a\n decision I am entitled to make alone; overruling either is a review comment, not a\n reopened task.\n- **Approval.** `AGENTS.md` §5 rule 1 — draft everything, approve nothing. This PR is not\n self-approved and must not be. The `PROPOSED` sections above are promoted by a human's\n approval, which is the point of the marker.\n\n","labels":[{"id":"LA_kwDOTv8O788AAAACvrDemQ","name":"by:agent","description":"Filed or authored by an AI agent, not a human","color":"5319e7"}],"title":"docs(launchpad): the cohort's vision, architecture, environments, security posture and requirements"} diff --git a/launchpad/scripts/testdata/pr86-pr.json b/launchpad/scripts/testdata/pr86-pr.json new file mode 100644 index 00000000000..72bce6b6369 --- /dev/null +++ b/launchpad/scripts/testdata/pr86-pr.json @@ -0,0 +1 @@ +{"url":"https://api.github.com/repos/launchpad-26/buzz/pulls/86","id":4251877725,"node_id":"PR_kwDOTv8O7879boFd","html_url":"https://github.com/launchpad-26/buzz/pull/86","diff_url":"https://github.com/launchpad-26/buzz/pull/86.diff","patch_url":"https://github.com/launchpad-26/buzz/pull/86.patch","issue_url":"https://api.github.com/repos/launchpad-26/buzz/issues/86","number":86,"state":"closed","locked":false,"title":"docs(launchpad): the cohort's vision, architecture, environments, security posture and requirements","user":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"## Summary\n\nAdds five documents under `launchpad/` — `VISION.md`, `ARCHITECTURE.md`, `ENVIRONMENTS.md`,\n`SECURITY-POSTURE.md` and `REQUIREMENTS.md` — recording what the cohort is building, the\nintended end state, where Buzz runs, where the security controls actually stand, and what\nthe environment is required to do and to be. Every claim carries one of four status markers\nand a link a reader can follow, so a proposal is never readable as a decision. No upstream\nfile is touched and nothing is deployed by this change.\n\n**Scope widened after review.** `REQUIREMENTS.md` arrived via #92, which was reviewed\nseparately against #91 and squash-merged into this branch at @serina-mcfall's direction, so\nboth bodies of work land under a single approval rather than a two-stage merge. #92 carries\nits own review with its own verification output; this PR body covers the original four.\n\n### Related issue\n\nCloses #79\nCloses #91\n\n### Issue type\n\nTask\n\n---\n\n### Agent provenance\n\n| Field | Value |\n|---|---|\n| Harness / provider | Claude Code |\n| Model | `claude-opus-5[1m]` (Claude Opus 5, 1M-context variant) |\n| Session reference | N/A - the harness exposes no shareable run id |\n| Initiating human | @serina-mcfall (Serina Mcfall) |\n\n### Objective\n\nCreate the cohort's vision, target architecture, environments and security-posture\ndocuments as four reviewable files under `launchpad/`.\n\n### Impacted components\n\n```\nlaunchpad/VISION.md\nlaunchpad/ARCHITECTURE.md\nlaunchpad/ENVIRONMENTS.md\nlaunchpad/SECURITY-POSTURE.md\n```\n\n### Approach and rejected alternatives\n\nFour separate documents, each opening with a status legend or a link to the one in\n`VISION.md`, each ending with an \"Adding to this document\" block stating how the next\nperson extends it. Claims are tables of rows, and every row carries its own marker rather\nthan inheriting a section-level one, so appending a row cannot silently borrow a status it\nwas never given.\n\n**Rejected: one combined document.** It reads well once and then rots, because the four\nsubjects change on different clocks — the environment list changes when a host appears,\nthe posture changes when a control lands, the architecture changes when a decision is\ntaken. Splitting them means a pull request that changes one does not have to re-review the\nother three.\n\n**Rejected: stating a recommended containment mechanism at the agent execution boundary.**\n#42's non-goals reserve the agent execution security model to its own ADR (#43). Both\n`ARCHITECTURE.md § The execution boundary` and\n`SECURITY-POSTURE.md § The gap this document exists to surface` therefore name the risk\nand stop without ranking the options.\n\n**Rejected: writing a host sizing figure into `VISION.md § Constraints we build within`.**\n#18 is a task to *measure* whether the stack fits the proposed sizing and #21 has not\ndecided the specification, so the row is `OPEN` with both issues linked rather than\ncarrying an unmeasured number.\n\n### Verification\n\nCommand run:\n\n```\n# Step 1 — status markers, closed-set language, secrets, owner names\nfor f in launchpad/VISION.md launchpad/ARCHITECTURE.md launchpad/ENVIRONMENTS.md launchpad/SECURITY-POSTURE.md; do\n echo \"=== $f ===\"\n echo \"-- markers used:\"\n grep -oE '\\b(IMPLEMENTED|DECIDED|PROPOSED|OPEN)\\b' \"$f\" | sort | uniq -c\n echo \"-- closed-set language (want none):\"\n grep -niE 'the (two|three|four|five|six|seven) (reasons|constraints|environments|documents|goals)' \"$f\"\n echo \"-- secrets or rosters (want none):\"\n grep -niE 'password|token|secret|api[_-]?key|BEGIN [A-Z ]*PRIVATE KEY' \"$f\"\n echo \"-- owner names (want none):\"\n grep -nE '@[A-Za-z0-9_-]+' \"$f\"\ndone\n```\n\nRaw output:\n\n```\n=== launchpad/VISION.md ===\n-- markers used:\n 14 DECIDED\n 12 IMPLEMENTED\n 8 OPEN\n 12 PROPOSED\n-- closed-set language (want none):\n-- secrets or rosters (want none):\n115:| No secrets, keys, tokens, private hostnames or member rosters in tracked files | `IMPLEMENTED` | `launchpad/AGENTS.md` §8 and [#42](https://github.com/launchpad-26/buzz/issues/42) |\n-- owner names (want none):\n=== launchpad/ARCHITECTURE.md ===\n-- markers used:\n 1 DECIDED\n 4 IMPLEMENTED\n 22 OPEN\n 1 PROPOSED\n-- closed-set language (want none):\n-- secrets or rosters (want none):\n154:| adr: CI/CD deployment identity, privilege boundary and secret storage | `OPEN` — [#25](https://github.com/launchpad-26/buzz/issues/25) |\n-- owner names (want none):\n=== launchpad/ENVIRONMENTS.md ===\n-- markers used:\n 5 IMPLEMENTED\n 6 OPEN\n-- closed-set language (want none):\n-- secrets or rosters (want none):\n-- owner names (want none):\n=== launchpad/SECURITY-POSTURE.md ===\n-- markers used:\n 9 DECIDED\n 5 IMPLEMENTED\n 27 OPEN\n-- closed-set language (want none):\n-- secrets or rosters (want none):\n45:| Detection of secret material in pull-request diffs and in git history | `OPEN` — [#67](https://github.com/launchpad-26/buzz/issues/67), [#63](https://github.com/launchpad-26/buzz/issues/63) |\n61:[#71](https://github.com/launchpad-26/buzz/issues/71). Whether secret scanning and push\n93:| Secret-shaped material already in git history is not removed by the hygiene work | `DECIDED` | \"remediation is a separate decision with its own blast radius — this PRD delivers the finding, not the force-push\" | [#62](https://github.com/launchpad-26/buzz/issues/62) non-goals |\n138:| adr: CI/CD deployment identity, privilege boundary and secret storage | `OPEN` — [#25](https://github.com/launchpad-26/buzz/issues/25) |\n147:| adr: secret-scanning engine for the fork, and where its allowlist lives | `OPEN` — [#63](https://github.com/launchpad-26/buzz/issues/63) |\n181:> fine; credentials never are. Parameterise secrets out of files from the first commit.\n182:> - Never add a secret, key, token, or private hostname to a tracked file.\n195:§8: detection of secret material in diffs and history is `OPEN`, and whether secret\n-- owner names (want none):\n```\n\n**Reading that output rather than summarising it.** The closed-set-language check and the\nowner-name check printed nothing for all four files. The secrets check is a keyword scan,\nnot a credential detector, and it printed eleven lines across two files. Every one of them\nis the *word* `secret`, `secrets` or `tokens` appearing in prose about the policy — the\n`AGENTS.md` §8 quotation, the constraint row that forbids credentials in tracked files, and\nfive ADR titles that contain the word. None is a credential value. A reviewer should read\nthose eleven lines rather than take this paragraph's word for it.\n\nCommand run:\n\n```\n# Step 2 — every link target resolves\nfor f in launchpad/VISION.md launchpad/ARCHITECTURE.md launchpad/ENVIRONMENTS.md launchpad/SECURITY-POSTURE.md; do\n grep -oE '\\]\\(([^)]+)\\)' \"$f\" | sed -E 's/^\\]\\(//; s/\\)$//' | sort -u | while read -r t; do\n case \"$t\" in\n http*) code=$(curl -s -o /dev/null -w '%{http_code}' -L \"$t\"); [ \"$code\" = 200 ] || echo \"$f -> $t = $code\" ;;\n \\#*) anchor=$(printf '%s' \"$t\" | tr -d '#'); grep -qiE \"^#+ .*\" \"$f\" || echo \"$f -> $t no headings\" ;;\n *) base=${t%%#*}; [ -z \"$base\" ] && continue\n test -e \"launchpad/$base\" || test -e \"$base\" || echo \"BROKEN $f -> $base\" ;;\n esac\n done\ndone\necho \"(no output above means every target resolved: no BROKEN paths, no non-200 responses)\"\n```\n\nRaw output:\n\n```\n(no output above means every target resolved: no BROKEN paths, no non-200 responses)\n```\n\n65 unique link targets were checked. The loop printed nothing before the trailing echo, so\nno relative path was missing and no HTTP target returned anything other than 200 after\nredirects. GitHub did not rate-limit this run.\n\nCommand run:\n\n```\n# Anchor verification — every ](file.md#anchor) and ](#anchor) target against the\n# heading slugs GitHub derives (lowercase, punctuation stripped, spaces -> hyphens).\npython3 anchors.py\n```\n\nRaw output (heading listing elided, resolution results in full):\n\n```\n== anchor targets found in links, and whether they resolve ==\nOK launchpad/ARCHITECTURE.md:15 -> VISION.md#how-to-read-this\nOK launchpad/ARCHITECTURE.md:48 -> #what-exists-today\nOK launchpad/ARCHITECTURE.md:77 -> VISION.md#relationship-to-rhizomorph\nOK launchpad/ARCHITECTURE.md:166 -> SECURITY-POSTURE.md#open-security-decisions\nOK launchpad/ARCHITECTURE.md:173 -> SECURITY-POSTURE.md#what-is-true-today\nOK launchpad/ENVIRONMENTS.md:15 -> VISION.md#how-to-read-this\nOK launchpad/ENVIRONMENTS.md:85 -> #the-environments\nOK launchpad/SECURITY-POSTURE.md:14 -> VISION.md#how-to-read-this\nOK launchpad/SECURITY-POSTURE.md:29 -> ARCHITECTURE.md#what-exists-today\nOK launchpad/SECURITY-POSTURE.md:92 -> #what-is-true-today\nOK launchpad/SECURITY-POSTURE.md:120 -> ARCHITECTURE.md#the-execution-boundary\nOK launchpad/SECURITY-POSTURE.md:156 -> ARCHITECTURE.md#open-decisions\nOK launchpad/SECURITY-POSTURE.md:159 -> ARCHITECTURE.md#open-decisions\nOK launchpad/SECURITY-POSTURE.md:197 -> #what-is-true-today\n\nanchor targets checked: 14; failures: 0\n```\n\nStep 2's `\\#*` branch only checks that the file has headings at all — it cannot tell a live\nanchor from a dead one, and three earlier reviews left that as an unresolved ⚠️. The script\nabove closes it by deriving each target file's slugs from its `##`/`###` headings and\nmatching the link targets against them. `VISION.md` appears only as a target, never as a\nsource: it links to no anchors, which is why no line names it on the left.\n\nCommand run:\n\n```\n# Quotation check — every double-quoted span of 25+ characters in the four documents,\n# compared against the normalised bodies of the issues, milestones and ADR they cite.\n# Both sides are whitespace-collapsed first, because issue bodies hard-wrap.\npython3 quotes.py\n```\n\nRaw output:\n\n```\nFAIL launchpad/VISION.md: \"One surface for humans, agents and knowledge\"\nOK launchpad/VISION.md: \"A live, replayable localhost dashboard for a git-worktree agent swarm ...\"\nOK launchpad/VISION.md: \"Agent observability tooling\"\nOK launchpad/VISION.md: \"Sibling project `launchpad-26/rhizomorph` is deliberately\nread-only; a...\"\nFAIL launchpad/ARCHITECTURE.md: \"Owner: upstream `block/buzz`\"\nFAIL launchpad/ARCHITECTURE.md: \". On the toolset described above, the same issue says:\n\"\nOK launchpad/ENVIRONMENTS.md: \"The desktop client assumes `wss://` for any scheme-less host, so a pla...\"\nOK launchpad/SECURITY-POSTURE.md: \"nobody without admin can currently answer 'is push protection on?', an...\"\nOK launchpad/SECURITY-POSTURE.md: \"No requirement to eliminate the initial privileged bootstrap step\"\nOK launchpad/SECURITY-POSTURE.md: \"a **reproducible, defensible internet-facing cohort server**, not a co...\"\nOK launchpad/SECURITY-POSTURE.md: \"No claim that host hardening makes the application itself invulnerable\"\nOK launchpad/SECURITY-POSTURE.md: \"A larger observability or SIEM platform is outside scope; establishing...\"\nOK launchpad/SECURITY-POSTURE.md: \"can only be *enabled* by a repository admin\"\nOK launchpad/SECURITY-POSTURE.md: \"closing it is a separate, human, privileged act\"\nOK launchpad/SECURITY-POSTURE.md: \"remediation is a separate decision with its own blast radius — this PR...\"\nOK launchpad/SECURITY-POSTURE.md: \"#5 currently treats the host as an asset to protect, and this makes it...\"\nOK launchpad/SECURITY-POSTURE.md: \"the agent execution security model in particular is a separate ADR and...\"\nOK launchpad/SECURITY-POSTURE.md: \"All three were caught by human\nattention alone.\"\nOK launchpad/SECURITY-POSTURE.md: \"Three catches in one session is not evidence that\nattention works; it ...\"\n\nquoted spans checked: 19; not found in cited issue bodies: 3\n```\n\nThree `FAIL` lines, adjudicated rather than waved through:\n\n- `\"Owner: upstream `block/buzz`\"` — `ARCHITECTURE.md` quoting the value in its own\n Components table, not quoting an issue. Correct as written.\n- `\". On the toolset described above, the same issue says:\"` — a regex artefact. The\n matcher paired the closing quote of one quotation with the opening quote of the next,\n capturing the connecting prose between them. Both real quotations either side of it\n appear as `OK` lines. Not a defect in the document.\n- `\"One surface for humans, agents and knowledge\"` — a real observation, carried below in\n *Not verified*.\n\nThis check found one genuine defect, now fixed in `493531a7f`: `VISION.md` quoted #43 as\n*\"rhizomorph is deliberately read-only…\"* where the issue reads *\"Sibling project\n`launchpad-26/rhizomorph` is deliberately read-only…\"*, and added a full stop the source\nbullet does not have. The elision was silent, so the quotation is now given in full.\n\nCommand run:\n\n```\n# Step 4 — branch scope\ngit diff --stat origin/launchpad...HEAD\ngit status --short\n```\n\nRaw output:\n\n```\n launchpad/ARCHITECTURE.md | 189 +++++++++++++++++++++++++++++++++++++\n launchpad/ENVIRONMENTS.md | 86 +++++++++++++++++\n launchpad/SECURITY-POSTURE.md | 214 ++++++++++++++++++++++++++++++++++++++++++\n launchpad/VISION.md | 201 +++++++++++++++++++++++++++++++++++++++\n 4 files changed, 690 insertions(+)\n?? .claude/.edit-count\n?? .claude/.session-tracker\n?? .claude/.sid-a43b0276\n?? .claude/.sid-e9ab6c70\n?? .claude/.verified\n?? .claude/.worklog.lock\n?? .claude/session-journal.md\n?? .claude/worklog.md\n?? .repoql/\n```\n\nFour added files, all under `launchpad/`, no upstream file modified — `AGENTS.md` §3. The\nuntracked entries are local agent scratch state and are deliberately unstaged; none appears\nin the diff above.\n\n- [x] Tests or checks were run and the raw output is pasted above\n- [x] The diff is confined to the scope of the linked issue\n- [x] No secrets, keys, tokens or hostnames were added to tracked files\n\n### #42's success criteria, one line each\n\nThis PR closes #79, not #42. #42 is the PRD; this walkthrough says where each of its\neleven criteria now lives so the PRD can be judged on evidence rather than on this PR's\nmerge.\n\n1. **Vision document — what, for whom, what \"done\" looks like.** `VISION.md` §§ Mission,\n Who this is for, What we are building, What success looks like.\n2. **Target architecture, marking what exists and what does not.** `ARCHITECTURE.md`\n §§ The intended end state, How an agent is initiated and where it executes, The\n execution boundary, What exists today.\n3. **Relationship to `launchpad-26/rhizomorph`, including which project owns which\n concern.** `VISION.md § Relationship to rhizomorph` — an ownership table with a row for\n each concern, including the undecided one.\n4. **Environments document, at minimum the local VM harness and the cohort VPS.**\n `ENVIRONMENTS.md` §§ The environments (both are rows), What each one proves, What must\n never differ between them.\n5. **A development process document exists, or points at `launchpad/AGENTS.md` rather\n than restating it.** Satisfied by pointing, not by a new document: `VISION.md §\n Constraints we build within` closes by directing the reader to `AGENTS.md` for the\n filing, review and merge rules, and each document's \"Adding to this document\" block\n cites `AGENTS.md` §2.\n6. **Observability strategy recorded as a decision naming candidates.** **Carried by\n #83**, not by a document — #79's scope explicitly excludes it. #83 is now listed in\n `ARCHITECTURE.md § Open decisions` so the list is not stale on the day it merges.\n7. **Testing methodology recorded the same way.** **Carried by #84**, not by a document,\n and by design it appears in neither document's decision list — see *Not verified*.\n8. **Security posture and known gaps, accepted and open.** `SECURITY-POSTURE.md`\n §§ What is true today, Accepted risks, The gap this document exists to surface, Open\n security decisions.\n9. **Milestones documented with what each is for.** **Carried by #81**. Nothing in this PR\n delivers it; the four documents cite M0 and M1 as links but do not describe them.\n10. **Every document states honestly what is decided, proposed and unknown.** The marker\n convention, defined once in `VISION.md § How to read this` and linked from the other\n three. The Step 1 marker counts above are the evidence that all four use it; the\n twelve `PROPOSED` markers in `VISION.md` are the ones this review ratifies or does not.\n11. **Nothing restates something a linked issue or ADR already owns.** The two places it\n would be most tempting both stop deliberately: `ARCHITECTURE.md § The execution\n boundary` and `SECURITY-POSTURE.md § The gap this document exists to surface` name the\n risk and decline to rank the options, citing #42's non-goals; `VISION.md § Scope\n boundaries` leaves publishing to #4.\n\n### Not verified\n\n- **Whether the Mission wording reflects the group's intent.** `VISION.md § Mission`,\n § Why Buzz and § What we are building are marked `PROPOSED` precisely because nobody has\n agreed these particular sentences. **Approving this pull request is the act that promotes\n them.** Leaving them `PROPOSED` is a legitimate outcome — if the group has not settled\n the wording, say so in review rather than approving around it.\n- **`\"One surface for humans, agents and knowledge\"`.** `VISION.md § Why Buzz` says the\n wording of that row \"comes from milestone M1\". M1's text reads \"one coherent,\n provenance-preserving knowledge surface\" and \"humans and agents\"; the row is a\n condensation of those, not a verbatim string in the milestone. The two questions quoted\n in the same sentence *are* verbatim. A reviewer who reads \"comes from\" as \"is quoted\n from\" would be misled by a sentence that is otherwise accurate.\n- **Whether #83 also belongs in `SECURITY-POSTURE.md § Open security decisions`.** It was\n added to `ARCHITECTURE.md § Open decisions` only, with the reasoning stated in the\n document. That is a judgement call, not a verified fact, and a reviewer may overrule it.\n- **Whether #84 belongs in either list.** It was added to neither. A testing methodology\n is a process decision: the architecture's shape does not depend on it, and the posture\n asserts no control that it defines. #84 is a live `type:adr` issue on M0 and is carried\n there. If the group wants process decisions visible in this document set, that is a new\n section rather than a row in a list scoped to something else.\n- **No document was tested against a running system**, because there is none — the cohort\n runs Buzz on `localhost` only. Every `OPEN` and `PROPOSED` row is sourced to an issue or\n a milestone, never to an observation of a deployment.\n- **Issue and milestone states were read on 2026-08-11.** Any issue closed after that makes\n the row citing it stale; the check is a re-read, not a rerun of anything here.\n- **The `curl` loop confirms an HTTP 200, not that a page's content still says what the\n document claims.** A renamed issue would still return 200.\n- **No human has read these documents end to end yet.** The checks above are structural.\n Whether the prose is *right* is what this review is for.\n\n### Security implications\n\nThis repository is public, and a written architecture is a map for an attacker as much as\nfor a contributor — recorded as an accepted risk in `SECURITY-POSTURE.md § Accepted risks`\nwith its reasoning. Nothing here names a hostname, a credential, an IP address or a cohort\nmember; the Step 1 scan output above is the evidence, read honestly in the paragraph under\nit. No control changes, because these documents apply none — `SECURITY-POSTURE.md § What\nis true today` states plainly that the cohort's controls are designed, not applied, and\nthat exactly one row in it (`cargo-deny check` in CI) is a control that runs.\n\nThe change with the largest security consequence is one of visibility rather than\nexposure: `SECURITY-POSTURE.md § The gap this document exists to surface` writes down, in\na world-readable file, that a relay initiating agents which execute on contributors'\nmachines stops being only a service to protect and becomes a distribution point for code\nexecution. That gap is already public in #42 and #43. Recording it here makes it harder to\nlose track of, which is the trade #42 already accepted.\n\n### Escalations\n\n- **Placement of #83 and #84.** Both ADRs were filed after these documents were written,\n so both lists predated them. I added #83 to `ARCHITECTURE.md § Open decisions` and added\n #84 to nothing, with reasoning stated in the document and in *Not verified*. Neither is a\n decision I am entitled to make alone; overruling either is a review comment, not a\n reopened task.\n- **Approval.** `AGENTS.md` §5 rule 1 — draft everything, approve nothing. This PR is not\n self-approved and must not be. The `PROPOSED` sections above are promoted by a human's\n approval, which is the point of the marker.\n\n","created_at":"2026-08-11T09:27:26Z","updated_at":"2026-08-12T12:00:42Z","closed_at":"2026-08-12T12:00:42Z","merged_at":"2026-08-12T12:00:42Z","merge_commit_sha":"e14f5fafb9cc13890530f019556a343b02997c34","assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":11789196953,"node_id":"LA_kwDOTv8O788AAAACvrDemQ","url":"https://api.github.com/repos/launchpad-26/buzz/labels/by:agent","name":"by:agent","color":"5319e7","default":false,"description":"Filed or authored by an AI agent, not a human"}],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/launchpad-26/buzz/pulls/86/commits","review_comments_url":"https://api.github.com/repos/launchpad-26/buzz/pulls/86/comments","review_comment_url":"https://api.github.com/repos/launchpad-26/buzz/pulls/comments{/number}","comments_url":"https://api.github.com/repos/launchpad-26/buzz/issues/86/comments","statuses_url":"https://api.github.com/repos/launchpad-26/buzz/statuses/4434519667afac207f8eeb3950cc5c5de726addd","head":{"label":"launchpad-26:docs/cohort-vision-and-architecture","ref":"docs/cohort-vision-and-architecture","sha":"4434519667afac207f8eeb3950cc5c5de726addd","user":{"login":"launchpad-26","id":309398704,"node_id":"O_kgDOEnEMsA","avatar_url":"https://avatars.githubusercontent.com/u/309398704?v=4","gravatar_id":"","url":"https://api.github.com/users/launchpad-26","html_url":"https://github.com/launchpad-26","followers_url":"https://api.github.com/users/launchpad-26/followers","following_url":"https://api.github.com/users/launchpad-26/following{/other_user}","gists_url":"https://api.github.com/users/launchpad-26/gists{/gist_id}","starred_url":"https://api.github.com/users/launchpad-26/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/launchpad-26/subscriptions","organizations_url":"https://api.github.com/users/launchpad-26/orgs","repos_url":"https://api.github.com/users/launchpad-26/repos","events_url":"https://api.github.com/users/launchpad-26/events{/privacy}","received_events_url":"https://api.github.com/users/launchpad-26/received_events","type":"Organization","user_view_type":"public","site_admin":false},"repo":{"id":1325338351,"node_id":"R_kgDOTv8O7w","name":"buzz","full_name":"launchpad-26/buzz","private":false,"owner":{"login":"launchpad-26","id":309398704,"node_id":"O_kgDOEnEMsA","avatar_url":"https://avatars.githubusercontent.com/u/309398704?v=4","gravatar_id":"","url":"https://api.github.com/users/launchpad-26","html_url":"https://github.com/launchpad-26","followers_url":"https://api.github.com/users/launchpad-26/followers","following_url":"https://api.github.com/users/launchpad-26/following{/other_user}","gists_url":"https://api.github.com/users/launchpad-26/gists{/gist_id}","starred_url":"https://api.github.com/users/launchpad-26/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/launchpad-26/subscriptions","organizations_url":"https://api.github.com/users/launchpad-26/orgs","repos_url":"https://api.github.com/users/launchpad-26/repos","events_url":"https://api.github.com/users/launchpad-26/events{/privacy}","received_events_url":"https://api.github.com/users/launchpad-26/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/launchpad-26/buzz","description":"A hive mind communication platform","fork":true,"url":"https://api.github.com/repos/launchpad-26/buzz","forks_url":"https://api.github.com/repos/launchpad-26/buzz/forks","keys_url":"https://api.github.com/repos/launchpad-26/buzz/keys{/key_id}","collaborators_url":"https://api.github.com/repos/launchpad-26/buzz/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/launchpad-26/buzz/teams","hooks_url":"https://api.github.com/repos/launchpad-26/buzz/hooks","issue_events_url":"https://api.github.com/repos/launchpad-26/buzz/issues/events{/number}","events_url":"https://api.github.com/repos/launchpad-26/buzz/events","assignees_url":"https://api.github.com/repos/launchpad-26/buzz/assignees{/user}","branches_url":"https://api.github.com/repos/launchpad-26/buzz/branches{/branch}","tags_url":"https://api.github.com/repos/launchpad-26/buzz/tags","blobs_url":"https://api.github.com/repos/launchpad-26/buzz/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/launchpad-26/buzz/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/launchpad-26/buzz/git/refs{/sha}","trees_url":"https://api.github.com/repos/launchpad-26/buzz/git/trees{/sha}","statuses_url":"https://api.github.com/repos/launchpad-26/buzz/statuses/{sha}","languages_url":"https://api.github.com/repos/launchpad-26/buzz/languages","stargazers_url":"https://api.github.com/repos/launchpad-26/buzz/stargazers","contributors_url":"https://api.github.com/repos/launchpad-26/buzz/contributors","subscribers_url":"https://api.github.com/repos/launchpad-26/buzz/subscribers","subscription_url":"https://api.github.com/repos/launchpad-26/buzz/subscription","commits_url":"https://api.github.com/repos/launchpad-26/buzz/commits{/sha}","git_commits_url":"https://api.github.com/repos/launchpad-26/buzz/git/commits{/sha}","comments_url":"https://api.github.com/repos/launchpad-26/buzz/comments{/number}","issue_comment_url":"https://api.github.com/repos/launchpad-26/buzz/issues/comments{/number}","contents_url":"https://api.github.com/repos/launchpad-26/buzz/contents/{+path}","compare_url":"https://api.github.com/repos/launchpad-26/buzz/compare/{base}...{head}","merges_url":"https://api.github.com/repos/launchpad-26/buzz/merges","archive_url":"https://api.github.com/repos/launchpad-26/buzz/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/launchpad-26/buzz/downloads","issues_url":"https://api.github.com/repos/launchpad-26/buzz/issues{/number}","pulls_url":"https://api.github.com/repos/launchpad-26/buzz/pulls{/number}","milestones_url":"https://api.github.com/repos/launchpad-26/buzz/milestones{/number}","notifications_url":"https://api.github.com/repos/launchpad-26/buzz/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/launchpad-26/buzz/labels{/name}","releases_url":"https://api.github.com/repos/launchpad-26/buzz/releases{/id}","deployments_url":"https://api.github.com/repos/launchpad-26/buzz/deployments","created_at":"2026-08-06T14:20:40Z","updated_at":"2026-08-12T20:14:46Z","pushed_at":"2026-08-12T20:56:02Z","git_url":"git://github.com/launchpad-26/buzz.git","ssh_url":"git@github.com:launchpad-26/buzz.git","clone_url":"https://github.com/launchpad-26/buzz.git","svn_url":"https://github.com/launchpad-26/buzz","homepage":"","size":473018,"stargazers_count":0,"watchers_count":0,"language":"Rust","has_issues":true,"has_projects":true,"has_downloads":false,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":96,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":[],"visibility":"public","forks":1,"open_issues":96,"watchers":0,"default_branch":"launchpad"}},"base":{"label":"launchpad-26:launchpad","ref":"launchpad","sha":"b3db9afbc8054693f6412ad1872c23b394ac12b5","user":{"login":"launchpad-26","id":309398704,"node_id":"O_kgDOEnEMsA","avatar_url":"https://avatars.githubusercontent.com/u/309398704?v=4","gravatar_id":"","url":"https://api.github.com/users/launchpad-26","html_url":"https://github.com/launchpad-26","followers_url":"https://api.github.com/users/launchpad-26/followers","following_url":"https://api.github.com/users/launchpad-26/following{/other_user}","gists_url":"https://api.github.com/users/launchpad-26/gists{/gist_id}","starred_url":"https://api.github.com/users/launchpad-26/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/launchpad-26/subscriptions","organizations_url":"https://api.github.com/users/launchpad-26/orgs","repos_url":"https://api.github.com/users/launchpad-26/repos","events_url":"https://api.github.com/users/launchpad-26/events{/privacy}","received_events_url":"https://api.github.com/users/launchpad-26/received_events","type":"Organization","user_view_type":"public","site_admin":false},"repo":{"id":1325338351,"node_id":"R_kgDOTv8O7w","name":"buzz","full_name":"launchpad-26/buzz","private":false,"owner":{"login":"launchpad-26","id":309398704,"node_id":"O_kgDOEnEMsA","avatar_url":"https://avatars.githubusercontent.com/u/309398704?v=4","gravatar_id":"","url":"https://api.github.com/users/launchpad-26","html_url":"https://github.com/launchpad-26","followers_url":"https://api.github.com/users/launchpad-26/followers","following_url":"https://api.github.com/users/launchpad-26/following{/other_user}","gists_url":"https://api.github.com/users/launchpad-26/gists{/gist_id}","starred_url":"https://api.github.com/users/launchpad-26/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/launchpad-26/subscriptions","organizations_url":"https://api.github.com/users/launchpad-26/orgs","repos_url":"https://api.github.com/users/launchpad-26/repos","events_url":"https://api.github.com/users/launchpad-26/events{/privacy}","received_events_url":"https://api.github.com/users/launchpad-26/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/launchpad-26/buzz","description":"A hive mind communication platform","fork":true,"url":"https://api.github.com/repos/launchpad-26/buzz","forks_url":"https://api.github.com/repos/launchpad-26/buzz/forks","keys_url":"https://api.github.com/repos/launchpad-26/buzz/keys{/key_id}","collaborators_url":"https://api.github.com/repos/launchpad-26/buzz/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/launchpad-26/buzz/teams","hooks_url":"https://api.github.com/repos/launchpad-26/buzz/hooks","issue_events_url":"https://api.github.com/repos/launchpad-26/buzz/issues/events{/number}","events_url":"https://api.github.com/repos/launchpad-26/buzz/events","assignees_url":"https://api.github.com/repos/launchpad-26/buzz/assignees{/user}","branches_url":"https://api.github.com/repos/launchpad-26/buzz/branches{/branch}","tags_url":"https://api.github.com/repos/launchpad-26/buzz/tags","blobs_url":"https://api.github.com/repos/launchpad-26/buzz/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/launchpad-26/buzz/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/launchpad-26/buzz/git/refs{/sha}","trees_url":"https://api.github.com/repos/launchpad-26/buzz/git/trees{/sha}","statuses_url":"https://api.github.com/repos/launchpad-26/buzz/statuses/{sha}","languages_url":"https://api.github.com/repos/launchpad-26/buzz/languages","stargazers_url":"https://api.github.com/repos/launchpad-26/buzz/stargazers","contributors_url":"https://api.github.com/repos/launchpad-26/buzz/contributors","subscribers_url":"https://api.github.com/repos/launchpad-26/buzz/subscribers","subscription_url":"https://api.github.com/repos/launchpad-26/buzz/subscription","commits_url":"https://api.github.com/repos/launchpad-26/buzz/commits{/sha}","git_commits_url":"https://api.github.com/repos/launchpad-26/buzz/git/commits{/sha}","comments_url":"https://api.github.com/repos/launchpad-26/buzz/comments{/number}","issue_comment_url":"https://api.github.com/repos/launchpad-26/buzz/issues/comments{/number}","contents_url":"https://api.github.com/repos/launchpad-26/buzz/contents/{+path}","compare_url":"https://api.github.com/repos/launchpad-26/buzz/compare/{base}...{head}","merges_url":"https://api.github.com/repos/launchpad-26/buzz/merges","archive_url":"https://api.github.com/repos/launchpad-26/buzz/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/launchpad-26/buzz/downloads","issues_url":"https://api.github.com/repos/launchpad-26/buzz/issues{/number}","pulls_url":"https://api.github.com/repos/launchpad-26/buzz/pulls{/number}","milestones_url":"https://api.github.com/repos/launchpad-26/buzz/milestones{/number}","notifications_url":"https://api.github.com/repos/launchpad-26/buzz/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/launchpad-26/buzz/labels{/name}","releases_url":"https://api.github.com/repos/launchpad-26/buzz/releases{/id}","deployments_url":"https://api.github.com/repos/launchpad-26/buzz/deployments","created_at":"2026-08-06T14:20:40Z","updated_at":"2026-08-12T20:14:46Z","pushed_at":"2026-08-12T20:56:02Z","git_url":"git://github.com/launchpad-26/buzz.git","ssh_url":"git@github.com:launchpad-26/buzz.git","clone_url":"https://github.com/launchpad-26/buzz.git","svn_url":"https://github.com/launchpad-26/buzz","homepage":"","size":473018,"stargazers_count":0,"watchers_count":0,"language":"Rust","has_issues":true,"has_projects":true,"has_downloads":false,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":96,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":[],"visibility":"public","forks":1,"open_issues":96,"watchers":0,"default_branch":"launchpad"}},"_links":{"self":{"href":"https://api.github.com/repos/launchpad-26/buzz/pulls/86"},"html":{"href":"https://github.com/launchpad-26/buzz/pull/86"},"issue":{"href":"https://api.github.com/repos/launchpad-26/buzz/issues/86"},"comments":{"href":"https://api.github.com/repos/launchpad-26/buzz/issues/86/comments"},"review_comments":{"href":"https://api.github.com/repos/launchpad-26/buzz/pulls/86/comments"},"review_comment":{"href":"https://api.github.com/repos/launchpad-26/buzz/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/launchpad-26/buzz/pulls/86/commits"},"statuses":{"href":"https://api.github.com/repos/launchpad-26/buzz/statuses/4434519667afac207f8eeb3950cc5c5de726addd"}},"author_association":"MEMBER","auto_merge":null,"assignee":null,"active_lock_reason":null,"merged":true,"mergeable":null,"rebaseable":null,"mergeable_state":"unknown","merged_by":{"login":"tucktuck101","id":70870926,"node_id":"MDQ6VXNlcjcwODcwOTI2","avatar_url":"https://avatars.githubusercontent.com/u/70870926?v=4","gravatar_id":"","url":"https://api.github.com/users/tucktuck101","html_url":"https://github.com/tucktuck101","followers_url":"https://api.github.com/users/tucktuck101/followers","following_url":"https://api.github.com/users/tucktuck101/following{/other_user}","gists_url":"https://api.github.com/users/tucktuck101/gists{/gist_id}","starred_url":"https://api.github.com/users/tucktuck101/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tucktuck101/subscriptions","organizations_url":"https://api.github.com/users/tucktuck101/orgs","repos_url":"https://api.github.com/users/tucktuck101/repos","events_url":"https://api.github.com/users/tucktuck101/events{/privacy}","received_events_url":"https://api.github.com/users/tucktuck101/received_events","type":"User","user_view_type":"public","site_admin":false},"comments":1,"review_comments":0,"maintainer_can_modify":false,"commits":12,"additions":1007,"deletions":0,"changed_files":6} \ No newline at end of file diff --git a/launchpad/scripts/testdata/pr86-review-decision.json b/launchpad/scripts/testdata/pr86-review-decision.json new file mode 100644 index 00000000000..4fabba78b34 --- /dev/null +++ b/launchpad/scripts/testdata/pr86-review-decision.json @@ -0,0 +1 @@ +{"reviewDecision":"REVIEW_REQUIRED"} diff --git a/launchpad/scripts/testdata/pr86-tree.json b/launchpad/scripts/testdata/pr86-tree.json new file mode 100644 index 00000000000..b2eb510eb5b --- /dev/null +++ b/launchpad/scripts/testdata/pr86-tree.json @@ -0,0 +1,967 @@ +{ + "sha": "4434519667afac207f8eeb3950cc5c5de726addd", + "url": "https://api.github.com/repos/launchpad-26/buzz/git/trees/4434519667afac207f8eeb3950cc5c5de726addd", + "truncated": false, + "tree": [ + { + "path": ".agents/skills/desktop-screenshot/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b", + "size": 65, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b" + }, + { + "path": ".agents/skills/sprout-cli/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "091795a0d44351523ca4e35eac0d74516ab88c25", + "size": 59, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/091795a0d44351523ca4e35eac0d74516ab88c25" + }, + { + "path": ".claude/skills/desktop-screenshot/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b", + "size": 65, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b" + }, + { + "path": ".claude/skills/sprout-cli/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "091795a0d44351523ca4e35eac0d74516ab88c25", + "size": 59, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/091795a0d44351523ca4e35eac0d74516ab88c25" + }, + { + "path": ".codex/skills/desktop-screenshot/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b", + "size": 65, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b" + }, + { + "path": ".codex/skills/sprout-cli/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "091795a0d44351523ca4e35eac0d74516ab88c25", + "size": 59, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/091795a0d44351523ca4e35eac0d74516ab88c25" + }, + { + "path": ".github/PULL_REQUEST_TEMPLATE.md", + "mode": "100644", + "type": "blob", + "sha": "b9adbdda312fa9640c7b16904d9f154fce0cd914", + "size": 578, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/b9adbdda312fa9640c7b16904d9f154fce0cd914" + }, + { + "path": ".goose/skills/desktop-screenshot/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b", + "size": 65, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b" + }, + { + "path": ".goose/skills/sprout-cli/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "091795a0d44351523ca4e35eac0d74516ab88c25", + "size": 59, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/091795a0d44351523ca4e35eac0d74516ab88c25" + }, + { + "path": "AGENTS.md", + "mode": "100644", + "type": "blob", + "sha": "49a85162636a048a5ff3e50374f1c435a9fc4c36", + "size": 29930, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/49a85162636a048a5ff3e50374f1c435a9fc4c36" + }, + { + "path": "ARCHITECTURE.md", + "mode": "100644", + "type": "blob", + "sha": "892082d96c6da9bed8a1b7305ea7c7034e5315db", + "size": 45578, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/892082d96c6da9bed8a1b7305ea7c7034e5315db" + }, + { + "path": "CHANGELOG.md", + "mode": "100644", + "type": "blob", + "sha": "7ca952507495551cf3737834b6d132a07d84952d", + "size": 265343, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/7ca952507495551cf3737834b6d132a07d84952d" + }, + { + "path": "CLAUDE.md", + "mode": "120000", + "type": "blob", + "sha": "47dc3e3d863cfb5727b87d785d09abf9743c0a72", + "size": 9, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/47dc3e3d863cfb5727b87d785d09abf9743c0a72" + }, + { + "path": "CODE_OF_CONDUCT.md", + "mode": "100644", + "type": "blob", + "sha": "a2bc369a3cf426b62e24666c73e3d7c70eab62f3", + "size": 5496, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/a2bc369a3cf426b62e24666c73e3d7c70eab62f3" + }, + { + "path": "CONTRIBUTING.md", + "mode": "100644", + "type": "blob", + "sha": "db0aea637fea3a6fda23165d44d57fad350fe517", + "size": 19053, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/db0aea637fea3a6fda23165d44d57fad350fe517" + }, + { + "path": "GOVERNANCE.md", + "mode": "100644", + "type": "blob", + "sha": "4af3958eedafeccc3669161f3cd8bbe443dfa0f6", + "size": 127, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/4af3958eedafeccc3669161f3cd8bbe443dfa0f6" + }, + { + "path": "LAUNCHPAD.md", + "mode": "100644", + "type": "blob", + "sha": "a7f639b206a5a680929efc555c2411371951565f", + "size": 2764, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/a7f639b206a5a680929efc555c2411371951565f" + }, + { + "path": "NOSTR.md", + "mode": "100644", + "type": "blob", + "sha": "cce70f2f77f8aa0ebf77783a5d3edf9a1f6ea6a9", + "size": 19578, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/cce70f2f77f8aa0ebf77783a5d3edf9a1f6ea6a9" + }, + { + "path": "README.md", + "mode": "100644", + "type": "blob", + "sha": "56439f00bc1565ce7aca273a886eecdc12df5c1d", + "size": 15945, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/56439f00bc1565ce7aca273a886eecdc12df5c1d" + }, + { + "path": "RELEASING.md", + "mode": "100644", + "type": "blob", + "sha": "53d580556191ad40f2197e3797cfe536087b1e34", + "size": 15023, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/53d580556191ad40f2197e3797cfe536087b1e34" + }, + { + "path": "SECURITY.md", + "mode": "100644", + "type": "blob", + "sha": "09ea73022b311594060dabde5d291c09865ffbf3", + "size": 5316, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/09ea73022b311594060dabde5d291c09865ffbf3" + }, + { + "path": "TESTING.md", + "mode": "100644", + "type": "blob", + "sha": "7c107da5754f442e7e4ae277a812d4e7e2273508", + "size": 16309, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/7c107da5754f442e7e4ae277a812d4e7e2273508" + }, + { + "path": "VISION.md", + "mode": "100644", + "type": "blob", + "sha": "66a106bdebc5e751ee39babb56056d0d7572cf1a", + "size": 12934, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/66a106bdebc5e751ee39babb56056d0d7572cf1a" + }, + { + "path": "VISION_ACTIVITY.md", + "mode": "100644", + "type": "blob", + "sha": "d1d1e0ac5c22b5a990bb18e15d433ea4553fc8ef", + "size": 5208, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/d1d1e0ac5c22b5a990bb18e15d433ea4553fc8ef" + }, + { + "path": "VISION_AGENT.md", + "mode": "100644", + "type": "blob", + "sha": "2b2597c8d2b542241f19e6e7cfff110c97188935", + "size": 4501, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/2b2597c8d2b542241f19e6e7cfff110c97188935" + }, + { + "path": "VISION_MESH.md", + "mode": "100644", + "type": "blob", + "sha": "d1354d50fee00cf7de0ea981c58973c406ec69f7", + "size": 4607, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/d1354d50fee00cf7de0ea981c58973c406ec69f7" + }, + { + "path": "VISION_MODERATION.md", + "mode": "100644", + "type": "blob", + "sha": "45d3ab86fd03c2f3dfd8d89374a663a47e00d45b", + "size": 7378, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/45d3ab86fd03c2f3dfd8d89374a663a47e00d45b" + }, + { + "path": "VISION_PROJECTS.md", + "mode": "100644", + "type": "blob", + "sha": "8601b87829eb3743671c1ed7cbebba42902a8d86", + "size": 15808, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/8601b87829eb3743671c1ed7cbebba42902a8d86" + }, + { + "path": "VISION_REMOTE_AGENTS.md", + "mode": "100644", + "type": "blob", + "sha": "4b187f355a5dd65ab7b7b1b598c62bead7a7e804", + "size": 8922, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/4b187f355a5dd65ab7b7b1b598c62bead7a7e804" + }, + { + "path": "VISION_SOVEREIGN.md", + "mode": "100644", + "type": "blob", + "sha": "77362b3f7e2f4cf9cc8b61837c5125c55e797244", + "size": 13541, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/77362b3f7e2f4cf9cc8b61837c5125c55e797244" + }, + { + "path": "benchmarks/harbor-buzz-orchestra/README.md", + "mode": "100644", + "type": "blob", + "sha": "0358c954e7d7b78c9a7bc05bec43c436059a6df0", + "size": 6342, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/0358c954e7d7b78c9a7bc05bec43c436059a6df0" + }, + { + "path": "benchmarks/harbor-buzz-orchestra/personas/orchestrator-m1.md", + "mode": "100644", + "type": "blob", + "sha": "d248806e4ec3414df6aa07fb0d2a8479f0560a7c", + "size": 1936, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/d248806e4ec3414df6aa07fb0d2a8479f0560a7c" + }, + { + "path": "benchmarks/harbor-buzz-orchestra/personas/orchestrator-tb.md", + "mode": "100644", + "type": "blob", + "sha": "c1dbd7b79578f2b0cabafc610f8b6bddbd8aedc2", + "size": 2424, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/c1dbd7b79578f2b0cabafc610f8b6bddbd8aedc2" + }, + { + "path": "benchmarks/harbor-buzz-orchestra/personas/worker-m1.md", + "mode": "100644", + "type": "blob", + "sha": "442d7b874d0f293ebe5b6f73813793b26fee4a6b", + "size": 1777, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/442d7b874d0f293ebe5b6f73813793b26fee4a6b" + }, + { + "path": "benchmarks/harbor-buzz-orchestra/personas/worker-tb.md", + "mode": "100644", + "type": "blob", + "sha": "49a9125a17bb9885ce31c0e3edb9e4766fe5b838", + "size": 1684, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/49a9125a17bb9885ce31c0e3edb9e4766fe5b838" + }, + { + "path": "benchmarks/harbor-buzz-orchestra/testbed/endpoints/README.md", + "mode": "100644", + "type": "blob", + "sha": "f8d2a560bf275232d3c81e722563001ff5bd6865", + "size": 1190, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/f8d2a560bf275232d3c81e722563001ff5bd6865" + }, + { + "path": "bin/README.hermit.md", + "mode": "100644", + "type": "blob", + "sha": "e889550ba4cbc92a720527042e5f6e7a303bfcbc", + "size": 270, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/e889550ba4cbc92a720527042e5f6e7a303bfcbc" + }, + { + "path": "crates/buzz-acp/README.md", + "mode": "100644", + "type": "blob", + "sha": "e6164b02dd3ef1e0d9d63571b7f863fb7ade07d9", + "size": 18083, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/e6164b02dd3ef1e0d9d63571b7f863fb7ade07d9" + }, + { + "path": "crates/buzz-acp/src/base_prompt.md", + "mode": "100644", + "type": "blob", + "sha": "1d85221f113e41f8a098fa13efaad413b8788512", + "size": 15007, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1d85221f113e41f8a098fa13efaad413b8788512" + }, + { + "path": "crates/buzz-agent/README.md", + "mode": "100644", + "type": "blob", + "sha": "5d942777d5e5b0b38a767d56ae7ec7b2bb505aad", + "size": 22677, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/5d942777d5e5b0b38a767d56ae7ec7b2bb505aad" + }, + { + "path": "crates/buzz-backend-kubernetes/tests/fixtures/provider-wire/README.md", + "mode": "100644", + "type": "blob", + "sha": "56a972f89420cebfcb74cfd73598d9ff90c9a7f1", + "size": 2323, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/56a972f89420cebfcb74cfd73598d9ff90c9a7f1" + }, + { + "path": "crates/buzz-cli/README.md", + "mode": "100644", + "type": "blob", + "sha": "a2dcdce6d2104b1ba6ddef3bcb1ccac467fc8e3f", + "size": 6553, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/a2dcdce6d2104b1ba6ddef3bcb1ccac467fc8e3f" + }, + { + "path": "crates/buzz-cli/TESTING.md", + "mode": "100644", + "type": "blob", + "sha": "77234b7faab6966bdf6429da9f17fa05a05463a9", + "size": 20746, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/77234b7faab6966bdf6429da9f17fa05a05463a9" + }, + { + "path": "crates/buzz-conformance/LIMITS.md", + "mode": "100644", + "type": "blob", + "sha": "37f0f28d3ee728f5ff633784bed6bbbe791ad6ec", + "size": 5767, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/37f0f28d3ee728f5ff633784bed6bbbe791ad6ec" + }, + { + "path": "crates/buzz-conformance/TRACE_SCHEMA.md", + "mode": "100644", + "type": "blob", + "sha": "c1dca674d559e5daf58576cb305ef9b9251b6fde", + "size": 6969, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/c1dca674d559e5daf58576cb305ef9b9251b6fde" + }, + { + "path": "crates/buzz-core/src/pairing/NIP-AB.md", + "mode": "100644", + "type": "blob", + "sha": "fc8f69245e80fb710e95694ea28dbe286047da6d", + "size": 54727, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/fc8f69245e80fb710e95694ea28dbe286047da6d" + }, + { + "path": "crates/buzz-media/tests/fixtures/android/README.md", + "mode": "100644", + "type": "blob", + "sha": "b0b06bb0607d985b5c0a9e7d6c4360b55131cf17", + "size": 3067, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/b0b06bb0607d985b5c0a9e7d6c4360b55131cf17" + }, + { + "path": "crates/buzz-media/tests/fixtures/ios/README.md", + "mode": "100644", + "type": "blob", + "sha": "2f6d5f5e256a3f0e9e40508c1668ae150a09e61e", + "size": 2209, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/2f6d5f5e256a3f0e9e40508c1668ae150a09e61e" + }, + { + "path": "crates/buzz-pairing-cli/README.md", + "mode": "100644", + "type": "blob", + "sha": "4f9fd1e530943ce732c22244725522bb210ab4b4", + "size": 5016, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/4f9fd1e530943ce732c22244725522bb210ab4b4" + }, + { + "path": "crates/buzz-persona/PERSONA_PACK_SPEC.md", + "mode": "100644", + "type": "blob", + "sha": "cb3a7d1c05a74495c929a677f6dc2125561cb409", + "size": 47257, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/cb3a7d1c05a74495c929a677f6dc2125561cb409" + }, + { + "path": "crates/buzz-relay/CHANGELOG.md", + "mode": "100644", + "type": "blob", + "sha": "7ee7e9a303b4c3030fa85e25e60f31d9d4e7a208", + "size": 29334, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/7ee7e9a303b4c3030fa85e25e60f31d9d4e7a208" + }, + { + "path": "crates/git-credential-nostr/README.md", + "mode": "100644", + "type": "blob", + "sha": "827f8d55a330d508cc870113d680b981acd1f696", + "size": 2482, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/827f8d55a330d508cc870113d680b981acd1f696" + }, + { + "path": "crates/git-sign-nostr/README.md", + "mode": "100644", + "type": "blob", + "sha": "908682fd7fe4e037e0f8e64812db8d234f86bbd8", + "size": 1345, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/908682fd7fe4e037e0f8e64812db8d234f86bbd8" + }, + { + "path": "deploy/charts/buzz/README.md", + "mode": "100644", + "type": "blob", + "sha": "b2778df28b57d11f3ec5a179a9b1b442079e7903", + "size": 12342, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/b2778df28b57d11f3ec5a179a9b1b442079e7903" + }, + { + "path": "deploy/compose/README.md", + "mode": "100644", + "type": "blob", + "sha": "bb0e63fe15d1c540fe0fe6fa76a25045df36952a", + "size": 2338, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/bb0e63fe15d1c540fe0fe6fa76a25045df36952a" + }, + { + "path": "desktop/README.md", + "mode": "100644", + "type": "blob", + "sha": "d53698a9d04fca2636b0475d126acb4f7c152730", + "size": 627, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/d53698a9d04fca2636b0475d126acb4f7c152730" + }, + { + "path": "desktop/public/harness-logos/CREDITS.md", + "mode": "100644", + "type": "blob", + "sha": "716c43e1ae32eca14f4ec19daf0b60c3751fa580", + "size": 3597, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/716c43e1ae32eca14f4ec19daf0b60c3751fa580" + }, + { + "path": "desktop/scripts/texture-card/README.md", + "mode": "100644", + "type": "blob", + "sha": "1de2addce99cf2a340013a83ca82e4a0a4511a54", + "size": 1014, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1de2addce99cf2a340013a83ca82e4a0a4511a54" + }, + { + "path": "desktop/src-tauri/resources/pocket-voices/NOTICE.md", + "mode": "100644", + "type": "blob", + "sha": "9cc515dea3c733f540a707c34f94d273261700dc", + "size": 2133, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/9cc515dea3c733f540a707c34f94d273261700dc" + }, + { + "path": "desktop/src-tauri/src/managed_agents/nest_agents.md", + "mode": "100644", + "type": "blob", + "sha": "7cb7489b85291e0d2cf9d72eb5b1a9fd878e12c7", + "size": 3620, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/7cb7489b85291e0d2cf9d72eb5b1a9fd878e12c7" + }, + { + "path": "desktop/src-tauri/src/managed_agents/nest_skill.md", + "mode": "100644", + "type": "blob", + "sha": "79a5ea301d467444fafe53bb137b2c5f86be58b3", + "size": 11014, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/79a5ea301d467444fafe53bb137b2c5f86be58b3" + }, + { + "path": "desktop/src-tauri/src/managed_agents/screenshot_skill.md", + "mode": "100644", + "type": "blob", + "sha": "361e747fe28b62e3ae80772cccf8e35eb4c46e69", + "size": 3726, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/361e747fe28b62e3ae80772cccf8e35eb4c46e69" + }, + { + "path": "desktop/src/features/agents/AGENTS.md", + "mode": "100644", + "type": "blob", + "sha": "b578326eba33859512ecf0a3a7325186bea0a686", + "size": 13748, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/b578326eba33859512ecf0a3a7325186bea0a686" + }, + { + "path": "docs/MCP_DRIVEN_HOOKS.md", + "mode": "100644", + "type": "blob", + "sha": "6812a5b9a6c47d536b0d315d90eb81579991dac8", + "size": 4775, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/6812a5b9a6c47d536b0d315d90eb81579991dac8" + }, + { + "path": "docs/admin/README.md", + "mode": "100644", + "type": "blob", + "sha": "f49cbdd71ad76341ef427b9053ef388edb2de07d", + "size": 3139, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/f49cbdd71ad76341ef427b9053ef388edb2de07d" + }, + { + "path": "docs/bridge-channel-window.md", + "mode": "100644", + "type": "blob", + "sha": "42f1d82835e176e7e438a5b75c1020c2ce67751b", + "size": 6250, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/42f1d82835e176e7e438a5b75c1020c2ce67751b" + }, + { + "path": "docs/buzz-entity-links.md", + "mode": "100644", + "type": "blob", + "sha": "df32037898e93c608d7e474f6eb925ca2c81d4ab", + "size": 12939, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/df32037898e93c608d7e474f6eb925ca2c81d4ab" + }, + { + "path": "docs/buzz-shared-compute-dev.md", + "mode": "100644", + "type": "blob", + "sha": "3712bb7965d9f4cdbcb18b6016888508a463eafb", + "size": 6842, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/3712bb7965d9f4cdbcb18b6016888508a463eafb" + }, + { + "path": "docs/formal/STATEFUL_GATEWAY.md", + "mode": "100644", + "type": "blob", + "sha": "77b472619a59d053b9d3318264eba8fa7f43f9de", + "size": 1268, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/77b472619a59d053b9d3318264eba8fa7f43f9de" + }, + { + "path": "docs/formal/nip-pl/NOTE.md", + "mode": "100644", + "type": "blob", + "sha": "0cb70a54ad91d31d64c49bdb57b9d2ee464123db", + "size": 2196, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/0cb70a54ad91d31d64c49bdb57b9d2ee464123db" + }, + { + "path": "docs/formal/nip-rs-unread/NOTE.md", + "mode": "100644", + "type": "blob", + "sha": "7472c3dca3a93c33793e98b01266173723ba9383", + "size": 39649, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/7472c3dca3a93c33793e98b01266173723ba9383" + }, + { + "path": "docs/git-on-object-storage.md", + "mode": "100644", + "type": "blob", + "sha": "1d87720edc7fed73de31151fe6cf36141c163999", + "size": 36714, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1d87720edc7fed73de31151fe6cf36141c163999" + }, + { + "path": "docs/linux-rendering-troubleshooting.md", + "mode": "100644", + "type": "blob", + "sha": "4bc8e80eb83ec0820397212a5cfe42dfaa8ff1b0", + "size": 8452, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/4bc8e80eb83ec0820397212a5cfe42dfaa8ff1b0" + }, + { + "path": "docs/multi-tenant-conformance.md", + "mode": "100644", + "type": "blob", + "sha": "d8877b5931969fe0c714ed3f2937fc5d76f64334", + "size": 17124, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/d8877b5931969fe0c714ed3f2937fc5d76f64334" + }, + { + "path": "docs/multi-tenant-relay.md", + "mode": "100644", + "type": "blob", + "sha": "85de728b12c63df1f8b1aa00fa8cf4bf282c2bea", + "size": 74264, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/85de728b12c63df1f8b1aa00fa8cf4bf282c2bea" + }, + { + "path": "docs/nips/NIP-AA.md", + "mode": "100644", + "type": "blob", + "sha": "920ee7851087e716058a44eda97d2060c00522a9", + "size": 22346, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/920ee7851087e716058a44eda97d2060c00522a9" + }, + { + "path": "docs/nips/NIP-AE.md", + "mode": "100644", + "type": "blob", + "sha": "c24078d8a45672a282d9e2f7f0069f6fa4356d8d", + "size": 21677, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/c24078d8a45672a282d9e2f7f0069f6fa4356d8d" + }, + { + "path": "docs/nips/NIP-AM.md", + "mode": "100644", + "type": "blob", + "sha": "794a6722d54550a0786921e0580f70b3b3e6e14c", + "size": 13973, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/794a6722d54550a0786921e0580f70b3b3e6e14c" + }, + { + "path": "docs/nips/NIP-AO.md", + "mode": "100644", + "type": "blob", + "sha": "36adea04871c830e38717c1badf149150b7b781e", + "size": 9972, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/36adea04871c830e38717c1badf149150b7b781e" + }, + { + "path": "docs/nips/NIP-AP.md", + "mode": "100644", + "type": "blob", + "sha": "ad23d92282dbba3ba9ee4d60e4d8ebf6e1150c9d", + "size": 25710, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/ad23d92282dbba3ba9ee4d60e4d8ebf6e1150c9d" + }, + { + "path": "docs/nips/NIP-CW.md", + "mode": "100644", + "type": "blob", + "sha": "f21b63405b2d240677207a6696813bd5a0844364", + "size": 21151, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/f21b63405b2d240677207a6696813bd5a0844364" + }, + { + "path": "docs/nips/NIP-DV.md", + "mode": "100644", + "type": "blob", + "sha": "2efba0ba659c14c7ef43590b9e79bae79513e43d", + "size": 11390, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/2efba0ba659c14c7ef43590b9e79bae79513e43d" + }, + { + "path": "docs/nips/NIP-ER.md", + "mode": "100644", + "type": "blob", + "sha": "f384ff7ef6c9c01611a86937aad341ed328f3d07", + "size": 19511, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/f384ff7ef6c9c01611a86937aad341ed328f3d07" + }, + { + "path": "docs/nips/NIP-GS.md", + "mode": "100644", + "type": "blob", + "sha": "7ef37bcc276f3566a994db5861178b16db641ba7", + "size": 38515, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/7ef37bcc276f3566a994db5861178b16db641ba7" + }, + { + "path": "docs/nips/NIP-IA.md", + "mode": "100644", + "type": "blob", + "sha": "28b2cfa52d479f4f28465a9fbe0b2f5db9d6b390", + "size": 39959, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/28b2cfa52d479f4f28465a9fbe0b2f5db9d6b390" + }, + { + "path": "docs/nips/NIP-MP.md", + "mode": "100644", + "type": "blob", + "sha": "14936d2a7cd38affdc8002e3b92785048ca8c967", + "size": 41745, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/14936d2a7cd38affdc8002e3b92785048ca8c967" + }, + { + "path": "docs/nips/NIP-OA.md", + "mode": "100644", + "type": "blob", + "sha": "d148841440effe97533deab1471f9cfb5dfdbae9", + "size": 9096, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/d148841440effe97533deab1471f9cfb5dfdbae9" + }, + { + "path": "docs/nips/NIP-PL.md", + "mode": "100644", + "type": "blob", + "sha": "08fa6ca34d9b504b8a19819d489fe0446c5ff85d", + "size": 47748, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/08fa6ca34d9b504b8a19819d489fe0446c5ff85d" + }, + { + "path": "docs/nips/NIP-PMA.md", + "mode": "100644", + "type": "blob", + "sha": "82592b1eca059755cfddefc49ef7bf8b161737e0", + "size": 5870, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/82592b1eca059755cfddefc49ef7bf8b161737e0" + }, + { + "path": "docs/nips/NIP-RS.md", + "mode": "100644", + "type": "blob", + "sha": "6a095df60ab672f09df192ab3dcbc69c6b69df1e", + "size": 73928, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/6a095df60ab672f09df192ab3dcbc69c6b69df1e" + }, + { + "path": "docs/nips/NIP-WP.md", + "mode": "100644", + "type": "blob", + "sha": "ebaf612de9d2ffe703468593c55854304993e8fb", + "size": 6741, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/ebaf612de9d2ffe703468593c55854304993e8fb" + }, + { + "path": "docs/push-gateway-deployment.md", + "mode": "100644", + "type": "blob", + "sha": "63c63355a1140ba0d1a50dafb70f36380f2246ad", + "size": 13855, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/63c63355a1140ba0d1a50dafb70f36380f2246ad" + }, + { + "path": "docs/remote-agents.md", + "mode": "100644", + "type": "blob", + "sha": "45289ef910af7379a20b66f91ece0e49c3ed4aaa", + "size": 114647, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/45289ef910af7379a20b66f91ece0e49c3ed4aaa" + }, + { + "path": "docs/welcome-kickoff-silent-failures.md", + "mode": "100644", + "type": "blob", + "sha": "58a4a5fa9580dc5cb1453edc9ee35d295e5c5fe7", + "size": 27131, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/58a4a5fa9580dc5cb1453edc9ee35d295e5c5fe7" + }, + { + "path": "examples/README.md", + "mode": "100644", + "type": "blob", + "sha": "8649ba360ddad538a5940499ebf153c17db6ac3d", + "size": 912, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/8649ba360ddad538a5940499ebf153c17db6ac3d" + }, + { + "path": "examples/countdown-bot/README.md", + "mode": "100644", + "type": "blob", + "sha": "09b3c135200a12b6f8330c483f4773bb943722b0", + "size": 4025, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/09b3c135200a12b6f8330c483f4773bb943722b0" + }, + { + "path": "examples/meadow-core/README.md", + "mode": "100644", + "type": "blob", + "sha": "3eedd1391eace6b6ac32224ef60a66ece9f104cc", + "size": 1767, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/3eedd1391eace6b6ac32224ef60a66ece9f104cc" + }, + { + "path": "examples/meadow-core/agents/bana.persona.md", + "mode": "100644", + "type": "blob", + "sha": "19b8b975ac7794e8fd72968112aa5a7f51e53e73", + "size": 1557, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/19b8b975ac7794e8fd72968112aa5a7f51e53e73" + }, + { + "path": "examples/meadow-core/agents/lev.persona.md", + "mode": "100644", + "type": "blob", + "sha": "5963eedd563d56763ab648f0d3c8fde411959916", + "size": 1509, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/5963eedd563d56763ab648f0d3c8fde411959916" + }, + { + "path": "examples/meadow-core/agents/skip.persona.md", + "mode": "100644", + "type": "blob", + "sha": "a3897991a4548afa1dae190866974e2883b1f262", + "size": 1458, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/a3897991a4548afa1dae190866974e2883b1f262" + }, + { + "path": "examples/meadow-core/instructions.md", + "mode": "100644", + "type": "blob", + "sha": "4517467c0cfe4a27701d315f0189dd5f19db1e7f", + "size": 538, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/4517467c0cfe4a27701d315f0189dd5f19db1e7f" + }, + { + "path": "examples/meadow-core/skills/github-research/SKILL.md", + "mode": "100644", + "type": "blob", + "sha": "b940e55a890d9ee3b5de9bb95f81d16e70717b65", + "size": 1445, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/b940e55a890d9ee3b5de9bb95f81d16e70717b65" + }, + { + "path": "launchpad/AGENTS.md", + "mode": "100644", + "type": "blob", + "sha": "f2183732930e627cbe33823eb849a23aee694771", + "size": 10253, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/f2183732930e627cbe33823eb849a23aee694771" + }, + { + "path": "launchpad/AGENT_PR_TEMPLATE.md", + "mode": "100644", + "type": "blob", + "sha": "501f37ef40fa427294e8269870c7f31451c85c06", + "size": 3258, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/501f37ef40fa427294e8269870c7f31451c85c06" + }, + { + "path": "launchpad/ARCHITECTURE.md", + "mode": "100644", + "type": "blob", + "sha": "fb1f8f3a1accbcc8668f03e02ea766aa26aedbf0", + "size": 11021, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/fb1f8f3a1accbcc8668f03e02ea766aa26aedbf0" + }, + { + "path": "launchpad/ENVIRONMENTS.md", + "mode": "100644", + "type": "blob", + "sha": "56463bea4cab23735cc53d979bc08185cf6a7a34", + "size": 4708, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/56463bea4cab23735cc53d979bc08185cf6a7a34" + }, + { + "path": "launchpad/README.md", + "mode": "100644", + "type": "blob", + "sha": "8b80be52119803674ed1cdc5fc14cdc47d81bf4f", + "size": 7015, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/8b80be52119803674ed1cdc5fc14cdc47d81bf4f" + }, + { + "path": "launchpad/REQUIREMENTS.md", + "mode": "100644", + "type": "blob", + "sha": "58f3180ecacc33053a0760d7a6dfd5d3753509ec", + "size": 25656, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/58f3180ecacc33053a0760d7a6dfd5d3753509ec" + }, + { + "path": "launchpad/SECURITY-POSTURE.md", + "mode": "100644", + "type": "blob", + "sha": "89a97fe6fb2a940b6f6fb643c02cb705e29e1121", + "size": 15705, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/89a97fe6fb2a940b6f6fb643c02cb705e29e1121" + }, + { + "path": "launchpad/VISION.md", + "mode": "100644", + "type": "blob", + "sha": "7f507cbd50be61b7ccc55e3673551d7305a40015", + "size": 13273, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/7f507cbd50be61b7ccc55e3673551d7305a40015" + }, + { + "path": "launchpad/decisions/ADR-0001-handbook-repository-location-and-publication-target.md", + "mode": "100644", + "type": "blob", + "sha": "94e18b1db020d7e818aa63565d54f69ce9558923", + "size": 2972, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/94e18b1db020d7e818aa63565d54f69ce9558923" + }, + { + "path": "launchpad/decisions/ADR-0002-handbook-source-repository-scope.md", + "mode": "100644", + "type": "blob", + "sha": "944d8c9a7d898efe7d3ce20e64ecf00685e1c9c3", + "size": 2226, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/944d8c9a7d898efe7d3ce20e64ecf00685e1c9c3" + }, + { + "path": "launchpad/decisions/ADR-0003-handbook-page-provenance-contract.md", + "mode": "100644", + "type": "blob", + "sha": "cb35d7578adfb4f6946a47b23df829aa896543ba", + "size": 3075, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/cb35d7578adfb4f6946a47b23df829aa896543ba" + }, + { + "path": "launchpad/decisions/ADR-0004-handbook-staleness-detection-mechanism.md", + "mode": "100644", + "type": "blob", + "sha": "3c94dd0f6ff1fedea2498c6860646df588a00863", + "size": 2995, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/3c94dd0f6ff1fedea2498c6860646df588a00863" + }, + { + "path": "launchpad/decisions/README.md", + "mode": "100644", + "type": "blob", + "sha": "0aebced273eb63336990a4c3ca5cbebec170c18f", + "size": 1426, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/0aebced273eb63336990a4c3ca5cbebec170c18f" + }, + { + "path": "mobile/CHANGELOG.md", + "mode": "100644", + "type": "blob", + "sha": "08421f4d9d009fe4ba2ca3a74c3e991b5d7db723", + "size": 6785, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/08421f4d9d009fe4ba2ca3a74c3e991b5d7db723" + }, + { + "path": "mobile/README.md", + "mode": "100644", + "type": "blob", + "sha": "ada0568e28a21626ba1da6c40f8ab3749627820f", + "size": 3663, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/ada0568e28a21626ba1da6c40f8ab3749627820f" + }, + { + "path": "mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md", + "mode": "100644", + "type": "blob", + "sha": "89c2725b70f1882be97f5214fafe22d27a0ec01e", + "size": 336, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/89c2725b70f1882be97f5214fafe22d27a0ec01e" + }, + { + "path": "perf/RELAY_BUS_SCALING.md", + "mode": "100644", + "type": "blob", + "sha": "835ab4fe5e9eef35486a0591551413eab08d3055", + "size": 3043, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/835ab4fe5e9eef35486a0591551413eab08d3055" + }, + { + "path": "scripts/cutover/README.md", + "mode": "100644", + "type": "blob", + "sha": "27388bdaa2b95418ac93b7baf130ea178eaa4f48", + "size": 3836, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/27388bdaa2b95418ac93b7baf130ea178eaa4f48" + } + ] +} diff --git a/launchpad/scripts/testdata/pr92-closing-refs.json b/launchpad/scripts/testdata/pr92-closing-refs.json new file mode 100644 index 00000000000..b7177c4192d --- /dev/null +++ b/launchpad/scripts/testdata/pr92-closing-refs.json @@ -0,0 +1 @@ +{"data":{"repository":{"pullRequest":{"closingIssuesReferences":{"nodes":[]}}}}} \ No newline at end of file diff --git a/launchpad/scripts/testdata/pr92-meta.json b/launchpad/scripts/testdata/pr92-meta.json new file mode 100644 index 00000000000..f9763e27f42 --- /dev/null +++ b/launchpad/scripts/testdata/pr92-meta.json @@ -0,0 +1 @@ +{"body":"## Summary\n\nAdds `launchpad/REQUIREMENTS.md`, holding the cohort's business and non-functional requirements as individually identified, prioritised, traceable and verifiable objects. It closes a gap the four documents in #86 do not cover: the cohort had no non-functional requirements anywhere, and no requirement that outlives the PRD delivering it. Stacked on #86 so its cross-links to `VISION.md` resolve; GitHub retargets this to `launchpad` when #86 merges.\n\n### Related issue\n\nCloses #91\n\n### Issue type\n\nTask\n\n---\n\n### Agent provenance\n\n| Field | Value |\n|---|---|\n| Harness / provider | Claude Code |\n| Model | claude-opus-5 |\n| Session reference | N/A - the harness does not expose a run id in the PR context |\n| Initiating human | @serina-mcfall |\n\n### Objective\n\nCreate `launchpad/REQUIREMENTS.md` — 7 business requirements and 13 non-functional requirements, each with a MoSCoW priority, a status marker, a followable source, and for every non-functional requirement an observable verification method.\n\n### Impacted components\n\n```\nlaunchpad/REQUIREMENTS.md\nlaunchpad/README.md\n```\n\n### Approach and rejected alternatives\n\nRequirements are derived from milestone M0 and M1's completion statements and from the success criteria and rulings of #2, #5, #34, #41, #43, #47, #62, #67 and #68, plus `launchpad/AGENTS.md` §1, §2 and §8. Each cites its source; none was invented. The document is **structured following** ISO/IEC/IEEE 29148:2018's Business Requirements Specification shape and states plainly that conformance is unverified — the standard is paywalled and nobody in the cohort has checked this against it.\n\nRejected: **modelling stakeholder needs and evidence as separate addressable objects**, as the reference corpus at `tucktuck101/ForgePilot` `docs/business-analysis/` does. That gives a stronger evidence → need → requirement chain, but costs roughly 30–40 files for a cohort of this size; requirements cite their sources directly instead. Recorded as a non-goal in #91, to revisit if the set outgrows one document.\n\nRejected: **folding this content into `VISION.md`**, which is where its constraints and success measures live today. Prose cannot carry per-requirement priority, traceability or verification. The split that resolves it: a requirement is stable knowledge and a PRD is active work, so a requirement outlives the initiative that satisfies it — which is `AGENTS.md` §2's own rule rather than an exception to it.\n\nRejected: **writing functional requirements.** The cohort operates Buzz and does not develop it, so functional requirements for the product belong to `block/buzz`. A section in the document states this boundary, because it is the reason this set is business + non-functional only.\n\n### Verification\n\nCommand run:\n```\n# see raw output below; each command is echoed above its result\n```\n\nRaw output:\n```\n$ # 1. every relative link and anchor target resolves\n-> no BROKEN lines\n\n$ # 2. every requirement ID carries BOTH a MoSCoW priority and a status marker\n-> no MISSING lines; 20 distinct requirement IDs\n\n$ # 3. every NFR has an observable verification row\n-> no missing rows; matrix contains 13 rows for 13 NFRs\n\n$ # 4. no closed-set language, @handles, or credential material\n-> no output\n\n$ git diff --name-status docs/cohort-vision-and-architecture...HEAD\nM\tlaunchpad/README.md\nA\tlaunchpad/REQUIREMENTS.md\n\n$ git log --format='%h %s | %(trailers:key=Signed-off-by,valueonly)' docs/cohort-vision-and-architecture..HEAD\n8e03ba653 docs(launchpad): correct three requirement citations | Serina Mcfall \ne4bb4b497 docs(launchpad): add the cohort business and non-functional requirements | Serina Mcfall \n```\n\n- [x] Tests or checks were run and the raw output is pasted above\n- [x] The diff is confined to the scope of the linked issue\n- [x] No secrets, keys, tokens or hostnames were added to tracked files\n\n### Not verified\n\n- **The full text of ISO/IEC/IEEE 29148:2018.** It is paywalled. Only the Business Requirements Specification outline as far as clause 9.3 was available from secondary sources, so the document's structure follows the standard's published shape and claims nothing more. If a cohort member has access, checking this is worth doing.\n- **That the requirements are the ones the cohort would choose.** They are derived faithfully from written sources, which is a different thing from being agreed. Every one carries a status marker saying where it stands.\n- **Priorities.** MoSCoW values are proposed from each source's own language and its milestone placement; nobody has ratified them.\n- **Nothing was run against a deployed system,** because none exists. Every verification method in the matrix describes an observation that *would* demonstrate the property; none has been performed.\n- The desktop and mobile CI suites were not run locally; this branch changes no code they cover.\n\n### Security implications\n\nNo change to exposure, trust or blast radius: this adds documentation and no code, configuration or automation. Two second-order effects worth naming. It records in a public repository which security properties the cohort requires and which are unmet, which is a map for an attacker as much as for a contributor — the same trade #42 accepted knowingly and `SECURITY-POSTURE.md` records as an accepted risk. Against that, NFR-012 states that containment at the agent execution boundary is required before any relay-initiated agent executes on a contributor's machine, which makes an obligation checkable that previously existed only inside an open ADR. NFR-012 names no mechanism: #43 owns that decision and deliberately left its outcome blank.\n\n### Escalations\n\n- **BR-002 is a `Should` with no owner and no milestone.** Migrating the cohort's own coordination off Discord has no work item, and the traceability section says so rather than inventing one. That is exactly the untraceable-obligation problem #91 exists to fix. Raising it rather than filing an issue myself, because deciding whether the cohort commits to that migration is not an agent's call.\n- **Where a requirements specification belongs in the issue tree.** Filed standalone rather than as a child of prd-06 (#42), because a requirements specification is not among #42's eleven success criteria. Re-parent it if the cohort reads that differently.\n- **Priorities are proposed, not agreed.** MoSCoW values were derived from source language; a reviewer disagreeing with any of them is the expected outcome, not an error.\n- **One claim about the reference corpus was corrected mid-review.** An earlier draft described ForgePilot's requirement summaries as `shall`-form. Checking all 117 objects showed 78 use `shall`, 25 `must`, 12 `should` and 1 `may`, so the claim now describes \"a summary carrying one dominant obligation\" and records `shall`-throughout as this document's own deliberate departure.\n","labels":[{"id":"LA_kwDOTv8O788AAAACvrDemQ","name":"by:agent","description":"Filed or authored by an AI agent, not a human","color":"5319e7"}],"title":"docs(launchpad): the cohort business and non-functional requirements"} diff --git a/launchpad/scripts/testdata/pr92-review-decision.json b/launchpad/scripts/testdata/pr92-review-decision.json new file mode 100644 index 00000000000..62b2b447174 --- /dev/null +++ b/launchpad/scripts/testdata/pr92-review-decision.json @@ -0,0 +1 @@ +{"reviewDecision":""} diff --git a/launchpad/scripts/testdata/prdelete-compare.json b/launchpad/scripts/testdata/prdelete-compare.json new file mode 100644 index 00000000000..5d63eb5f8a4 --- /dev/null +++ b/launchpad/scripts/testdata/prdelete-compare.json @@ -0,0 +1,25 @@ +{ + "base_commit": { + "sha": "e14f5fafb9cc13890530f019556a343b02997c34" + }, + "merge_base_commit": { + "sha": "e14f5fafb9cc13890530f019556a343b02997c34" + }, + "status": "ahead", + "ahead_by": 1, + "behind_by": 0, + "files": [ + { + "filename": "launchpad/AGENTS.md", + "status": "removed", + "additions": 0, + "deletions": 244 + }, + { + "filename": "launchpad/README.md", + "status": "modified", + "additions": 2, + "deletions": 0 + } + ] +} diff --git a/launchpad/scripts/testdata/prdelete-pr.json b/launchpad/scripts/testdata/prdelete-pr.json new file mode 100644 index 00000000000..26de531143a --- /dev/null +++ b/launchpad/scripts/testdata/prdelete-pr.json @@ -0,0 +1 @@ +{"url":"https://api.github.com/repos/launchpad-26/buzz/pulls/142","id":4265469730,"node_id":"PR_kwDOTv8O787-Peci","html_url":"https://github.com/launchpad-26/buzz/pull/142","diff_url":"https://github.com/launchpad-26/buzz/pull/142.diff","patch_url":"https://github.com/launchpad-26/buzz/pull/142.patch","issue_url":"https://api.github.com/repos/launchpad-26/buzz/issues/142","number":142,"state":"closed","locked":false,"title":"chore(fixtures): throwaway — recording a rules-file-DELETE response (closing immediately)","user":{"login":"serina-mcfall","id":240091489,"node_id":"U_kgDODk-BYQ","avatar_url":"https://avatars.githubusercontent.com/u/240091489?v=4","gravatar_id":"","url":"https://api.github.com/users/serina-mcfall","html_url":"https://github.com/serina-mcfall","followers_url":"https://api.github.com/users/serina-mcfall/followers","following_url":"https://api.github.com/users/serina-mcfall/following{/other_user}","gists_url":"https://api.github.com/users/serina-mcfall/gists{/gist_id}","starred_url":"https://api.github.com/users/serina-mcfall/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serina-mcfall/subscriptions","organizations_url":"https://api.github.com/users/serina-mcfall/orgs","repos_url":"https://api.github.com/users/serina-mcfall/repos","events_url":"https://api.github.com/users/serina-mcfall/events{/privacy}","received_events_url":"https://api.github.com/users/serina-mcfall/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"Do not review. Do not merge. Closing as soon as two API responses are recorded.\n\nThis PR exists only so that launchpad-26/buzz#116 can record what the compare\nand tree endpoints return for a pull request that **deletes** the nearest rules\nfile for a path it also touches. No PR in this fork's history does that, and\nnone was found upstream, so the add direction (PR #14, real) would otherwise\nhave had to stand in for both by assertion.\n\nRefs #116","created_at":"2026-08-12T19:42:07Z","updated_at":"2026-08-12T19:42:15Z","closed_at":"2026-08-12T19:42:13Z","merged_at":null,"merge_commit_sha":"7593173d9ba18b64e701ae21147a66ec197d0d4c","assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/launchpad-26/buzz/pulls/142/commits","review_comments_url":"https://api.github.com/repos/launchpad-26/buzz/pulls/142/comments","review_comment_url":"https://api.github.com/repos/launchpad-26/buzz/pulls/comments{/number}","comments_url":"https://api.github.com/repos/launchpad-26/buzz/issues/142/comments","statuses_url":"https://api.github.com/repos/launchpad-26/buzz/statuses/9d7af78528e1672ea3fd53bbcc60eec404c58f72","head":{"label":"launchpad-26:fixture/rules-file-delete-throwaway","ref":"fixture/rules-file-delete-throwaway","sha":"9d7af78528e1672ea3fd53bbcc60eec404c58f72","user":{"login":"launchpad-26","id":309398704,"node_id":"O_kgDOEnEMsA","avatar_url":"https://avatars.githubusercontent.com/u/309398704?v=4","gravatar_id":"","url":"https://api.github.com/users/launchpad-26","html_url":"https://github.com/launchpad-26","followers_url":"https://api.github.com/users/launchpad-26/followers","following_url":"https://api.github.com/users/launchpad-26/following{/other_user}","gists_url":"https://api.github.com/users/launchpad-26/gists{/gist_id}","starred_url":"https://api.github.com/users/launchpad-26/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/launchpad-26/subscriptions","organizations_url":"https://api.github.com/users/launchpad-26/orgs","repos_url":"https://api.github.com/users/launchpad-26/repos","events_url":"https://api.github.com/users/launchpad-26/events{/privacy}","received_events_url":"https://api.github.com/users/launchpad-26/received_events","type":"Organization","user_view_type":"public","site_admin":false},"repo":{"id":1325338351,"node_id":"R_kgDOTv8O7w","name":"buzz","full_name":"launchpad-26/buzz","private":false,"owner":{"login":"launchpad-26","id":309398704,"node_id":"O_kgDOEnEMsA","avatar_url":"https://avatars.githubusercontent.com/u/309398704?v=4","gravatar_id":"","url":"https://api.github.com/users/launchpad-26","html_url":"https://github.com/launchpad-26","followers_url":"https://api.github.com/users/launchpad-26/followers","following_url":"https://api.github.com/users/launchpad-26/following{/other_user}","gists_url":"https://api.github.com/users/launchpad-26/gists{/gist_id}","starred_url":"https://api.github.com/users/launchpad-26/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/launchpad-26/subscriptions","organizations_url":"https://api.github.com/users/launchpad-26/orgs","repos_url":"https://api.github.com/users/launchpad-26/repos","events_url":"https://api.github.com/users/launchpad-26/events{/privacy}","received_events_url":"https://api.github.com/users/launchpad-26/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/launchpad-26/buzz","description":"A hive mind communication platform","fork":true,"url":"https://api.github.com/repos/launchpad-26/buzz","forks_url":"https://api.github.com/repos/launchpad-26/buzz/forks","keys_url":"https://api.github.com/repos/launchpad-26/buzz/keys{/key_id}","collaborators_url":"https://api.github.com/repos/launchpad-26/buzz/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/launchpad-26/buzz/teams","hooks_url":"https://api.github.com/repos/launchpad-26/buzz/hooks","issue_events_url":"https://api.github.com/repos/launchpad-26/buzz/issues/events{/number}","events_url":"https://api.github.com/repos/launchpad-26/buzz/events","assignees_url":"https://api.github.com/repos/launchpad-26/buzz/assignees{/user}","branches_url":"https://api.github.com/repos/launchpad-26/buzz/branches{/branch}","tags_url":"https://api.github.com/repos/launchpad-26/buzz/tags","blobs_url":"https://api.github.com/repos/launchpad-26/buzz/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/launchpad-26/buzz/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/launchpad-26/buzz/git/refs{/sha}","trees_url":"https://api.github.com/repos/launchpad-26/buzz/git/trees{/sha}","statuses_url":"https://api.github.com/repos/launchpad-26/buzz/statuses/{sha}","languages_url":"https://api.github.com/repos/launchpad-26/buzz/languages","stargazers_url":"https://api.github.com/repos/launchpad-26/buzz/stargazers","contributors_url":"https://api.github.com/repos/launchpad-26/buzz/contributors","subscribers_url":"https://api.github.com/repos/launchpad-26/buzz/subscribers","subscription_url":"https://api.github.com/repos/launchpad-26/buzz/subscription","commits_url":"https://api.github.com/repos/launchpad-26/buzz/commits{/sha}","git_commits_url":"https://api.github.com/repos/launchpad-26/buzz/git/commits{/sha}","comments_url":"https://api.github.com/repos/launchpad-26/buzz/comments{/number}","issue_comment_url":"https://api.github.com/repos/launchpad-26/buzz/issues/comments{/number}","contents_url":"https://api.github.com/repos/launchpad-26/buzz/contents/{+path}","compare_url":"https://api.github.com/repos/launchpad-26/buzz/compare/{base}...{head}","merges_url":"https://api.github.com/repos/launchpad-26/buzz/merges","archive_url":"https://api.github.com/repos/launchpad-26/buzz/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/launchpad-26/buzz/downloads","issues_url":"https://api.github.com/repos/launchpad-26/buzz/issues{/number}","pulls_url":"https://api.github.com/repos/launchpad-26/buzz/pulls{/number}","milestones_url":"https://api.github.com/repos/launchpad-26/buzz/milestones{/number}","notifications_url":"https://api.github.com/repos/launchpad-26/buzz/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/launchpad-26/buzz/labels{/name}","releases_url":"https://api.github.com/repos/launchpad-26/buzz/releases{/id}","deployments_url":"https://api.github.com/repos/launchpad-26/buzz/deployments","created_at":"2026-08-06T14:20:40Z","updated_at":"2026-08-12T20:14:46Z","pushed_at":"2026-08-12T20:56:02Z","git_url":"git://github.com/launchpad-26/buzz.git","ssh_url":"git@github.com:launchpad-26/buzz.git","clone_url":"https://github.com/launchpad-26/buzz.git","svn_url":"https://github.com/launchpad-26/buzz","homepage":"","size":473018,"stargazers_count":0,"watchers_count":0,"language":"Rust","has_issues":true,"has_projects":true,"has_downloads":false,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":96,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":[],"visibility":"public","forks":1,"open_issues":96,"watchers":0,"default_branch":"launchpad"}},"base":{"label":"launchpad-26:launchpad","ref":"launchpad","sha":"e14f5fafb9cc13890530f019556a343b02997c34","user":{"login":"launchpad-26","id":309398704,"node_id":"O_kgDOEnEMsA","avatar_url":"https://avatars.githubusercontent.com/u/309398704?v=4","gravatar_id":"","url":"https://api.github.com/users/launchpad-26","html_url":"https://github.com/launchpad-26","followers_url":"https://api.github.com/users/launchpad-26/followers","following_url":"https://api.github.com/users/launchpad-26/following{/other_user}","gists_url":"https://api.github.com/users/launchpad-26/gists{/gist_id}","starred_url":"https://api.github.com/users/launchpad-26/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/launchpad-26/subscriptions","organizations_url":"https://api.github.com/users/launchpad-26/orgs","repos_url":"https://api.github.com/users/launchpad-26/repos","events_url":"https://api.github.com/users/launchpad-26/events{/privacy}","received_events_url":"https://api.github.com/users/launchpad-26/received_events","type":"Organization","user_view_type":"public","site_admin":false},"repo":{"id":1325338351,"node_id":"R_kgDOTv8O7w","name":"buzz","full_name":"launchpad-26/buzz","private":false,"owner":{"login":"launchpad-26","id":309398704,"node_id":"O_kgDOEnEMsA","avatar_url":"https://avatars.githubusercontent.com/u/309398704?v=4","gravatar_id":"","url":"https://api.github.com/users/launchpad-26","html_url":"https://github.com/launchpad-26","followers_url":"https://api.github.com/users/launchpad-26/followers","following_url":"https://api.github.com/users/launchpad-26/following{/other_user}","gists_url":"https://api.github.com/users/launchpad-26/gists{/gist_id}","starred_url":"https://api.github.com/users/launchpad-26/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/launchpad-26/subscriptions","organizations_url":"https://api.github.com/users/launchpad-26/orgs","repos_url":"https://api.github.com/users/launchpad-26/repos","events_url":"https://api.github.com/users/launchpad-26/events{/privacy}","received_events_url":"https://api.github.com/users/launchpad-26/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/launchpad-26/buzz","description":"A hive mind communication platform","fork":true,"url":"https://api.github.com/repos/launchpad-26/buzz","forks_url":"https://api.github.com/repos/launchpad-26/buzz/forks","keys_url":"https://api.github.com/repos/launchpad-26/buzz/keys{/key_id}","collaborators_url":"https://api.github.com/repos/launchpad-26/buzz/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/launchpad-26/buzz/teams","hooks_url":"https://api.github.com/repos/launchpad-26/buzz/hooks","issue_events_url":"https://api.github.com/repos/launchpad-26/buzz/issues/events{/number}","events_url":"https://api.github.com/repos/launchpad-26/buzz/events","assignees_url":"https://api.github.com/repos/launchpad-26/buzz/assignees{/user}","branches_url":"https://api.github.com/repos/launchpad-26/buzz/branches{/branch}","tags_url":"https://api.github.com/repos/launchpad-26/buzz/tags","blobs_url":"https://api.github.com/repos/launchpad-26/buzz/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/launchpad-26/buzz/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/launchpad-26/buzz/git/refs{/sha}","trees_url":"https://api.github.com/repos/launchpad-26/buzz/git/trees{/sha}","statuses_url":"https://api.github.com/repos/launchpad-26/buzz/statuses/{sha}","languages_url":"https://api.github.com/repos/launchpad-26/buzz/languages","stargazers_url":"https://api.github.com/repos/launchpad-26/buzz/stargazers","contributors_url":"https://api.github.com/repos/launchpad-26/buzz/contributors","subscribers_url":"https://api.github.com/repos/launchpad-26/buzz/subscribers","subscription_url":"https://api.github.com/repos/launchpad-26/buzz/subscription","commits_url":"https://api.github.com/repos/launchpad-26/buzz/commits{/sha}","git_commits_url":"https://api.github.com/repos/launchpad-26/buzz/git/commits{/sha}","comments_url":"https://api.github.com/repos/launchpad-26/buzz/comments{/number}","issue_comment_url":"https://api.github.com/repos/launchpad-26/buzz/issues/comments{/number}","contents_url":"https://api.github.com/repos/launchpad-26/buzz/contents/{+path}","compare_url":"https://api.github.com/repos/launchpad-26/buzz/compare/{base}...{head}","merges_url":"https://api.github.com/repos/launchpad-26/buzz/merges","archive_url":"https://api.github.com/repos/launchpad-26/buzz/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/launchpad-26/buzz/downloads","issues_url":"https://api.github.com/repos/launchpad-26/buzz/issues{/number}","pulls_url":"https://api.github.com/repos/launchpad-26/buzz/pulls{/number}","milestones_url":"https://api.github.com/repos/launchpad-26/buzz/milestones{/number}","notifications_url":"https://api.github.com/repos/launchpad-26/buzz/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/launchpad-26/buzz/labels{/name}","releases_url":"https://api.github.com/repos/launchpad-26/buzz/releases{/id}","deployments_url":"https://api.github.com/repos/launchpad-26/buzz/deployments","created_at":"2026-08-06T14:20:40Z","updated_at":"2026-08-12T20:14:46Z","pushed_at":"2026-08-12T20:56:02Z","git_url":"git://github.com/launchpad-26/buzz.git","ssh_url":"git@github.com:launchpad-26/buzz.git","clone_url":"https://github.com/launchpad-26/buzz.git","svn_url":"https://github.com/launchpad-26/buzz","homepage":"","size":473018,"stargazers_count":0,"watchers_count":0,"language":"Rust","has_issues":true,"has_projects":true,"has_downloads":false,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":96,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":[],"visibility":"public","forks":1,"open_issues":96,"watchers":0,"default_branch":"launchpad"}},"_links":{"self":{"href":"https://api.github.com/repos/launchpad-26/buzz/pulls/142"},"html":{"href":"https://github.com/launchpad-26/buzz/pull/142"},"issue":{"href":"https://api.github.com/repos/launchpad-26/buzz/issues/142"},"comments":{"href":"https://api.github.com/repos/launchpad-26/buzz/issues/142/comments"},"review_comments":{"href":"https://api.github.com/repos/launchpad-26/buzz/pulls/142/comments"},"review_comment":{"href":"https://api.github.com/repos/launchpad-26/buzz/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/launchpad-26/buzz/pulls/142/commits"},"statuses":{"href":"https://api.github.com/repos/launchpad-26/buzz/statuses/9d7af78528e1672ea3fd53bbcc60eec404c58f72"}},"author_association":"MEMBER","auto_merge":null,"assignee":null,"active_lock_reason":null,"merged":false,"mergeable":true,"rebaseable":false,"mergeable_state":"blocked","merged_by":null,"comments":1,"review_comments":0,"maintainer_can_modify":false,"commits":1,"additions":2,"deletions":244,"changed_files":2} \ No newline at end of file diff --git a/launchpad/scripts/testdata/prdelete-tree.json b/launchpad/scripts/testdata/prdelete-tree.json new file mode 100644 index 00000000000..664e60fca6a --- /dev/null +++ b/launchpad/scripts/testdata/prdelete-tree.json @@ -0,0 +1,1231 @@ +{ + "sha": "9d7af78528e1672ea3fd53bbcc60eec404c58f72", + "url": "https://api.github.com/repos/launchpad-26/buzz/git/trees/9d7af78528e1672ea3fd53bbcc60eec404c58f72", + "truncated": false, + "tree": [ + { + "path": ".agents/skills/desktop-screenshot/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b", + "size": 65, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b" + }, + { + "path": ".agents/skills/sprout-cli/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "091795a0d44351523ca4e35eac0d74516ab88c25", + "size": 59, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/091795a0d44351523ca4e35eac0d74516ab88c25" + }, + { + "path": ".claude/skills/desktop-screenshot/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b", + "size": 65, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b" + }, + { + "path": ".claude/skills/sprout-cli/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "091795a0d44351523ca4e35eac0d74516ab88c25", + "size": 59, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/091795a0d44351523ca4e35eac0d74516ab88c25" + }, + { + "path": ".codex/skills/desktop-screenshot/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b", + "size": 65, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b" + }, + { + "path": ".codex/skills/sprout-cli/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "091795a0d44351523ca4e35eac0d74516ab88c25", + "size": 59, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/091795a0d44351523ca4e35eac0d74516ab88c25" + }, + { + "path": ".github/PULL_REQUEST_TEMPLATE.md", + "mode": "100644", + "type": "blob", + "sha": "b9adbdda312fa9640c7b16904d9f154fce0cd914", + "size": 578, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/b9adbdda312fa9640c7b16904d9f154fce0cd914" + }, + { + "path": ".goose/skills/desktop-screenshot/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b", + "size": 65, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1c6f8ccbd331e627ccf407f8d13d1855d40c4a7b" + }, + { + "path": ".goose/skills/sprout-cli/SKILL.md", + "mode": "120000", + "type": "blob", + "sha": "091795a0d44351523ca4e35eac0d74516ab88c25", + "size": 59, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/091795a0d44351523ca4e35eac0d74516ab88c25" + }, + { + "path": "AGENTS.md", + "mode": "100644", + "type": "blob", + "sha": "49a85162636a048a5ff3e50374f1c435a9fc4c36", + "size": 29930, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/49a85162636a048a5ff3e50374f1c435a9fc4c36" + }, + { + "path": "ARCHITECTURE.md", + "mode": "100644", + "type": "blob", + "sha": "892082d96c6da9bed8a1b7305ea7c7034e5315db", + "size": 45578, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/892082d96c6da9bed8a1b7305ea7c7034e5315db" + }, + { + "path": "CHANGELOG.md", + "mode": "100644", + "type": "blob", + "sha": "7ca952507495551cf3737834b6d132a07d84952d", + "size": 265343, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/7ca952507495551cf3737834b6d132a07d84952d" + }, + { + "path": "CLAUDE.md", + "mode": "120000", + "type": "blob", + "sha": "47dc3e3d863cfb5727b87d785d09abf9743c0a72", + "size": 9, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/47dc3e3d863cfb5727b87d785d09abf9743c0a72" + }, + { + "path": "CODE_OF_CONDUCT.md", + "mode": "100644", + "type": "blob", + "sha": "a2bc369a3cf426b62e24666c73e3d7c70eab62f3", + "size": 5496, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/a2bc369a3cf426b62e24666c73e3d7c70eab62f3" + }, + { + "path": "CONTRIBUTING.md", + "mode": "100644", + "type": "blob", + "sha": "db0aea637fea3a6fda23165d44d57fad350fe517", + "size": 19053, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/db0aea637fea3a6fda23165d44d57fad350fe517" + }, + { + "path": "GOVERNANCE.md", + "mode": "100644", + "type": "blob", + "sha": "4af3958eedafeccc3669161f3cd8bbe443dfa0f6", + "size": 127, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/4af3958eedafeccc3669161f3cd8bbe443dfa0f6" + }, + { + "path": "LAUNCHPAD.md", + "mode": "100644", + "type": "blob", + "sha": "a7f639b206a5a680929efc555c2411371951565f", + "size": 2764, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/a7f639b206a5a680929efc555c2411371951565f" + }, + { + "path": "NOSTR.md", + "mode": "100644", + "type": "blob", + "sha": "cce70f2f77f8aa0ebf77783a5d3edf9a1f6ea6a9", + "size": 19578, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/cce70f2f77f8aa0ebf77783a5d3edf9a1f6ea6a9" + }, + { + "path": "README.md", + "mode": "100644", + "type": "blob", + "sha": "56439f00bc1565ce7aca273a886eecdc12df5c1d", + "size": 15945, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/56439f00bc1565ce7aca273a886eecdc12df5c1d" + }, + { + "path": "RELEASING.md", + "mode": "100644", + "type": "blob", + "sha": "53d580556191ad40f2197e3797cfe536087b1e34", + "size": 15023, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/53d580556191ad40f2197e3797cfe536087b1e34" + }, + { + "path": "SECURITY.md", + "mode": "100644", + "type": "blob", + "sha": "09ea73022b311594060dabde5d291c09865ffbf3", + "size": 5316, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/09ea73022b311594060dabde5d291c09865ffbf3" + }, + { + "path": "TESTING.md", + "mode": "100644", + "type": "blob", + "sha": "7c107da5754f442e7e4ae277a812d4e7e2273508", + "size": 16309, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/7c107da5754f442e7e4ae277a812d4e7e2273508" + }, + { + "path": "VISION.md", + "mode": "100644", + "type": "blob", + "sha": "66a106bdebc5e751ee39babb56056d0d7572cf1a", + "size": 12934, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/66a106bdebc5e751ee39babb56056d0d7572cf1a" + }, + { + "path": "VISION_ACTIVITY.md", + "mode": "100644", + "type": "blob", + "sha": "d1d1e0ac5c22b5a990bb18e15d433ea4553fc8ef", + "size": 5208, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/d1d1e0ac5c22b5a990bb18e15d433ea4553fc8ef" + }, + { + "path": "VISION_AGENT.md", + "mode": "100644", + "type": "blob", + "sha": "2b2597c8d2b542241f19e6e7cfff110c97188935", + "size": 4501, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/2b2597c8d2b542241f19e6e7cfff110c97188935" + }, + { + "path": "VISION_MESH.md", + "mode": "100644", + "type": "blob", + "sha": "d1354d50fee00cf7de0ea981c58973c406ec69f7", + "size": 4607, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/d1354d50fee00cf7de0ea981c58973c406ec69f7" + }, + { + "path": "VISION_MODERATION.md", + "mode": "100644", + "type": "blob", + "sha": "45d3ab86fd03c2f3dfd8d89374a663a47e00d45b", + "size": 7378, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/45d3ab86fd03c2f3dfd8d89374a663a47e00d45b" + }, + { + "path": "VISION_PROJECTS.md", + "mode": "100644", + "type": "blob", + "sha": "8601b87829eb3743671c1ed7cbebba42902a8d86", + "size": 15808, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/8601b87829eb3743671c1ed7cbebba42902a8d86" + }, + { + "path": "VISION_REMOTE_AGENTS.md", + "mode": "100644", + "type": "blob", + "sha": "4b187f355a5dd65ab7b7b1b598c62bead7a7e804", + "size": 8922, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/4b187f355a5dd65ab7b7b1b598c62bead7a7e804" + }, + { + "path": "VISION_SOVEREIGN.md", + "mode": "100644", + "type": "blob", + "sha": "77362b3f7e2f4cf9cc8b61837c5125c55e797244", + "size": 13541, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/77362b3f7e2f4cf9cc8b61837c5125c55e797244" + }, + { + "path": "benchmarks/harbor-buzz-orchestra/README.md", + "mode": "100644", + "type": "blob", + "sha": "0358c954e7d7b78c9a7bc05bec43c436059a6df0", + "size": 6342, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/0358c954e7d7b78c9a7bc05bec43c436059a6df0" + }, + { + "path": "benchmarks/harbor-buzz-orchestra/personas/orchestrator-m1.md", + "mode": "100644", + "type": "blob", + "sha": "d248806e4ec3414df6aa07fb0d2a8479f0560a7c", + "size": 1936, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/d248806e4ec3414df6aa07fb0d2a8479f0560a7c" + }, + { + "path": "benchmarks/harbor-buzz-orchestra/personas/orchestrator-tb.md", + "mode": "100644", + "type": "blob", + "sha": "c1dbd7b79578f2b0cabafc610f8b6bddbd8aedc2", + "size": 2424, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/c1dbd7b79578f2b0cabafc610f8b6bddbd8aedc2" + }, + { + "path": "benchmarks/harbor-buzz-orchestra/personas/worker-m1.md", + "mode": "100644", + "type": "blob", + "sha": "442d7b874d0f293ebe5b6f73813793b26fee4a6b", + "size": 1777, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/442d7b874d0f293ebe5b6f73813793b26fee4a6b" + }, + { + "path": "benchmarks/harbor-buzz-orchestra/personas/worker-tb.md", + "mode": "100644", + "type": "blob", + "sha": "49a9125a17bb9885ce31c0e3edb9e4766fe5b838", + "size": 1684, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/49a9125a17bb9885ce31c0e3edb9e4766fe5b838" + }, + { + "path": "benchmarks/harbor-buzz-orchestra/testbed/endpoints/README.md", + "mode": "100644", + "type": "blob", + "sha": "f8d2a560bf275232d3c81e722563001ff5bd6865", + "size": 1190, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/f8d2a560bf275232d3c81e722563001ff5bd6865" + }, + { + "path": "bin/README.hermit.md", + "mode": "100644", + "type": "blob", + "sha": "e889550ba4cbc92a720527042e5f6e7a303bfcbc", + "size": 270, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/e889550ba4cbc92a720527042e5f6e7a303bfcbc" + }, + { + "path": "crates/buzz-acp/README.md", + "mode": "100644", + "type": "blob", + "sha": "e6164b02dd3ef1e0d9d63571b7f863fb7ade07d9", + "size": 18083, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/e6164b02dd3ef1e0d9d63571b7f863fb7ade07d9" + }, + { + "path": "crates/buzz-acp/src/base_prompt.md", + "mode": "100644", + "type": "blob", + "sha": "1d85221f113e41f8a098fa13efaad413b8788512", + "size": 15007, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1d85221f113e41f8a098fa13efaad413b8788512" + }, + { + "path": "crates/buzz-agent/README.md", + "mode": "100644", + "type": "blob", + "sha": "5d942777d5e5b0b38a767d56ae7ec7b2bb505aad", + "size": 22677, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/5d942777d5e5b0b38a767d56ae7ec7b2bb505aad" + }, + { + "path": "crates/buzz-backend-kubernetes/tests/fixtures/provider-wire/README.md", + "mode": "100644", + "type": "blob", + "sha": "56a972f89420cebfcb74cfd73598d9ff90c9a7f1", + "size": 2323, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/56a972f89420cebfcb74cfd73598d9ff90c9a7f1" + }, + { + "path": "crates/buzz-cli/README.md", + "mode": "100644", + "type": "blob", + "sha": "a2dcdce6d2104b1ba6ddef3bcb1ccac467fc8e3f", + "size": 6553, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/a2dcdce6d2104b1ba6ddef3bcb1ccac467fc8e3f" + }, + { + "path": "crates/buzz-cli/TESTING.md", + "mode": "100644", + "type": "blob", + "sha": "77234b7faab6966bdf6429da9f17fa05a05463a9", + "size": 20746, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/77234b7faab6966bdf6429da9f17fa05a05463a9" + }, + { + "path": "crates/buzz-conformance/LIMITS.md", + "mode": "100644", + "type": "blob", + "sha": "37f0f28d3ee728f5ff633784bed6bbbe791ad6ec", + "size": 5767, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/37f0f28d3ee728f5ff633784bed6bbbe791ad6ec" + }, + { + "path": "crates/buzz-conformance/TRACE_SCHEMA.md", + "mode": "100644", + "type": "blob", + "sha": "c1dca674d559e5daf58576cb305ef9b9251b6fde", + "size": 6969, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/c1dca674d559e5daf58576cb305ef9b9251b6fde" + }, + { + "path": "crates/buzz-core/src/pairing/NIP-AB.md", + "mode": "100644", + "type": "blob", + "sha": "fc8f69245e80fb710e95694ea28dbe286047da6d", + "size": 54727, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/fc8f69245e80fb710e95694ea28dbe286047da6d" + }, + { + "path": "crates/buzz-media/tests/fixtures/android/README.md", + "mode": "100644", + "type": "blob", + "sha": "b0b06bb0607d985b5c0a9e7d6c4360b55131cf17", + "size": 3067, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/b0b06bb0607d985b5c0a9e7d6c4360b55131cf17" + }, + { + "path": "crates/buzz-media/tests/fixtures/ios/README.md", + "mode": "100644", + "type": "blob", + "sha": "2f6d5f5e256a3f0e9e40508c1668ae150a09e61e", + "size": 2209, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/2f6d5f5e256a3f0e9e40508c1668ae150a09e61e" + }, + { + "path": "crates/buzz-pairing-cli/README.md", + "mode": "100644", + "type": "blob", + "sha": "4f9fd1e530943ce732c22244725522bb210ab4b4", + "size": 5016, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/4f9fd1e530943ce732c22244725522bb210ab4b4" + }, + { + "path": "crates/buzz-persona/PERSONA_PACK_SPEC.md", + "mode": "100644", + "type": "blob", + "sha": "cb3a7d1c05a74495c929a677f6dc2125561cb409", + "size": 47257, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/cb3a7d1c05a74495c929a677f6dc2125561cb409" + }, + { + "path": "crates/buzz-relay/CHANGELOG.md", + "mode": "100644", + "type": "blob", + "sha": "7ee7e9a303b4c3030fa85e25e60f31d9d4e7a208", + "size": 29334, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/7ee7e9a303b4c3030fa85e25e60f31d9d4e7a208" + }, + { + "path": "crates/git-credential-nostr/README.md", + "mode": "100644", + "type": "blob", + "sha": "827f8d55a330d508cc870113d680b981acd1f696", + "size": 2482, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/827f8d55a330d508cc870113d680b981acd1f696" + }, + { + "path": "crates/git-sign-nostr/README.md", + "mode": "100644", + "type": "blob", + "sha": "908682fd7fe4e037e0f8e64812db8d234f86bbd8", + "size": 1345, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/908682fd7fe4e037e0f8e64812db8d234f86bbd8" + }, + { + "path": "deploy/charts/buzz/README.md", + "mode": "100644", + "type": "blob", + "sha": "b2778df28b57d11f3ec5a179a9b1b442079e7903", + "size": 12342, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/b2778df28b57d11f3ec5a179a9b1b442079e7903" + }, + { + "path": "deploy/compose/README.md", + "mode": "100644", + "type": "blob", + "sha": "bb0e63fe15d1c540fe0fe6fa76a25045df36952a", + "size": 2338, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/bb0e63fe15d1c540fe0fe6fa76a25045df36952a" + }, + { + "path": "desktop/README.md", + "mode": "100644", + "type": "blob", + "sha": "d53698a9d04fca2636b0475d126acb4f7c152730", + "size": 627, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/d53698a9d04fca2636b0475d126acb4f7c152730" + }, + { + "path": "desktop/public/harness-logos/CREDITS.md", + "mode": "100644", + "type": "blob", + "sha": "716c43e1ae32eca14f4ec19daf0b60c3751fa580", + "size": 3597, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/716c43e1ae32eca14f4ec19daf0b60c3751fa580" + }, + { + "path": "desktop/scripts/texture-card/README.md", + "mode": "100644", + "type": "blob", + "sha": "1de2addce99cf2a340013a83ca82e4a0a4511a54", + "size": 1014, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1de2addce99cf2a340013a83ca82e4a0a4511a54" + }, + { + "path": "desktop/src-tauri/resources/pocket-voices/NOTICE.md", + "mode": "100644", + "type": "blob", + "sha": "9cc515dea3c733f540a707c34f94d273261700dc", + "size": 2133, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/9cc515dea3c733f540a707c34f94d273261700dc" + }, + { + "path": "desktop/src-tauri/src/managed_agents/nest_agents.md", + "mode": "100644", + "type": "blob", + "sha": "7cb7489b85291e0d2cf9d72eb5b1a9fd878e12c7", + "size": 3620, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/7cb7489b85291e0d2cf9d72eb5b1a9fd878e12c7" + }, + { + "path": "desktop/src-tauri/src/managed_agents/nest_skill.md", + "mode": "100644", + "type": "blob", + "sha": "79a5ea301d467444fafe53bb137b2c5f86be58b3", + "size": 11014, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/79a5ea301d467444fafe53bb137b2c5f86be58b3" + }, + { + "path": "desktop/src-tauri/src/managed_agents/screenshot_skill.md", + "mode": "100644", + "type": "blob", + "sha": "361e747fe28b62e3ae80772cccf8e35eb4c46e69", + "size": 3726, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/361e747fe28b62e3ae80772cccf8e35eb4c46e69" + }, + { + "path": "desktop/src/features/agents/AGENTS.md", + "mode": "100644", + "type": "blob", + "sha": "b578326eba33859512ecf0a3a7325186bea0a686", + "size": 13748, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/b578326eba33859512ecf0a3a7325186bea0a686" + }, + { + "path": "docs/MCP_DRIVEN_HOOKS.md", + "mode": "100644", + "type": "blob", + "sha": "6812a5b9a6c47d536b0d315d90eb81579991dac8", + "size": 4775, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/6812a5b9a6c47d536b0d315d90eb81579991dac8" + }, + { + "path": "docs/admin/README.md", + "mode": "100644", + "type": "blob", + "sha": "f49cbdd71ad76341ef427b9053ef388edb2de07d", + "size": 3139, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/f49cbdd71ad76341ef427b9053ef388edb2de07d" + }, + { + "path": "docs/bridge-channel-window.md", + "mode": "100644", + "type": "blob", + "sha": "42f1d82835e176e7e438a5b75c1020c2ce67751b", + "size": 6250, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/42f1d82835e176e7e438a5b75c1020c2ce67751b" + }, + { + "path": "docs/buzz-entity-links.md", + "mode": "100644", + "type": "blob", + "sha": "df32037898e93c608d7e474f6eb925ca2c81d4ab", + "size": 12939, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/df32037898e93c608d7e474f6eb925ca2c81d4ab" + }, + { + "path": "docs/buzz-shared-compute-dev.md", + "mode": "100644", + "type": "blob", + "sha": "3712bb7965d9f4cdbcb18b6016888508a463eafb", + "size": 6842, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/3712bb7965d9f4cdbcb18b6016888508a463eafb" + }, + { + "path": "docs/formal/STATEFUL_GATEWAY.md", + "mode": "100644", + "type": "blob", + "sha": "77b472619a59d053b9d3318264eba8fa7f43f9de", + "size": 1268, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/77b472619a59d053b9d3318264eba8fa7f43f9de" + }, + { + "path": "docs/formal/nip-pl/NOTE.md", + "mode": "100644", + "type": "blob", + "sha": "0cb70a54ad91d31d64c49bdb57b9d2ee464123db", + "size": 2196, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/0cb70a54ad91d31d64c49bdb57b9d2ee464123db" + }, + { + "path": "docs/formal/nip-rs-unread/NOTE.md", + "mode": "100644", + "type": "blob", + "sha": "7472c3dca3a93c33793e98b01266173723ba9383", + "size": 39649, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/7472c3dca3a93c33793e98b01266173723ba9383" + }, + { + "path": "docs/git-on-object-storage.md", + "mode": "100644", + "type": "blob", + "sha": "1d87720edc7fed73de31151fe6cf36141c163999", + "size": 36714, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1d87720edc7fed73de31151fe6cf36141c163999" + }, + { + "path": "docs/linux-rendering-troubleshooting.md", + "mode": "100644", + "type": "blob", + "sha": "4bc8e80eb83ec0820397212a5cfe42dfaa8ff1b0", + "size": 8452, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/4bc8e80eb83ec0820397212a5cfe42dfaa8ff1b0" + }, + { + "path": "docs/multi-tenant-conformance.md", + "mode": "100644", + "type": "blob", + "sha": "d8877b5931969fe0c714ed3f2937fc5d76f64334", + "size": 17124, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/d8877b5931969fe0c714ed3f2937fc5d76f64334" + }, + { + "path": "docs/multi-tenant-relay.md", + "mode": "100644", + "type": "blob", + "sha": "85de728b12c63df1f8b1aa00fa8cf4bf282c2bea", + "size": 74264, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/85de728b12c63df1f8b1aa00fa8cf4bf282c2bea" + }, + { + "path": "docs/nips/NIP-AA.md", + "mode": "100644", + "type": "blob", + "sha": "920ee7851087e716058a44eda97d2060c00522a9", + "size": 22346, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/920ee7851087e716058a44eda97d2060c00522a9" + }, + { + "path": "docs/nips/NIP-AE.md", + "mode": "100644", + "type": "blob", + "sha": "c24078d8a45672a282d9e2f7f0069f6fa4356d8d", + "size": 21677, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/c24078d8a45672a282d9e2f7f0069f6fa4356d8d" + }, + { + "path": "docs/nips/NIP-AM.md", + "mode": "100644", + "type": "blob", + "sha": "794a6722d54550a0786921e0580f70b3b3e6e14c", + "size": 13973, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/794a6722d54550a0786921e0580f70b3b3e6e14c" + }, + { + "path": "docs/nips/NIP-AO.md", + "mode": "100644", + "type": "blob", + "sha": "36adea04871c830e38717c1badf149150b7b781e", + "size": 9972, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/36adea04871c830e38717c1badf149150b7b781e" + }, + { + "path": "docs/nips/NIP-AP.md", + "mode": "100644", + "type": "blob", + "sha": "ad23d92282dbba3ba9ee4d60e4d8ebf6e1150c9d", + "size": 25710, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/ad23d92282dbba3ba9ee4d60e4d8ebf6e1150c9d" + }, + { + "path": "docs/nips/NIP-CW.md", + "mode": "100644", + "type": "blob", + "sha": "f21b63405b2d240677207a6696813bd5a0844364", + "size": 21151, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/f21b63405b2d240677207a6696813bd5a0844364" + }, + { + "path": "docs/nips/NIP-DV.md", + "mode": "100644", + "type": "blob", + "sha": "2efba0ba659c14c7ef43590b9e79bae79513e43d", + "size": 11390, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/2efba0ba659c14c7ef43590b9e79bae79513e43d" + }, + { + "path": "docs/nips/NIP-ER.md", + "mode": "100644", + "type": "blob", + "sha": "f384ff7ef6c9c01611a86937aad341ed328f3d07", + "size": 19511, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/f384ff7ef6c9c01611a86937aad341ed328f3d07" + }, + { + "path": "docs/nips/NIP-GS.md", + "mode": "100644", + "type": "blob", + "sha": "7ef37bcc276f3566a994db5861178b16db641ba7", + "size": 38515, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/7ef37bcc276f3566a994db5861178b16db641ba7" + }, + { + "path": "docs/nips/NIP-IA.md", + "mode": "100644", + "type": "blob", + "sha": "28b2cfa52d479f4f28465a9fbe0b2f5db9d6b390", + "size": 39959, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/28b2cfa52d479f4f28465a9fbe0b2f5db9d6b390" + }, + { + "path": "docs/nips/NIP-MP.md", + "mode": "100644", + "type": "blob", + "sha": "14936d2a7cd38affdc8002e3b92785048ca8c967", + "size": 41745, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/14936d2a7cd38affdc8002e3b92785048ca8c967" + }, + { + "path": "docs/nips/NIP-OA.md", + "mode": "100644", + "type": "blob", + "sha": "d148841440effe97533deab1471f9cfb5dfdbae9", + "size": 9096, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/d148841440effe97533deab1471f9cfb5dfdbae9" + }, + { + "path": "docs/nips/NIP-PL.md", + "mode": "100644", + "type": "blob", + "sha": "08fa6ca34d9b504b8a19819d489fe0446c5ff85d", + "size": 47748, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/08fa6ca34d9b504b8a19819d489fe0446c5ff85d" + }, + { + "path": "docs/nips/NIP-PMA.md", + "mode": "100644", + "type": "blob", + "sha": "82592b1eca059755cfddefc49ef7bf8b161737e0", + "size": 5870, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/82592b1eca059755cfddefc49ef7bf8b161737e0" + }, + { + "path": "docs/nips/NIP-RS.md", + "mode": "100644", + "type": "blob", + "sha": "6a095df60ab672f09df192ab3dcbc69c6b69df1e", + "size": 73928, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/6a095df60ab672f09df192ab3dcbc69c6b69df1e" + }, + { + "path": "docs/nips/NIP-WP.md", + "mode": "100644", + "type": "blob", + "sha": "ebaf612de9d2ffe703468593c55854304993e8fb", + "size": 6741, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/ebaf612de9d2ffe703468593c55854304993e8fb" + }, + { + "path": "docs/push-gateway-deployment.md", + "mode": "100644", + "type": "blob", + "sha": "63c63355a1140ba0d1a50dafb70f36380f2246ad", + "size": 13855, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/63c63355a1140ba0d1a50dafb70f36380f2246ad" + }, + { + "path": "docs/remote-agents.md", + "mode": "100644", + "type": "blob", + "sha": "45289ef910af7379a20b66f91ece0e49c3ed4aaa", + "size": 114647, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/45289ef910af7379a20b66f91ece0e49c3ed4aaa" + }, + { + "path": "docs/welcome-kickoff-silent-failures.md", + "mode": "100644", + "type": "blob", + "sha": "58a4a5fa9580dc5cb1453edc9ee35d295e5c5fe7", + "size": 27131, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/58a4a5fa9580dc5cb1453edc9ee35d295e5c5fe7" + }, + { + "path": "examples/README.md", + "mode": "100644", + "type": "blob", + "sha": "8649ba360ddad538a5940499ebf153c17db6ac3d", + "size": 912, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/8649ba360ddad538a5940499ebf153c17db6ac3d" + }, + { + "path": "examples/countdown-bot/README.md", + "mode": "100644", + "type": "blob", + "sha": "09b3c135200a12b6f8330c483f4773bb943722b0", + "size": 4025, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/09b3c135200a12b6f8330c483f4773bb943722b0" + }, + { + "path": "examples/meadow-core/README.md", + "mode": "100644", + "type": "blob", + "sha": "3eedd1391eace6b6ac32224ef60a66ece9f104cc", + "size": 1767, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/3eedd1391eace6b6ac32224ef60a66ece9f104cc" + }, + { + "path": "examples/meadow-core/agents/bana.persona.md", + "mode": "100644", + "type": "blob", + "sha": "19b8b975ac7794e8fd72968112aa5a7f51e53e73", + "size": 1557, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/19b8b975ac7794e8fd72968112aa5a7f51e53e73" + }, + { + "path": "examples/meadow-core/agents/lev.persona.md", + "mode": "100644", + "type": "blob", + "sha": "5963eedd563d56763ab648f0d3c8fde411959916", + "size": 1509, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/5963eedd563d56763ab648f0d3c8fde411959916" + }, + { + "path": "examples/meadow-core/agents/skip.persona.md", + "mode": "100644", + "type": "blob", + "sha": "a3897991a4548afa1dae190866974e2883b1f262", + "size": 1458, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/a3897991a4548afa1dae190866974e2883b1f262" + }, + { + "path": "examples/meadow-core/instructions.md", + "mode": "100644", + "type": "blob", + "sha": "4517467c0cfe4a27701d315f0189dd5f19db1e7f", + "size": 538, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/4517467c0cfe4a27701d315f0189dd5f19db1e7f" + }, + { + "path": "examples/meadow-core/skills/github-research/SKILL.md", + "mode": "100644", + "type": "blob", + "sha": "b940e55a890d9ee3b5de9bb95f81d16e70717b65", + "size": 1445, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/b940e55a890d9ee3b5de9bb95f81d16e70717b65" + }, + { + "path": "launchpad/AGENT_PR_TEMPLATE.md", + "mode": "100644", + "type": "blob", + "sha": "501f37ef40fa427294e8269870c7f31451c85c06", + "size": 3258, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/501f37ef40fa427294e8269870c7f31451c85c06" + }, + { + "path": "launchpad/ARCHITECTURE.md", + "mode": "100644", + "type": "blob", + "sha": "fb1f8f3a1accbcc8668f03e02ea766aa26aedbf0", + "size": 11021, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/fb1f8f3a1accbcc8668f03e02ea766aa26aedbf0" + }, + { + "path": "launchpad/ENVIRONMENTS.md", + "mode": "100644", + "type": "blob", + "sha": "56463bea4cab23735cc53d979bc08185cf6a7a34", + "size": 4708, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/56463bea4cab23735cc53d979bc08185cf6a7a34" + }, + { + "path": "launchpad/README.md", + "mode": "100644", + "type": "blob", + "sha": "4f48e0c952d5dd8a78f9a121bead705c43f47a89", + "size": 7064, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/4f48e0c952d5dd8a78f9a121bead705c43f47a89" + }, + { + "path": "launchpad/REQUIREMENTS.md", + "mode": "100644", + "type": "blob", + "sha": "58f3180ecacc33053a0760d7a6dfd5d3753509ec", + "size": 25656, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/58f3180ecacc33053a0760d7a6dfd5d3753509ec" + }, + { + "path": "launchpad/Research/hardening-linux-servers-gap-analysis.md", + "mode": "100644", + "type": "blob", + "sha": "2a04ebdffbcf46f18d5b8d03ad21f7dbe84c50af", + "size": 37208, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/2a04ebdffbcf46f18d5b8d03ad21f7dbe84c50af" + }, + { + "path": "launchpad/Research/hardening-linux-servers.md", + "mode": "100644", + "type": "blob", + "sha": "bbf2cf113821026ae4584bafa57823c7122c3713", + "size": 30319, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/bbf2cf113821026ae4584bafa57823c7122c3713" + }, + { + "path": "launchpad/SECURITY-POSTURE.md", + "mode": "100644", + "type": "blob", + "sha": "89a97fe6fb2a940b6f6fb643c02cb705e29e1121", + "size": 15705, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/89a97fe6fb2a940b6f6fb643c02cb705e29e1121" + }, + { + "path": "launchpad/VISION.md", + "mode": "100644", + "type": "blob", + "sha": "7f507cbd50be61b7ccc55e3673551d7305a40015", + "size": 13273, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/7f507cbd50be61b7ccc55e3673551d7305a40015" + }, + { + "path": "launchpad/decisions/ADR-0001-handbook-repository-location-and-publication-target.md", + "mode": "100644", + "type": "blob", + "sha": "94e18b1db020d7e818aa63565d54f69ce9558923", + "size": 2972, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/94e18b1db020d7e818aa63565d54f69ce9558923" + }, + { + "path": "launchpad/decisions/ADR-0002-handbook-source-repository-scope.md", + "mode": "100644", + "type": "blob", + "sha": "944d8c9a7d898efe7d3ce20e64ecf00685e1c9c3", + "size": 2226, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/944d8c9a7d898efe7d3ce20e64ecf00685e1c9c3" + }, + { + "path": "launchpad/decisions/ADR-0003-handbook-page-provenance-contract.md", + "mode": "100644", + "type": "blob", + "sha": "cb35d7578adfb4f6946a47b23df829aa896543ba", + "size": 3075, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/cb35d7578adfb4f6946a47b23df829aa896543ba" + }, + { + "path": "launchpad/decisions/ADR-0004-handbook-staleness-detection-mechanism.md", + "mode": "100644", + "type": "blob", + "sha": "3c94dd0f6ff1fedea2498c6860646df588a00863", + "size": 2995, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/3c94dd0f6ff1fedea2498c6860646df588a00863" + }, + { + "path": "launchpad/decisions/README.md", + "mode": "100644", + "type": "blob", + "sha": "0aebced273eb63336990a4c3ca5cbebec170c18f", + "size": 1426, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/0aebced273eb63336990a4c3ca5cbebec170c18f" + }, + { + "path": "launchpad/deploy/AGENTS.md", + "mode": "100644", + "type": "blob", + "sha": "2dec6843f30527037d02347020dfedea3d3d8156", + "size": 1432, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/2dec6843f30527037d02347020dfedea3d3d8156" + }, + { + "path": "launchpad/deploy/CLAUDE.md", + "mode": "120000", + "type": "blob", + "sha": "47dc3e3d863cfb5727b87d785d09abf9743c0a72", + "size": 9, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/47dc3e3d863cfb5727b87d785d09abf9743c0a72" + }, + { + "path": "launchpad/deploy/README.md", + "mode": "100644", + "type": "blob", + "sha": "4d4aa174d0470f32827ded30bd18e2a6e09e4577", + "size": 1023, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/4d4aa174d0470f32827ded30bd18e2a6e09e4577" + }, + { + "path": "launchpad/deploy/VPS-DEPLOYMENT-AUDIT.md", + "mode": "100644", + "type": "blob", + "sha": "e8aa8ec5758f68817b10c330ab86318e07179d68", + "size": 5638, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/e8aa8ec5758f68817b10c330ab86318e07179d68" + }, + { + "path": "launchpad/deploy/archived/AGENTS.md", + "mode": "100644", + "type": "blob", + "sha": "e05829c2e8220d12b600fb940e3e86e55b8e1562", + "size": 757, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/e05829c2e8220d12b600fb940e3e86e55b8e1562" + }, + { + "path": "launchpad/deploy/archived/CLAUDE.md", + "mode": "120000", + "type": "blob", + "sha": "47dc3e3d863cfb5727b87d785d09abf9743c0a72", + "size": 9, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/47dc3e3d863cfb5727b87d785d09abf9743c0a72" + }, + { + "path": "launchpad/deploy/archived/HISTORICAL-AGENTS.md", + "mode": "100644", + "type": "blob", + "sha": "1abeb0504b42325a319f2f70b4643436ecf9d00d", + "size": 5579, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1abeb0504b42325a319f2f70b4643436ecf9d00d" + }, + { + "path": "launchpad/deploy/archived/HISTORICAL-CLAUDE.md", + "mode": "100644", + "type": "blob", + "sha": "2fb592f36f95c5fc0100902156d35dbfea69b4d3", + "size": 75, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/2fb592f36f95c5fc0100902156d35dbfea69b4d3" + }, + { + "path": "launchpad/deploy/archived/HISTORICAL-README.md", + "mode": "100644", + "type": "blob", + "sha": "41d0fd34a3d9187a802bd0391a3a40708093e06f", + "size": 8006, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/41d0fd34a3d9187a802bd0391a3a40708093e06f" + }, + { + "path": "launchpad/deploy/archived/README.md", + "mode": "100644", + "type": "blob", + "sha": "00aa4767282d576eabead5f2d1ea5d95ceada7e9", + "size": 732, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/00aa4767282d576eabead5f2d1ea5d95ceada7e9" + }, + { + "path": "launchpad/deploy/archived/ansible/README.md", + "mode": "100644", + "type": "blob", + "sha": "fe662ad97f818c8eb1a43c5eba966e9b7f392299", + "size": 7449, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/fe662ad97f818c8eb1a43c5eba966e9b7f392299" + }, + { + "path": "launchpad/deploy/archived/docker/README.md", + "mode": "100644", + "type": "blob", + "sha": "9c2078d62a6e22668345ff3ea86797812cb848ef", + "size": 1848, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/9c2078d62a6e22668345ff3ea86797812cb848ef" + }, + { + "path": "launchpad/deploy/archived/runbooks/README.md", + "mode": "100644", + "type": "blob", + "sha": "4cc920cd4d79da9f20182c04b5b2bc47ee747c87", + "size": 2071, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/4cc920cd4d79da9f20182c04b5b2bc47ee747c87" + }, + { + "path": "launchpad/deploy/archived/runbooks/chunks/00-preflight.md", + "mode": "100644", + "type": "blob", + "sha": "ca261d43290b656de7a363c56532f9522bc719c7", + "size": 5351, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/ca261d43290b656de7a363c56532f9522bc719c7" + }, + { + "path": "launchpad/deploy/archived/runbooks/chunks/01-base-image.md", + "mode": "100644", + "type": "blob", + "sha": "86bbe4e751f1452c6be699ab3be5f161ad955e8b", + "size": 1908, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/86bbe4e751f1452c6be699ab3be5f161ad955e8b" + }, + { + "path": "launchpad/deploy/archived/runbooks/chunks/02-vm-create.md", + "mode": "100644", + "type": "blob", + "sha": "9d5735902ce8a339e25be595073d13d42ec21ba0", + "size": 5396, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/9d5735902ce8a339e25be595073d13d42ec21ba0" + }, + { + "path": "launchpad/deploy/archived/runbooks/chunks/03-host-dns.md", + "mode": "100644", + "type": "blob", + "sha": "a19c68cfc41423c7ea8621a183c06406f37ff382", + "size": 10675, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/a19c68cfc41423c7ea8621a183c06406f37ff382" + }, + { + "path": "launchpad/deploy/archived/runbooks/chunks/04-docker.md", + "mode": "100644", + "type": "blob", + "sha": "cc10f146f6163d1c07d821c50fed2a29df4bce10", + "size": 5553, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/cc10f146f6163d1c07d821c50fed2a29df4bce10" + }, + { + "path": "launchpad/deploy/archived/runbooks/chunks/05-compose-bundle.md", + "mode": "100644", + "type": "blob", + "sha": "f63b7725feb5e6e625447febbe12576b5666368d", + "size": 6411, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/f63b7725feb5e6e625447febbe12576b5666368d" + }, + { + "path": "launchpad/deploy/archived/runbooks/chunks/06-config.md", + "mode": "100644", + "type": "blob", + "sha": "134625eaaee9cc9c1beef398623d25fd6d2a578d", + "size": 6078, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/134625eaaee9cc9c1beef398623d25fd6d2a578d" + }, + { + "path": "launchpad/deploy/archived/runbooks/chunks/07-up.md", + "mode": "100644", + "type": "blob", + "sha": "7da8f40a094daece9b3862017eb9f7a162808c73", + "size": 7326, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/7da8f40a094daece9b3862017eb9f7a162808c73" + }, + { + "path": "launchpad/deploy/archived/runbooks/chunks/08-desktop.md", + "mode": "100644", + "type": "blob", + "sha": "5a2cd6c0210b4cae75ff9f044812a60ba55d1071", + "size": 16847, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/5a2cd6c0210b4cae75ff9f044812a60ba55d1071" + }, + { + "path": "launchpad/deploy/archived/runbooks/chunks/09-membership.md", + "mode": "100644", + "type": "blob", + "sha": "1c73b1ceb0081f03fd1fe71923fb180db2c0be7c", + "size": 5662, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/1c73b1ceb0081f03fd1fe71923fb180db2c0be7c" + }, + { + "path": "launchpad/deploy/archived/runbooks/chunks/10-harden.md", + "mode": "100644", + "type": "blob", + "sha": "d6565ed8862030a20e2e8c05d62e521df37cd38c", + "size": 6465, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/d6565ed8862030a20e2e8c05d62e521df37cd38c" + }, + { + "path": "launchpad/deploy/archived/runbooks/chunks/11-verify.md", + "mode": "100644", + "type": "blob", + "sha": "80c3cb1b17bfe6ea818cb400740ac617236f3942", + "size": 3759, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/80c3cb1b17bfe6ea818cb400740ac617236f3942" + }, + { + "path": "launchpad/deploy/archived/runbooks/chunks/README.md", + "mode": "100644", + "type": "blob", + "sha": "357c154d3e8b9fe8c4b42fd7d864e7b94d9b4089", + "size": 32775, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/357c154d3e8b9fe8c4b42fd7d864e7b94d9b4089" + }, + { + "path": "launchpad/deploy/archived/runbooks/dev-deployment-SOP.md", + "mode": "100644", + "type": "blob", + "sha": "9ac5645919933d342b958c562283eabf5cfc2094", + "size": 138613, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/9ac5645919933d342b958c562283eabf5cfc2094" + }, + { + "path": "launchpad/deploy/archived/runbooks/hardening-spec.md", + "mode": "100644", + "type": "blob", + "sha": "e9fe5474e8046fcc70b9248fc620889131e40a4c", + "size": 58658, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/e9fe5474e8046fcc70b9248fc620889131e40a4c" + }, + { + "path": "launchpad/deploy/archived/runbooks/relay-build-list.md", + "mode": "100644", + "type": "blob", + "sha": "0508a626eadeeb0e6a2b2075f017da98a5ac603c", + "size": 14830, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/0508a626eadeeb0e6a2b2075f017da98a5ac603c" + }, + { + "path": "launchpad/deploy/archived/scripts/README.md", + "mode": "100644", + "type": "blob", + "sha": "6f05ff28e59f78bd17718a6e63d551d89c0f61fc", + "size": 2790, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/6f05ff28e59f78bd17718a6e63d551d89c0f61fc" + }, + { + "path": "launchpad/deploy/archived/temp-handoff.md", + "mode": "100644", + "type": "blob", + "sha": "0ce28833e79880cfe9b0486197da3f2e4c71e701", + "size": 12316, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/0ce28833e79880cfe9b0486197da3f2e4c71e701" + }, + { + "path": "launchpad/deploy/archived/virtual-box/README.md", + "mode": "100644", + "type": "blob", + "sha": "c38be742caa99894b27761803d100fafe82254d5", + "size": 2889, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/c38be742caa99894b27761803d100fafe82254d5" + }, + { + "path": "mobile/CHANGELOG.md", + "mode": "100644", + "type": "blob", + "sha": "08421f4d9d009fe4ba2ca3a74c3e991b5d7db723", + "size": 6785, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/08421f4d9d009fe4ba2ca3a74c3e991b5d7db723" + }, + { + "path": "mobile/README.md", + "mode": "100644", + "type": "blob", + "sha": "ada0568e28a21626ba1da6c40f8ab3749627820f", + "size": 3663, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/ada0568e28a21626ba1da6c40f8ab3749627820f" + }, + { + "path": "mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md", + "mode": "100644", + "type": "blob", + "sha": "89c2725b70f1882be97f5214fafe22d27a0ec01e", + "size": 336, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/89c2725b70f1882be97f5214fafe22d27a0ec01e" + }, + { + "path": "perf/RELAY_BUS_SCALING.md", + "mode": "100644", + "type": "blob", + "sha": "835ab4fe5e9eef35486a0591551413eab08d3055", + "size": 3043, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/835ab4fe5e9eef35486a0591551413eab08d3055" + }, + { + "path": "scripts/cutover/README.md", + "mode": "100644", + "type": "blob", + "sha": "27388bdaa2b95418ac93b7baf130ea178eaa4f48", + "size": 3836, + "url": "https://api.github.com/repos/launchpad-26/buzz/git/blobs/27388bdaa2b95418ac93b7baf130ea178eaa4f48" + } + ] +} diff --git a/launchpad/scripts/testdata/record-delete-fixture.sh b/launchpad/scripts/testdata/record-delete-fixture.sh new file mode 100755 index 00000000000..a3c9d17dcd6 --- /dev/null +++ b/launchpad/scripts/testdata/record-delete-fixture.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Manufacture the one fixture that has no real source: a pull request that +# DELETES the nearest rules file for a path it also touches. +# +# Why this exists. Nearest-rules resolution must read the PR's head tree, not the +# local worktree — and the direction that proves it is deletion. No PR in this +# fork's history deletes a rules file, and none was found upstream, so the add +# case (PR 14, real) would otherwise have to stand in for both by assertion. It +# does not: a resolver that reads the local checkout passes the add case and +# fails this one. +# +# What it does to the world: pushes a branch, opens a PR against launchpad, +# records two responses, then closes the PR unmerged and deletes the branch. +# Nothing is merged. The PR number is left in delete-pr.number so record.sh can +# read the responses without ever opening a PR itself. +# +# Usage: launchpad/scripts/testdata/record-delete-fixture.sh +set -euo pipefail + +cd "$(dirname "$0")" +HERE=$(pwd) +REPO=launchpad-26/buzz +BRANCH=fixture/rules-file-delete-throwaway +WORK=$(mktemp -d) + +cleanup() { + git -C "$HERE" worktree remove --force "$WORK" 2>/dev/null || true + rm -rf "$WORK" +} +trap cleanup EXIT + +git -C "$HERE" fetch origin launchpad +git -C "$HERE" worktree add -f -b "$BRANCH" "$WORK" origin/launchpad + +# Delete the nearest rules file for launchpad/, and touch a path it governs, so +# the deleted file and an affected path are in the same diff. +git -C "$WORK" rm -q launchpad/AGENTS.md +printf '\n\n' >> "$WORK/launchpad/README.md" +git -C "$WORK" add launchpad/README.md + +# --no-verify: the pre-commit hook runs `just desktop-tauri-fmt`, which fails in +# any git worktree (cargo fmt resolves workspace paths from the worktree root). +# This commit exists to produce an API response and is deleted minutes later. +git -C "$WORK" commit -s --no-verify -q -m 'chore(fixtures): throwaway — delete a rules file to record the API response + +Not for review and not for merge. Opened to record the DELETE direction of the +nearest-rules fixture for the pre-flight in launchpad-26/buzz#116, because no +real pull request in this fork or upstream deletes a rules file. Closed +unmerged as soon as the response is recorded.' + +git -C "$WORK" push -q --set-upstream origin "$BRANCH" + +gh pr create --repo "$REPO" --base launchpad --head "$BRANCH" \ + --title 'chore(fixtures): throwaway — recording a rules-file-DELETE response (closing immediately)' \ + --body 'Do not review. Do not merge. Closing as soon as two API responses are recorded. + +This PR exists only so that launchpad-26/buzz#116 can record what the compare +and tree endpoints return for a pull request that **deletes** the nearest rules +file for a path it also touches. No PR in this fork'"'"'s history does that, and +none was found upstream, so the add direction (PR #14, real) would otherwise +have had to stand in for both by assertion. + +Refs #116' > /dev/null + +DEL=$(gh pr list --repo "$REPO" --head "$BRANCH" --json number -q '.[0].number') +[ -n "$DEL" ] || { echo "could not read the PR number back" >&2; exit 1; } +echo "$DEL" > delete-pr.number +echo "opened throwaway PR #$DEL" + +DEL_HEAD=$(gh pr view "$DEL" --repo "$REPO" --json headRefOid -q .headRefOid) +# BY SHA, per README: a branch name resolves to its tip, and comparing a merged +# head against a moved tip answers 200 OK with zero files. +DEL_BASE=$(gh api "repos/$REPO/pulls/$DEL" -q .base.sha) +gh api "repos/$REPO/compare/$DEL_BASE...$DEL_HEAD" \ + | jq '{base_commit: {sha: .base_commit.sha}, merge_base_commit: {sha: .merge_base_commit.sha}, + status, files: [.files[] | {filename, status, additions, deletions}]}' > prdelete-compare.json +gh api "repos/$REPO/git/trees/$DEL_HEAD?recursive=1" \ + | jq '{sha, url, truncated, tree: [.tree[] | select(.path | endswith(".md"))]}' > prdelete-tree.json + +gh pr close "$DEL" --repo "$REPO" --delete-branch \ + --comment 'Recorded. Closing unmerged — this PR was only ever a fixture source for #116.' +echo "closed PR #$DEL and deleted $BRANCH" + +python3 - <<'PY' +import json +c = json.load(open('prdelete-compare.json')) +t = json.load(open('prdelete-tree.json')) +deleted = [f['filename'] for f in c['files'] if f['status'] == 'removed'] +paths = {e['path'] for e in t['tree']} +assert 'launchpad/AGENTS.md' in deleted, f'expected a rules-file deletion, got {deleted}' +assert 'launchpad/AGENTS.md' not in paths, 'head tree still lists the deleted file' +assert 'AGENTS.md' in paths, 'root AGENTS.md should still be there to fall back to' +print('verified: launchpad/AGENTS.md deleted, absent from the head tree, root AGENTS.md intact') +PY diff --git a/launchpad/scripts/testdata/record.sh b/launchpad/scripts/testdata/record.sh new file mode 100755 index 00000000000..8b7041c226d --- /dev/null +++ b/launchpad/scripts/testdata/record.sh @@ -0,0 +1,216 @@ +#!/usr/bin/env bash +# Record every pre-flight fixture from the live API. +# +# Fixtures are RECORDED, never hand-written: a fixture someone typed proves only +# that the code agrees with the author's belief about the response shape. Run +# this to re-record after an API change, and read the diff. +# +# Two fixtures are projections rather than whole responses, because the whole +# response is megabytes of noise. Each projection keeps every key of every entry +# it keeps — the shape is untouched, only irrelevant entries are dropped, and the +# jq filter that did it is right here: +# pr86-tree.json 4337 entries -> the 120 *.md entries (1.1 MB -> 30 KB) +# tree-truncated.json 71798 entries -> the first 20 entries (11 MB -> 4 KB) +# Nothing in the pre-flight reads a non-markdown tree entry, and the truncated +# fixture exists for its `truncated: true` flag alone. +# +# Usage: launchpad/scripts/testdata/record.sh +set -euo pipefail + +cd "$(dirname "$0")" +REPO=launchpad-26/buzz +UP=block/buzz + +say() { printf ' %-34s %s\n' "$1" "$2"; } + +# ---------------------------------------------------------------- fixture (i) +# PR 86 — the everything-is-readable case. Its check list is the reason the +# record carries checks as a list: three of its checks share the name "check", +# so any name-keyed map silently drops two of them. +gh api "repos/$REPO/pulls/86" > pr86-pr.json +say pr86-pr.json "the PR itself (identity: base ref, head sha)" + +# Compare BY SHA, base.sha...head.sha — never by base branch NAME. A branch name +# resolves to its tip today, and for a merged PR the tip already contains the +# head, so `compare/launchpad...{head}` answers with ZERO files: a real PR +# rendered as a PR that changed nothing. Recorded here the wrong way once, and +# the empty file list is what caught it. +PR86_HEAD=$(jq -r .head.sha pr86-pr.json) +PR86_BASE=$(jq -r .base.sha pr86-pr.json) + +gh pr view 86 --repo "$REPO" --json title,body,labels > pr86-meta.json +say pr86-meta.json "title, body, labels" + +# isRequired is GraphQL-only — it is not a field on `gh pr view --json +# statusCheckRollup`, whose keys are __typename, completedAt, conclusion, +# detailsUrl, name, startedAt, status, workflowName. +gh api graphql -f owner="${REPO%%/*}" -f repo="${REPO##*/}" -F pr=86 -f query=' +query($owner:String!,$repo:String!,$pr:Int!){ + repository(owner:$owner,name:$repo){ + pullRequest(number:$pr){ + commits(last:1){nodes{commit{oid statusCheckRollup{state contexts(first:100){totalCount nodes{ + __typename + ... on CheckRun{name status conclusion detailsUrl isRequired(pullRequestNumber:$pr) checkSuite{workflowRun{workflow{name}}}} + ... on StatusContext{context state targetUrl isRequired(pullRequestNumber:$pr)} + }}}}}} + } + } +}' > pr86-checks.json +say pr86-checks.json "the check list, with per-context isRequired" + +gh api "repos/$REPO/compare/$PR86_BASE...$PR86_HEAD" > pr86-compare.json +say pr86-compare.json "merge base + per-file diff" + +gh api "repos/$REPO/git/trees/$PR86_HEAD?recursive=1" \ + | jq '{sha, url, truncated, tree: [.tree[] | select(.path | endswith(".md"))]}' > pr86-tree.json +say pr86-tree.json "head tree, projected to *.md entries" + +# --------------------------------------------------------------- fixture (ii) +# A closing keyword in visible body text. +gh pr view 92 --repo "$REPO" --json title,body,labels > pr92-meta.json +say pr92-meta.json "closing keyword present, in visible text" + +# -------------------------------------------------------------- fixture (iii) +# The only closing keyword sits inside an HTML comment — an unfilled template +# placeholder. GitHub ignores it and so must the record. +gh pr view 5695 --repo "$UP" --json title,body,labels > upstream5695-meta.json +say upstream5695-meta.json "closing keyword ONLY inside " + +# ------------------------------------------- GitHub's own answer to "what does +# this PR close?" — closingIssuesReferences. Recorded for three PRs because the +# text and GitHub disagree in three different directions, and a control needs all +# three: +# 86 body says Closes #79; GitHub says #79 AND #91 — a regex takes the first +# match and under-reports the rest +# 92 body says Closes #n; GitHub says NOTHING, because the PR's base was not +# the default branch, so merging it would close no issue at all +# 5695 body's only keyword is inside ; GitHub agrees it closes nothing +CLOSING_QUERY='query($owner:String!,$repo:String!,$pr:Int!){ + repository(owner:$owner,name:$repo){ + pullRequest(number:$pr){closingIssuesReferences(first:50){nodes{number}}}}}' + +gh api graphql -f owner="${REPO%%/*}" -f repo="${REPO##*/}" -F pr=86 \ + -f query="$CLOSING_QUERY" > pr86-closing-refs.json +say pr86-closing-refs.json "GitHub: PR 86 closes TWO issues" + +gh api graphql -f owner="${REPO%%/*}" -f repo="${REPO##*/}" -F pr=92 \ + -f query="$CLOSING_QUERY" > pr92-closing-refs.json +say pr92-closing-refs.json "GitHub: PR 92 closes NOTHING despite its keyword" + +gh api graphql -f owner=block -f repo=buzz -F pr=5695 \ + -f query="$CLOSING_QUERY" > upstream5695-closing-refs.json +say upstream5695-closing-refs.json "GitHub: a commented-out keyword closes nothing" + +# ------------------------------------- the ONE gate signal readable without +# admin:org. launchpad/AGENTS.md §6: the ruleset requiring two approving reviews +# is invisible to this token, but a live PR's reviewDecision confirms that review +# is REQUIRED. Two PRs, because the signal genuinely differs: +# 86 base launchpad (protected) -> "REVIEW_REQUIRED" +# 92 base a topic branch -> "" (gh renders GraphQL null as an empty +# string; review is not required there) +gh pr view 86 --repo "$REPO" --json reviewDecision > pr86-review-decision.json +say pr86-review-decision.json "reviewDecision: review IS required on launchpad" + +gh pr view 92 --repo "$REPO" --json reviewDecision > pr92-review-decision.json +say pr92-review-decision.json "reviewDecision: empty on an unprotected base" + +# --------------------------------------------------------------- fixture (iv) +# A PR number that does not exist. `gh api` prints the error body on stdout and +# exits 1; both halves are the fixture, so the exit code is recorded beside it. +set +e +gh api "repos/$REPO/pulls/999999" > pr-notfound.json 2> pr-notfound.stderr +echo "$?" > pr-notfound.exit +set -e +say pr-notfound.json "404 body, exit code recorded alongside" + +# ---------------------------------------------------------------- fixture (v) +# Repo-level rules for the base branch: readable, and empty. An empty list is a +# fact ("no rules configured"), not a failure, and must not read as either a +# missing gate or a scope error. +gh api "repos/$REPO/rules/branches/launchpad" > rules-branches-launchpad.json +say rules-branches-launchpad.json "repo rules for launchpad — readable, empty" + +# Org-level rulesets need admin:org, which this token does not hold (gist, +# project, read:org, repo, workflow). Forbidden is a different fact from empty, +# and this is the fixture that proves the record says so. +set +e +gh api "orgs/${REPO%%/*}/rulesets" > orgs-rulesets-forbidden.json 2> orgs-rulesets-forbidden.stderr +echo "$?" > orgs-rulesets-forbidden.exit +set -e +say orgs-rulesets-forbidden.json "org rulesets — forbidden, not empty" + +# --------------------------------------------------------------- fixture (vi) +# A DIVERGENT base: the base branch tip is AHEAD of the commit the head branch +# forked from. Without this, no control can tell a three-dot diff from a +# two-dot one — every open PR in our fork has base tip == merge base. +# +# Recorded from whichever upstream PR is divergent TODAY. The SHAs are not +# pinned here on purpose: an upstream PR merges and the property evaporates. +DIVERGENT="" +for n in $(gh pr list --repo "$UP" --state open --limit 40 --json number -q '.[].number'); do + base_oid=$(gh pr view "$n" --repo "$UP" --json baseRefOid -q .baseRefOid) + head_oid=$(gh pr view "$n" --repo "$UP" --json headRefOid -q .headRefOid) + base_sha=$(gh api "repos/$UP/pulls/$n" -q .base.sha) + mb=$(gh api "repos/$UP/compare/$base_sha...$head_oid" -q .merge_base_commit.sha 2>/dev/null) || continue + if [ -n "$mb" ] && [ "$mb" != "$base_oid" ]; then + DIVERGENT="$n" + gh api "repos/$UP/pulls/$n" > upstream-divergent-pr.json + gh api "repos/$UP/compare/$base_sha...$head_oid" \ + | jq '{base_commit, merge_base_commit, status, ahead_by, behind_by, total_commits, + files: [.files[] | {sha, filename, status, additions, deletions, changes}]}' \ + > upstream-divergent-compare.json + break + fi +done +[ -n "$DIVERGENT" ] || { echo "no divergent-base PR found upstream — fixture (vi) NOT recorded" >&2; exit 1; } +say upstream-divergent-pr.json "PR $DIVERGENT: baseRefOid != merge base" +say upstream-divergent-compare.json "its compare, files projected" + +# ------------------------------------------------------- fixture (vii), ADD +# PR 14 adds AGENTS.md and launchpad/AGENTS.md. Real, merged, in this fork. +gh api "repos/$REPO/pulls/14" > pr14-pr.json +PR14_HEAD=$(jq -r .head.sha pr14-pr.json) +PR14_BASE=$(jq -r .base.sha pr14-pr.json) +gh api "repos/$REPO/compare/$PR14_BASE...$PR14_HEAD" \ + | jq '{base_commit: {sha: .base_commit.sha}, merge_base_commit: {sha: .merge_base_commit.sha}, + status, ahead_by, behind_by, files: [.files[] | {filename, status, additions, deletions}]}' > pr14-compare.json +gh api "repos/$REPO/git/trees/$PR14_HEAD?recursive=1" \ + | jq '{sha, url, truncated, tree: [.tree[] | select(.path | endswith(".md"))]}' > pr14-tree.json +say pr14-pr.json "PR 14 identity" +say pr14-compare.json "rules-file ADD — real, PR 14" +say pr14-tree.json "its head tree, *.md entries" + +# ------------------------------------------------------ fixture (vii), DELETE +# No PR in this fork's history and none found upstream deletes a nearest rules +# file, so this half is recorded from a throwaway PR opened for the purpose and +# closed unmerged. PR number is read from delete-pr.number, written by +# record-delete-fixture.sh, so this script never opens a PR as a side effect. +if [ -f delete-pr.number ]; then + DEL=$(cat delete-pr.number) + gh api "repos/$REPO/pulls/$DEL" > prdelete-pr.json + DEL_HEAD=$(jq -r .head.sha prdelete-pr.json) + DEL_BASE=$(jq -r .base.sha prdelete-pr.json) + gh api "repos/$REPO/compare/$DEL_BASE...$DEL_HEAD" \ + | jq '{base_commit: {sha: .base_commit.sha}, merge_base_commit: {sha: .merge_base_commit.sha}, + status, ahead_by, behind_by, files: [.files[] | {filename, status, additions, deletions}]}' > prdelete-compare.json + gh api "repos/$REPO/git/trees/$DEL_HEAD?recursive=1" \ + | jq '{sha, url, truncated, tree: [.tree[] | select(.path | endswith(".md"))]}' > prdelete-tree.json + say prdelete-compare.json "rules-file DELETE — throwaway PR $DEL" + say prdelete-tree.json "its head tree, launchpad/AGENTS.md absent" +else + echo " delete-pr.number absent — run record-delete-fixture.sh first" >&2 +fi + +# ------------------------------------------------------------ truncated tree +# The trees API answers HTTP 200 with a PARTIAL list once a tree passes roughly +# 100,000 entries or 7 MB. It does not fail. A path whose rules file sits beyond +# the boundary then looks exactly like "this path has no rules file" while the +# real cause is an incomplete read — which is why the pre-flight treats +# truncated: true as a failure. This fork is nowhere near the limit, so the +# fixture is recorded from a repository that is. +gh api 'repos/torvalds/linux/git/trees/master?recursive=1' \ + | jq '{sha, url, truncated, tree: [.tree[:20][]]}' > tree-truncated.json +say tree-truncated.json "truncated: true, entries projected to 20" + +echo +echo "recorded $(ls -1 ./*.json | wc -l) json fixtures" diff --git a/launchpad/scripts/testdata/rules-branches-launchpad.json b/launchpad/scripts/testdata/rules-branches-launchpad.json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/launchpad/scripts/testdata/rules-branches-launchpad.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/launchpad/scripts/testdata/tree-truncated.json b/launchpad/scripts/testdata/tree-truncated.json new file mode 100644 index 00000000000..b975877e6bb --- /dev/null +++ b/launchpad/scripts/testdata/tree-truncated.json @@ -0,0 +1,164 @@ +{ + "sha": "3d6d817622b0a9721e3cc404df3469171582be13", + "url": "https://api.github.com/repos/torvalds/linux/git/trees/3d6d817622b0a9721e3cc404df3469171582be13", + "truncated": true, + "tree": [ + { + "path": ".clang-format", + "mode": "100644", + "type": "blob", + "sha": "6a3de86ab27a4fdc824a04277319faf890e99ddb", + "size": 24322, + "url": "https://api.github.com/repos/torvalds/linux/git/blobs/6a3de86ab27a4fdc824a04277319faf890e99ddb" + }, + { + "path": ".clippy.toml", + "mode": "100644", + "type": "blob", + "sha": "b0a78cc8be20c73e66c2fd1e874a0fdbe4cda339", + "size": 748, + "url": "https://api.github.com/repos/torvalds/linux/git/blobs/b0a78cc8be20c73e66c2fd1e874a0fdbe4cda339" + }, + { + "path": ".cocciconfig", + "mode": "100644", + "type": "blob", + "sha": "43967c6b20151ee126db08e24758e3c789bcb844", + "size": 59, + "url": "https://api.github.com/repos/torvalds/linux/git/blobs/43967c6b20151ee126db08e24758e3c789bcb844" + }, + { + "path": ".editorconfig", + "mode": "100644", + "type": "blob", + "sha": "69718ac91747a4d3eb3315a00b93f3e0d0215ca1", + "size": 566, + "url": "https://api.github.com/repos/torvalds/linux/git/blobs/69718ac91747a4d3eb3315a00b93f3e0d0215ca1" + }, + { + "path": ".get_maintainer.ignore", + "mode": "100644", + "type": "blob", + "sha": "60b6b2a374cd6e6543b9d706848a822aea21d525", + "size": 305, + "url": "https://api.github.com/repos/torvalds/linux/git/blobs/60b6b2a374cd6e6543b9d706848a822aea21d525" + }, + { + "path": ".gitattributes", + "mode": "100644", + "type": "blob", + "sha": "2325c529e1854545a1b611b72ad156bdbc4319cf", + "size": 105, + "url": "https://api.github.com/repos/torvalds/linux/git/blobs/2325c529e1854545a1b611b72ad156bdbc4319cf" + }, + { + "path": ".gitignore", + "mode": "100644", + "type": "blob", + "sha": "9875120ea7bde02d4ac6b7ebd19d2177eb07d1e3", + "size": 2412, + "url": "https://api.github.com/repos/torvalds/linux/git/blobs/9875120ea7bde02d4ac6b7ebd19d2177eb07d1e3" + }, + { + "path": ".mailmap", + "mode": "100644", + "type": "blob", + "sha": "8079e9560748b09f102a96c9a5463405fe4fe78b", + "size": 57428, + "url": "https://api.github.com/repos/torvalds/linux/git/blobs/8079e9560748b09f102a96c9a5463405fe4fe78b" + }, + { + "path": ".pylintrc", + "mode": "100644", + "type": "blob", + "sha": "8c6fc2b628b3e933b51cbbca54b1cf9ce266e12f", + "size": 66, + "url": "https://api.github.com/repos/torvalds/linux/git/blobs/8c6fc2b628b3e933b51cbbca54b1cf9ce266e12f" + }, + { + "path": ".rustfmt.toml", + "mode": "100644", + "type": "blob", + "sha": "3de5cc497465c2722e160cb76188a8b287e48b7f", + "size": 369, + "url": "https://api.github.com/repos/torvalds/linux/git/blobs/3de5cc497465c2722e160cb76188a8b287e48b7f" + }, + { + "path": "COPYING", + "mode": "100644", + "type": "blob", + "sha": "a635a38ef9405fdfcfe97f3a435393c1e9cae971", + "size": 496, + "url": "https://api.github.com/repos/torvalds/linux/git/blobs/a635a38ef9405fdfcfe97f3a435393c1e9cae971" + }, + { + "path": "CREDITS", + "mode": "100644", + "type": "blob", + "sha": "91c51c14e993adff64c850f4221a4380560c169f", + "size": 108983, + "url": "https://api.github.com/repos/torvalds/linux/git/blobs/91c51c14e993adff64c850f4221a4380560c169f" + }, + { + "path": "Documentation", + "mode": "040000", + "type": "tree", + "sha": "a7d654ae7390aaf238e024bb1968edacab84d5b9", + "url": "https://api.github.com/repos/torvalds/linux/git/trees/a7d654ae7390aaf238e024bb1968edacab84d5b9" + }, + { + "path": "Documentation/.gitignore", + "mode": "100644", + "type": "blob", + "sha": "d6dc7c9b8e25020f1f3b28811df2291c38695d5f", + "size": 53, + "url": "https://api.github.com/repos/torvalds/linux/git/blobs/d6dc7c9b8e25020f1f3b28811df2291c38695d5f" + }, + { + "path": "Documentation/.renames.txt", + "mode": "100644", + "type": "blob", + "sha": "aa7e5aa4a81b31b60f65fab9c9d1a5482ba51cd4", + "size": 66666, + "url": "https://api.github.com/repos/torvalds/linux/git/blobs/aa7e5aa4a81b31b60f65fab9c9d1a5482ba51cd4" + }, + { + "path": "Documentation/ABI", + "mode": "040000", + "type": "tree", + "sha": "1a1f1256e9642a8b2e66a73d19caaa9ce56d7dfa", + "url": "https://api.github.com/repos/torvalds/linux/git/trees/1a1f1256e9642a8b2e66a73d19caaa9ce56d7dfa" + }, + { + "path": "Documentation/ABI/README", + "mode": "100644", + "type": "blob", + "sha": "315fffe1f831fd6c4e7c07325473820f3cca4894", + "size": 4139, + "url": "https://api.github.com/repos/torvalds/linux/git/blobs/315fffe1f831fd6c4e7c07325473820f3cca4894" + }, + { + "path": "Documentation/ABI/obsolete", + "mode": "040000", + "type": "tree", + "sha": "9a4c0c9a0bf0ae4a79fad13977b4e59942c8261f", + "url": "https://api.github.com/repos/torvalds/linux/git/trees/9a4c0c9a0bf0ae4a79fad13977b4e59942c8261f" + }, + { + "path": "Documentation/ABI/obsolete/automount-tracefs-debugfs", + "mode": "100644", + "type": "blob", + "sha": "a5196ec78cb598f5e2e59f00eb31727b851fa48b", + "size": 894, + "url": "https://api.github.com/repos/torvalds/linux/git/blobs/a5196ec78cb598f5e2e59f00eb31727b851fa48b" + }, + { + "path": "Documentation/ABI/obsolete/o2cb", + "mode": "100644", + "type": "blob", + "sha": "8f39b596731d235eddc9ed562b5a37099a3eb709", + "size": 470, + "url": "https://api.github.com/repos/torvalds/linux/git/blobs/8f39b596731d235eddc9ed562b5a37099a3eb709" + } + ] +} diff --git a/launchpad/scripts/testdata/upstream-divergent-compare.json b/launchpad/scripts/testdata/upstream-divergent-compare.json new file mode 100644 index 00000000000..54e2a110f63 --- /dev/null +++ b/launchpad/scripts/testdata/upstream-divergent-compare.json @@ -0,0 +1,236 @@ +{ + "base_commit": { + "sha": "72d56e7bd3a94fa3ee20b5a50bd1b868a9329d9c", + "node_id": "C_kwDORgXb2toAKDcyZDU2ZTdiZDNhOTRmYTNlZTIwYjVhNTBiZDFiODY4YTkzMjlkOWM", + "commit": { + "author": { + "name": "Atish Patel", + "email": "atishpatel2012@gmail.com", + "date": "2026-08-12T20:57:11Z" + }, + "committer": { + "name": "GitHub", + "email": "noreply@github.com", + "date": "2026-08-12T20:57:11Z" + }, + "message": "fix(agent): raise output limit and allow 3 recoveries (#5475)\n\n## Summary\n\nRaise the built-in output and recovery defaults so long-running agents\nhave more room to finish useful work instead of terminating after\nrepeated 32,768-token reasoning-only responses.\n\n- Raise `BUZZ_AGENT_MAX_OUTPUT_TOKENS` from 32,768 to 65,536\n- Raise the finite output-truncation recovery allowance from 2 to 3 via\n`BUZZ_AGENT_MAX_TOKEN_RECOVERIES`; `0` still disables recovery\n- Strengthen the recovery prompt so the model stops prolonged reasoning,\nuses tools immediately, and builds scripts or artifacts in small\nverifiable steps\n- Preserve the safety invariant that incomplete truncated tool calls are\ndiscarded and never executed\n- Keep proactive handoff independently at 90% of\n`BUZZ_AGENT_MAX_CONTEXT_TOKENS` (180,000 tokens with the 200,000\ndefault), regardless of the output allowance\n- Add request-loop and configuration regressions for exact-N recovery,\ndisabled recovery, successful tool-first recovery, discarded truncated\ncalls, and finite round bounds\n\n`BUZZ_AGENT_MAX_OUTPUT_TOKENS` remains an explicit per-agent deployment\nsetting. Operators should configure it at or below the served model's\noutput limit; this PR does not perform live provider capability\ndiscovery or automatic clamping.\n\n**Risk:** Medium — this increases the default request size and permits\none additional recovery attempt by default. Recovery remains finite and\nbounded by `BUZZ_AGENT_MAX_ROUNDS`. Deployments whose served model\nrejects 65,536 output tokens must set a lower per-agent value.\n\nCurrent output limits\n- model - output token max\n- DeepSeek V4 Flash - 384,000 tokens\n- Qwen 3.8 (Max) - 131,072 tokens\n- GLM 5.2 - 131,072 tokens\n- GPT 5.6 - 128,000 tokens\n- Claude Opus 5 - 128,000 tokens\n- Gemini 3.6 Flash - 65,536 tokens\n- Kimi K3 (Moonshot)- 131,072 tokens\n\n### Related issue\n\nNone found. Originating benchmark analysis:\n`buzz://message?channel=c3252dd2-0142-4e01-88c7-a2183c3960a5&id=91e991aab5fd49094583c3937477f6c12db57a41d86edf7fd4745d0d57d10017`\n\n### Testing\n\n- `cargo fmt --all -- --check`\n- `cargo test -p buzz-agent` — 595 passed, 0 failed, 0 ignored at\n`bd6de557b367850f50325bafdd3c046131942bef`\n- `cargo clippy -p buzz-agent --all-targets -- -D warnings`\n- Previously failing\n`cancelled_turn_with_usage_emits_notification_before_response` passed\nalone and in the full rerun\n- Push hooks passed: organization guard, branch skew, Rust tests, and\nDesktop Tauri checks\n\n### Update — 2026-08-11\n\nPer review feedback, the recovery default is 3. The OpenRouter live\n`/models` output-cap discovery, cache, request clamp, and related\ntests/documentation were removed. Per-agent output configuration is now\nthe sole output-cap mechanism. Proactive handoff and its pre-usage byte\nfallback now depend only on 90% of `BUZZ_AGENT_MAX_CONTEXT_TOKENS`; with\nthe 200,000 default, the handoff threshold is 180,000 regardless of\n`BUZZ_AGENT_MAX_OUTPUT_TOKENS`.\n\nGenerated with Brainy Bumble\n\n\n### Targeted validation — 2026-08-11\n\nRan the exact PR binary once on each of the 11 benchmark tasks causally\naffected by the previous 32,768-token ceiling, using OpenRouter with\n`deepseek/deepseek-v4-flash-0731` pinned to Fireworks and maximum\nreasoning effort. Relay-429 collection failures were excluded and rerun\nat concurrency 2.\n\n- **6/11 passed:** `circuit-fibsqrt`, `feal-linear-cryptanalysis`,\n`model-extraction-relu-logits`, `path-tracing`,\n`schemelike-metacircular-eval`, and `sqlite-db-truncate`\n- **5/11 reached the benchmark deadline:** `adaptive-rejection-sampler`,\n`dna-assembly`, `path-tracing-reverse`, `regex-chess`, and\n`write-compressor`\n- `regex-chess` reached exactly 65,536 output tokens, triggered one\noutput-limit recovery, and then reached the deadline. This directly\nconfirms that the larger ceiling and recovery path were active, but not\nthat recovery guarantees completion.\n\nFor context, ten of these tasks were 0/5 in the historical baseline;\n`sqlite-db-truncate`, the clean control, was 4/5. This is targeted\none-attempt-per-task validation rather than a statistically powered\ncomparison. The result should not be attributed solely to the recovery\ndefault of 3: this PR also raises the output ceiling and strengthens\nrecovery behavior, and OpenRouter routing conditions may differ from the\nhistorical direct-Fireworks runs.\n\nGenerated with Brainy Bumble\n\n---------\n\nSigned-off-by: Atish Patel \nSigned-off-by: Brainy Bumble <0ed7657b57c0e8a9f5288390dd6c8d5d0a3a06abe9b01b9006814f52077d6cdf@buzz.block.builderlab.xyz>\nSigned-off-by: Hardworking Honey \nCo-authored-by: Brainy Bumble <0ed7657b57c0e8a9f5288390dd6c8d5d0a3a06abe9b01b9006814f52077d6cdf@buzz.block.builderlab.xyz>\nCo-authored-by: Hardworking Honey ", + "tree": { + "sha": "58e609b2d08f16771c4731ac81b0d6c2d7308d5d", + "url": "https://api.github.com/repos/block/buzz/git/trees/58e609b2d08f16771c4731ac81b0d6c2d7308d5d" + }, + "url": "https://api.github.com/repos/block/buzz/git/commits/72d56e7bd3a94fa3ee20b5a50bd1b868a9329d9c", + "comment_count": 0, + "verification": { + "verified": true, + "reason": "valid", + "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJqfN4nCRC1aQ7uu5UhlAAAqtoQAIXIHZeX+YnwmKIlYSbZLR0N\naWv7qQO8VKvtqKe61GzLyKd+hQj8EVIc1j14Wzt/sbp3AXRfjaF569w/qhOIeffA\nyKNj4Ep+swirfseuKMCmE6l/U8xEPeyZJQ7UkV5qtTqAJGYQraF3/yAyYgtPBP1g\noF4j5U5jNlG0BmpU5X0R2Q1x10aAbpAKey/bqnYQWAhWIrF/5dWeevWXN1biacIF\nbtuTtxAqlUyMJCgFobLPQzUyq3AVoPuqXvLbVnK/r6wW/0d+gsSNJ+OZnL23eNFo\noNn3SFtne/W/pPRAFuA2vGZOrNmRsYD7ZrZyUKNLr8Dob98DOuHgHKyAaw1jRlnZ\nAjVNV8lSRIpD+OrQmNGk3lxqldm2yu/JrGyecdLqe6m5TM8irK7icVsjBcBeubsK\ncwkj1CSFFT9npyXbren9RjiiyywxyLT34zjcsj25Yy4y0eOcts5RRcgwzQfRDVL/\npr8KtuMeTMDsovUwqn2ASi5JuX5qe7jBfayrahgJ3uHz3OIfOwYNSUtLE1Suf8L2\nwz0z1azk2n4fJg74zaPHr3srA3uRBR27vl3tsykwCYXeGyLZmFbeKiinPtc1zpGn\nb7R9w7ojJqPuAUcGvH4u3PiW4mVFKjDCRRC0zThtELcjK0REK4DCK+2qVkFgL+Iw\nM/Nj8Gf8tlO0ZYQVeGso\n=ecqx\n-----END PGP SIGNATURE-----\n", + "payload": "tree 58e609b2d08f16771c4731ac81b0d6c2d7308d5d\nparent 59f613c404958d8ac99525b4aaaf26843257de31\nauthor Atish Patel 1786568231 -0400\ncommitter GitHub 1786568231 -0400\n\nfix(agent): raise output limit and allow 3 recoveries (#5475)\n\n## Summary\n\nRaise the built-in output and recovery defaults so long-running agents\nhave more room to finish useful work instead of terminating after\nrepeated 32,768-token reasoning-only responses.\n\n- Raise `BUZZ_AGENT_MAX_OUTPUT_TOKENS` from 32,768 to 65,536\n- Raise the finite output-truncation recovery allowance from 2 to 3 via\n`BUZZ_AGENT_MAX_TOKEN_RECOVERIES`; `0` still disables recovery\n- Strengthen the recovery prompt so the model stops prolonged reasoning,\nuses tools immediately, and builds scripts or artifacts in small\nverifiable steps\n- Preserve the safety invariant that incomplete truncated tool calls are\ndiscarded and never executed\n- Keep proactive handoff independently at 90% of\n`BUZZ_AGENT_MAX_CONTEXT_TOKENS` (180,000 tokens with the 200,000\ndefault), regardless of the output allowance\n- Add request-loop and configuration regressions for exact-N recovery,\ndisabled recovery, successful tool-first recovery, discarded truncated\ncalls, and finite round bounds\n\n`BUZZ_AGENT_MAX_OUTPUT_TOKENS` remains an explicit per-agent deployment\nsetting. Operators should configure it at or below the served model's\noutput limit; this PR does not perform live provider capability\ndiscovery or automatic clamping.\n\n**Risk:** Medium — this increases the default request size and permits\none additional recovery attempt by default. Recovery remains finite and\nbounded by `BUZZ_AGENT_MAX_ROUNDS`. Deployments whose served model\nrejects 65,536 output tokens must set a lower per-agent value.\n\nCurrent output limits\n- model - output token max\n- DeepSeek V4 Flash - 384,000 tokens\n- Qwen 3.8 (Max) - 131,072 tokens\n- GLM 5.2 - 131,072 tokens\n- GPT 5.6 - 128,000 tokens\n- Claude Opus 5 - 128,000 tokens\n- Gemini 3.6 Flash - 65,536 tokens\n- Kimi K3 (Moonshot)- 131,072 tokens\n\n### Related issue\n\nNone found. Originating benchmark analysis:\n`buzz://message?channel=c3252dd2-0142-4e01-88c7-a2183c3960a5&id=91e991aab5fd49094583c3937477f6c12db57a41d86edf7fd4745d0d57d10017`\n\n### Testing\n\n- `cargo fmt --all -- --check`\n- `cargo test -p buzz-agent` — 595 passed, 0 failed, 0 ignored at\n`bd6de557b367850f50325bafdd3c046131942bef`\n- `cargo clippy -p buzz-agent --all-targets -- -D warnings`\n- Previously failing\n`cancelled_turn_with_usage_emits_notification_before_response` passed\nalone and in the full rerun\n- Push hooks passed: organization guard, branch skew, Rust tests, and\nDesktop Tauri checks\n\n### Update — 2026-08-11\n\nPer review feedback, the recovery default is 3. The OpenRouter live\n`/models` output-cap discovery, cache, request clamp, and related\ntests/documentation were removed. Per-agent output configuration is now\nthe sole output-cap mechanism. Proactive handoff and its pre-usage byte\nfallback now depend only on 90% of `BUZZ_AGENT_MAX_CONTEXT_TOKENS`; with\nthe 200,000 default, the handoff threshold is 180,000 regardless of\n`BUZZ_AGENT_MAX_OUTPUT_TOKENS`.\n\nGenerated with Brainy Bumble\n\n\n### Targeted validation — 2026-08-11\n\nRan the exact PR binary once on each of the 11 benchmark tasks causally\naffected by the previous 32,768-token ceiling, using OpenRouter with\n`deepseek/deepseek-v4-flash-0731` pinned to Fireworks and maximum\nreasoning effort. Relay-429 collection failures were excluded and rerun\nat concurrency 2.\n\n- **6/11 passed:** `circuit-fibsqrt`, `feal-linear-cryptanalysis`,\n`model-extraction-relu-logits`, `path-tracing`,\n`schemelike-metacircular-eval`, and `sqlite-db-truncate`\n- **5/11 reached the benchmark deadline:** `adaptive-rejection-sampler`,\n`dna-assembly`, `path-tracing-reverse`, `regex-chess`, and\n`write-compressor`\n- `regex-chess` reached exactly 65,536 output tokens, triggered one\noutput-limit recovery, and then reached the deadline. This directly\nconfirms that the larger ceiling and recovery path were active, but not\nthat recovery guarantees completion.\n\nFor context, ten of these tasks were 0/5 in the historical baseline;\n`sqlite-db-truncate`, the clean control, was 4/5. This is targeted\none-attempt-per-task validation rather than a statistically powered\ncomparison. The result should not be attributed solely to the recovery\ndefault of 3: this PR also raises the output ceiling and strengthens\nrecovery behavior, and OpenRouter routing conditions may differ from the\nhistorical direct-Fireworks runs.\n\nGenerated with Brainy Bumble\n\n---------\n\nSigned-off-by: Atish Patel \nSigned-off-by: Brainy Bumble <0ed7657b57c0e8a9f5288390dd6c8d5d0a3a06abe9b01b9006814f52077d6cdf@buzz.block.builderlab.xyz>\nSigned-off-by: Hardworking Honey \nCo-authored-by: Brainy Bumble <0ed7657b57c0e8a9f5288390dd6c8d5d0a3a06abe9b01b9006814f52077d6cdf@buzz.block.builderlab.xyz>\nCo-authored-by: Hardworking Honey ", + "verified_at": "2026-08-12T20:57:14Z" + } + }, + "url": "https://api.github.com/repos/block/buzz/commits/72d56e7bd3a94fa3ee20b5a50bd1b868a9329d9c", + "html_url": "https://github.com/block/buzz/commit/72d56e7bd3a94fa3ee20b5a50bd1b868a9329d9c", + "comments_url": "https://api.github.com/repos/block/buzz/commits/72d56e7bd3a94fa3ee20b5a50bd1b868a9329d9c/comments", + "author": { + "login": "atishpatel", + "id": 6618430, + "node_id": "MDQ6VXNlcjY2MTg0MzA=", + "avatar_url": "https://avatars.githubusercontent.com/u/6618430?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/atishpatel", + "html_url": "https://github.com/atishpatel", + "followers_url": "https://api.github.com/users/atishpatel/followers", + "following_url": "https://api.github.com/users/atishpatel/following{/other_user}", + "gists_url": "https://api.github.com/users/atishpatel/gists{/gist_id}", + "starred_url": "https://api.github.com/users/atishpatel/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/atishpatel/subscriptions", + "organizations_url": "https://api.github.com/users/atishpatel/orgs", + "repos_url": "https://api.github.com/users/atishpatel/repos", + "events_url": "https://api.github.com/users/atishpatel/events{/privacy}", + "received_events_url": "https://api.github.com/users/atishpatel/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "committer": { + "login": "web-flow", + "id": 19864447, + "node_id": "MDQ6VXNlcjE5ODY0NDQ3", + "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/web-flow", + "html_url": "https://github.com/web-flow", + "followers_url": "https://api.github.com/users/web-flow/followers", + "following_url": "https://api.github.com/users/web-flow/following{/other_user}", + "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", + "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", + "organizations_url": "https://api.github.com/users/web-flow/orgs", + "repos_url": "https://api.github.com/users/web-flow/repos", + "events_url": "https://api.github.com/users/web-flow/events{/privacy}", + "received_events_url": "https://api.github.com/users/web-flow/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "parents": [ + { + "sha": "59f613c404958d8ac99525b4aaaf26843257de31", + "url": "https://api.github.com/repos/block/buzz/commits/59f613c404958d8ac99525b4aaaf26843257de31", + "html_url": "https://github.com/block/buzz/commit/59f613c404958d8ac99525b4aaaf26843257de31" + } + ] + }, + "merge_base_commit": { + "sha": "c6c6e7eca70d6b526c43af925e596e8616b19fb8", + "node_id": "C_kwDORgXb2toAKGM2YzZlN2VjYTcwZDZiNTI2YzQzYWY5MjVlNTk2ZTg2MTZiMTlmYjg", + "commit": { + "author": { + "name": "Will Pfleger", + "email": "pfleger.will@gmail.com", + "date": "2026-08-12T19:06:43Z" + }, + "committer": { + "name": "GitHub", + "email": "noreply@github.com", + "date": "2026-08-12T19:06:43Z" + }, + "message": "perf(desktop): coalesce thread-activity localStorage writes (#5693)\n\nEach incoming thread reply drove a full `JSON.stringify` + `setItem` of\nthe ~600 KB thread-activity buffer. A burst of replies serialized the\nwhole blob once per event on the main thread, which is one of the\nrenderer stalls under load in the desktop-longevity arc.\n\nThis collapses the burst into a single debounced write, applying the\ncoalescing pattern Wes introduced for read-state persistence in #5591\n(`readStateManager`) to the thread-activity path.\n\n## What changed\n\n- **`threadActivityStorage.ts`** — coalescing primitives:\n- `scheduleThreadActivityWrite` — first-writer-wins (a pending timer is\n*not* reset), 1s trailing edge. The timer reads the live buffer *at fire\ntime* and re-checks the loaded scope, so N replies within the window\npersist exactly once with the burst's final state, and a write that\noutlives a scope switch can neither land under the new key nor persist\nthe wrong buffer.\n- `flushThreadActivityWrite` — synchronous persist + timer cancel; a\nno-op when nothing is pending.\n- `removeLegacyThreadActivityKey` — idempotent one-time cleanup of the\norphaned pre-relay-scoping `buzz-thread-activity.v1:` key.\n- **`useThreadActivityPersistence.ts`** (new companion hook) — owns the\nloaded scope, the write timer, the `pagehide` /\n`visibilitychange`→hidden / unmount flush, and hydration + legacy\ncleanup on identity/relay change. Mirrors the existing\n`useObservedUnreadPersistence` sibling.\n- **`useUnreadChannels.ts`** — rewired to instantiate the hook and call\n`activityPersistence.schedule(...)` at both writer sites instead of\nwriting per event. The buffer (`threadActivityRef`) stays parent-owned;\nthe hook decides when it is durably persisted. Net **990** lines (was\n1021), back under the 1000-line ceiling.\n\n## Durability\n\n`pagehide`, `visibilitychange`→hidden, unmount, and scope-reseed all\nflush synchronously, so the last burst of replies survives a `Cmd+R` or\nan idle reload that tears the webview down inside the coalescing window.\n\n## Tests\n\n- `threadActivityWriteScheduler.test.mjs` — fake-timer unit coverage:\nburst→one `setItem`, live-buffer-at-fire-time, scope-mismatch rejection,\nstale-scope timer abort, flush persists+cancels, flush no-op, legacy-key\nremoval.\n- `useThreadActivityPersistence.test.mjs` — mounts the real hook via\n`createRoot`+`act`: `pagehide` / visibility / unmount flush of the live\nbuffer, scope switch flushing A under A's key without leaking into B,\nB-bucket rehydration, legacy-key cleanup, and the empty-scope write\nfence.\n\n## Related\n\nBased on [#5591](https://github.com/block/buzz/pull/5591) (Wes) —\n`perf(desktop): coalesce read state localStorage persistence`, the\nproven first-writer-wins coalescing pattern this extends to thread\nactivity.\n\nSigned-off-by: Will Pfleger \nCo-authored-by: Duncan ", + "tree": { + "sha": "3add29f19dabafb09be40ab06816b271a910eb5d", + "url": "https://api.github.com/repos/block/buzz/git/trees/3add29f19dabafb09be40ab06816b271a910eb5d" + }, + "url": "https://api.github.com/repos/block/buzz/git/commits/c6c6e7eca70d6b526c43af925e596e8616b19fb8", + "comment_count": 0, + "verification": { + "verified": true, + "reason": "valid", + "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJqfMRDCRC1aQ7uu5UhlAAAsTcQAARRS/YKqX2bGQ9gy6W2bKrl\nhPZnPPQYM86zfjyi//qdaLonhvYEr/fLzVFSmAga+BrKldQhyXTmhJlE+ChSYM8y\ndUjIB3bYYwGnSn81CfbzhQ/fXgusweoujm5QgtkB7CX6j95JPSz7UKiLvXaI8u7x\nkoWHbFThloUbKy2rCSP3pY3vVfdKEV6h1NCVa4D8hI8BUhQ7/K3DpZNkcDZqntuk\n2HAJvZztXsvoZq/LRaCTgMBapt8CQFf4Q69evc7L7XFFP8mkp330WWHuaA9Fsoeq\nHr4bWeVTz6q7iGvnITBsU2LnFaOk/aJe24cXVLkJd787NznyC05w7Ipk3f4FghNt\nnzvdZ6EOuO2jY318vXZab7d+fv076Bng4y+DjhSiX+XXmUPbfyr6KR/bOY7U7jl2\nOt0rJ9W6LBcruhpPBHkpVgp2J7p+HqFHMe6jhEt23u4SBzZdABwFXwA5N6f0tL6A\nVlWUCjefWX63UnTLISH1swGqBcLC5uI+qolziZkgwWLLhYgQI2VRCnGV3/XAupwk\nDQqfIRIzPcPO1JSN3UVvqX9LgLBGZrLRYhGSHeJpBJAYYVdKZ5alJy4CA1jC60hz\nrwyg0RIhRstOUvDv1F2E4sjf0fJR9KBTYjJCVQCP78qCGysjrxfEPGrM/HrnmBJM\naKYXtTqWybBWUSofmCMV\n=eDKk\n-----END PGP SIGNATURE-----\n", + "payload": "tree 3add29f19dabafb09be40ab06816b271a910eb5d\nparent c3b0ccf383fe4ee936abbe6b9c9134b5728cc2b5\nauthor Will Pfleger 1786561603 -0400\ncommitter GitHub 1786561603 -0700\n\nperf(desktop): coalesce thread-activity localStorage writes (#5693)\n\nEach incoming thread reply drove a full `JSON.stringify` + `setItem` of\nthe ~600 KB thread-activity buffer. A burst of replies serialized the\nwhole blob once per event on the main thread, which is one of the\nrenderer stalls under load in the desktop-longevity arc.\n\nThis collapses the burst into a single debounced write, applying the\ncoalescing pattern Wes introduced for read-state persistence in #5591\n(`readStateManager`) to the thread-activity path.\n\n## What changed\n\n- **`threadActivityStorage.ts`** — coalescing primitives:\n- `scheduleThreadActivityWrite` — first-writer-wins (a pending timer is\n*not* reset), 1s trailing edge. The timer reads the live buffer *at fire\ntime* and re-checks the loaded scope, so N replies within the window\npersist exactly once with the burst's final state, and a write that\noutlives a scope switch can neither land under the new key nor persist\nthe wrong buffer.\n- `flushThreadActivityWrite` — synchronous persist + timer cancel; a\nno-op when nothing is pending.\n- `removeLegacyThreadActivityKey` — idempotent one-time cleanup of the\norphaned pre-relay-scoping `buzz-thread-activity.v1:` key.\n- **`useThreadActivityPersistence.ts`** (new companion hook) — owns the\nloaded scope, the write timer, the `pagehide` /\n`visibilitychange`→hidden / unmount flush, and hydration + legacy\ncleanup on identity/relay change. Mirrors the existing\n`useObservedUnreadPersistence` sibling.\n- **`useUnreadChannels.ts`** — rewired to instantiate the hook and call\n`activityPersistence.schedule(...)` at both writer sites instead of\nwriting per event. The buffer (`threadActivityRef`) stays parent-owned;\nthe hook decides when it is durably persisted. Net **990** lines (was\n1021), back under the 1000-line ceiling.\n\n## Durability\n\n`pagehide`, `visibilitychange`→hidden, unmount, and scope-reseed all\nflush synchronously, so the last burst of replies survives a `Cmd+R` or\nan idle reload that tears the webview down inside the coalescing window.\n\n## Tests\n\n- `threadActivityWriteScheduler.test.mjs` — fake-timer unit coverage:\nburst→one `setItem`, live-buffer-at-fire-time, scope-mismatch rejection,\nstale-scope timer abort, flush persists+cancels, flush no-op, legacy-key\nremoval.\n- `useThreadActivityPersistence.test.mjs` — mounts the real hook via\n`createRoot`+`act`: `pagehide` / visibility / unmount flush of the live\nbuffer, scope switch flushing A under A's key without leaking into B,\nB-bucket rehydration, legacy-key cleanup, and the empty-scope write\nfence.\n\n## Related\n\nBased on [#5591](https://github.com/block/buzz/pull/5591) (Wes) —\n`perf(desktop): coalesce read state localStorage persistence`, the\nproven first-writer-wins coalescing pattern this extends to thread\nactivity.\n\nSigned-off-by: Will Pfleger \nCo-authored-by: Duncan ", + "verified_at": "2026-08-12T19:06:46Z" + } + }, + "url": "https://api.github.com/repos/block/buzz/commits/c6c6e7eca70d6b526c43af925e596e8616b19fb8", + "html_url": "https://github.com/block/buzz/commit/c6c6e7eca70d6b526c43af925e596e8616b19fb8", + "comments_url": "https://api.github.com/repos/block/buzz/commits/c6c6e7eca70d6b526c43af925e596e8616b19fb8/comments", + "author": { + "login": "wpfleger96", + "id": 15384764, + "node_id": "MDQ6VXNlcjE1Mzg0NzY0", + "avatar_url": "https://avatars.githubusercontent.com/u/15384764?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wpfleger96", + "html_url": "https://github.com/wpfleger96", + "followers_url": "https://api.github.com/users/wpfleger96/followers", + "following_url": "https://api.github.com/users/wpfleger96/following{/other_user}", + "gists_url": "https://api.github.com/users/wpfleger96/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wpfleger96/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wpfleger96/subscriptions", + "organizations_url": "https://api.github.com/users/wpfleger96/orgs", + "repos_url": "https://api.github.com/users/wpfleger96/repos", + "events_url": "https://api.github.com/users/wpfleger96/events{/privacy}", + "received_events_url": "https://api.github.com/users/wpfleger96/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "committer": { + "login": "web-flow", + "id": 19864447, + "node_id": "MDQ6VXNlcjE5ODY0NDQ3", + "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/web-flow", + "html_url": "https://github.com/web-flow", + "followers_url": "https://api.github.com/users/web-flow/followers", + "following_url": "https://api.github.com/users/web-flow/following{/other_user}", + "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", + "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", + "organizations_url": "https://api.github.com/users/web-flow/orgs", + "repos_url": "https://api.github.com/users/web-flow/repos", + "events_url": "https://api.github.com/users/web-flow/events{/privacy}", + "received_events_url": "https://api.github.com/users/web-flow/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "parents": [ + { + "sha": "c3b0ccf383fe4ee936abbe6b9c9134b5728cc2b5", + "url": "https://api.github.com/repos/block/buzz/commits/c3b0ccf383fe4ee936abbe6b9c9134b5728cc2b5", + "html_url": "https://github.com/block/buzz/commit/c3b0ccf383fe4ee936abbe6b9c9134b5728cc2b5" + } + ] + }, + "status": "diverged", + "ahead_by": 1, + "behind_by": 2, + "total_commits": 1, + "files": [ + { + "sha": "0305a3c0a2471b85b695d49a59d6f3bf340ae8dc", + "filename": ".github/workflows/release.yml", + "status": "modified", + "additions": 37, + "deletions": 1, + "changes": 38 + }, + { + "sha": "9e47178427565d226db3607ae027e676207b58a6", + "filename": "Justfile", + "status": "modified", + "additions": 4, + "deletions": 0, + "changes": 4 + }, + { + "sha": "5dda9a099b51f30333c9a8ee6f169803831d934c", + "filename": "desktop/.gitignore", + "status": "modified", + "additions": 1, + "deletions": 0, + "changes": 1 + }, + { + "sha": "fe1389d53ec9d2f9c9ea7807229828fe74f3f31b", + "filename": "desktop/package.json", + "status": "modified", + "additions": 1, + "deletions": 0, + "changes": 1 + }, + { + "sha": "ff4c284562560aee8722bf8a410c1608db984ab2", + "filename": "desktop/playwright.release-smoke.config.ts", + "status": "added", + "additions": 29, + "deletions": 0, + "changes": 29 + }, + { + "sha": "3c65fcde955413fdcc8c6fbb8e4e81e29d51b1e7", + "filename": "desktop/tests/e2e/perf/metrics.ts", + "status": "added", + "additions": 68, + "deletions": 0, + "changes": 68 + }, + { + "sha": "19beb276171e7f09cddba8e39a1ffd251c306322", + "filename": "desktop/tests/e2e/release-smoke.spec.ts", + "status": "added", + "additions": 246, + "deletions": 0, + "changes": 246 + }, + { + "sha": "33f7013ab3e9543a0046822d1b86ac7b46207971", + "filename": "scripts/run-desktop-release-smoke.sh", + "status": "added", + "additions": 148, + "deletions": 0, + "changes": 148 + } + ] +} diff --git a/launchpad/scripts/testdata/upstream-divergent-pr.json b/launchpad/scripts/testdata/upstream-divergent-pr.json new file mode 100644 index 00000000000..29374b430d1 --- /dev/null +++ b/launchpad/scripts/testdata/upstream-divergent-pr.json @@ -0,0 +1 @@ +{"url":"https://api.github.com/repos/block/buzz/pulls/5699","id":4265983322,"node_id":"PR_kwDORgXb2s7-Rb1a","html_url":"https://github.com/block/buzz/pull/5699","diff_url":"https://github.com/block/buzz/pull/5699.diff","patch_url":"https://github.com/block/buzz/pull/5699.patch","issue_url":"https://api.github.com/repos/block/buzz/issues/5699","number":5699,"state":"open","locked":false,"title":"test: add deterministic desktop release smoke","user":{"login":"wesbillman","id":51647,"node_id":"MDQ6VXNlcjUxNjQ3","avatar_url":"https://avatars.githubusercontent.com/u/51647?v=4","gravatar_id":"","url":"https://api.github.com/users/wesbillman","html_url":"https://github.com/wesbillman","followers_url":"https://api.github.com/users/wesbillman/followers","following_url":"https://api.github.com/users/wesbillman/following{/other_user}","gists_url":"https://api.github.com/users/wesbillman/gists{/gist_id}","starred_url":"https://api.github.com/users/wesbillman/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/wesbillman/subscriptions","organizations_url":"https://api.github.com/users/wesbillman/orgs","repos_url":"https://api.github.com/users/wesbillman/repos","events_url":"https://api.github.com/users/wesbillman/events{/privacy}","received_events_url":"https://api.github.com/users/wesbillman/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"## Summary\n\n- add `just desktop-release-smoke`, a deterministic desktop correctness/reachability smoke against an ephemeral real local relay\n- seed a 10,000-event dense-second fixture and verify exact event-ID reachability, SHA-256 identity, ordering, duplicate absence, bounded mounted rows, and drained render work\n- isolate Postgres per run, serialize the shared Redis DB, retain phase/relay/Playwright diagnostics, and gate desktop release manifest assembly on the smoke\n\nThis is deliberately **not a performance-regression gate**. CDP and action timing fields are informational only. There is no candidate/baseline comparison or threshold. A future performance lane needs repeated equivalent fixtures, discrete interaction samples, and an explicit comparator/noise policy.\n\nThe diagnostics record the fixture version, row count, wall-clock base timestamp (`fixtureSecond`), expected event-ID hash, observed state, and measurements. Because the created-at floor requires a current timestamp, paired comparison remains disabled.\n\nThe release job runs on an isolated GitHub-hosted runner. The script also guards automatic local runs with a Redis allocation lock. Its remaining direct-PID cleanup and free-port selection race mean it should not be repurposed onto a persistent concurrent shared runner without first hardening process-group cleanup and port reservation.\n\n### Related issue\n\nN/A\n\n### Testing\n\n- `pnpm --dir desktop typecheck`\n- focused real-local-relay release smoke passed after adversarial review fixes\n- full pre-push hook suite passed on `6da18868d15aef95743d8a3cc8bab472a69e9f63`\n- full 10,000-event scenario reached 10,000/10,000 exact IDs with matching SHA-256, 199 continuation requests, and 95 mounted rows in about 4.4 minutes\n- reduced-row review run passed in 18.4 seconds\n","created_at":"2026-08-12T21:01:53Z","updated_at":"2026-08-12T21:01:54Z","closed_at":null,"merged_at":null,"merge_commit_sha":"cac034cc9eda04ea770ca0de4bb04f37cb59eeac","assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/block/buzz/pulls/5699/commits","review_comments_url":"https://api.github.com/repos/block/buzz/pulls/5699/comments","review_comment_url":"https://api.github.com/repos/block/buzz/pulls/comments{/number}","comments_url":"https://api.github.com/repos/block/buzz/issues/5699/comments","statuses_url":"https://api.github.com/repos/block/buzz/statuses/6da18868d15aef95743d8a3cc8bab472a69e9f63","head":{"label":"block:carl/desktop-release-smoke","ref":"carl/desktop-release-smoke","sha":"6da18868d15aef95743d8a3cc8bab472a69e9f63","user":{"login":"block","id":185116535,"node_id":"O_kgDOCwindw","avatar_url":"https://avatars.githubusercontent.com/u/185116535?v=4","gravatar_id":"","url":"https://api.github.com/users/block","html_url":"https://github.com/block","followers_url":"https://api.github.com/users/block/followers","following_url":"https://api.github.com/users/block/following{/other_user}","gists_url":"https://api.github.com/users/block/gists{/gist_id}","starred_url":"https://api.github.com/users/block/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/block/subscriptions","organizations_url":"https://api.github.com/users/block/orgs","repos_url":"https://api.github.com/users/block/repos","events_url":"https://api.github.com/users/block/events{/privacy}","received_events_url":"https://api.github.com/users/block/received_events","type":"Organization","user_view_type":"public","site_admin":false},"repo":{"id":1174789082,"node_id":"R_kgDORgXb2g","name":"buzz","full_name":"block/buzz","private":false,"owner":{"login":"block","id":185116535,"node_id":"O_kgDOCwindw","avatar_url":"https://avatars.githubusercontent.com/u/185116535?v=4","gravatar_id":"","url":"https://api.github.com/users/block","html_url":"https://github.com/block","followers_url":"https://api.github.com/users/block/followers","following_url":"https://api.github.com/users/block/following{/other_user}","gists_url":"https://api.github.com/users/block/gists{/gist_id}","starred_url":"https://api.github.com/users/block/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/block/subscriptions","organizations_url":"https://api.github.com/users/block/orgs","repos_url":"https://api.github.com/users/block/repos","events_url":"https://api.github.com/users/block/events{/privacy}","received_events_url":"https://api.github.com/users/block/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/block/buzz","description":"A hive mind communication platform","fork":false,"url":"https://api.github.com/repos/block/buzz","forks_url":"https://api.github.com/repos/block/buzz/forks","keys_url":"https://api.github.com/repos/block/buzz/keys{/key_id}","collaborators_url":"https://api.github.com/repos/block/buzz/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/block/buzz/teams","hooks_url":"https://api.github.com/repos/block/buzz/hooks","issue_events_url":"https://api.github.com/repos/block/buzz/issues/events{/number}","events_url":"https://api.github.com/repos/block/buzz/events","assignees_url":"https://api.github.com/repos/block/buzz/assignees{/user}","branches_url":"https://api.github.com/repos/block/buzz/branches{/branch}","tags_url":"https://api.github.com/repos/block/buzz/tags","blobs_url":"https://api.github.com/repos/block/buzz/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/block/buzz/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/block/buzz/git/refs{/sha}","trees_url":"https://api.github.com/repos/block/buzz/git/trees{/sha}","statuses_url":"https://api.github.com/repos/block/buzz/statuses/{sha}","languages_url":"https://api.github.com/repos/block/buzz/languages","stargazers_url":"https://api.github.com/repos/block/buzz/stargazers","contributors_url":"https://api.github.com/repos/block/buzz/contributors","subscribers_url":"https://api.github.com/repos/block/buzz/subscribers","subscription_url":"https://api.github.com/repos/block/buzz/subscription","commits_url":"https://api.github.com/repos/block/buzz/commits{/sha}","git_commits_url":"https://api.github.com/repos/block/buzz/git/commits{/sha}","comments_url":"https://api.github.com/repos/block/buzz/comments{/number}","issue_comment_url":"https://api.github.com/repos/block/buzz/issues/comments{/number}","contents_url":"https://api.github.com/repos/block/buzz/contents/{+path}","compare_url":"https://api.github.com/repos/block/buzz/compare/{base}...{head}","merges_url":"https://api.github.com/repos/block/buzz/merges","archive_url":"https://api.github.com/repos/block/buzz/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/block/buzz/downloads","issues_url":"https://api.github.com/repos/block/buzz/issues{/number}","pulls_url":"https://api.github.com/repos/block/buzz/pulls{/number}","milestones_url":"https://api.github.com/repos/block/buzz/milestones{/number}","notifications_url":"https://api.github.com/repos/block/buzz/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/block/buzz/labels{/name}","releases_url":"https://api.github.com/repos/block/buzz/releases{/id}","deployments_url":"https://api.github.com/repos/block/buzz/deployments","created_at":"2026-03-06T21:00:56Z","updated_at":"2026-08-12T21:11:13Z","pushed_at":"2026-08-12T21:07:10Z","git_url":"git://github.com/block/buzz.git","ssh_url":"git@github.com:block/buzz.git","clone_url":"https://github.com/block/buzz.git","svn_url":"https://github.com/block/buzz","homepage":"","size":492456,"stargazers_count":26670,"watchers_count":26670,"language":"Rust","has_issues":true,"has_projects":false,"has_downloads":false,"has_wiki":false,"has_pages":true,"has_discussions":false,"forks_count":3243,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2481,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":[],"visibility":"public","forks":3243,"open_issues":2481,"watchers":26670,"default_branch":"main"}},"base":{"label":"block:main","ref":"main","sha":"72d56e7bd3a94fa3ee20b5a50bd1b868a9329d9c","user":{"login":"block","id":185116535,"node_id":"O_kgDOCwindw","avatar_url":"https://avatars.githubusercontent.com/u/185116535?v=4","gravatar_id":"","url":"https://api.github.com/users/block","html_url":"https://github.com/block","followers_url":"https://api.github.com/users/block/followers","following_url":"https://api.github.com/users/block/following{/other_user}","gists_url":"https://api.github.com/users/block/gists{/gist_id}","starred_url":"https://api.github.com/users/block/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/block/subscriptions","organizations_url":"https://api.github.com/users/block/orgs","repos_url":"https://api.github.com/users/block/repos","events_url":"https://api.github.com/users/block/events{/privacy}","received_events_url":"https://api.github.com/users/block/received_events","type":"Organization","user_view_type":"public","site_admin":false},"repo":{"id":1174789082,"node_id":"R_kgDORgXb2g","name":"buzz","full_name":"block/buzz","private":false,"owner":{"login":"block","id":185116535,"node_id":"O_kgDOCwindw","avatar_url":"https://avatars.githubusercontent.com/u/185116535?v=4","gravatar_id":"","url":"https://api.github.com/users/block","html_url":"https://github.com/block","followers_url":"https://api.github.com/users/block/followers","following_url":"https://api.github.com/users/block/following{/other_user}","gists_url":"https://api.github.com/users/block/gists{/gist_id}","starred_url":"https://api.github.com/users/block/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/block/subscriptions","organizations_url":"https://api.github.com/users/block/orgs","repos_url":"https://api.github.com/users/block/repos","events_url":"https://api.github.com/users/block/events{/privacy}","received_events_url":"https://api.github.com/users/block/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/block/buzz","description":"A hive mind communication platform","fork":false,"url":"https://api.github.com/repos/block/buzz","forks_url":"https://api.github.com/repos/block/buzz/forks","keys_url":"https://api.github.com/repos/block/buzz/keys{/key_id}","collaborators_url":"https://api.github.com/repos/block/buzz/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/block/buzz/teams","hooks_url":"https://api.github.com/repos/block/buzz/hooks","issue_events_url":"https://api.github.com/repos/block/buzz/issues/events{/number}","events_url":"https://api.github.com/repos/block/buzz/events","assignees_url":"https://api.github.com/repos/block/buzz/assignees{/user}","branches_url":"https://api.github.com/repos/block/buzz/branches{/branch}","tags_url":"https://api.github.com/repos/block/buzz/tags","blobs_url":"https://api.github.com/repos/block/buzz/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/block/buzz/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/block/buzz/git/refs{/sha}","trees_url":"https://api.github.com/repos/block/buzz/git/trees{/sha}","statuses_url":"https://api.github.com/repos/block/buzz/statuses/{sha}","languages_url":"https://api.github.com/repos/block/buzz/languages","stargazers_url":"https://api.github.com/repos/block/buzz/stargazers","contributors_url":"https://api.github.com/repos/block/buzz/contributors","subscribers_url":"https://api.github.com/repos/block/buzz/subscribers","subscription_url":"https://api.github.com/repos/block/buzz/subscription","commits_url":"https://api.github.com/repos/block/buzz/commits{/sha}","git_commits_url":"https://api.github.com/repos/block/buzz/git/commits{/sha}","comments_url":"https://api.github.com/repos/block/buzz/comments{/number}","issue_comment_url":"https://api.github.com/repos/block/buzz/issues/comments{/number}","contents_url":"https://api.github.com/repos/block/buzz/contents/{+path}","compare_url":"https://api.github.com/repos/block/buzz/compare/{base}...{head}","merges_url":"https://api.github.com/repos/block/buzz/merges","archive_url":"https://api.github.com/repos/block/buzz/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/block/buzz/downloads","issues_url":"https://api.github.com/repos/block/buzz/issues{/number}","pulls_url":"https://api.github.com/repos/block/buzz/pulls{/number}","milestones_url":"https://api.github.com/repos/block/buzz/milestones{/number}","notifications_url":"https://api.github.com/repos/block/buzz/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/block/buzz/labels{/name}","releases_url":"https://api.github.com/repos/block/buzz/releases{/id}","deployments_url":"https://api.github.com/repos/block/buzz/deployments","created_at":"2026-03-06T21:00:56Z","updated_at":"2026-08-12T21:11:13Z","pushed_at":"2026-08-12T21:07:10Z","git_url":"git://github.com/block/buzz.git","ssh_url":"git@github.com:block/buzz.git","clone_url":"https://github.com/block/buzz.git","svn_url":"https://github.com/block/buzz","homepage":"","size":492456,"stargazers_count":26670,"watchers_count":26670,"language":"Rust","has_issues":true,"has_projects":false,"has_downloads":false,"has_wiki":false,"has_pages":true,"has_discussions":false,"forks_count":3243,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2481,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":[],"visibility":"public","forks":3243,"open_issues":2481,"watchers":26670,"default_branch":"main"}},"_links":{"self":{"href":"https://api.github.com/repos/block/buzz/pulls/5699"},"html":{"href":"https://github.com/block/buzz/pull/5699"},"issue":{"href":"https://api.github.com/repos/block/buzz/issues/5699"},"comments":{"href":"https://api.github.com/repos/block/buzz/issues/5699/comments"},"review_comments":{"href":"https://api.github.com/repos/block/buzz/pulls/5699/comments"},"review_comment":{"href":"https://api.github.com/repos/block/buzz/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/block/buzz/pulls/5699/commits"},"statuses":{"href":"https://api.github.com/repos/block/buzz/statuses/6da18868d15aef95743d8a3cc8bab472a69e9f63"}},"author_association":"COLLABORATOR","auto_merge":null,"assignee":null,"active_lock_reason":null,"merged":false,"mergeable":true,"rebaseable":false,"mergeable_state":"blocked","merged_by":null,"comments":0,"review_comments":0,"maintainer_can_modify":false,"commits":1,"additions":534,"deletions":1,"changed_files":8} \ No newline at end of file diff --git a/launchpad/scripts/testdata/upstream5695-closing-refs.json b/launchpad/scripts/testdata/upstream5695-closing-refs.json new file mode 100644 index 00000000000..b7177c4192d --- /dev/null +++ b/launchpad/scripts/testdata/upstream5695-closing-refs.json @@ -0,0 +1 @@ +{"data":{"repository":{"pullRequest":{"closingIssuesReferences":{"nodes":[]}}}}} \ No newline at end of file diff --git a/launchpad/scripts/testdata/upstream5695-meta.json b/launchpad/scripts/testdata/upstream5695-meta.json new file mode 100644 index 00000000000..86b472b4d58 --- /dev/null +++ b/launchpad/scripts/testdata/upstream5695-meta.json @@ -0,0 +1 @@ +{"body":"…awns\r\n\r\nA child spawned by AcpClient::spawn inherited the parent environment and only received persona env vars under operator-wins semantics, so with a missing or stale parent BUZZ_RELAY_URL it silently fell back to the ws://localhost:3000 default and died with \"404 no community for this host\" after consuming the mention. The mention is lost: the harness has already taken the work, and the child never reaches the relay to reply.\r\n\r\nThis is reachable whenever the operator passes --relay-url without also exporting BUZZ_RELAY_URL, which the flag's existence invites.\r\n\r\n- config.rs: Config::from_args pushes the resolved relay URL (CLI over env over default, per clap) into persona_env_vars, so every pool spawn/respawn path carries it. Preflight: an unparseable relay URL now fails terminally (missing-config) before the harness subscribes or takes work; the error prints only the URL, never keys.\r\n- acp.rs: AcpClient::spawn force-sets BUZZ_RELAY_URL from extra_env, overriding both missing and stale parent env. All other keys keep operator-wins semantics.\r\n- Tests: child-env probe for unset-parent and stale-parent scenarios; full-chain CLI-to-PoolStartup extra_env test; missing-config preflight test.\r\n\r\nNo unsafe. New unwrap()/expect() calls are confined to #[cfg(test)] modules.\r\n\r\n## Summary\r\n\r\n\r\n### Related issue\r\n\r\n\r\n### Testing\r\n\r\n","labels":[],"title":"fix(buzz-acp): make resolved relay URL authoritative for ACP child sp…"}