Skip to content
Merged
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ Only the first is a real local scan. Note that the synthetic template is **below
floor** (`names=7, estate=13, site_prefixes=1`) by design — passing locally with it does not mean you
would pass CI's gate, only that nothing structural was found.

It also prints a `token source:` line naming **where those counts came from** — the
`MEFOR_FORBIDDEN_TOKENS` path, that variable carrying the list inline (named, never printed), or
`scripts/security/scan-tokens.local.txt` — and says `OVERRIDDEN` when the environment won over the
file the run just installed. That variable takes precedence, so without the line `-Synthetic` could
install the template and then truthfully report the *real* set as configured, which reads as a
contradiction. The scanner's exit code is propagated too: a refusal is reported as `VERIFY FAILED`,
not as `CONFIGURED`.

## Finding something to work on

Browse issues labeled **`good first issue`** (small, self-contained) and **`help wanted`**. For
Expand Down
8 changes: 4 additions & 4 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3054,7 +3054,7 @@ The distinction matters because these two paths do not look like the case AV cov

## 1000. Prove each required merge context can fail: negative controls for the gates that block merge

> 🔢 **Filed 2026-08-03 — not started.** Value **7/10** · Difficulty **3/10** · _quick win_. Thirteen contexts are the entire merge gate and not one of them is proven able to go red — a gate nobody has watched fail is an assumption wearing a green tick, and the class has now fired at least four times in this repo with no CI signal; the build is a negative-control fixture per context plus a job that fails if a context has none, no new dependency and no change to what the gates check.
> ✅ **SHIPPED (test-only; no branch-protection change, no new workflow).** All 13 required contexts now carry a registered negative control - the count READ from `.github/required-contexts.txt` at run time, never from memory. `tests/negative_controls.toml` records per context what the control plants, the pytest nodes that must go red without it, the shapes it deliberately does NOT break, and the nodes for that half; `tests/_negative_controls.py` + `tests/test_negative_controls.py` reconcile it against the LIVE required set and fail when a context has no control, when a control names a non-required context, when a node id resolves to no test, when the asymmetry half is empty, or when a `ci` control names a command no step invokes. The reconciliation runs inside the already-required `test` legs rather than in a new workflow, so it blocks today and adds NO new required context; two rows in `tests/test_ci_docs_only_detector.py` pin that the required-contexts file and the registry classify as CODE, because pytest is gated on `code == 'true'` and a docs-only classification would skip the gate on exactly the PR shape it exists for. `tests/test_merge_gate_controls.py` supplies the controls the contexts lacked, including the item's motivating instance RUN AGAINST THE PRE-FIX GATE: backlog-hygiene's own shell, lifted from the workflow, exits 1 through the shipped three-dot diff and 0 through the two-dot form on the identical synthetic PR. Watched fail, each restored in the same run: deleting `cancelled` from ci.yml's roll-up reddened 2 of 4 and left 2 green; widening one `.gitleaks.toml` allowlist entry reddened 2 of 4; adding `-ll` to the real bandit invocation reddened 1 of 4. **Residual, stated rather than closed:** the scanner binaries' own detection (bandit, gitleaks, npm-audit, pip-audit's audit half) runs in their own CI jobs, not in the pytest legs - what the legs hold is the property those jobs lose silently. Whether to add an ADVISORY workflow re-running the registered controls is an owner decision, deliberately not taken.

**Cluster:** Security / CI gates. **Priority:** P1. **Verdict:** build. **Severity:** medium.

Expand Down Expand Up @@ -4924,7 +4924,7 @@ The second step's arithmetic is measured: `GetFullPath('.git', <abs primary>)` r

## 1068. A `-Force` claim release leaves no record of who released whose claim

> 🔢 **Filed 2026-08-06 — not started.** Value **5/10** · Difficulty **2/10** · _quick win_. `scripts/coord/claim.ps1:164` gates a non-holder release behind `-not $Force`, so `-Force` lets any session release a claim it does not hold. That escape hatch is **necessary and must stay** — a claim whose holder's worktree is gone would otherwise be permanently stuck. The defect is that the release is `Remove-Item -LiteralPath $file -Force` and **nothing else**: no log, no audit line, no record of who released whose claim, when, or why. The guardrails are on the *advice*, not the *action*.
> ✅ **Shipped 2026-08-10.** Every `claim.ps1 -Release` now appends one JSON line to `<git-common-dir>/mefor-coord/claims/.history` — the key, the releasing worktree and branch, the prior holder, its branch and its note, when the claim was taken, and `force` true/false. **`-Force` is KEPT**: a claim whose holder worktree is gone would otherwise be stuck forever, and the alternative people reach for is hand-deleting the file, which leaves less evidence still. The item was auditability, not prevention. **And it is not hypothetical** — on 2026-08-10 a coordinator force-released claim #344 after establishing on evidence that the holder worktree was gone and that the work its note guarded had in fact merged as PR #153, while the note still read *"UNPUSHED, NO PR, GitHub finds NOTHING"*. That release was **correct**, and it left no trace, which is exactly the gap. Nothing is deployed, so on a first deployment there would be nobody to mislead (§0) — but the claim registry is a shared coordination artifact **today**, and a stale note in it has already blocked a lane from claiming an item. **The record is written BEFORE the claim file is removed, and the release is refused if it cannot be written.** Both orders can lie once and only one lie is recoverable: recording after the removal reproduces this defect exactly (a completed release with nothing left to write the record from), whereas recording first can at worst claim a release that then failed — and a failed removal appends a `release-failed` correction rather than leaving the first line false. Refusing costs nothing because a release is always retryable; the claim simply stays where it was. JSON Lines, LF-only, one line per release written in a SINGLE write to a handle opened `FileMode::Append` + `FileShare::Read`, so two worktrees releasing in the same instant cannot interleave a record. Safe inside the claims directory because every reader there keys on a file NAME (`-List` and `prune-merged.ps1` glob `*.json`, `claim_check.py` opens `<item>.json`) and `ConvertTo-KeyFile` always appends `.json`, so no key can fold onto `.history`. `tests/test_coord_claim_release_history.py` (10 tests) was **red first, 8 of 10** against the unchanged script; the two that passed pre-fix are the guards against a WRONG fix — a refused release and an unclaimed key must record NOTHING. Per #1000 it carries the negatives that a bare "somebody released something" record would sail through: a `-Force` takeover must name the PRIOR holder and the releaser as DIFFERENT paths, and `force` must record **false** when the flag was not passed. Also asserts the on-disk shape (LF, no CR, newline-terminated, every line parseable) and that `-List` still reports "No active claims." with the ledger present. **Not covered, deliberately:** `prune-merged.ps1` clears a stranded claim with its own `Remove-Item` and writes no `.history` line — reported rather than folded in, because its releases are proven-own-worktree-only and already surface in its `claimsReleased` receipt, so the ledger is a complete record of `claim.ps1` releases and not of every release.

**Cluster:** Coordination tooling / claim integrity. **Priority:** P3. **Verdict:** build (add a record; do **not** remove `-Force`). **Severity:** moderate — nothing is silently mis-authorised, but a coordination primitive can be overridden by the party it constrains with no trace, which makes it a convention rather than a control.

Expand Down Expand Up @@ -5211,7 +5211,7 @@ Both readings reach the same operational conclusion, which is the whole point of

## 1080. `setup-leak-gate.ps1` reports the LOADED token set as if it were the one it just installed

> 🔢 **Filed 2026-08-06 — not started.** Value **3/10** · Difficulty **1/10** · _quick win_. `-Synthetic` prints *"Installed the SYNTHETIC template"* and then, three lines later, *"CONFIGURED with the real token set."* Both are individually true when `MEFOR_FORBIDDEN_TOKENS` is set: the script installed the synthetic file, and the scanner loaded the real list from the environment, which wins over the file. Nothing in the output says the env source **overrode** what was just installed, so the two lines read as a contradiction or, worse, as confirmation that the synthetic install produced a real-token gate.
> ✅ **Shipped 2026-08-10.** `setup-leak-gate.ps1`'s verify step now prints a `token source:` line naming what the scanner **actually loaded** — `MEFOR_FORBIDDEN_TOKENS -> <path>`, that variable carrying the list inline, or `scripts/security/scan-tokens.local.txt` — plus an `OVERRIDDEN` banner when an install ran and the environment won over the file just written. Reproduced before the fix on a throwaway checkout, in exactly the filed shape: `-Synthetic` with `MEFOR_FORBIDDEN_TOKENS` pointing at another list printed *"Installed the SYNTHETIC template"* and then *"CONFIGURED with the real token set"*, with nothing between them to reconcile the two. **An inline value is named but never PRINTED** — a non-path value IS the token list, and echoing it would publish what the gate protects into whatever log the operator was capturing. That is the load-bearing negative in the test file, because the obvious implementation of "name the resolved source" prints the variable's value and passes everything else. An explicitly **EMPTY** `MEFOR_FORBIDDEN_TOKENS` is now named as the CAUSE of `NOT CONFIGURED`, because that state means "no source" and does **not** fall back to the file, so the ordinary advice (`-From <path>` / `-Synthetic`) sends the operator round the same loop forever. **A second reporting defect in the same file is closed in the same commit:** the scanner's exit code was discarded entirely, so a refusal came back out as `CONFIGURED` and exit 0 — contradicting this script's own header promise to *"exit non-zero if the sections are empty"*. Measured with an impossible `MEFOR_MIN_DETECTORS` floor: scanner exit **2**, script exit **0**. It now exits with the scanner's code under `VERIFY FAILED`, and deliberately does **not** reprint the scanner's output, since a hit line can quote matched content. `scan_forbidden.py` was **not** modified. Precedence is defined by its `_resolve_token_text` and this script necessarily re-expresses it to name the source, so `tests/test_setup_leak_gate_reports_source.py` (8 tests, **7 red first**; the one green is the paired positive control) pins the copy against the scanner's own `loaded names=` counts **in the same run**, and asserts the two sources yield **different** counts — a fixture that could not tell them apart would pass while measuring nothing. Its token fixtures are DERIVED from the shipped example rather than hand-written, so no numeric site prefix is written into tracked test source and every floor section stays non-empty. Its child environment is pinned explicitly (`MEFOR_*` and `PYTHONIOENCODING`/`PYTHONUTF8` removed, `python` resolved through a shim), and that pinning is proven in both directions: the suite passes under a hostile ambient shell whose values make the same invocation exit 1 when inherited. **Related:** #1063 (same script, anchoring rather than reporting) — this closes the reporting half of that pair.

**Cluster:** Developer tooling / reporting accuracy. **Priority:** P4. **Verdict:** build (trivial). **Severity:** low, and the direction is safe — the gate really is loaded with the real set, so the operator is better protected than the message implies, not worse. The cost is that a reader cannot tell which source armed the gate, which is the one question the script's own docstring says the verify step exists to answer.

Expand Down Expand Up @@ -7415,7 +7415,7 @@ filing.

## 1208. no guard asserts that a credential factory PARAMETER maps to a SETTING name the redactor covers

> 🔢 **Filed 2026-08-09 - not started. THREE MEASURED INSTANCES of one shape, not a hypothesis.** Value **7/10** · Difficulty **4/10**. A connector factory takes a credential parameter and emits it under a DIFFERENT setting name. Every redaction control operates on the SETTING name. Nothing asserts the two agree, so a rename silently moves a credential outside the control's domain.
> ✅ **SHIPPED (test-only; `messagefoundry/config/wiring.py` unchanged).** `tests/test_credential_parameter_mapping.py` follows the VALUE across the rename boundary: a unique sentinel is injected into ONE factory parameter at a time, the factory is called, and the destination setting is read off the emitted settings - so no rename has to be taught to it, which is the fourth name list this item forbids. The real redactor is then asked about whatever key the sentinel was found under. A parameter that reaches no setting, or that cannot be built at all, is a FAILURE with a declared reason rather than a silent pass, and every declared exemption is asserted REACHED so the tables cannot go stale. Domain re-derived rather than inherited: 23 spec-returning factories (asserted `>= 23`), 58 credential- and URL-bearing parameters probed, 3 followed through an `env()`-only refusal. Watched fail: pre-#1106 reddens exactly the 2 renamed `with_signing` parameters and leaves 56 green; pre-#1207 reddens exactly the 10 URL-bearing ones and leaves 48 green; shipped code reddens none. It reaches surfaces the outcome-level sibling cannot: de-classifying `intake_api_key`, `intake_api_key_next`, `credential_password`, `ws_password` and `client_key_password` reddens this file on all five and `test_connection_factory_redaction_domain.py` on none, because that file drops a connector's credential arguments when the connector refuses to be built with them. `proxy` -> `proxy_url` is now covered BY DESIGN rather than by luck: URL-bearing parameters are selected by a suffix rule whose own coverage is checked against the destinations the redactor's URL rule owns.

**Cluster:** Security / secret disclosure - prevention. **Priority:** P2. **Verdict:** build.
**Severity:** no live defect at filing - the three known instances are closed. This is the guard that
Expand Down
7 changes: 7 additions & 0 deletions docs/SESSION-DRIFT-CONTROLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ Frequently forgotten in discussions of "the gate", but it is the same problem cl
`-Release` then `-Take` — which drops the claim in between and re-opens the race it exists to close.
The note is what `announce-session.ps1` broadcasts to every joining session *in preference to the
worktree name*, so a note that cannot be corrected is announced as current intent indefinitely.
Every `-Release` — **`-Force` included** — appends one JSON line to `claims/.history` naming the key,
the releasing worktree and branch, the prior holder, its branch and note, and whether `-Force` was
used. The flag stays: a claim whose holder's worktree is gone would otherwise be stuck, and the
alternative people reach for is hand-deleting the file, which leaves less evidence still. What
changed is that the override is no longer invisible. The record is written *before* the claim file
is removed, and a release that cannot be recorded is refused rather than performed silently
(BACKLOG #1068).
- **[`scripts/hooks/claim_check.py`](../scripts/hooks/claim_check.py)** — `commit-msg` gate: a commit whose
*subject* declares `BACKLOG #N` with a code-touching diff must hold a claim on N **for this worktree**.
Motivated by a recorded incident: three sessions independently fixed one npm advisory; two PRs were
Expand Down
Loading
Loading