You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This issue is a wayfinder map. It is an index, not a store: each decision lives in exactly
one place — its own child ticket — and is only gisted here. The original epic body (the audit's
evidence record) is preserved verbatim in the first comment.
The session-by-session narrative that used to fill the Notes section is archived, verbatim, in part 1, part 2 and part 3; newer
session notes are separate comments on this issue. Notes below is standing rules only.
Destination
Every open question in the 2026-07 fleet-audit remediation programme is decided and implemented — on main, CI green, and where the change is a contract, carrying a gate that goes red if it regresses. The map is finished when no issue carrying wayfinder:247 remains open.
Notes
Domain. Nine repos across two orgs implementing PostGuard: IBE/IBS crypto (pg-core), a PKG service, C ABI + .NET bindings, a Rust file-transfer service (cryptify), a JS SDK monorepo with three email clients, an e2e harness, and Terraform ops. The organising insight from the July 2026 audit (11 repos, code review + a year of failure archaeology) is that the dominant failure class is cross-repo contract drift — contracts existed only as convention, so they drifted silently ~40 times a year. The strategy is: remove seams where possible, and make the surviving ones executable. Prose specs are explicitly out — fixtures and CI gates are the spec.
This map carries execution, not just decisions. Wayfinder normally stops at the decision; this effort deliberately overrides that. A ticket is done when the thing is built, not when the approach is chosen.
Done bar. Merged to main with CI green, and a gate that fails on regression where the ticket touches a contract. Publishing a release and deploying to prod are explicitly not required to close a ticket — with one deliberate exception, #348, whose deliverable is a published advisory. For the ops repo, "merged" means merged and applied, since Terraform's merge is its deploy.
Membership is the wayfinder:247 label, and so is the frontier. The tree hit GitHub's hard cap of 100 sub-issues on 2026-09-02, which silently stopped the map accepting new work and made four charted tickets invisible (#396). The label is now the membership record — on every member, open and closed, so a reopened ticket returns to the frontier — and the parent/child links are UI convenience. Unwire a child when you close it, in the same step as the resolution comment and the index entry; the cap counts closed children too, and this map filled 100 slots in five weeks. Run the query; do not read a list, and do not count leaves from a number written here:
# every open member, across all seven reposforrin encryption4all/postguard encryption4all/postguard-js \
encryption4all/postguard-e2e encryption4all/postguard-business \
encryption4all/postguard-docs encryption4all/renovate-config \
privacybydesign/postguard-ops;do
gh issue list -R "$r" --label wayfinder:247 --state open --limit 200 \
--json number,title,assignees --jq ".[] | \"$r#\(.number) \(.assignees|length) \(.title)\""done
The frontier is the open, unblocked, unassigned members. This replaced a subIssues walk that only ever returned depth-1 children, so takeable leaves sitting inside umbrellas never appeared in it. Umbrellas are wired blocked by their own open children and drop off the frontier the moment those close — check them for the open-but-done shape rather than waiting to trip over it. A closed wired blocker is not the only blocker: no dependency edge models a release pipeline, an unpublished artifact, or "this PR before that one", so a ticket can read takeable and not be. Where that is true it is said on the ticket.
Division of labour. dobby (the dobby-coder GitHub App) writes the code and tests but cannot push .github/workflows/*.yml — it lacks workflows: write, so it leaves the YAML in a comment for a maintainer to apply. Both halves of that handover fail, in two distinct ways: a patch never posted (#324 — check the URL it is claimed to be at), and a patch posted correctly and never applied (#272, and again on postguard-e2e — checking the URL passes, only reading the file catches it). postguard and postguard-js machine-read their own workflows (pg-core/tests/ci_wiring.rs, packages/pg-js/tests/ci-wiring.test.ts), so an unapplied patch there reds a suite; delivery.yml and all of postguard-e2e are unguarded. A workflow handover is verified by reading the file on main and then by reading the step's output in a run on main — a green PR is evidence about the PR, and a green wiring test is evidence about jobs and contexts. Neither is evidence a step landed. privacybydesign/postguard-opshas no coding agent at all — zero dobby-coder PRs in its history — so its tickets can never be dispatched however well written.
Verify before you build — and the thing to re-read is rarely the source file. Read the ticket's target at origin/main (git show origin/main:<path>), never the working tree. Then extend the same suspicion to every other kind of premise, because each of these has cost this map at least one cycle: a ticket's line numbers (stale, and once inverted); its citations (closing an issue is exactly what makes a reference to it wrong); its dates (a ticket can instruct an agent to write a stale date onto the one line recording currency); its closed sets (an allowlist has to be checked against every emitter, not against the reader — there are five, and checking two and inferring three is how pg-cli and dotnet were nearly dropped); its cost estimates (one was inverted by four minutes of running the formatter); and its premises against its own siblings, which keep moving while it waits, and not monotonically — one merge made #370 more dispatchable and less, at once. A PR is authoritative about its own closes, so ask it rather than inferring from timing: gh api graphql -f query='{repository(owner:"O",name:"R"){pullRequest(number:N){closingIssuesReferences(first:10){nodes{number}}}}}'.
An agent's self-report is unreliable in both directions, and silence carries no information. Four shapes, all observed: a handover reported that never happened (#324); "CI is green" three seconds after the push that started the runs, true of the previous sha (#343); "no changes were made" over a complete, green, 40-check branch, with an invitation to re-dispatch that would have discarded it (#362, then #364); and a /dobby on a pull request, which routes to the review pipeline and blocks nothing, so the PR merges as written (#384). Read the branch and the checks of the sha the PR points at now — never the narration. Dispatch acknowledgement latency is bimodal (seconds when dobby is free, unbounded when rate-limited) and its absence means nothing at all: it cannot distinguish not started from never arrived. Never re-fire on silence — ask the queue; a second /dobby buys a duplicate, not a retry. And verify a verification before trusting it: a watcher written to poll for acks reported clean negatives because its gh calls were failing into || echo 0.
Harvest owns more than reading a label, and it runs at both ends of a session.Harvest by label, not by state — a closed ticket still carrying wayfinder:in-flight is an unfinished harvest, and that has been the rule rather than the anomaly. Clearing the mark is not the harvest; a cleared mark is indistinguishable from a finished one. A closed dispatch ticket is not a finished one — diff what the PR touched against what the ticket asked for, because a trailer closes an issue on the strength of a branch nobody read. Three free commands stand between a finished branch and a review queue and none is visible to any query: un-draft (gh pr ready — four consecutive dobby dispatches delivered complete green work and lost only the finalize step), re-run a flake, and check the release PRs. Re-measure the mid-flight set every time rather than carrying the previous session's table forward — this map has twice described a PR as awaiting review that had merged days earlier, and once counted a PR that had been superseded six weeks before. An open-PR count cannot tell a stalled review from a dead PR. An in-flight mark older than a day with no branch is failed, not pending; an in-flight mark outside every frontier query is a claim nobody will collect — wire the ticket in or drop the mark.
The dispatch gate, and the five questions it does not ask. The gate is in the skill; what this map has added is what slips past it. Gate 1 is settled by reading the code the ticket points at, not by reading the ticket — a body can be specific, confident and well-written and still describe a move the tree does not offer. Ask "what artifact does this ticket write, and can the agent write that kind of artifact"before reading the body: a workflow file, an org-wide App installation, and edits to someone else's PR branch are all gate-5 rejects knowable from the deliverable's path or verb. Check a batch for file collisions, which no gate question asks — two dispatches against one file is a bad merge waiting to happen, and wiring blocked by at charting time for that reason alone is good practice. Check that a ticket's own tests are reachable under its own scope fence. Answer gate 3 by reading the target repo's test config, never by guessing which directory gates. And expect a pre-flight amendment: a dispatch body is checked against the code it names and not against the code that names it, so grep the identifier rather than re-reading the ticket's line numbers. Amendments demonstrably work — the tickets that need none are the ones a previous wayfinder session wrote to the gate, and whose premises were measurements rather than readings. Roughly seven in eight swept candidates fail, almost always on gate 1: the frontier's remaining wayfinder:task tickets are mostly grilling tickets wearing task labels. Relabel when you take one.
The fail-open family: for every new gate or signal, ask what it reports when its subject is absent rather than wrong. Instances, all real: a required check with a paths filter and nothing to report (#299); a metric that drops what it cannot parse, so an empty panel reads as a confident zero (#371); a wayfinder:in-flight query against a repo where the label was never created; an in-flight query against a repo with no coding agent; pnpm --filter skipping a package with no lint script, silently, ten lines below the guard that fixes exactly that (examples.yml); alloy validate exiting 0 on an empty endpoint URL; and the inversion — a check with something real to say and no permission to say it, where four days of awaiting approval is indistinguishable from four days of nothing wrong (#394). Two habits: test the checker against a known-bad input before believing its pass, and a rationale written into a code comment is a claim like any other.
Reaching main is roughly half the distance to a user. The other half is a release PR that arrives pre-gated — check-runs: 0, action_required, BLOCKED — and waits on a human who is never notified. The discriminator is the actor: app/github-actions (what secrets.GITHUB_TOKEN makes release-plz) gets zero check-runs where rubenhensen and app/dobby-coder get dozens. It is one approval per sha, and a release PR re-shas whenever main moves under it, so the busier main is the further the release falls behind — a treadmill, not a checkpoint. It has stranded a security fix on main and on no registry three times. #334 owns this. Not every zero-runs PR is that gate — CONFLICTING looks identical through gh pr checks. Check the release PRs at every harvest, because an empty in-flight sweep now reads as done while releases sit still. Downstream, a caret resolves a fix; it does not require one, and a pre-1.0 caret cannot cross a minor boundary — consumers pinned ^0.5.x can never resolve a 0.6.x fix, which is the one case cargo audit/npm audit cannot resolve for a consumer. And check that a fix reaches the code path before calling something either a gap or a vulnerability — it has pointed both ways here.
Measure registries and pipelines together, and believe the pipeline first. A registry read lags a publish and fails in the exact shape of one that never happened: for 2m08s after @e4a/pg-js@2.6.0 published, dist-tags.latest was stale, GET /<pkg>/<version>404'd, time.modified read three weeks old, and cache-busting changed none of it — the JSON endpoint is no better than npm view, both read one lagging document, and no registry field distinguishes not yet from never. The pipeline log is the authority; the registry only confirms. One gh api .../jobs call separates the publish failed from it has not run yet from it landed and the registry lags, and only the first is a finding. crates.io's API needs a User-Agent or its 404 looks like a crate never published.
Repo posture: admin merges are the normal path here, deliberately.dobby-coder cannot self-approve and neither can a maintainer authoring their own PR. Classic protection keeps enforce_admins: false, so --admin still walks past the review requirement — but a separate ruleset main: required checks with bypass_actors: [] means it cannot walk past a red or absent gate. postguard-js has the same posture via a ruleset split. bypass_actors is ruleset-level, not per-rule — that is why differential enforcement needs two objects, and why re-adding one actor to the wrong ruleset silently makes 20 contexts advisory again. This repo's required-context registry is asserted by the ruleset-drift job against the pins in ci_wiring.rs; postguard-js's 20 are still unguarded (#331). Both repos squash-merge, so git merge-base --is-ancestor reports not on main for work that plainly is — read the squash commit on main, not the PR's head sha.
History-preserving imports silently close issues here.ba380a14 closed live issue #146 via an imported commit's Closes encryption4all/postguard-website#146, resolved against this repo's numbering. Nothing warned. Audit closing keywords before and after any such merge; the guard is postguard-js#139. Measure the hazard rather than fearing it — postguard-dotnet's nine keywords all target already-closed items, which made a history-preserving import the cheap option there. And when retiring a repo, turn its release automation off first: release-plz-pr re-created cryptify's superseded release PR one minute after it was closed. Automation off and README finalized in one PR, then close the PRs, then transfer, then archive.
Repo consolidation is finished. All five source repos — the four app repos plus cryptify — are transferred and archived as of 2026-08-09 (#294), and the 62 issues stranded in archived read-only repos are moved. Nothing in the fleet is left half-moved.
Skills./grilling and /domain-modeling for the wayfinder:grilling tickets; /prototype for wayfinder:prototype. Repo-durable knowledge lives in root CLAUDE.md — read it before touching any gate, and update it in the same PR when a ticket teaches something lasting. It carries its own 4,000-byte budget with a test behind it; do not relieve pressure here by spending it there.
Transferring an issue rewrites every issue that mentions it.gh issue transfer of #447 qualified all 72 bare #N in this body to encryption4all/postguard#N 42 s later, under the transferrer's name — +1.7 KB and 31 entries over 400 bytes. map-budget.yml caught it on its first trigger. (1 — renovate: create encryption4all/renovate-config with its validator workflow #446)
Production add-in ships a dead Cryptify hostname — a live prod bug: the released add-in baked fileshare.postguard.eu, which does not resolve, silently breaking file sends for every Outlook user. Repointed to storage.postguard.eu and verified in the deployed bundle.
ci: breaking-change gate on api-description.yaml (oasdiff) — live at fail-on WARN plus two opt-in checks, action pinned by sha, pg-pkg/tests/api_gate.rs as its executable spec. The escape hatch is a /v3 route beside /v2, deliberately not an override label.
decide where the archived apps' 62 stranded open issues live — move all 62 as-is, no triage, into postguard-js — the repo all four already consolidated into. New area:<app> labels, README banners on the three repos missing one, the rule generalised into root CLAUDE.md. Split into four wayfinder:task tickets.
fixtures: container corpus + archival verify job (append-only) — live: a 10-container corpus opened in CI by pinned pg-wasm, current pg-js and working-tree pg-core, one child process per case. Append-only enforced twice — a ledger plus a merge-base diff that also freezes the ledger. Proven by a real tamper PR.
store: restore sessions on boot; chunk replay and resume survive restart — live: Store::restore_sessions rebuilds the map at construction. Two wall-clock deadlines decide whether a row returns — last_active_at + idle_ttl and the 14-day expires — and the idle window keeps running while the process is down.
compat: bump-manifest.mjs moves the manifest but not the fixtures' pg-js pin — fixed in the bumper, not the gate — its strictness was the point. COUPLED_PINS names pins that must follow a manifest entry; fixtures' @e4a/pg-wasm is deliberately uncoupled so it ages against the corpus. Pins are written before the manifest.
e2e: provision business-db from real postguard-business migrations — fixed: business-schema-source fetches postguard-business's drizzle/migrations and applies them before seeding data. The fork was decided by a tooling constraint: the alternative needed an e2e.yml edit the App cannot push.
e2e: keyshare-backed disclosure flow (test scheme + keyshare server) — fixed: TestKeyshareDisclosureFlow sits beside existing flows. What it asserts is the reusable part — a ProofStatusValid-only check would pass over a credential the holder can prove alone, so it also asserts the attribute, keyshare calls and a PIN prompt.
cryptify: serve the configured max upload-chunk size on POST /fileupload/init — shipped, the second dispatch. max_chunk_size_bytes rides the init 200 body from config.chunk_size() — the same accessor the enforcement check reads, so served and enforced cannot disagree. The gate is a round trip at a non-default 1 MiB.
backlog: envelope format 2 — self-describing part, fix double-base64 — decided, not built; the ticket dissolved — there is no envelope format 2. It bundled an outer-layer detection change with an inner-MIME encoding one. Detection becomes a union (preamble OR filename OR uuid-link), needing no wire format. Five children.
Signed attributes get lost — the Outlook half was done; Thunderbird was never in scope until consolidation put it there. A deferral can expire the same way a premise does — out of scope for this repo was true when written and false once the repos merged. js#264, js#265.
may an unauthenticated caller learn cryptify's default-tier upload limits? — yes, and cryptify becomes the single authority: a new unauthenticated GET /limits, default tier only, byte-identical for every caller. Three premises were wrong — /usage never served these numbers, and the limits are consts, not config.
cryptify: split the notification email into attributed and neutral renderings — shipped: email_attributed.{html,txt} beside the neutral pair, the render path restructured around the claim rather than the template. The kill switch is a downgrade applied after the claim resolves, so it can only take attribution away.
packages/pg-js and the plain-JS examples have no formatter — decided in seven parts, two of the ticket's three premises already dead. The style is a prettier key in package.json — the key is what halts prettier's upward search — duplicated into three packages rather than extracted. Execution is js#269.
pg-pkg: postguard_clients' 64-version cap is one budget for the whole fleet — shipped. One client_version budget per client, keyed on the post-allowlist client, so cardinality is bounded at 8 x 64 rather than handing an attacker a fresh budget per invented name. Discharges the made-and-never-applied entry.
Bump prod cryptify off 0.1.25 once a 0.1.29 image exists — merged and applied; prod runs cryptify_image_tag = "0.1.29". The harvest matters more: that commit is the last green apply this repo has had, the next sat undeployed twelve days (ops#72). "merged" is not a status this repo can be asked for.
renovate: write the fleet preset (default.json) and its self-test — shipped (renovate-config#2, 70f697f): exactly the specified keys, first-party rule last, validator green, three mutation proofs red. Unblocks the five per-repo renovate.json tickets and ops#76.
Which routes, fields, and headers actually get removed under #257, and when. Collection is ops#70/ops#71. #371narrowed this patch rather than clearing it, and inverted what it is waiting on. The earlier note here said the data was unreadable as a deprecation signal until #371 added an unknown bucket; that is wrong, and the bucket does not make it readable. The split to carry: the path label is bounded by the server's own route table, so route-level removal is answerable once scraping runs — but no measurement distinguishes one client version from another when the client does not send the header, so that half waits on nothing and is an announcement plus a window, not a metric. Split it that way when it graduates rather than filing one ticket that half-can-be-answered.
What reaches Cockpit Loki, and what log_format prod should run instead. Opened by ops#72 decision 1: Cockpit log shipping is wanted in prod, and ops#73 leaves the Alloy blocks written and switched off behind two absent credentials, so turning it on is now a two-secret operation with no code change — which is exactly why the question has to be answered before someone does it. The procolix vhost sets no access_log, so nginx's default combined format applies and logs $remote_addr plus the full $request; cryptify's routes are GET /usage?<email>, /fileupload/<uuid>, /fileupload/<uuid>/status and /filedownload/<filename>. So the default arrangement ships client IPs, recipient email addresses and download references to a third-party log store — the same identifier set apps/website/src/lib/reportScrub.ts exists to strip out of Sentry. Not yet a ticket because the sharp question is which log_format (or which redacting map) prod should run, not whether the pipeline should exist. And nginx is not the only sink.cryptify's own application log writes the accounting key — since #402 a namespaced unproven:/proven:/api-key: key, so still the canonicalized claimed sender email on the default tier — together with that address's 14-day used_bytes, at info, on every finalize: the log::info! above the rolling-limit check in upload_finalize. Found while resolving #387 and deliberately left there, because changing what an operational log line says is a judgement about diagnostic value and belongs to this decision rather than to a security fix. So the question is which format and which application log lines, with two code owners rather than one — settling the nginx half alone would ship the same identifier set from a different file and read as done.
An RFC, a protocol registry, or capability negotiation. The audit's central negative finding: it would have prevented zero of the year's ~60 incidents. Recorded here so it is not re-litigated — executable contracts are the answer instead.
Product bugs the audit surfaced but deliberately tracks in place, not as children of this map: #197, the mobile Yivi cluster (postguard-website#264/#270/#271/#272), cryptify#47, postguard-website#280.
The superseded programme: EPIC #201 and its phases #202/#208/#213/#214/#215, closed when this epic replaced them. Its phases 0–3 delivered the harness, the pinned contracts, and the drift detector that this map builds on.
Zeroizing USK/MSK on drop in pg-core. A known-open security gap (no zeroize dependency yet), but never part of the audit's remediation set and never a child here. Needs its own effort.
Moving map-budget onto the fleet filing action. It comments on the map instead of filing an issue, a different kind of output (#460).
Destination
Every open question in the 2026-07 fleet-audit remediation programme is decided and implemented — on
main, CI green, and where the change is a contract, carrying a gate that goes red if it regresses. The map is finished when no issue carryingwayfinder:247remains open.Notes
Domain. Nine repos across two orgs implementing PostGuard: IBE/IBS crypto (
pg-core), a PKG service, C ABI + .NET bindings, a Rust file-transfer service (cryptify), a JS SDK monorepo with three email clients, an e2e harness, and Terraform ops. The organising insight from the July 2026 audit (11 repos, code review + a year of failure archaeology) is that the dominant failure class is cross-repo contract drift — contracts existed only as convention, so they drifted silently ~40 times a year. The strategy is: remove seams where possible, and make the surviving ones executable. Prose specs are explicitly out — fixtures and CI gates are the spec.This map carries execution, not just decisions. Wayfinder normally stops at the decision; this effort deliberately overrides that. A ticket is done when the thing is built, not when the approach is chosen.
Done bar. Merged to
mainwith CI green, and a gate that fails on regression where the ticket touches a contract. Publishing a release and deploying to prod are explicitly not required to close a ticket — with one deliberate exception,#348, whose deliverable is a published advisory. For the ops repo, "merged" means merged and applied, since Terraform's merge is its deploy.Membership is the
wayfinder:247label, and so is the frontier. The tree hit GitHub's hard cap of 100 sub-issues on 2026-09-02, which silently stopped the map accepting new work and made four charted tickets invisible (#396). The label is now the membership record — on every member, open and closed, so a reopened ticket returns to the frontier — and the parent/child links are UI convenience. Unwire a child when you close it, in the same step as the resolution comment and the index entry; the cap counts closed children too, and this map filled 100 slots in five weeks. Run the query; do not read a list, and do not count leaves from a number written here:The frontier is the open, unblocked, unassigned members. This replaced a
subIssueswalk that only ever returned depth-1 children, so takeable leaves sitting inside umbrellas never appeared in it. Umbrellas are wiredblocked bytheir own open children and drop off the frontier the moment those close — check them for the open-but-done shape rather than waiting to trip over it. A closed wired blocker is not the only blocker: no dependency edge models a release pipeline, an unpublished artifact, or "this PR before that one", so a ticket can read takeable and not be. Where that is true it is said on the ticket.Division of labour. dobby (the
dobby-coderGitHub App) writes the code and tests but cannot push.github/workflows/*.yml— it lacksworkflows: write, so it leaves the YAML in a comment for a maintainer to apply. Both halves of that handover fail, in two distinct ways: a patch never posted (#324— check the URL it is claimed to be at), and a patch posted correctly and never applied (#272, and again onpostguard-e2e— checking the URL passes, only reading the file catches it).postguardandpostguard-jsmachine-read their own workflows (pg-core/tests/ci_wiring.rs,packages/pg-js/tests/ci-wiring.test.ts), so an unapplied patch there reds a suite;delivery.ymland all ofpostguard-e2eare unguarded. A workflow handover is verified by reading the file onmainand then by reading the step's output in a run onmain— a green PR is evidence about the PR, and a green wiring test is evidence about jobs and contexts. Neither is evidence a step landed.privacybydesign/postguard-opshas no coding agent at all — zerodobby-coderPRs in its history — so its tickets can never be dispatched however well written.Verify before you build — and the thing to re-read is rarely the source file. Read the ticket's target at
origin/main(git show origin/main:<path>), never the working tree. Then extend the same suspicion to every other kind of premise, because each of these has cost this map at least one cycle: a ticket's line numbers (stale, and once inverted); its citations (closing an issue is exactly what makes a reference to it wrong); its dates (a ticket can instruct an agent to write a stale date onto the one line recording currency); its closed sets (an allowlist has to be checked against every emitter, not against the reader — there are five, and checking two and inferring three is howpg-clianddotnetwere nearly dropped); its cost estimates (one was inverted by four minutes of running the formatter); and its premises against its own siblings, which keep moving while it waits, and not monotonically — one merge made#370more dispatchable and less, at once. A PR is authoritative about its own closes, so ask it rather than inferring from timing:gh api graphql -f query='{repository(owner:"O",name:"R"){pullRequest(number:N){closingIssuesReferences(first:10){nodes{number}}}}}'.An agent's self-report is unreliable in both directions, and silence carries no information. Four shapes, all observed: a handover reported that never happened (
#324); "CI is green" three seconds after the push that started the runs, true of the previous sha (#343); "no changes were made" over a complete, green, 40-check branch, with an invitation to re-dispatch that would have discarded it (#362, then#364); and a/dobbyon a pull request, which routes to the review pipeline and blocks nothing, so the PR merges as written (#384). Read the branch and the checks of the sha the PR points at now — never the narration. Dispatch acknowledgement latency is bimodal (seconds when dobby is free, unbounded when rate-limited) and its absence means nothing at all: it cannot distinguish not started from never arrived. Never re-fire on silence — ask the queue; a second/dobbybuys a duplicate, not a retry. And verify a verification before trusting it: a watcher written to poll for acks reported clean negatives because itsghcalls were failing into|| echo 0.Harvest owns more than reading a label, and it runs at both ends of a session. Harvest by label, not by state — a closed ticket still carrying
wayfinder:in-flightis an unfinished harvest, and that has been the rule rather than the anomaly. Clearing the mark is not the harvest; a cleared mark is indistinguishable from a finished one. A closed dispatch ticket is not a finished one — diff what the PR touched against what the ticket asked for, because a trailer closes an issue on the strength of a branch nobody read. Three free commands stand between a finished branch and a review queue and none is visible to any query: un-draft (gh pr ready— four consecutive dobby dispatches delivered complete green work and lost only the finalize step), re-run a flake, and check the release PRs. Re-measure the mid-flight set every time rather than carrying the previous session's table forward — this map has twice described a PR as awaiting review that had merged days earlier, and once counted a PR that had been superseded six weeks before. An open-PR count cannot tell a stalled review from a dead PR. An in-flight mark older than a day with no branch is failed, not pending; an in-flight mark outside every frontier query is a claim nobody will collect — wire the ticket in or drop the mark.The dispatch gate, and the five questions it does not ask. The gate is in the skill; what this map has added is what slips past it. Gate 1 is settled by reading the code the ticket points at, not by reading the ticket — a body can be specific, confident and well-written and still describe a move the tree does not offer. Ask "what artifact does this ticket write, and can the agent write that kind of artifact" before reading the body: a workflow file, an org-wide App installation, and edits to someone else's PR branch are all gate-5 rejects knowable from the deliverable's path or verb. Check a batch for file collisions, which no gate question asks — two dispatches against one file is a bad merge waiting to happen, and wiring
blocked byat charting time for that reason alone is good practice. Check that a ticket's own tests are reachable under its own scope fence. Answer gate 3 by reading the target repo's test config, never by guessing which directory gates. And expect a pre-flight amendment: a dispatch body is checked against the code it names and not against the code that names it, so grep the identifier rather than re-reading the ticket's line numbers. Amendments demonstrably work — the tickets that need none are the ones a previous wayfinder session wrote to the gate, and whose premises were measurements rather than readings. Roughly seven in eight swept candidates fail, almost always on gate 1: the frontier's remainingwayfinder:tasktickets are mostly grilling tickets wearing task labels. Relabel when you take one.The fail-open family: for every new gate or signal, ask what it reports when its subject is absent rather than wrong. Instances, all real: a required check with a
pathsfilter and nothing to report (#299); a metric that drops what it cannot parse, so an empty panel reads as a confident zero (#371); awayfinder:in-flightquery against a repo where the label was never created; an in-flight query against a repo with no coding agent;pnpm --filterskipping a package with nolintscript, silently, ten lines below the guard that fixes exactly that (examples.yml);alloy validateexiting 0 on an empty endpoint URL; and the inversion — a check with something real to say and no permission to say it, where four days of awaiting approval is indistinguishable from four days of nothing wrong (#394). Two habits: test the checker against a known-bad input before believing its pass, and a rationale written into a code comment is a claim like any other.Reaching
mainis roughly half the distance to a user. The other half is a release PR that arrives pre-gated —check-runs: 0,action_required,BLOCKED— and waits on a human who is never notified. The discriminator is the actor:app/github-actions(whatsecrets.GITHUB_TOKENmakes release-plz) gets zero check-runs whererubenhensenandapp/dobby-coderget dozens. It is one approval per sha, and a release PR re-shas whenevermainmoves under it, so the busiermainis the further the release falls behind — a treadmill, not a checkpoint. It has stranded a security fix onmainand on no registry three times. #334 owns this. Not every zero-runs PR is that gate —CONFLICTINGlooks identical throughgh pr checks. Check the release PRs at every harvest, because an empty in-flight sweep now reads as done while releases sit still. Downstream, a caret resolves a fix; it does not require one, and a pre-1.0 caret cannot cross a minor boundary — consumers pinned^0.5.xcan never resolve a0.6.xfix, which is the one casecargo audit/npm auditcannot resolve for a consumer. And check that a fix reaches the code path before calling something either a gap or a vulnerability — it has pointed both ways here.Measure registries and pipelines together, and believe the pipeline first. A registry read lags a publish and fails in the exact shape of one that never happened: for 2m08s after
@e4a/pg-js@2.6.0published,dist-tags.latestwas stale,GET /<pkg>/<version>404'd,time.modifiedread three weeks old, and cache-busting changed none of it — the JSON endpoint is no better thannpm view, both read one lagging document, and no registry field distinguishes not yet from never. The pipeline log is the authority; the registry only confirms. Onegh api .../jobscall separates the publish failed from it has not run yet from it landed and the registry lags, and only the first is a finding. crates.io's API needs aUser-Agentor its 404 looks like a crate never published.Repo posture: admin merges are the normal path here, deliberately.
dobby-codercannot self-approve and neither can a maintainer authoring their own PR. Classic protection keepsenforce_admins: false, so--adminstill walks past the review requirement — but a separate rulesetmain: required checkswithbypass_actors: []means it cannot walk past a red or absent gate.postguard-jshas the same posture via a ruleset split.bypass_actorsis ruleset-level, not per-rule — that is why differential enforcement needs two objects, and why re-adding one actor to the wrong ruleset silently makes 20 contexts advisory again. This repo's required-context registry is asserted by theruleset-driftjob against the pins inci_wiring.rs;postguard-js's 20 are still unguarded (#331). Both repos squash-merge, sogit merge-base --is-ancestorreports not on main for work that plainly is — read the squash commit onmain, not the PR's head sha.History-preserving imports silently close issues here.
ba380a14closed live issue #146 via an imported commit'sCloses encryption4all/postguard-website#146, resolved against this repo's numbering. Nothing warned. Audit closing keywords before and after any such merge; the guard is postguard-js#139. Measure the hazard rather than fearing it —postguard-dotnet's nine keywords all target already-closed items, which made a history-preserving import the cheap option there. And when retiring a repo, turn its release automation off first:release-plz-prre-created cryptify's superseded release PR one minute after it was closed. Automation off and README finalized in one PR, then close the PRs, then transfer, then archive.Repo consolidation is finished. All five source repos — the four app repos plus
cryptify— are transferred and archived as of 2026-08-09 (#294), and the 62 issues stranded in archived read-only repos are moved. Nothing in the fleet is left half-moved.Skills.
/grillingand/domain-modelingfor thewayfinder:grillingtickets;/prototypeforwayfinder:prototype. Repo-durable knowledge lives in rootCLAUDE.md— read it before touching any gate, and update it in the same PR when a ticket teaches something lasting. It carries its own 4,000-byte budget with a test behind it; do not relieve pressure here by spending it there.Findings not yet in Notes
gh issue list --labelreported a mark just cleared (changelog coverage: assert a released tag's entry lists every commit the tag contains #412); an issue'sblockedBy/blockingread empty right aftergh issue transfer, and re-adding failed "already been taken" (renovate: create encryption4all/renovate-config with its validator workflow #446). Confirm with a write or a later read before acting on it. (3 — changelog coverage: assert a released tag's entry lists every commit the tag contains #412, renovate: create encryption4all/renovate-config with its validator workflow #446; 09-24:gh issue list --assigneestill showed two claims just released) — due for promotion into Notes, which needs an eviction there first.CONFLICTINGlooks identical throughgh pr checks: GitHub will not runpull_requestworkflows it cannot build a merge commit for. (1 — critical OpenSSL CVE in the Debian base blocks the release; the vulnerable path is unreachable from our binaries #394)gh issue transferof #447 qualified all 72 bare#Nin this body toencryption4all/postguard#N42 s later, under the transferrer's name — +1.7 KB and 31 entries over 400 bytes.map-budget.ymlcaught it on its first trigger. (1 — renovate: create encryption4all/renovate-config with its validator workflow #446)Decisions so far
fail-on WARNplus named opt-in checks;ERRalone silently passes real breaks (a removed key, a renamed field, a dropped parameter).fileshare.postguard.eu, which does not resolve, silently breaking file sends for every Outlook user. Repointed tostorage.postguard.euand verified in the deployed bundle.bincode-nextdrift by construction. Its repo/issue home split out as decide cryptify's repo and issue home now that the crate is in the workspace #286.fail-on WARNplus two opt-in checks, action pinned by sha,pg-pkg/tests/api_gate.rsas its executable spec. The escape hatch is a/v3route beside/v2, deliberately not an override label.irmars0.2.2 in-tree back in May; what closed it was pg-core 0.6.2 shipping on 2026-07-30, since published 0.6.1 still dragsirma0.2.1 andreqwest0.11.postguard-website#44not satisfied by the Playwright gate.postguard-js— the repo all four already consolidated into. Newarea:<app>labels, README banners on the three repos missing one, the rule generalised into rootCLAUDE.md. Split into fourwayfinder:tasktickets.cryptify, its nine issues going topostguardwhere the code landed, notpostguard-js. The archive step is gated on cutting the Docker publish over first, so execution split into task: cut cryptify's Docker publish pipeline over to postguard, then retire the old repo's build #293 and task: transfer cryptify's 9 open issues into postguard, then archive #294.Wire compat. Red means the change must be additive, or read support ships a release ahead of the flip.pg-compat/tests/support_window.rsnow mirrors the npm drift check againstCOMPATIBILITY.md. The nuget row stays ungated, covered by postguard-e2e#21.enforce_adminsbeing off as a standing gap, split out as decide: enforce_admins is off on main, so admin merges bypass required checks that have nothing to report #299.postguard-examples's one open issue moved to postguard-js#160 witharea:examples, README banner added, repo re-archived.postguard-js#161–#189witharea:website, banner added, re-archived. Found thattransferIssueauto-repoints sub-issue parent links, so this map's edge followed the child without a manual relink.postguard-js#190–#204witharea:outlook-addon, re-archived. No pipeline-cutover blocker — unlike cryptify and website, this repo published no image of its own.postguard-js#205–#221witharea:tb-addon, banner via PR + admin merge, re-archived. All four app-transfer tasks done, leaving only cryptify's own (task: transfer cryptify's 9 open issues into postguard, then archive #294) in the consolidation family.postguardWrite, and thePUBLISH_CRYPTIFY_IMAGEgate was removed outright (ci: publish cryptify's image unconditionally now that GHCR grants postguard Write #309), leavingpostguardsole publisher. cryptify's release-plz jobs were deliberately left live — an open question that went on to bite task: transfer cryptify's 9 open issues into postguard, then archive #294.cryptifyis archived, closing repo consolidation. The cost was the question task: cut cryptify's Docker publish pipeline over to postguard, then retire the old repo's build #293 left open: release-plz re-opened the superseded release PR a minute after it closed. Also: archiving a GHCR package's source repo does not revoke another repo's Write grant.main: required checks(bypass_actors: []) rejects--admineven when a check is absent (fix(compat): close reader-list coverage gaps for crates.io rows #297 merged with zero runs).enforce_adminsstays off — one switch over all classic rules.postguardhalf is live:ci_wiring.rsmachine-readsbuild.ymland asserts what the four gates are wired to. Three lessons outlast it —needsis not enforcement, placement is load-bearing, verify by mutation. postguard-js#222.ci-wiring.test.tsmachine-reads all eight workflow files. Both premises were stale — 20 required contexts, not ten, and from the ruleset, not protection. Two assertions over one step can be jointly weaker than either looks. Gaps: decide: the developers team bypasses all 20 required checks on postguard-js's main ruleset #317, spike: assert the required-context list itself, not just the workflow wiring (the registry half of #272/#222) #318.UsageDbbecameStateDb—upload_sessionsalongsideusage, upserted at init/chunk/finalize, DB errors logged not propagated. Restore-on-boot is store: restore sessions on boot; chunk replay and resume survive restart #303. Prod never reads this repo'sconfig.toml(ops templates its own), so this was never dark there.lint-stagedforapps/tb-addon. The hoist premise was wrong — prettier walks up from each file, so a root config would become the style of every package that formats nothing. Gaps: decide: packages/pg-js and the examples bypass prettier entirely — including the published SDK #323, tb-addon's formatting is enforced only by a git hook, so --no-verify lands unformatted #324.cryptify-v0.1.28has no image at all, cut 85 min beforedelivery.ymlhad a publish job.pg-wasm, currentpg-jsand working-treepg-core, one child process per case. Append-only enforced twice — a ledger plus a merge-base diff that also freezes the ledger. Proven by a real tamper PR.Store::restore_sessionsrebuilds the map at construction. Two wall-clock deadlines decide whether a row returns —last_active_at + idle_ttland the 14-dayexpires— and the idle window keeps running while the process is down.COUPLED_PINSnames pins that must follow a manifest entry; fixtures'@e4a/pg-wasmis deliberately uncoupled so it ages against the corpus. Pins are written before the manifest.business-schema-sourcefetches postguard-business'sdrizzle/migrationsand applies them before seeding data. The fork was decided by a tooling constraint: the alternative needed ane2e.ymledit the App cannot push.TestKeyshareDisclosureFlowsits beside existing flows. What it asserts is the reusable part — aProofStatusValid-only check would pass over a credential the holder can prove alone, so it also asserts the attribute, keyshare calls and a PIN prompt.src/matrix.mjs, not a workflowmatrix:block, so nightly, pre-deploy and hand sweeps cover one set. Two legs:wire,decrypt.--no-verifylands unformatted — fixed: onePrettier (format check)step insidetb-addon.yml's existingbuildjob — a step rather than a job, so it reports under the already-requiredBuild & testand needs no ruleset change. It checks wider than the hook's globs.developersteam bypasses all 20 required checks onpostguard-js'smainruleset — split it, decide: enforce_admins is off on main, so admin merges bypass required checks that have nothing to report #299-style: rulesetmainkeeps the 1-review rule and thedevelopersbypass; a newmain: required checks(bypass_actors: []) holds the 20 contexts. 10 of the last 12 merges had 0 approvals.COMPATIBILITY.md,cargo-semver-checks, the JS public-API snapshot gate,PublicApiAnalyzerson .NET, and the deploy-time version sweep. Every one replaced a promise with something that fails. Live descendants: pg-compat cannot pin two same-minor pg-core versions, so the documented support-window recipe fails #327, compat gates: close the reader-list coverage gaps (crates.io rows unchecked, nuget row ungated) #268.rules/branches/{branch}answers 200 to the built-inGITHUB_TOKENat every permission level, across repositories. Deliberately not required, noschedule:. Gap: guard postguard-js's 20 required contexts against its ruleset (the other half of #318) #331/decide: is asserting bypass_actors worth an admin-scoped credential in CI? #332.upload_chunkis a seam — cryptify serves a globalmax_chunk_size_byteson the init 200 body, absent field falling back to5_000_000. The durable half is why the other three are not seams. Split into cryptify: serve the configured max upload-chunk size on POST /fileupload/init #343 and postguard-js#231.identity.rscarriescanonicalize/is_canonicalandRULES. The canonical form is Yivi's. Canonicalization is a coarsening of equality: it can only merge identities, never split a matching pair.VERSION_0/_1/_2canonical,VERSION_V1/V2/V3kept as#[deprecated]aliases, which stopped a naming fix costing pg-core a major.pg-compatbuilds against published pg-core, so it must keep the old names.max_chunk_size_bytesrides the init 200 body fromconfig.chunk_size()— the same accessor the enforcement check reads, so served and enforced cannot disagree. The gate is a round trip at a non-default 1 MiB..canonical()is load-bearing for what a recipient is shown, not what is derived. fixtures: give the wire-compat sample set a non-canonical sender identity, and make pg-compat assert the sender policy #355.Readyblock and cryptify. A relay is not a reader. docs(pg-core, pg-wasm): the pre-decrypt sender identity is claimed, not verified #357, cryptify trusts the container's unbound sender for the notification email and the default-tier quota key #358, postguard-js#232.=0.6.1→=0.6.3andno_two_pinned_readers_share_a_minor_linenow enforces the constraint. It is cargo's resolver rule, not semver's — cargo unifies on the major alone.postguard.encryptedname is kept forever. The move mattered more than the wording: a guarantee must live beside the process that could revoke it.X-POSTGUARD-CLIENT-VERSIONand the middleware has nounknownbucket, sopostguard_clientsreports zero 1.x traffic forever.SENDERis now" Sender@Sample.TEST "and both policies carry a non-canonical vector, because a fixture on one leaves the other blind. Both reverts shown red.pg_core::challengewithsign_challenge/verify_challenge. The domain separator is applied inside the functions, not passed in, so a verifier cannot get a validh_sig_extfrom a header dressed as a challenge.#358's quota half:get_usageandrecord_uploadshare oneaccounting_keyhelper, so check and accrual cannot disagree, and the no-tenant branch canonicalizes.13 passed. The fixture could not be hand-written — HEAD cannot produce armor — so the generator calls a published old sender.#347's check only fires on containers sealed at 0.6.4+, and every corpus set predates it, so there was nothing to tamper with. Graduated e2e#47, e2e#48.compat-bump.yml's 05:00 cron already tracks three of the four packages, so the whole missing piece is one crates.io backend. COMPATIBILITY.md: drop the nuget row from the Reader list — E4A.PostGuard cannot read a container #378, the three stale reader rows: move pg-core to 0.6.5, pg-wasm to 0.6.5, pg-js to 2.4.0 #379, e2e#50.#358's checked half:upload_finalizereducesX-PostGuard-Proofto aSenderClaim. The migration ispragma_table_info-driven —CREATE TABLE IF NOT EXISTSwould have been a silent no-op on every deployed database.pub_polera; a second writer crate rather than a third pin, since a same-minor pin cannot sit beside 0.6.1.meta.jsongoes to schema 3, and the version number is the deliverable.#364's verifier, soProvenwas unreachable.resumeUploadrehydrates without the challenge, so a resumed session finalizes unproven.rejectsTamperedtable. A default chosen by sorting is a default any addition can capture —-tamperedsorted after0.6.4and silently took the sweep's key universe.setSignPrefills, with both failure shapes shown red against different sets — only test 4 catches the drop shape#199reported. An untestable claim and an out-of-scope claim look identical from outside.GET /limits, default tier only, byte-identical for every caller. Three premises were wrong —/usagenever served these numbers, and the limits are consts, not config.E4A.PostGuardis producer-only, so a reader gate would have nothing to call. The durable half is the distinction between two lists: the support window says what is supported, the reader list says what can read. Supersedes compat gates: close the reader-list coverage gaps (crates.io rows unchecked, nuget row ungated) #268 by name.email_attributed.{html,txt}beside the neutral pair, the render path restructured around the claim rather than the template. The kill switch is a downgrade applied after the claim resolves, so it can only take attribution away./dobbyin a pull request comment routes to the review pipeline, so a change request posted there acknowledges nothing and blocks nothing — post them on the issue. pg-pkg: postguard_clients' 64-version cap is one budget for the whole fleet, so real releases will read as other #388.prettierkey inpackage.json— the key is what halts prettier's upward search — duplicated into three packages rather than extracted. Execution is js#269.no_two_pinned_readers_share_a_minor_linewill not resolve two=pins on one line. Expected stale again within days, which is e2e#50's argument.client_versionbudget per client, keyed on the post-allowlistclient, so cardinality is bounded at 8 x 64 rather than handing an attacker a fresh budget per invented name. Discharges the made-and-never-applied entry.#344's server half.fn limits(config: &State<CryptifyConfig>)takes only config — nothing that can vary the body byAuthorization— so caller-invariance is structural rather than merely tested.check-reader-rows.mjsoncompat-bump.yml's cron. The workflow half was handed over correctly and never applied — so checking the URL is not enough; the file has to be read.postguardaspg-compat, where the sealer already lives, so nothing crosses a repo boundary. Only a write-only change separates it from the archival gate.cryptify_image_tag = "0.1.29". The harvest matters more: that commit is the last green apply this repo has had, the next sat undeployed twelve days (ops#72). "merged" is not a status this repo can be asked for.VITE_*copies are gone andGET /limitsis fetched, send disabled until it answers. A ticket's list of consumers is a premise like a line number, and the one most likely to be short — it named two of four.ops#36: the signal was nothing has deployed, not a deploy failed.packages/pg-jsand the two plain-JS examples, and the fail-open examples Lint step — prettier shipped as#227decided; the workflow half did not, and the guard built to catch it missed it —ci-wiring.test.tsasserts jobs and contexts, not steps inside them. After any handover, read the file onmain.Verify before you buildhas to point at an issue body's links too.e2e.ymlgainedpushonfixtures/**, so a direct push rewriting a fixture and its ledger together is no longer caught a day late. The premise dissolved —append-only.mjsalready reduces any ref withgit merge-base.blocked byedge to the last child was missing: edges are wired when an umbrella is charted, and nothing re-adds them for children filed later.wayfinder:247label. Superseded by the map body is 9 KB over its own budget: Decisions-so-far is 109 entries in 132 KB, and #396's rule never covered it #442.used_bytesmeans the same on both 413 arms,resets_aton neither. The gate chose the shape, not the code — oasdiff atfail-on: WARNmade a structural removal a workflow handover.api-key:,proven:,unproven:— and the check sums only its own. Requiring a proof was rejected: it would be a flag day.file.flush()afterwrite_all. The load-bearing part is not the race — the durable row advanced while bytes were unwritten, which needs no scheduling luck. Gate replaced by the chunk-durability gate added with the fix fails open ~14 runs in 15 #409.pg-pkg-v0.6.0carries 19 entries for 16 commits, so an equality check reds a correct release. Execution is changelog coverage: assert a released tag's entry lists every commit the tag contains #412.#272/#324, because it inverted the failure: the assertion was red until the patch applied. A merged PR with noClosestrailer leaves its ticket open.OpenOptionsis gone frommain.rs, soUploadFileowns opening as well as writing — a wrapper you can bypass is a convention. The mutation bar was met as numbers: 20/20 red mutated, 20/20 green intact..github/required-rules.json. The token boundary decided it: apostguardjob can readpostguard-js's ruleset but cannot file into it.filesfield. A published artifact makes its own permanent fixture.set +emanages a shell you do not control; a bare script invocation makes the hazard stop existing. Stub the effect, not the collaborator.mainand the step's own output read from a run there. Assert the order, not just the presence.cryptifyChannelonPostGuardConfig. The client half of a two-repo fix whose server half (fix(pg-pkg): allow X-Cryptify-Source on the CORS preflight #437) shipped separately — neither repo's tests can see the seam, which is the audit's thesis in one incident.Compilinglines release-plz does not forward.mainat the step output rather than the check mark. The handover's second instance: RED assertions, maintainer patch applied onto the branch before merge.postguard-js's 20 contexts were unguarded (guard postguard-js's 20 required contexts against its ruleset (the other half of #318) #331). A third failure mode for the handover class: the patch was posted twice and only the base64 copy carried the fix.ruleset-driftissue on exit 1, push-to-mainonly; exit 2 still reds and files nothing. Verified in a run onmain. ci: file an issue when the ruleset-drift gate detects drift #443 sat green and in draft for 11 days — the fifth dispatch to lose onlygh pr ready.onboarding: falseis inert in a preset. renovate: create encryption4all/renovate-config with its validator workflow #446–decide the fleet dependency-audit gate: tool per ecosystem, required or scheduled, and its voice #451.renovate@44on Node 24 after a barenpxran a stale cached 37.x; red until the preset lands. Preset ticket is now renovate-config#1, with its edges intact.goto; wasm browser job: an honest timeout, no retry wrapper, and ci_wiring pins so the patch cannot be unapplied #432's "per-test" comment was wrong and its 30 clips Safari's tail (green max 30.9 s, four runs cut at ~27 s). Fix to 70 is wasm browser job: WASM_BINDGEN_TEST_TIMEOUT is a whole-run timer — raise 30 to 70, correct its comment, pin it in ci_wiring #454. A code comment's claim about a dependency is a premise too.map-budget.sh+ a livemap-budget.ymlon every body edit, workflow half applied pre-merge. dobby's review pass moved the decided 150,000 to 153,600 unasked — a settled number is still open to a reviewer. Self-test is GNU-sed-only.70f697f): exactly the specified keys, first-party rule last, validator green, three mutation proofs red. Unblocks the five per-reporenovate.jsontickets and ops#76.bfd65b7). Workflow half applied pre-merge; Safari ran green at 33.57s under70, which the old 30 would have clipped.pg-pkg/cryptifywhen the dependency is in their graph. Handed to guard the release-PR merge: the window is irreducible, so decide what notices main moving under release-plz-pr #441.apps/**/examples/**bump rule,packages/**stays onupdate-lockfile, and no rule re-enables@e4a/*.compat/**+fixtures/**:fixtures-verifyreachespg-coreby path, so the ticket's third glob had nothing to pin.bump, no repo rule, vitest guard.bump, no repo rule,node --testguard.postguard-js, the source repos are archived, and every child is closed. js#139 is unlinked from it, because what remains of js#139 guards the dotnet fold (decide & spike: fold postguard-dotnet into postguard (ship pg-ffi and bindings together) #256), not this.mainscan that files, with expiring baselines. GitHub's graph sees 1 postguard-js package;pnpm auditfinds 49 advisories. dependency-audit: the scheduled main half — daily trigger, per-advisory filing, and expiring baselines in five repos #461.ghfailure reds, sha+tag pin. Migrations file-issues: move changelog-coverage and ruleset-drift onto the fleet filing action, and give test-wasm-browsers a filer #463, js#283.Not yet specified
#257, and when. Collection is ops#70/ops#71. #371 narrowed this patch rather than clearing it, and inverted what it is waiting on. The earlier note here said the data was unreadable as a deprecation signal until#371added anunknownbucket; that is wrong, and the bucket does not make it readable. The split to carry: thepathlabel is bounded by the server's own route table, so route-level removal is answerable once scraping runs — but no measurement distinguishes one client version from another when the client does not send the header, so that half waits on nothing and is an announcement plus a window, not a metric. Split it that way when it graduates rather than filing one ticket that half-can-be-answered.log_formatprod should run instead. Opened byops#72decision 1: Cockpit log shipping is wanted in prod, andops#73leaves the Alloy blocks written and switched off behind two absent credentials, so turning it on is now a two-secret operation with no code change — which is exactly why the question has to be answered before someone does it. The procolix vhost sets noaccess_log, so nginx's defaultcombinedformat applies and logs$remote_addrplus the full$request; cryptify's routes areGET /usage?<email>,/fileupload/<uuid>,/fileupload/<uuid>/statusand/filedownload/<filename>. So the default arrangement ships client IPs, recipient email addresses and download references to a third-party log store — the same identifier setapps/website/src/lib/reportScrub.tsexists to strip out of Sentry. Not yet a ticket because the sharp question is whichlog_format(or which redactingmap) prod should run, not whether the pipeline should exist. And nginx is not the only sink.cryptify's own application log writes the accounting key — since #402 a namespacedunproven:/proven:/api-key:key, so still the canonicalized claimed sender email on the default tier — together with that address's 14-dayused_bytes, atinfo, on every finalize: thelog::info!above the rolling-limit check inupload_finalize. Found while resolving #387 and deliberately left there, because changing what an operational log line says is a judgement about diagnostic value and belongs to this decision rather than to a security fix. So the question is which format and which application log lines, with two code owners rather than one — settling the nginx half alone would ship the same identifier set from a different file and read as done.^0.5.xcan never resolve a0.6.xfix, and no lockfile audit can fix that for them. The fix is either a patched0.5.xor an advisory that says plainly no such patch exists. decide the fleet dependency-audit gate: tool per ecosystem, required or scheduled, and its voice #451 ruled it out of the audit gate: it is release policy, and it probably meets task: publish GHSA and RUSTSEC advisories for the sender-spoofing defect once the fix ships #348's advisory wording first.Out of scope
#197, the mobile Yivi cluster (postguard-website#264/#270/#271/#272),cryptify#47,postguard-website#280.#201and its phases#202/#208/#213/#214/#215, closed when this epic replaced them. Its phases 0–3 delivered the harness, the pinned contracts, and the drift detector that this map builds on.pg-core. A known-open security gap (nozeroizedependency yet), but never part of the audit's remediation set and never a child here. Needs its own effort.