docs(coord): the session record has no branch field, and two rosters answer different questions - #259
docs(coord): the session record has no branch field, and two rosters answer different questions#259wshallwshall wants to merge 2 commits into
Conversation
…ee about one
A session was told a coordinator "might not be there" on the strength of the
session-list MCP tool's `isRunning: false`, and separately the two rosters
reported different branches for one checkout. Both readings were wrong the same
way -- a field answering a question adjacent to the one asked -- and neither trap
was written down anywhere a reader would look.
MEASURED 2026-08-06. A session record holds exactly cwd, entrypoint, kind, name,
nameSource, peerProtocol, pid, procStart, sessionId, startedAt, version. There is
NO branch field. So any branch printed beside a session came from elsewhere, and
the two sources answer different questions while both being labelled "branch":
presence.ps1 / occupancy.ps1 the WORKTREE's branch, live from
`git worktree list --porcelain`. Current.
session-list MCP tool a SESSION attribute captured at registration.
Does not follow a later `git switch`.
For one checkout they reported two different names -- the live roster the branch
that checkout had been switched onto, the session list the one it registered
with. Neither was wrong. A disagreement is not evidence that either roster is
broken, and a branch from the session list must never be quoted as a checkout's
current branch.
ALSO RECORDED, same family: `isRunning` means "currently EXECUTING A TURN", not
"alive". An idle session between turns reads false while being perfectly
reachable. It is not a liveness fence and must not be used as one --
Get-SessionLiveness is, subject to the rule already stated directly above it that
ONLY THE POSITIVE ANSWER IS SAFE TO ACT ON. That rule is why the original
inference was doubly wrong: it drew a negative conclusion from a signal that
cannot support one, using a field that answers a different question.
The concrete branch names are deliberately NOT quoted -- the leak gate refused
the first attempt because a real worktree slug is an internal project name, and
the lesson does not need them.
Documentation only; no behaviour change. Both files parse, presence.ps1 still
runs, 272 tests pass across the coord/presence/occupancy suites.
…KLOG #1057) `test (windows-2025, py3.14)` timed out on PR #261: "The action 'Tests (pytest)' has timed out after 36 minutes". Not a flake and not a defect in the gate change -- the leg has almost no headroom and this file's 249 lines consumed what was left. #261 05:00:57 -> 05:38:03 = 37m06s step timed out at 36m #260 04:59:22 -> 05:32:31 = 33m09s success #259 22:00:10 -> 22:38:30 = 38m20s success step_timeout is 36 for windows-2025, read from ci.yml's matrix rather than inferred from the error text. A suite habitually finishing near 33 against a 36 cap has ~17% headroom. THE TELL THAT IT IS ELAPSED TIME AND NOT CONTENT: the timeout named tests/test_tls_floor_probe.py, while #261 touches worktree_gate.ps1, its tests and one BACKLOG line -- nothing TLS-related. Location and change do not line up. Also ruled out: environmental (the same leg is SUCCESS on 13 other open PRs), and the exit-139 native-crash class of #264 (this is a clean timeout, not a crash). THE FIX HERE IS TO MAKE THIS FILE CHEAPER, NOT TO RAISE THE CAP. step_timeout is deliberately held under job_timeout (46) so a process-level deadlock below pytest surfaces as a STEP failure rather than a job kill; ci.yml says so explicitly. Raising it would trade a real diagnostic for a green tick. The `repo` fixture is subprocess-bound (six `git` spawns) and READ-ONLY -- every test feeds a payload to the hook, which DENIES before git runs, so no worktree is ever actually removed against it. Function scope paid for it six times: 36 process launches, cheap on Linux and expensive on a hosted Windows runner. Module-scoped it removes 30 of those. Measured locally: 10.66s -> 5.26s, a 51% cut, with setup no longer appearing in the top durations at all. The docstring records WHY sharing is safe rather than asserting that it is, and says what would make it unsafe: a case that really removes a worktree needs its own function-scoped fixture. Files #1084 for the underlying condition, which outlives this file. The margin is invisible until it is gone -- nothing reports "the Windows leg finished at 92% of its budget" -- and the measured spread across three PRs the same day (33m09s / 37m06s / 38m20s) means runner variance alone is comparable to the headroom, so a re-run can flip the verdict with nothing changed. That is also why #261 must not be merged on a lucky green. Diagnosis by the coordinator, who ruled out three alternative explanations against evidence before accepting the elapsed-time one, and who recorded a prediction before re-running rather than after.
…256 #259 #264) (#275) * feat(api): report-only TLS key-exchange groups posture field; correct stale "pinned" doc claims (BACKLOG #338) The engine's TLS key-exchange (KEX) groups are INHERITED from OpenSSL's default group list, not pinned to the approved set. harden_kex_groups pins nothing until SSLContext.set_groups lands in Python 3.15, so on every interpreter this project currently runs on the approved pin is inert. This is documentation accuracy plus observability -- it changes no live TLS behaviour (the TLS 1.2+ floor is the enforced control), and on a NOT-DEPLOYED beta there is no exposure today; the pin is a future 3.15 hardening. Two parts: 1. Report-only surfacing. New pure helper config/tls_policy.kex_groups_report() builds a throwaway probe context and asks the ONE authority, harden_kex_groups, what it manages to pin -- so the read-out can never drift from what the connectors actually do. It returns "inherited (...)" on a pre-3.15 interpreter and "pinned: ..." on 3.15+. Surfaced as an additive SecurityPosture.kex_groups field (str | None, default None), wired in create_app beside fips_attestation(), rendered as a status-page row in the web console beside the FIPS/OpenSSL rows. Report-only: it reflects, and changes, no TLS behaviour. 2. Three doc-accuracy edits correcting restatements that still read as "pinned": CONTAINER-EXPOSURE-EVALUATION.md (verification table), ASVS-L2-PHASE0-CHANGES.md (PQC roadmap row), and #200's Closes line in docs/archive/backlog/BACKLOG-CLOSED.md (11.6.2 annotated PARTIAL). Each links to PHI.md's data-in-transit section, the single source of record for the measured accepted set, rather than restating it. The two Python-3.15 tripwire tests in test_tls_policy.py that fire when set_groups/get_groups land are left untouched -- they are the signal to actually set the pin. Engine UI seam bumped 17 -> 18: the golden seam snapshot introspects SecurityPosture's field set, so a purely additive field trips the handshake; SUPPORTED_ENGINE_SEAMS and the golden snapshot updated to match. Tests: test_tls_policy.test_kex_groups_report_reports_inherited_today (the helper reports inherited + names the approved list, never "pinned:"); test_api_auth.test_security_posture_reports_kex_groups (the field flows through the MONITORING_READ-gated, audited posture route and matches the helper); a status-builder assertion that the console renders the row. All three falsified: blanking the helper reddens the two report tests (assert 'inherited' in ''); removing the console row reddens the status-builder test (assert 'key-exchange' in html). * docs(backlog): flip #338 banner to shipped (BACKLOG #338) Flip the #338 status banner from filed/not-started to shipped, now that the report-only kex_groups posture field and the three doc-accuracy corrections have landed. The banner records that the KEX groups are documented as inherited (the pin is inert until Python 3.15) plus the report-only surfacing behind engine seam v18. Banner line only, under the #338 heading, verified by number. The ranked table, the four census distribution lines, and every other item's banner are untouched. The census was NOT recomputed. * docs(research): OpenFlow step-attribute vocabulary gap-map (BACKLOG #238) A findings note comparing Windmill's seven OpenFlow step-attributes (retry, timeout, stop_after_if, skip_if, continue_on_error, mock, cache_ttl) against MessageFoundry's engine/handler vocabulary. Per attribute: what it is, the engine analogue (grounded in a real symbol/file), and the gap (covered-different-locus / partial / absent). This is a review, not a feature. OpenFlow is explicitly NOT a compatibility target; the note is an informational vocabulary map, not a gap-to-close list. Adopting a declarative artifact stays declined by ADR 0076 section 7 and BACKLOG #26. Framing is conditional throughout (not-deployed beta). Lands at docs/research/openflow-step-attributes.md, mirroring the existing docs/research/ review-note convention (config-ux-review.md, message-model-eval.md, ide-low-code-options.md). The mock row cites config/db_lookup.py / config/fhir_lookup.py for the pure-dry-run raise guard (config/db_lookup.py raise DbLookupError / config/fhir_lookup.py raise FhirLookupError), not config/wiring.py whose raise text is about the router phase. * docs(backlog): flip #238 banner to CLOSED (BACKLOG #238) The findings note (docs/research/openflow-step-attributes.md) is the item's expected output and is now delivered, so #238's banner flips from open to closed. One banner line only, under the #238 heading. Census NOT recomputed: this commit changes only #238's banner line and does not touch the ranked table or the four census distribution lines. * fix(serve): auth-off startup arm now refuses a declared terminator (BACKLOG #1013) The `[auth] enabled=false` startup arm keyed on the bind alone (`not settings.api.is_loopback`), so it did not fire for a loopback bind behind a declared upstream TLS terminator. A PHI instance with authentication entirely off behind a declared terminator would have started with no refusal and no warning on first deployment, while the same topology with auth on but MFA off is already refused by the gate #326 fixed. The two arms disagreed about what "exposed" means in the same file. Hoist the single `instance_exposed` definition (#326: an off-loopback bind OR a declared upstream TLS terminator) above the auth-off arm and widen the arm to consult it, so it refuses on a non-loopback bind OR a declared terminator. The existing loopback refusal is kept; the condition is widened, not replaced. Load order verified: `instance_exposed`'s inputs -- `settings.api.host` (through `is_loopback`) and `settings.api.tls_terminated_upstream` -- are read straight off the loaded config, and the only in-place mutation of `settings.api.*` between the hoisted definition and the former site is `serve_ui` (twice), which the predicate does not read. The definition remains defined exactly once. Tests (tests/test_cli.py): auth off + declared terminator on a loopback bind refuses (positive); auth off + true loopback with no terminator still starts (negative control); auth on + declared terminator is unaffected by the arm. Each was falsified -- reverting the arm to the bare bind check reds the positive test, firing on any auth-off reds the negative control, and dropping the auth check reds the auth-on test; each was restored. Docs updated so the contract travels with the code (CLAUDE.md 11): DEPLOYMENT.md, SYSTEM-REQUIREMENTS.md, SECURITY.md, REMOTE-CONSOLE.md and SECURITY-LOOSENING.md now describe the auth-off refusal as firing on an exposed instance (a non-loopback bind OR a declared terminator), not on the bind alone. BACKLOG #1013 * docs(backlog): mark #1013 fixed; record the resolved load-order question (BACKLOG #1013) Flip the #1013 banner from filed/open to fixed and record, in the AMENDED blockquote, that the load-order prerequisite the item flagged as unproven holds: `instance_exposed`'s inputs resolve where the auth-off arm runs. Name the single-definition pointer comment rather than pin its line number, since the hoist shifts that line. Banner flip only: the ranked table and the four census distribution lines were NOT recomputed. BACKLOG #1013 * docs(coord): the session record has no branch, and two rosters disagree about one A session was told a coordinator "might not be there" on the strength of the session-list MCP tool's `isRunning: false`, and separately the two rosters reported different branches for one checkout. Both readings were wrong the same way -- a field answering a question adjacent to the one asked -- and neither trap was written down anywhere a reader would look. MEASURED 2026-08-06. A session record holds exactly cwd, entrypoint, kind, name, nameSource, peerProtocol, pid, procStart, sessionId, startedAt, version. There is NO branch field. So any branch printed beside a session came from elsewhere, and the two sources answer different questions while both being labelled "branch": presence.ps1 / occupancy.ps1 the WORKTREE's branch, live from `git worktree list --porcelain`. Current. session-list MCP tool a SESSION attribute captured at registration. Does not follow a later `git switch`. For one checkout they reported two different names -- the live roster the branch that checkout had been switched onto, the session list the one it registered with. Neither was wrong. A disagreement is not evidence that either roster is broken, and a branch from the session list must never be quoted as a checkout's current branch. ALSO RECORDED, same family: `isRunning` means "currently EXECUTING A TURN", not "alive". An idle session between turns reads false while being perfectly reachable. It is not a liveness fence and must not be used as one -- Get-SessionLiveness is, subject to the rule already stated directly above it that ONLY THE POSITIVE ANSWER IS SAFE TO ACT ON. That rule is why the original inference was doubly wrong: it drew a negative conclusion from a signal that cannot support one, using a field that answers a different question. The concrete branch names are deliberately NOT quoted -- the leak gate refused the first attempt because a real worktree slug is an internal project name, and the lesson does not need them. Documentation only; no behaviour change. Both files parse, presence.ps1 still runs, 272 tests pass across the coord/presence/occupancy suites. * docs(supply-chain): correct at least two claims the shipped v0.3.2 release assets do not support Verified against the actual release assets, not the prose: `gh release download v0.3.2` gives an SBOM with licenses on 40/40 components and hashes on 0/40, and a VEX with `"statements": []`. Two claims did not survive that check. VEX contents. `docs/SUPPLY-CHAIN.md` described the OpenVEX asset as "our per-CVE exploitability assessments" and told the reader it "records, per CVE, whether the vulnerable code is reachable" -- a statement about the contents of a published artifact, false in any tense. It sat immediately after "Do not demand a zero-CVE clean scan", so a procurement reader who applied the VEX, saw no suppressions, and read that as an assessed all-clear would have had no assessment behind it: a compensating control resting on a false premise (CLAUDE.md section 11). The page now names the artifact, says what a statement carries when one exists, and says plainly that where we have not assessed a CVE the document is silent and the scanner's finding stands. The empty-state rule stays stated once, at security/vex/README.md:17-18 and ADR 0149, and is linked rather than re-copied -- the removed sentence was itself the divergent third copy. Component hashes. The inventory sentence enumerated "components, versions, PackageURLs, hashes, and licenses". Backfilling hashes was investigated and rejected on semantics rather than effort: CycloneDX `component.hashes` means the hash of THE file, while requirements-core.lock carries 301 `--hash=sha256:` lines over 41 packages and no package with exactly one (cryptography alone has 40). cyclonedx-py deliberately routes lock hashes to `externalReferences` for that reason, the 1.6 schema imposes no uniqueness constraint so a multi-entry set would validate clean as a silent false claim, and `pip` is an inventoried component with no lock line at all. So the sentence is corrected instead: "at least" replaces the closed enumeration, the reason given is the verified one, and no substitute integrity control is offered -- Sigstore and SLSA attest the SBOM document and our own release files, which is an adjacent question, and the lock is not a released artifact. The same false enumeration sat in scripts/security/sbom_finalize.py's docstring, one hop from the corrected page, which names that script by path in its "for auditors" section. Fixed there too rather than leaving the repo self-contradictory on the fact this commit is about. Nothing is deployed, so nobody has been misled; the defect is that the shipped page WOULD mislead a first reader who tried to verify components against hashes the SBOM does not carry. Deliberately unchanged: the true "hash-locked" phrases at :16 and :86 refer to the lock the inventory is built from, and a blanket scrub of the word would have deleted accurate claims -- the new text disambiguates them instead. No VEX statement is written here; see the notes handed to the coordinator.
|
Superseded by merge train #275, which merged as Your commits went to Verified before closing. #275 was squash-merged, so The question is "is this PR's own change present in Run for all five train B members; all five already-applied. On this PR's red |
Pull request was closed
…efused (BACKLOG #1057) (#261) * fix(gate): rule 3d named a remedy that cannot reach the worktree it refused (BACKLOG #1057) The refusal was right and the sentence after it was not. A caller told "no" is handed a command to run instead, and for the population that most often reaches this rule that command throws. * `remove.ps1 -Name <dir>` resolves to <repo-parent>/<repo-leaf>-<dir>. new.ps1:79-81 ASSERTS that shape after deriving it, so the <primary>-<name> sibling family is the only one it can produce and the only one remove.ps1 can resolve; anything else fails its Test-Path and throws "No such worktree". * `prune-merged.ps1` excludes anything with a `.claude/worktrees/` path segment OUTRIGHT, by its own header, and -Name cannot reach them either. The exclusion is deliberate: those are the trees a live session gets relocated into. Census on this clone 2026-08-06: 45 sibling worktrees, 8 Claude-managed, 4 other -- and all six live sessions sat in the 8. Verified against the INSTALLED gate, not only the repo copy. Same defect as #1032 one rule over, where rule 3b printed a new.ps1 command new.ps1 refuses to run. A refusal the reader cannot act on is the standing invitation to route around the guard, which costs more than the refusal buys. The deny now branches on worktree family. The family neither script can serve gets a literal `git -C "<primary>" worktree remove "<path>"` plus a line saying why prune-merged.ps1 does not apply. The sibling family KEEPS prune-merged.ps1 -- it is dry-run by default and consults occupancy, so it is strictly better there than a bare git command, and the fix must not become "stop naming the scripts". A test pins that. WRITING THE TEST ENLARGED THE DEFECT. The own-tree branch printed the literal placeholder `-Name <directory-name>` for BOTH families: the right tool, named correctly, and still not pasteable. The gate has already resolved the path by then, so it now names the directory. REMEDY TEXT ONLY. Which worktrees rule 3d refuses is untouched, and nothing security-relevant reads the classification -- which is what makes a misclassification cheap here and expensive in rule 3c. Failure direction is pinned by test rather than asserted: a junction or UNC spelling breaks the prefix match, classifies NOT-sibling, and the not-sibling remedy is valid for every family including siblings. The dangerous direction needs a non-sibling to SPURIOUSLY match <primary>-<name>, which an unresolved alias makes less likely, not more. #1064's measured rule 3d fail-open is deliberately not touched here. TWO ASSERTIONS IN THE NEW TESTS EXIST BECAUSE THE FIRST VERSIONS MEASURED THE WRONG THING: * every deny opens by quoting the command back, so searching the WHOLE reason for a runnable `git ... worktree remove` matched the refusal's own echo, and the own-tree case for the Claude-managed family went GREEN over a deny that offered nothing runnable. Assertions are now scoped to the text after "What to do instead:"; * the remedy deliberately says prune-merged.ps1 CANNOT help here, so a bare token search fails on the prohibition itself. The property is "no runnable invocation", not "the name never appears" -- the same distinction test_coord_claim_liveness.py already draws for -Force, and for the same reason. Negative control run, not assumed: reverted, 4 of the 6 cases go red and the 2 that stay green are the controls. Full gate suite 247 passed. Rule 3d is byte-identical between main and claude/worktree-gate-backlog-061bdc's tip, checked before writing, so this cannot revert or duplicate that session's in-flight work. This closes the REMEDY half of #1057. The occupancy half is untouched and the item stays open: rule 3d still has no occupancy or authorship signal, so an unoccupied worktree is still refused rather than allowed. * test(gate): the junction case ERRORED on Linux instead of skipping (BACKLOG #1057) `test (ubuntu-latest, py3.14)` failed on PR #261. The junction test shells out to `cmd /c mklink /J` and guarded with `if made.returncode != 0: pytest.skip(...)`. There is no `cmd` on Linux, so subprocess.run RAISES FileNotFoundError before there is a returncode to inspect -- the guard never runs and the test errors. PLATFORM-MASKED, AND WINDOWS IS THE PLATFORM THAT CANNOT SEE IT: the guard is only reachable where `cmd` exists, so a green local quartet on Windows was structurally incapable of catching this. Same shape rule 3d's own comment already records, where a construct "passed on Windows and failed on the Linux CI leg". Gate on the platform FIRST, and keep the OSError arm: `cmd` existing does not mean junction creation is permitted, so the two guards catch different things and both are load-bearing. VERIFIED BY REPRODUCING THE LINUX CONDITION, not by re-running on Windows: cmd absent, unfixed -> FileNotFoundError, test ERRORS (the CI failure, reproduced) cmd absent, fixed -> SKIPPED via the OSError arm os.name=posix, fixed -> SKIPPED via the platform gate Windows, real cmd -> 6 passed, unchanged The first probe written for this was WRONG and is worth recording: patching `os.name` alone does not reproduce Linux, because `cmd` still exists here -- the unfixed test happily created a junction and PASSED. A control that cannot see the defect is not a control. Making subprocess.run raise for `cmd` is the faithful reproduction, and it discriminates. That also settles the OSError arm empirically rather than by argument: with `cmd` absent and os.name still "nt", the platform gate does NOT fire and the OSError arm is what prevents the error. Full gate suite 247 passed. Patch authored by the coordinator under route (c); the claim gate correctly refused their commit because another worktree held #1057, so it is committed here rather than forced past. It then refused THIS worktree too, for the same reason and equally correctly -- entitlement is keyed on the committing worktree, and building the fix in a fresh worktree moved me out of my own claim. Salvaged the documented way: released my own claim without -Force, re-took it here. * perf(test): module-scope the remedy-families fixture; file #1084 (BACKLOG #1057) `test (windows-2025, py3.14)` timed out on PR #261: "The action 'Tests (pytest)' has timed out after 36 minutes". Not a flake and not a defect in the gate change -- the leg has almost no headroom and this file's 249 lines consumed what was left. #261 05:00:57 -> 05:38:03 = 37m06s step timed out at 36m #260 04:59:22 -> 05:32:31 = 33m09s success #259 22:00:10 -> 22:38:30 = 38m20s success step_timeout is 36 for windows-2025, read from ci.yml's matrix rather than inferred from the error text. A suite habitually finishing near 33 against a 36 cap has ~17% headroom. THE TELL THAT IT IS ELAPSED TIME AND NOT CONTENT: the timeout named tests/test_tls_floor_probe.py, while #261 touches worktree_gate.ps1, its tests and one BACKLOG line -- nothing TLS-related. Location and change do not line up. Also ruled out: environmental (the same leg is SUCCESS on 13 other open PRs), and the exit-139 native-crash class of #264 (this is a clean timeout, not a crash). THE FIX HERE IS TO MAKE THIS FILE CHEAPER, NOT TO RAISE THE CAP. step_timeout is deliberately held under job_timeout (46) so a process-level deadlock below pytest surfaces as a STEP failure rather than a job kill; ci.yml says so explicitly. Raising it would trade a real diagnostic for a green tick. The `repo` fixture is subprocess-bound (six `git` spawns) and READ-ONLY -- every test feeds a payload to the hook, which DENIES before git runs, so no worktree is ever actually removed against it. Function scope paid for it six times: 36 process launches, cheap on Linux and expensive on a hosted Windows runner. Module-scoped it removes 30 of those. Measured locally: 10.66s -> 5.26s, a 51% cut, with setup no longer appearing in the top durations at all. The docstring records WHY sharing is safe rather than asserting that it is, and says what would make it unsafe: a case that really removes a worktree needs its own function-scoped fixture. Files #1084 for the underlying condition, which outlives this file. The margin is invisible until it is gone -- nothing reports "the Windows leg finished at 92% of its budget" -- and the measured spread across three PRs the same day (33m09s / 37m06s / 38m20s) means runner variance alone is comparable to the headroom, so a re-run can flip the verdict with nothing changed. That is also why #261 must not be merged on a lucky green. Diagnosis by the coordinator, who ruled out three alternative explanations against evidence before accepting the elapsed-time one, and who recorded a prediction before re-running rather than after.
Docs-only, 2 files, +30. No behaviour change.
TWO TRAPS, both walked into on 2026-08-06, neither written down where a reader would look.
peerProtocol, pid, procStart, sessionId, startedAt, version -- and never has. So any branch shown
beside a session came from somewhere else:
git worktree list --porcelain(Get-RepoWorktrees). CURRENT.follow a later
git switch.Both are labelled 'branch'. For one checkout they reported two different names and NEITHER WAS
WRONG -- they answer different questions. A disagreement between them is not evidence of a defect.
isRunningMEANS 'CURRENTLY EXECUTING A TURN', NOT 'ALIVE'. An idle session between turns readsfalse while being perfectly reachable. A session was reported as possibly gone on that basis and was
in fact live throughout.
⭐ AND session-registry.ps1 ALREADY SAID SO, two paragraphs above where the reader was looking:
'ONLY THE POSITIVE ANSWER IS SAFE TO ACT ON ... nothing here can PROVE a session is gone -- only that
it is present.' So the inference was wrong twice over: a NEGATIVE conclusion drawn from a signal that
cannot support one, using a field that answers a different question. Both traps are now documented in
session-registry.ps1's header and at Get-RepoWorktrees -- the two places a reader actually lands,
rather than only in the docstring that was already being skipped.
Concrete branch and worktree names are deliberately absent: the leak guard refused a first attempt
because a real worktree slug is an internal project name, and the lesson does not need them.
VERIFIED: both files parse; presence.ps1 still runs and still lists 5 live sessions; 272 tests pass
across the coord, presence and occupancy suites. Based on afdc5cb, merge-tree clean.
PROVENANCE: first committed onto a branch whose PR had ALREADY MERGED, so its parent was no longer an
ancestor of main. Caught with
git merge-base --is-ancestorbefore handover, re-cut off current main,cherry-picked. The pre-squash-base trap does not stop being available just because you know about it.