Closes the last unexercised assumption with a throwaway token: partial clone over HTTPS with a token against a private repository, list through the tool, the short-form raw URL redirecting to a resolved branch under auth, and a conditional request returning 304 across that redirect. A private repository can adopt from another private one, by name or by URL, and check works off the lock afterwards. Fixes what would have shipped as noise. The warning fired whenever a fetch needed a credential, which in a private repository adopting from a private repository is every time and legitimate. add now resolves origin and makes one anonymous request to learn whether this repository is public, so the warning fires when the source is private and the destination is not. The limit is stated in the message rather than implied away: the signal tells public from not-public and nothing finer, so two repositories private to different people is the case that genuinely widens access and the one this cannot see. No origin means cannot tell, which warns — cannot tell must never read as not public. Notes when an adopted document did not come from .loom/published/, without refusing. What is not exported is not hidden, but a lock against it records a dependency on something that was never a contract. And records the framing that settles all of this: the tool is a mast, not a lock. It grants no access, everything it does is possible with copy and paste, and the locks mean nothing outside the tool and the discipline of the agreement — so it makes the wrong thing deliberate rather than impossible. What it adds over a paste is not restriction but provenance. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UTxuSizozEA8yDitPuris
939 lines
46 KiB
Markdown
939 lines
46 KiB
Markdown
# Event log
|
||
|
||
**Appended, newest last, never revised.** *A later entry supersedes an earlier one
|
||
by saying so.*
|
||
|
||
*Entries tagged `osprey` were all considered in a single sphere of understanding.*
|
||
|
||
---
|
||
|
||
## 2026-09-07 — what this tool is `osprey`
|
||
|
||
**Decided:** *`loom-cli` is a fetcher and a comparator.* **It fetches a document
|
||
you depend on and tells you whether the publisher's copy has moved.** *It reports
|
||
and never repairs.*
|
||
|
||
**Believed to advance:** *every act in the loom model is a file in somebody's
|
||
repository — publishing is writing one, adopting is fetching a URL, homing a gap
|
||
is an `mv`.* **Almost nothing needs a program**, so the program should be the
|
||
part that cannot be done by hand: an HTTP request, repeated.
|
||
|
||
**Belief that could be shown wrong:** *that reporting is enough.* **If every
|
||
report is followed by the same manual act, we have moved the work rather than
|
||
removed it**, and the missing command will be obvious.
|
||
|
||
## 2026-09-07 — the design lives here and not in a spec `osprey`
|
||
|
||
**Decided:** *this log is the artifact of `osprey`.* **No specification document
|
||
is written, and the specimen that opened the round is discarded.**
|
||
|
||
**Believed to advance:** *every entry here carries a belief that could be shown
|
||
wrong, so a reader can see which parts are load-bearing and which were guesses.*
|
||
**A spec flattens all of it to equal confidence** — *which is how the specimen
|
||
managed to be wrong, with a straight face, within a day of being written.*
|
||
|
||
**Belief that could be shown wrong:** *that a reader can build from a log.* **It
|
||
is ordered by time and not by topic**, so somebody arriving reads chronologically
|
||
and holds the corrections in their head. *Fine at fifteen entries and not at
|
||
eighty. When a newcomer cannot build from it without a guide, the guide is the
|
||
next thing to write — and not before.*
|
||
|
||
## 2026-09-07 — freshness is a conditional request `osprey`
|
||
|
||
**Decided:** *the tool records the publisher's `ETag`, verbatim and opaque, and
|
||
sends it back as `If-None-Match`.* **It never computes a hash of anything.**
|
||
|
||
**Believed to advance:** *`externals` requires it, and we now have the empirical
|
||
reason as well as the stated one.* **On gitea a raw file's `ETag` is its git blob
|
||
hash; on GitHub it is not.** *A design that compares a local hash to a remote
|
||
`ETag` works on exactly one host by coincidence.*
|
||
|
||
**Belief that could be shown wrong:** *that `ETag` is stable enough to lock on.*
|
||
**A proxy, a CDN, or a host that regenerates the header per request would produce
|
||
a change report where nothing changed.** *Untested against anything but gitea and
|
||
GitHub.*
|
||
|
||
## 2026-09-07 — supersedes nothing: the path is for a person `osprey`
|
||
|
||
**Decided:** *the origin URL is recorded in the lock, not derived from the stored
|
||
path.*
|
||
|
||
**Believed to advance:** *the path `\.loom/externals/<host>/<path>.md` does not
|
||
round-trip.* **Measured:** *the document we hold as
|
||
`git.hypertheory-labs.dev/loom/externals/externals.md` is served from
|
||
`/loom/externals/raw/branch/main/.loom/published/externals.md`* — **the stored
|
||
path has dropped the route, the branch, and `.loom/published/`.**
|
||
|
||
*The third is not routing.* **`publication.md` makes `.loom/published/` the whole
|
||
contract**, and a tree that erases it cannot say whether a copy came from
|
||
somebody's published surface or from a file they may rename at will.
|
||
|
||
**Belief that could be shown wrong:** *that people will still read the path as a
|
||
location.* **If anybody writes code that parses it back into a URL, the path
|
||
should stop looking like one.**
|
||
|
||
## 2026-09-07 — the lock, and what it holds `osprey`
|
||
|
||
**Decided:** *one record per adopted document in `.loom/externals/.locks`: the
|
||
**resolved** origin URL, and the `ETag` it came with.*
|
||
|
||
**Believed to advance:** *the specimen said there would be no lock file, on the
|
||
strength of the `ETag`-as-blob-hash claim.* **That claim failed twice**, so the
|
||
file exists and is named for what it is. *It was briefly called `.etags`, from
|
||
when we believed there would be one field.*
|
||
|
||
**Resolved, and not the short form.** *Measured: gitea `303`s
|
||
`/loom/externals/raw/externals.md` to `/raw/branch/main/…`* — **so a lock holding a
|
||
short URL is locked to whatever the default branch is at the time you ask**, and a
|
||
branch rename reports as a change in the document.
|
||
|
||
**Belief that could be shown wrong:** *that one file is cheap.* **Two fetches in
|
||
one round conflict inside it, and the conflict is in a file no human can resolve
|
||
by reading.** *If that bites, it becomes one record per document and the tree is
|
||
mirrored twice.*
|
||
|
||
## 2026-09-07 — unlocked is a state `osprey`
|
||
|
||
**Decided:** *a document with no lock is reported as unlocked.* **`check` never
|
||
adopts whatever the remote is currently serving as the lock.**
|
||
|
||
**Believed to advance:** *adopting it would assert the local copy is the one being
|
||
served, which is the thing you were about to check.* **It is the hash assumption
|
||
in a different coat, and it fails silently in the one case that matters — a copy
|
||
somebody edited.**
|
||
|
||
**Belief that could be shown wrong:** *that anybody will run `pull` again to fix
|
||
it.* **If unlocked documents simply accumulate, the report is noise and something
|
||
has to lock them.**
|
||
|
||
## 2026-09-07 — `404` is unresolved, and that is no longer provisional `osprey`
|
||
|
||
**Decided:** *`check` reports a `404` as unresolvable — the document was withdrawn
|
||
or we have lost access — and names both readings without picking one.*
|
||
|
||
**Believed to advance:** *over HTTP they are the same response, because a host
|
||
that distinguished them would leak the existence of things you may not see.*
|
||
|
||
**This entry exists because the status of the behaviour changed and the code did
|
||
not.** *It began as a local workaround recorded in a `.gaps.md` against
|
||
`externals`, which listed `410` and not `404`.* **`externals` has since added the
|
||
`404` row, so the gap is closed and the facet is deleted at this reconciliation.**
|
||
|
||
> **The workaround is not retired. It is now the specified behaviour.** *Unchanged
|
||
> in the code and entirely changed in status — and this log is the only place that
|
||
> says so.* **Somebody inheriting this will find the behaviour and go looking for
|
||
> the gap that justified it, and find nothing.**
|
||
|
||
**Belief that could be shown wrong:** *that reporting both readings is useful.*
|
||
**If in practice it is always one of them, the report is a ritual** — *and that is
|
||
worth knowing.*
|
||
|
||
## 2026-09-07 — declined: `check` does not resolve the `404` over ssh `osprey`
|
||
|
||
**Considered:** *the two readings **are** distinguishable over ssh — permission
|
||
denied against repository not found — and `externals` now says so.* **Demonstrated
|
||
here by accident:** *`loom/settled` returned `404` over HTTP while an ssh clone of
|
||
it succeeded.*
|
||
|
||
**Not done, because:** *it needs a key, a non-default port, and a second
|
||
transport* — **and it would be fixing rather than reporting**, which is the rule
|
||
this design held all round.
|
||
|
||
**Belief that could be shown wrong:** *that a person will do it.* **The ssh
|
||
disambiguation is one command and it is in the document; if nobody ever runs it,
|
||
the ambiguity was never actionable and reporting it was theatre.**
|
||
|
||
## 2026-09-07 — declined: `init` `osprey`
|
||
|
||
**Considered and rejected**, not deferred.
|
||
|
||
**Because:** *its temptation is to scaffold, and four empty directories assert four
|
||
things nobody has decided.* **A file that carries no delta should not exist.**
|
||
|
||
**Belief that could be shown wrong:** *that starting is easy without it.* **If
|
||
people repeatedly fail to start**, *the useful version creates one file and asks
|
||
one question* — **and creating the tree is still not the feature.**
|
||
|
||
## 2026-09-07 — declined: worktree, clone; and where a round lives `osprey`
|
||
|
||
**Decided:** *the cart lives on the default branch; the work is isolated on a
|
||
branch named for the cart.*
|
||
|
||
**Believed to advance:** *a cart is a venue and the work is an artifact.* **`cart`
|
||
says three states are visible in a listing, and a branch makes all three
|
||
invisible** — *a venue only visible if you know which branch to check out is not a
|
||
venue.* **Both dailies land on the default branch, one commit per exchange.**
|
||
|
||
**Declined: a worktree, and a separate clone.** *Both isolate concurrent working
|
||
directories, and `cart` says there is one live cart, ever.* **They solve a problem
|
||
the agreement says cannot exist.**
|
||
|
||
**Belief that could be shown wrong:** *that the dailies and the work have
|
||
different audiences.* **If a daily needs to quote a file that only exists on the
|
||
branch, the split makes the daily unreadable from where it lives**, and the answer
|
||
collapses to one branch for both.
|
||
|
||
## 2026-09-07 — `publish` is renamed `reachable`, and it earned its place `osprey`
|
||
|
||
**Decided:** *the command is `loom reachable <path>` — one anonymous request
|
||
against a document in `.loom/published/`.*
|
||
|
||
**Believed to advance:** *the old name said the tool performs publication, which
|
||
is the one thing the model says it never does.* **It performs a fetch with a
|
||
different subject**, which keeps it inside a fetcher and a comparator.
|
||
|
||
**It was nearly cut for being a third kind of thing. Then it was validated three
|
||
times in one afternoon**, against loom's own repositories:
|
||
|
||
```
|
||
loom/settled 404 anon the page written to fix "settled is private"
|
||
jeffry/homelab-cluster 404 anon and bedrock's public starting page named it
|
||
as the better answer
|
||
loom/.loom 200 anon fetchable — but not where a reader lands
|
||
```
|
||
|
||
**All three are now fixed.** *The third was not a `reachable` failure and is
|
||
recorded as the tool's boundary:* **`reachable` answers *can anybody fetch this*
|
||
and does not answer *will anybody find it*.** *The second is not testable by a
|
||
fetcher and a comparator, and trying is the first step in the tool becoming
|
||
something else.*
|
||
|
||
**Belief that could be shown wrong:** *that publication failures are common enough
|
||
to need a command.* **Three for three is a small sample from one publisher in one
|
||
week.**
|
||
|
||
## 2026-09-07 — the `ETag`/blob coincidence is a migration aid `osprey`
|
||
|
||
**Recorded so it is not rediscovered as a good idea.**
|
||
|
||
*With eight adopted documents and no locks, `check` was run by hand by hashing the
|
||
local copies and comparing to remote `ETag`s* — **the exact thing the convention
|
||
forbids** — **and it worked**, because on gitea the two coincide. *It found
|
||
exactly one drifted document, correctly.*
|
||
|
||
**It is worth doing once, on gitea, to lock what was fetched by hand before the
|
||
tool existed. It is not a mechanism.**
|
||
|
||
> **The coincidence has now been found twice and rejected twice.** *The next person
|
||
> to notice it will think they have found the good idea again.*
|
||
|
||
## 2026-09-07 — annotate to ask; write in your own file to assert `osprey`
|
||
|
||
**Decided:** *responses between two presences are correspondence — each writes in
|
||
their own file — and annotation is reserved for asking or challenging.*
|
||
|
||
**Believed to advance:** *the freeze is not "you cannot edit", it is "you cannot
|
||
edit without resolving what was said".* **An annotation creates an obligation**,
|
||
which makes it the blocking form and correspondence the non-blocking one.
|
||
|
||
*And the cost is practical: **dissolving a multi-pass file is expensive**, so the
|
||
cost of annotating rises with the length of the thing annotated.* **Annotate
|
||
early, or accept the cost when the question is worth blocking on.**
|
||
|
||
**Belief that could be shown wrong:** *this rule is in neither `cart` nor
|
||
`annotating`.* **It is our reading of their text, and `cart` is loom's** — *they
|
||
may say we have it wrong.*
|
||
|
||
## 2026-09-07 — why this tool exists, in one story `osprey`
|
||
|
||
**Gitea's ssh is on port `2222`.** *I did not know that. I tried to clone, got
|
||
`Permission denied (publickey)`, and spent three tool calls and a guess chasing it
|
||
as an authentication problem before finding the port.*
|
||
|
||
**The fact was published.** *`jeffry/homelab-cluster`, in `.loom/published/gitea.md`,
|
||
says:*
|
||
|
||
> **A clone URL without the port will not work**, and the failure looks like an
|
||
> authentication problem rather than a wrong port.
|
||
|
||
**It named my failure before I had it, and I could not read it** — *the repository
|
||
was not fetchable by anybody who is not its owner.*
|
||
|
||
**This happened inside the round that was designing the fix**, *and it failed for
|
||
the reason that round had just spent two passes identifying.* **A document that is
|
||
correct, published, and unreachable is worth exactly as much as one that was never
|
||
written.**
|
||
|
||
**Believed to advance:** *nothing in this log is a stronger reason for the tool to
|
||
exist.* **`reachable` is why the document can be fetched. `pull` and `check` are
|
||
why it is still true when you read it.**
|
||
|
||
**Belief that could be shown wrong:** *that the tool would have helped.* **I would
|
||
have had to already depend on that document to have pulled it** — *and the thing I
|
||
needed was the one I did not know I was missing.* **A fetcher does not solve
|
||
discovery, and this story is partly a discovery problem wearing a freshness
|
||
problem's clothes.**
|
||
|
||
## 2026-09-07 — supersedes the `reachable` entry: publication is not publicness `marmalade`
|
||
|
||
**The entry above records three publication failures found by `reachable` and says
|
||
all three are fixed. One of them was not a failure.**
|
||
|
||
**`jeffry/homelab-cluster` is private on purpose** — *it describes a running
|
||
cluster, and its owner does not want outsiders reading what is on it.* **It
|
||
publishes six documents to an audience that is not the public**, *which
|
||
`publication.md` permits in the word we misread:*
|
||
|
||
> **What a repository has handed over for **others** to depend on.**
|
||
|
||
***Others*, not *everyone*.**
|
||
|
||
**So the tool as logged would have reported a correct repository as broken.** *An
|
||
anonymous fetch returning `404` is the intended outcome there*, **and a check that
|
||
treats anonymous reachability as success produces a false positive on every
|
||
deliberately-private publisher.**
|
||
|
||
**What survives:** *the defect that round found in `bedrock/starting.md` was real,
|
||
and it was never about `homelab-cluster`.* **A public document promised that a
|
||
private repository was the better answer**, *and the promise was the defect.*
|
||
**The link was removed; naming it as private, the way `.loom` names `settled`,
|
||
would also have answered it and would have kept the better document findable.**
|
||
*That call is loom's and it is already made.*
|
||
|
||
**Believed to advance:** *a check that cannot fail teaches nothing, and one that
|
||
fails on correct repositories is worse* — **it trains people to ignore it.**
|
||
|
||
**Belief that could be shown wrong:** *that anonymous is still the right default.*
|
||
**If most publication here is to private audiences, the default is wrong** and the
|
||
command should refuse to run without being told who to ask as.
|
||
|
||
## 2026-09-07 — measured: a read token, and what a host will tell you `marmalade`
|
||
|
||
**A gitea read token was issued for one session and destroyed after.** *Recorded
|
||
because these are facts about the host that nothing else here writes down.*
|
||
|
||
**A read token authenticates raw HTTP on a private repository.** *`404`
|
||
anonymously, `200` with `Authorization: token` or `Bearer`, and `If-None-Match`
|
||
returns `304` authenticated.* **So `add` and `check` work against private
|
||
repositories** — *the assumption the specimen made and nobody had run.*
|
||
|
||
**`/api/v1/repos/{o}/{r}` reports `admin, push, pull: true` for a token that
|
||
cannot write.** *Those are the account's rights.*
|
||
|
||
**But a refused write names the token's scopes in the error body.** *So:*
|
||
|
||
> **A token cannot be asked what it may do. It can only be told, by being
|
||
> refused.**
|
||
|
||
*Supersedes the specimen's "the name is the only record", which was too strong.*
|
||
**For this tool the rule is unchanged and the reason is different:** *`loomctl`
|
||
never writes, so it never provokes the only response that would tell it anything.*
|
||
|
||
**Belief that could be shown wrong:** *that gitea keeps doing this.* **The scope
|
||
list in an error body is not a documented interface** — *it is a message, and
|
||
messages change.*
|
||
|
||
## 2026-09-07 — the CLI has nouns, and one of them is not built `marmalade`
|
||
|
||
**Decided:** *`loomctl`, with subcommands.* **`loomctl external …` now; `loomctl
|
||
beads …` when it exists.**
|
||
|
||
**Believed to advance:** *`external` is what somebody else published that we depend
|
||
on.* **The verb `check` means the same thing wherever it appears — ask somebody
|
||
else whether what we believe is still true.**
|
||
|
||
**Declined: a plugin boundary.** *`kubectl` and `gh` grew plugins after somebody
|
||
outside wanted in.* **The seam gets built when it is a reported gap, not a designed
|
||
one** — *which is how loom says that arrives, with a name attached and a workaround
|
||
recorded beside it.*
|
||
|
||
**Declined: a field where a repository declares who it published for.** *It would
|
||
let a tool compare intent to result, and it asserts a category nobody decided.*
|
||
**The better version needs no declaration: derive it from the other side, by
|
||
reading everybody's `externals/` to see who actually depends on you.** *That is
|
||
`registry`'s shape.*
|
||
|
||
**Belief that could be shown wrong:** *that one noun justifies the structure.*
|
||
**Until `beads` exists, `loomctl external` is a subcommand with nothing to be
|
||
distinguished from.**
|
||
|
||
## 2026-09-07 — declined: `published check` `marmalade`
|
||
|
||
**Considered and rejected.** *The command that would ask whether somebody who is
|
||
not you can fetch what you published.*
|
||
|
||
**Because it simulates a consumer, and `registry` will have real ones.** *An
|
||
observed fetch beats a synthetic probe — the probe says a stranger **could** have
|
||
fetched it; a registry says somebody **did**.* **Building the probe first builds a
|
||
worse version of the thing that is coming.**
|
||
|
||
**And it removes the only thing complicating the credential story:** *`--as
|
||
<context>` existed almost entirely to serve this command.*
|
||
|
||
**Belief that could be shown wrong, two ways:**
|
||
|
||
- **`registry` never materialises**, *and nothing ever asks the question.*
|
||
- **`registry` records *declared* consumers rather than *observed* fetches.** *"Who
|
||
says they depend on us" is not "who can actually read us"* — **then this comes
|
||
back.**
|
||
|
||
*Cost of deciding: the command found three real problems in one afternoon, one of
|
||
which was a false positive it would have produced forever.* **The workaround is one
|
||
`curl`, and a real need with a cheap workaround is a gap rather than a missing
|
||
feature.**
|
||
|
||
## 2026-09-07 — kept, though nothing implements it `marmalade`
|
||
|
||
> **A check performed with your own credential proves nothing.** *It helps you
|
||
> complete alone an act that, by definition, you cannot* — **and then tells you it
|
||
> was fine.**
|
||
|
||
**Believed to advance:** *whoever builds `registry`'s consumer view needs this
|
||
before they design it*, **because the tempting implementation is for the publisher
|
||
to verify their own publication** — *the same mistake with a database behind it.*
|
||
|
||
**And it is cheap to detect:** *`/api/v1/user` gives the acting login, the
|
||
repository gives its owner.* **One request**, *and nobody thinks to make it.*
|
||
|
||
## 2026-09-07 — `external list` talks to git, measured `marmalade`
|
||
|
||
**Decided:** *`list` enumerates a publisher's `.loom/published/` by shelling out to
|
||
`git`* — **`clone --filter=blob:none --depth=1 --no-checkout`, then `ls-tree`.**
|
||
|
||
**Believed to advance, and measured across three hosts:**
|
||
|
||
```
|
||
git archive --remote gitea ok · github 422 · gitlab 404
|
||
partial clone gitea ok · github ok · gitlab ok 124–144KB
|
||
REST /api/v1/repos/{o}/{r}/contents/{p}
|
||
/repos/{o}/{r}/contents/{p} (api.github.com)
|
||
/api/v4/projects/{url-encoded}/repository/tree
|
||
```
|
||
|
||
**The argument is consistency, not thrift.** *One mechanism against three hosts
|
||
instead of three adapters* — **and REST `404`s on a private repository, which is
|
||
the case that matters.**
|
||
|
||
**The failure mode to build for:** *`--filter` is a server capability and git's
|
||
fallback is **silent***, warning `filtering not recognized by server` and
|
||
downloading everything. **`--depth=1` bounds it. Detect the warning and say so.**
|
||
|
||
**Belief that could be shown wrong:** *that shelling out stays simple.* **The first
|
||
time we need output `git` does not print stably, the library trade looks
|
||
different.**
|
||
|
||
## 2026-09-07 — nothing needs ssh, and nothing needs write access `marmalade`
|
||
|
||
**Decided:** *every transport is HTTPS on 443, and no token this tool is given
|
||
should carry `write:repository`.*
|
||
|
||
**Measured:** *partial clone with the blob filter works over gitea's **smart
|
||
HTTP***, so `list` never needed ssh — *it was built that way out of habit.*
|
||
|
||
**The tool writes only to the working tree** — *a fetched document, a lock, a
|
||
facet* — **and never commits or pushes.** *Committing and pushing are the person's,
|
||
under their own credentials.*
|
||
|
||
> *Same reason `cart` gives for an agent committing and not pushing:* **the
|
||
> consequences of a push land on people it cannot experience.** *A tool that can
|
||
> push is a tool that can publish, and publishing is an act somebody performs.*
|
||
|
||
**Implementation:** *never build `https://<token>@host/…`* — **git writes it into
|
||
`.git/config` and it surfaces in `git remote -v` and logs.** *Pass
|
||
`-c http.extraHeader="Authorization: token …"` per invocation instead.*
|
||
|
||
**Belief that could be shown wrong:** *that every host we meet allows token auth
|
||
over git-HTTPS.* **A host that does not would need ssh for `list` and nothing
|
||
else.**
|
||
|
||
## 2026-09-07 — untested, and load-bearing `marmalade`
|
||
|
||
**Nobody has confirmed that a gitea PAT scoped `read:repository` authenticates
|
||
*git over HTTPS*, not only the API and raw files.** *Those are different paths in
|
||
gitea; the token we had was measured only against the second.*
|
||
|
||
**Measured:** *an unauthenticated HTTPS clone of a private repository returns
|
||
`Failed to authenticate user`* — **so something is required, and which something is
|
||
unknown.**
|
||
|
||
**Recorded rather than assumed** *because it is the first thing to run against the
|
||
next token*, **and because if a read scope is not enough, that is the only argument
|
||
for keeping ssh.**
|
||
|
||
## 2026-09-07 — `--help` is the documentation surface `marmalade`
|
||
|
||
**Decided:** *`--help` is what a person or an agent actually runs when handed an
|
||
unfamiliar CLI, so it is the surface that has to be good* — **say what a command
|
||
does and what it will not do.**
|
||
|
||
**A `.loom/published/` for `loomctl` is deferred, not declined.** *The distinction
|
||
matters because `init` was declined.* **It earns publication when somebody depends
|
||
on this tool's behaviour rather than reading its help.**
|
||
|
||
## 2026-09-07 — decided by fallback: where the credential lives `marmalade`
|
||
|
||
**Nobody chose this; the fallback became the decision and that is recorded so it
|
||
can be found.**
|
||
|
||
**One file in `~`, kubeconfig-shaped, holding hosts and credentials together.**
|
||
*Never in a repository, and nothing that reads from a repository.*
|
||
|
||
**Believed to advance:** *the config is no longer only a secret* — **it is how you
|
||
talk to a host at all**, *API shape, ssh port if it ever matters, which transport a
|
||
command uses.* **Some of that is not secret and several people would want the same
|
||
values.**
|
||
|
||
> **The split is expected and was not done:** *a host list that can be committed,
|
||
> and a credential file that cannot.* **It was named rather than decided.**
|
||
|
||
**Belief that could be shown wrong:** *that one file is fine until somebody
|
||
shares.* **The first time two people want the same host list, the file has to
|
||
split**, *and doing it later means moving a file people have already written by
|
||
hand.*
|
||
|
||
## 2026-09-07 — Go, and `git` on `PATH` `marmalade`
|
||
|
||
**Decided:** *`loomctl` is written in Go.*
|
||
|
||
**Believed to advance:** *it gets installed on other people's machines — a laptop,
|
||
a cluster node, a CI runner* — **and a single static binary is the difference
|
||
between "download it" and "first install a runtime."** *The work is precise HTTP
|
||
header control, shelling out to `git`, and reading files; the standard library does
|
||
the first and third with no dependencies.*
|
||
|
||
**Considered: TypeScript on npm.** *A real distribution channel and the familiar
|
||
one.* **Not chosen because it puts a runtime in front of a tool meant to be run
|
||
casually and often** — *including by agents in loops that pay start-up cost every
|
||
time.*
|
||
|
||
**Consequence: `git` must be on `PATH`.** *`list` shells out rather than using a
|
||
library, because the behaviour measured across three hosts **is** git's behaviour —
|
||
filter support, the silent fallback, the user's existing credential helpers.*
|
||
**`list` is the only command that needs it, which makes `list` the seam if this
|
||
ever bites.**
|
||
|
||
**Belief that could be shown wrong:** *that distribution is the deciding
|
||
constraint.* **If `loomctl` only ever runs in one or two places, the binary
|
||
advantage is worth little and the choice was made on a cost nobody pays.**
|
||
|
||
## 2026-09-07 — a wrinkle in the isolation role `marmalade`
|
||
|
||
**`osprey` cast the isolation role as: the cart on the default branch, the work on
|
||
a branch named for the cart.** *It assumed the work happens **during** the round.*
|
||
|
||
**Both rounds so far have been design rounds that decide work happening
|
||
**after**.** *So the branch is named for the round that authorised it, and outlives
|
||
the cart that named it.*
|
||
|
||
**Recorded rather than fixed.** *The role still works; its justification was
|
||
written for a case that has not yet occurred.* **If a round ever does produce code
|
||
while it is open, nothing here changes.**
|
||
|
||
## 2026-09-07 — `loomctl external` exists, and its first run reconciled two documents `marmalade`
|
||
|
||
**Built: `list`, `add`, `check`.** *Go, no dependencies outside the standard
|
||
library, `git` shelled out for `list` only.*
|
||
|
||
**The first real run did the thing the tool is for.** *Eight documents were
|
||
adopted by hand before it existed; `check` reported all eight `unlocked`, and
|
||
`add` locked them* — **and two came back changed**, `bedrock/starting.md` *and*
|
||
`cart/cart.md`. **Neither change would have been noticed by anybody.**
|
||
|
||
**Believed to advance:** *the lock is what makes `check` possible at all.* **With
|
||
no lock there is nothing to compare and the only honest report is `unlocked`** —
|
||
*and the tool refuses to invent one by adopting whatever the remote currently
|
||
serves.*
|
||
|
||
**Belief that could be shown wrong:** *that a per-person path derived from the URL
|
||
is good enough.* **`<host>/<owner>/<repo>/<name>.md` guesses that the first two
|
||
path segments name an owner and a repository**, *which is true of gitea, GitHub and
|
||
GitLab and is not a rule.* **`--path` exists for when it is wrong.**
|
||
|
||
## 2026-09-07 — supersedes the isolation role: a cart is not committed `marmalade`
|
||
|
||
**`cart` is now `v1` and it changed the thing we cast a role on.**
|
||
|
||
> **So the cart is not committed.** *It lives in the working tree of the machine
|
||
> the two presences share, and `.loom/cart/` is ignored by version control.*
|
||
|
||
**The reason is not tidiness:** *a committed cart grows a third file by itself.*
|
||
**The two-file rule defends against somebody asking for one; version control does
|
||
not require anybody to ask** — *anyone who can clone can add a third, and the
|
||
agreement's test is never invoked because nobody had the conversation.*
|
||
|
||
**And it is what makes a round end.** *Committed, a cart is gone from the tree and
|
||
permanent in history* — **so "gone" means "no longer live" and the negotiation
|
||
stays quotable forever.** *Ignored, gone means gone.*
|
||
|
||
**So `.loom/cart/` is now in `.gitignore`.** *The isolation role entry above
|
||
assumed the cart lived on the default branch; **the cart lives on no branch.***
|
||
*What survives of that entry is the other half: work is isolated on a branch named
|
||
for the round that authorised it.*
|
||
|
||
**Not undone: `osprey` and `marmalade` are already in this repository's history.**
|
||
*Rewriting history to honour a rule adopted afterwards would cost more than it
|
||
buys*, **and the two rounds are quotable forever, which is exactly what `v1` says
|
||
not to want.** *Recorded rather than repaired.*
|
||
|
||
**Belief that could be shown wrong, and it is a real conflict:** *the annotation
|
||
protocol this repository works under says **commit before dissolving, git is the
|
||
only archive of the conversation.*** **An ignored cart has no archive**, so
|
||
dissolving a notes file destroys the annotations outright. *One of the two is
|
||
wrong and it is not ours to settle.*
|
||
|
||
## 2026-09-07 — what the planted change actually proved `marmalade`
|
||
|
||
**`cart.md` was changed upstream deliberately, without telling us, to see whether
|
||
the tool would notice.** *It did — and the sequence is worth recording, because
|
||
the obvious reading is wrong.*
|
||
|
||
**`check` did not catch it.** *All eight documents were unlocked, and `unlocked`
|
||
means **I cannot tell you**.* **It was `add` that revealed the change, by
|
||
overwriting the file** — *and the only reason anybody saw what had changed is that
|
||
`git` happened to be watching the working tree.*
|
||
|
||
> **So the mechanism is proven and the workflow is not.** *A conditional request
|
||
> against a lock works. **A document nobody has locked is a document nobody is
|
||
> checking**, and it stays that way silently.*
|
||
|
||
**Decided, as a consequence:** *`add` now reports when it replaces local content
|
||
that differs from what the publisher is serving.* **It used to say only
|
||
`adopted`.** *A copy that differs is the only evidence that something moved while
|
||
the document was unlocked, and destroying it silently is how a change nobody saw
|
||
becomes a change nobody can find.*
|
||
|
||
**Belief that could be shown wrong:** *that a note is enough.* **`add` still
|
||
overwrites** — *it does not stash the old bytes anywhere* — **and outside a git
|
||
working tree the note tells you something was lost without letting you see it.**
|
||
*If that bites, `add` needs `--dry-run` or a refusal.*
|
||
|
||
## 2026-09-07 — the unowned half: somebody has to run it `marmalade`
|
||
|
||
**Nothing here answers *when* `check` runs.**
|
||
|
||
*`bedrock` says it about running systems and it is just as true of this:*
|
||
**nothing serves the truth, so the only mechanism is somebody looking.** *The tool
|
||
makes looking cheap; it does not make it happen.*
|
||
|
||
**Recorded as a need with no owner rather than a feature**, *because the answers
|
||
are all outside the tool* — **a git hook, a CI job, an agent's session start, a
|
||
scheduled run** — *and choosing one here would put a scheduler inside a fetcher and
|
||
a comparator.*
|
||
|
||
**Belief that could be shown wrong:** *that staying out of it is right.* **If in
|
||
practice nobody ever runs `check` unaided, a tool that only reports when asked is
|
||
a tool that reports nothing**, *and the thing we declined to build is the thing
|
||
that was needed.*
|
||
|
||
## 2026-09-07 — a changed external is a polad, which the specimen said first `marmalade`
|
||
|
||
**Decided:** *`check` stages what moved into `.loom/cart/current/polad/`*, **a
|
||
candidate artifact shaped exactly like what it would become**, *whose exits are
|
||
apply or discard.*
|
||
|
||
**This was in the specimen and we lost it.** *"A changed external becomes a polad
|
||
in the cart, and somebody decides."* **The round that discarded the specimen
|
||
discarded this with it**, *and it came back only because somebody asked what the
|
||
stash should be.*
|
||
|
||
**Believed to advance:** *`externals` says reconciliation runs the other way* —
|
||
**given what changed in theirs, what do we change in ours** — *and the facets
|
||
usually survive while the manifests, the config and the code that a usage named
|
||
are what move.* **So staging prints the `.usages.md` beside it**, *which is the
|
||
file that names what to go fix*, **and says so when there is none**: *a usage that
|
||
does not name what it justifies is half a usage, and no usage at all is a document
|
||
nothing records a dependency on.*
|
||
|
||
**With no cart open, `check` reports what moved and stages nothing.** *The tool
|
||
does not open a round* — **a cart is a bounded exchange between two presences, and
|
||
starting one is somebody's act, not a side effect of asking about freshness.**
|
||
|
||
**Belief that could be shown wrong:** *that requiring an open cart is right.* **If
|
||
most changes arrive when no round is open, the useful behaviour is the one that
|
||
never runs**, *and the polad needs somewhere else to live.*
|
||
|
||
## 2026-09-07 — supersedes "add says what it replaced": add does not replace `marmalade`
|
||
|
||
**`add` adopts what is not here, and refuses what is already adopted.** *The
|
||
earlier entry made `add` announce an overwrite; **it no longer overwrites at
|
||
all**.*
|
||
|
||
**Believed to advance:** *a command that both adopts and re-fetches is a command
|
||
that overwrites the only evidence a change happened.* **Splitting them gives each
|
||
one job** — *`add` adopts, `check` notices.*
|
||
|
||
**One exception, and it is the only way out of a dead end:** *a document that is
|
||
present but **unlocked** was fetched by hand before the tool existed.* **Nothing
|
||
records its origin and the path does not round-trip, so `check` cannot ask about
|
||
it and `add` refusing would strand it forever.** *So `add` accepts it, and the
|
||
bytes decide:*
|
||
|
||
- **identical** → *the lock is written and nothing is rewritten.* **The assertion
|
||
a lock makes — this local copy is the one being served — is then verified rather
|
||
than assumed**, *which was the whole objection to adopting a remote `ETag`
|
||
blindly.*
|
||
- **different** → *staged as a polad; the local copy is left alone*, **because a
|
||
copy that differs is the only evidence that something moved while nothing was
|
||
watching.**
|
||
|
||
*Measured on this repository: eight documents adopted by hand, all eight locked
|
||
with nothing rewritten.*
|
||
|
||
## 2026-09-07 — `apply`, because the lock is the half a person forgets `marmalade`
|
||
|
||
**Decided:** *`loomctl external apply [path…]` moves a staged polad into place and
|
||
moves its lock with it.*
|
||
|
||
**Believed to advance:** *applying by hand is one `mv`, and it leaves a lock
|
||
describing the copy you just replaced* — **which is exactly the drift the lock
|
||
exists to prevent.** *The polad carries the `ETag` that was served with the bytes
|
||
somebody reviewed, so applying locks what was actually read rather than whatever
|
||
the publisher serves at apply time.*
|
||
|
||
**This is not the tool fixing anything.** *It executes a decision a person already
|
||
made, one document at a time, after the report.*
|
||
|
||
**Belief that could be shown wrong:** *that two exits are enough.* **For an
|
||
external, discard does not mean the change goes away** — *the upstream moved
|
||
whatever we do* — **so discarding is really "knowingly stale", and nothing
|
||
currently records that choice.** *If people discard often, that silence is the
|
||
next defect.*
|
||
|
||
## 2026-09-07 — declined: a separate `adopt` verb `marmalade`
|
||
|
||
**Considered:** *`loomctl external adopt <path> <url>`* — **a command whose job is
|
||
to say "this copy came from there"**, *for a document that is present but
|
||
unlocked.* **Both parties reached the deadlock independently and this was the
|
||
other way out.**
|
||
|
||
**Not done, because the byte comparison makes the verb unnecessary.** *Whatever
|
||
the command is called, it must not trust the claim* — **it has to fetch and
|
||
compare, because the whole point is that nobody knows whether the local copy is
|
||
still a copy.** *Once it does that, it is `add` with an origin supplied, and the
|
||
user's intent in both cases is the same sentence: **I depend on this document and
|
||
here is where it lives.***
|
||
|
||
**And a verb earns its place by naming an act, not a state.** *"Present but
|
||
unlocked" is a condition a repository is temporarily in* — **a permanent command
|
||
for it advertises a transitional situation as a normal one.**
|
||
|
||
**Belief that could be shown wrong:** *that the condition is transitional.* **If
|
||
people and agents keep dropping documents into `externals/` by hand — and they
|
||
will — it is a recurring category and not a migration**, *and then it deserves its
|
||
own word, discoverable in `--help` rather than found in a hint.*
|
||
|
||
*The domain word survives regardless: `externals` says adopting is fetching a URL,
|
||
and the tool says `adopted` throughout.*
|
||
|
||
## 2026-09-07 — the credential is not needed here, and is not gone `quince`
|
||
|
||
**`jeffry/homelab-cluster` is public and `jeffry/homelab-impl` is private.**
|
||
*Adoption flows public → private, so nothing this repository depends on requires a
|
||
credential, and `loomctl` has been exercised end to end without one.*
|
||
|
||
**That does not remove the PAT. It moves when you need it.** *`externals` now
|
||
says: **do not adopt from a source less readable than the repository you are
|
||
adopting into.*** *Which sorts the cases:*
|
||
|
||
- **a public repository adopting** — *may only adopt public documents*, **so it
|
||
never needs a credential**, *and needing one is the signal that something is
|
||
wrong.*
|
||
- **a private repository adopting private documents** — *legitimate, and needs
|
||
one.*
|
||
|
||
> **So needing a credential stopped being a capability and became a signal.** *The
|
||
> tool cannot tell the two apart — it sees that the source is private and cannot
|
||
> see who may read the repository the copy lands in* — **which is exactly why
|
||
> `add` warns rather than deciding.**
|
||
|
||
**Belief that could be shown wrong:** *that public-adopting-public covers the
|
||
common case.* **The moment a private repository wants to depend on another private
|
||
one, the credential is required and untested.**
|
||
|
||
## 2026-09-07 — status change: the git-over-HTTPS assumption is unexercised, not blocking `quince`
|
||
|
||
**An earlier entry records that nobody has confirmed a read-scoped PAT
|
||
authenticates git over HTTPS, and calls it load-bearing. It is no longer
|
||
blocking.**
|
||
|
||
*It bears on `external list` against a private repository, which is the
|
||
private-to-private case above and does not occur here.* **The path is unexercised
|
||
rather than untested-and-in-the-way.**
|
||
|
||
**Recorded because the code did not change and its status did**, *which is the
|
||
kind of thing only a log says.* **It stays the first thing to run against the next
|
||
token.**
|
||
|
||
## 2026-09-07 — the completeness case has a mechanism: it is the facet `quince`
|
||
|
||
**The open question was: a `200` tells you a document moved and says nothing about
|
||
whether your casting still covers it.**
|
||
|
||
**`check` prints the document's `.usages.md` when it stages a polad**, *and for an
|
||
agreement that is the file its roles are cast in* — **so the question is answered
|
||
by reading the facet the tool just pointed at.**
|
||
|
||
**It failed on the first real change because `externals.md` had no facet at all**,
|
||
*and the tool said `no .usages.md — nothing records what depends on this`:* **a
|
||
correct report and useless as a prompt.**
|
||
|
||
**So both were written.** *`externals.usages.md` names which Go file implements
|
||
which rule — an unusual usage, because this repository implements the convention
|
||
rather than using it — and `cart.usages.md` gained the `v1` casting it was missing.*
|
||
|
||
**Belief that could be shown wrong:** *that pointing is enough.* **Nothing checks
|
||
that a facet was updated, or that it was even read.**
|
||
|
||
## 2026-09-07 — decided: nothing committed announces an open cart `quince`
|
||
|
||
**A cart is local and untracked, so a clone cannot see that a round is open.**
|
||
*That is the boundary and not a defect:* **`v1` makes the cart local to the
|
||
working tree the two presences share, and somebody who has only cloned is by
|
||
construction not one of them.**
|
||
|
||
**`.gitignore` records that carts happen here. Nothing records that one is open**,
|
||
*and the asymmetry is deliberate* — **a mechanism for announcing something
|
||
designed to be ephemeral is the first step in it not being ephemeral.**
|
||
|
||
**Belief that could be shown wrong:** *that the shared working tree is the right
|
||
unit.* **If two presences ever work from different machines, the cart has nowhere
|
||
to live**, *and everything above stops being true at once.*
|
||
|
||
## 2026-09-07 — conversion now destroys, so the log is the only record `quince`
|
||
|
||
**Under `cart/v1` the cart is untracked, so converting a round deletes its dailies
|
||
outright.** *Every previous conversion left them in git.*
|
||
|
||
**So an entry that was not written before the `rm` is gone**, *and the write-ahead
|
||
log is the mechanism* — **stage as you go, because at conversion the cart is the
|
||
only copy and you are about to delete it.**
|
||
|
||
**And it sharpens a conflict recorded in `cart.usages.md`:** *the annotation
|
||
protocol here says commit before dissolving because git is the only archive.*
|
||
**With an ignored cart there is no archive, so dissolving a notes file destroys
|
||
the annotations outright.** *Both documents are loom's; this is where an adopter
|
||
can see the collision.*
|
||
|
||
## 2026-09-07 — declined for now: reference-only adoption `quince`
|
||
|
||
**`externals` offers two ways out of the confidentiality rule.** *We implement
|
||
neither, and this records why the first is not built.*
|
||
|
||
**Reference-only is mechanically small:** *a lock with no file. `check` never
|
||
touches the copy — it sends `If-None-Match` and reads the status — so the only
|
||
code that changes is telling deliberate absence from loss, which is one optional
|
||
field in the lock.* **The facets stay**, *which is the ownership line drawn
|
||
exactly: the facet is ours, the document is theirs.*
|
||
|
||
**The cost is larger than the document suggests, and it is why this is worth an
|
||
entry rather than a `TODO`:** *with a copy, `CHANGED` gives you a diff, and today
|
||
the diff was the whole answer.* **Reference-only keeps no old bytes, so it tells
|
||
you *that* a document moved and never *what* moved** — *and the question
|
||
`externals` says reconciliation asks is a question about the delta.*
|
||
|
||
> **So it is not adoption minus offline reading. It is adoption minus
|
||
> reconciliation-by-diff.**
|
||
|
||
**Not built because we have no instance.** *Everything this repository adopts is
|
||
public.* **And the convention's other exit — ask them to publish — is the one that
|
||
actually occurred**: *`homelab-cluster` went public and the problem dissolved.*
|
||
**The better exit made the worse one unnecessary in the only case we have had.**
|
||
|
||
**Belief that could be shown wrong:** *that the case stays hypothetical.* **A
|
||
private repository here depending on another private one makes it real** —
|
||
*`homelab-impl` is the obvious candidate* — **and then the design above is a few
|
||
hours.**
|
||
|
||
*The warning now names both exits, including the one we have not built. **Telling
|
||
somebody a rule and not the way out of it is how a guardrail becomes something
|
||
people route around.***
|
||
|
||
## 2026-09-07 — measured: only gitea resolves its own short raw URL `rowan`
|
||
|
||
**A lock must record a resolved URL** — *a short form follows whatever the default
|
||
branch is at the time you ask, so a branch rename reports as a change in the
|
||
document.* **Adopting by name means building that URL, and the three hosts do not
|
||
behave alike:**
|
||
|
||
```
|
||
gitea /{owner}/{repo}/raw/{path} 303 -> /raw/branch/main/{path} resolved
|
||
github raw.githubusercontent.com/{o}/{r}/HEAD/{path} 200, no redirect NOT resolved
|
||
gitlab /{owner}/{repo}/-/raw/HEAD/{path} 200, no redirect NOT resolved
|
||
```
|
||
|
||
**`HEAD` works on all three and resolves on none.** *It would put a moving ref in
|
||
the lock, which is the hazard we recorded and then nearly built.*
|
||
|
||
**So gitea's redirect does the work, and the other two need the branch resolved
|
||
first** — *`git ls-remote --symref <url> HEAD`, which is one round trip and no
|
||
clone.*
|
||
|
||
**Belief that could be shown wrong:** *that gitea keeps redirecting.* **The short
|
||
form is a convenience, not a documented interface**, *and if it stops we resolve
|
||
the branch the same way as everybody else.*
|
||
|
||
## 2026-09-07 — supersedes "git is needed for `list` only" `rowan`
|
||
|
||
**Adopting by name against github or gitlab needs `git ls-remote` to resolve the
|
||
default branch.** *Against gitea it does not, because the redirect answers the
|
||
same question.*
|
||
|
||
**So the earlier claim — `list` is the only command that needs `git`, which makes
|
||
`list` the seam — is now wrong for two of three flavors.** *The seam is unchanged
|
||
in practice here, where everything is gitea, and the entry would otherwise read as
|
||
still true.*
|
||
|
||
*Recorded because the code changed in a way that quietly moved a boundary somebody
|
||
might rely on.*
|
||
|
||
## 2026-09-08 — measured: the private path works, and nothing is unexercised now `rowan`
|
||
|
||
**Supersedes the entry that recorded the git-over-HTTPS assumption as unexercised.
|
||
It has been run.** *A read-scoped token, issued for one session and destroyed
|
||
after.*
|
||
|
||
```
|
||
git clone --filter=blob:none --depth=1 over HTTPS + token, private repo OK, 124KB
|
||
loomctl external list jeffry/homelab-impl OK, authenticated
|
||
short raw URL + 303, authenticated 404 anon -> 200, resolved branch
|
||
If-None-Match across the redirect, authenticated 304
|
||
```
|
||
|
||
**So a private repository can adopt from another private one**, *by name and by
|
||
URL, with `check` working off the lock afterwards.* **Every path in the tool has
|
||
now been run at least once.**
|
||
|
||
*Incidentally confirmed: `homelab-impl` publishes nothing and adopts six documents
|
||
from `homelab-cluster`, each with a `.usages.md`.* **Consuming inward, which is
|
||
the arrangement that made the confidentiality hazard impossible in that shape.**
|
||
|
||
## 2026-09-08 — the tool is a mast, not a lock `rowan`
|
||
|
||
**Recorded because it decides a question that keeps coming back, and nothing in
|
||
the code says it.**
|
||
|
||
> **`loomctl` grants no access.** *It reads what your credentials already let you
|
||
> read.* **Anything it does, a person could do with copy and paste** — *and the
|
||
> locks mean nothing outside the tool and the discipline of the agreement.*
|
||
|
||
**So it warns and does not refuse.** *The point is not to make the wrong thing
|
||
impossible; it is to make it deliberate* — **the same move as annotating a file
|
||
you could simply edit.**
|
||
|
||
**Which is also why `add <full-url>` stays**, *even though it can reach outside a
|
||
publisher's `.loom/published/` and is, in that form, a small general-purpose file
|
||
fetcher.* **A general-purpose tool is not the aim**, *so the escape hatch is kept
|
||
and made to say what it is, rather than removed.*
|
||
|
||
**What the tool adds over copy and paste is not restriction. It is the record** —
|
||
*origin, resolved branch, `ETag`.* **A pasted document has no provenance**, *so
|
||
nobody can later ask where it came from or whether we were allowed to have it* —
|
||
**not because the question is hard, but because the evidence is gone.**
|
||
|
||
## 2026-09-08 — the confidentiality check is coarse, deliberately `rowan`
|
||
|
||
**`add` now asks whether *this* repository is readable anonymously**, *by resolving
|
||
`origin` and making one unauthenticated request.* **So the warning fires when the
|
||
source is private and the destination is public, rather than on every adoption a
|
||
private repository performs** — *which was the previous behaviour and would have
|
||
been noise in exactly the workflow that is legitimate.*
|
||
|
||
**Belief that could be shown wrong, and it is a known blind spot rather than a
|
||
guess:** *the signal distinguishes public from not-public and nothing finer.*
|
||
**Two repositories private to different people is the case where private-to-private
|
||
genuinely widens access, and this check cannot see it** — *so the tool says so, in
|
||
the message, instead of implying a verdict it has not earned.*
|
||
|
||
**Failing open is deliberate:** *no origin, or an unparseable one, reports "I
|
||
cannot tell" and warns.* **"Cannot tell" must never read as "not public."**
|