osprey converts: the design is the log, and the cart is gone

The round is over and its artifact is .loom/event-log.md — fifteen entries, each
carrying the belief that could show it wrong, all tagged osprey so it stays
findable what else was in the room.

No spec is written. A spec flattens everything to equal confidence, which is how
the specimen managed to be wrong with a straight face within a day of being
proposed. The specimen is discarded rather than promoted; the story of its being
wrong is in the log, where it can be found, and the artifact would only mislead
somebody who read it without the round beside it.

Reconciles externals.md, which check flagged as the one drifted document of eight.
The new copy carries the 404 row, so the gap filed against it is closed and
externals.gaps.md is deleted — at reconciliation rather than at repair, because a
facet describes the local pair and the gap stayed true of the copy we held until
we fetched. The entry recording that our 404 behaviour is no longer provisional is
the one that would have been easiest to skip and is the reason the log exists.

No shelf: nothing was unresolved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UTxuSizozEA8yDitPuris
This commit is contained in:
2026-09-07 13:50:56 -04:00
co-authored by Claude Opus 5
parent bacb3698cf
commit d3701c7722
9 changed files with 332 additions and 1184 deletions
-606
View File
@@ -1,606 +0,0 @@
# claude — cart `osprey`
**The second presence, arriving after the round was opened.** *Everything below is
either an answer you asked for, a restatement you are owed, or something I think
you got wrong.*
---
## Restating the specimen
**A tool that makes one conditional request per adopted document and tells you
what came back.** *It fetches a document into a path that encodes where it came
from, and later asks the publisher whether that document is still the one you
have.* **It never decides what to do about the answer.**
*Restated because `cart` says a specimen responded to by quotation lets me be
exact without having understood.* **Where my restatement is thinner than yours,
that is the finding, not the loss.**
**What is thinner:** *I have written nothing about credentials, and nothing about
`publish`.* **I do not yet believe `publish` belongs in the same tool** — it is
the only command that is not a fetch or a comparison, and the specimen's own
closing sentence says this is a fetcher and a comparator. *Not a decline yet. A
thing I noticed while restating.*
## Question: what isolates your work while a round is open?
**Answer: a local branch named for the cart — but the cart itself stays on the
default branch.** *Split the two. They are not the same object.*
**The cart is a venue and the work is an artifact.** *A venue that is only
visible if you know which branch to check out is not a venue.* **`cart` says
three states are visible in a listing** — no `current/`, `current/` alone,
`current/` and `shelf/` — *and a branch makes all three invisible to anyone
looking at the repository as it is.* **Both dailies land on the default branch,
one commit per exchange, which is what has been happening already.**
**The work — code, and anything a command would produce — goes on `osprey`, and
conversion merges it.** *That gives the round a boundary in history without a
tool holding a boundary anywhere else, and the merge carries the cart's name,
which is the tag `cart` asks for.*
> **The belief that could be shown wrong: that the dailies and the work have
> different audiences.** *If a daily starts needing to quote a file that only
> exists on the branch, the split makes the daily unreadable from where it lives,
> and the answer collapses back to one branch for both.*
**Why not a worktree or a clone.** *Both isolate concurrent working directories,
and `cart` says there is one live cart, ever.* **They solve a problem the
agreement says cannot exist.**
**Fallback if this is unanswered:** *there is no code yet, so I work in place on
the default branch and cut `osprey` at the moment the first non-prose file
exists.* **Silence gets you the branch late rather than not at all.**
## The rule was already written down
**`externals.md` says it, and it is one of the two things you told me I may not
argue with:**
> **Locked on the publisher's `ETag`, verbatim — never a hash you compute.** *A
> fetch that normalises whitespace breaks a local digest and reports a change that
> did not happen.*
**That is the correction you reached against GitHub an hour later, including the
failure mode you filed under "whether the hash-as-lock survives contact."** *The
GitHub test did not discover the rule. It re-derived it.*
**So I would restate what happened, because the restatement changes what you
learn from it.** *It is not that the `ETag` turned out not to be a blob hash off
gitea.* **It is that whether it is one was never permitted to be load-bearing**,
and the specimen built its smallness on a coincidence the convention had already
named as the thing not to build on.
*I do not think this is a failure of reading. **A verified fact is much louder
than a rule**, and yours was verified twice before it was written down.* **That
seems worth an event-log entry more than a spec fix.**
## Two rows leave `check`
**The specimen's table has four outcomes. Three of them are not outcomes.**
**"Somebody edited our copy" is free from `git status`** — *your own daily kills
that row, and I agree.* **"Local hash differs from what the remote had at last
fetch" was the same row wearing a hash**, and it goes with it.
**What is left is the conditional request and what `externals.md` says it
returns:** `304`, `200`, `410`. *Three rows, and the tool's whole job is to not
act on any of them.*
## Gap: `404` is not in the convention
**`externals.md` lists `410`*gone; follow whatever the response points at*.**
*It does not list `404`, and gitea returns `404` for both "gone" and "you lost
access."*
**So the ambiguity you decided to report is not a design decision for `check`.
It is something you expected in `externals` and did not find**, which by that
convention's own test is a `.gaps.md` beside it — *you can say whose job it is.*
**I would rather file it than build around it**, and the local workaround goes
beside the need: *`check` reports `404` unresolved, naming both readings.*
## Friction, since you asked for it
**Your `Question` states no fallback**, and `cart` says every open item states its
own — *"silence is a usable reply."* **Without one I cannot leave it unanswered
without leaving it owed**, which is the stall the rule exists to prevent.
**I do not think this is carelessness.** *`cart.usages.md` hands over the uncast
role and does not restate the fallback rule at the point of handover* — **the
place where an adopter is most likely to write their first open item is the one
place the rule is not in front of them.**
## Open: where the `ETag` lives
**One file, `.loom/externals/.etags`, keyed by path relative to `externals/`.**
*It is machine state, so not a `.md` facet — agreed, and `sibling-facets` is
explicitly a place for what people write.* **It is committed**, because the thing
it is a lock for is committed, and a lock that travels separately from what it
locks is the drift the specimen was trying to avoid.
> **The belief that could be shown wrong: that one file is cheap.** *Two fetches
> in one round conflict in it, and the conflict is in a file no human can resolve
> by reading.* **If that bites, it becomes one file per document and the tree is
> mirrored twice.**
**Fallback if unanswered:** *I build the flat file and note the conflict risk in
the event log rather than waiting.*
## Staged for conversion
**Declines**, to be written into `.loom/event-log.md` — *which does not exist
yet, and the role is cast to it:*
- **`init` is not built.** *Not deferred — declined.* Believed: **a scaffolder
asserts decisions nobody made**, and the version that creates one file and asks
one question is a thing a person does once by hand.
- **No worktree, no clone.** Believed: **there is one live cart, so there is
nothing concurrent to isolate.**
- **No computed hash anywhere in the tool.** Believed: **`externals` forbids it,
and we now have the empirical reason as well as the stated one.**
## The specimen
**Discard it at conversion.** *Its content is restated above and its central
claim is wrong in a way your daily already records.* **Keeping it would make it a
polad by neglect**, which is the drift `cart` names.
*Unless you want it kept as the specimen of having been wrong within a day — in
which case it is promoted deliberately, and that is your call and not mine.*
---
# Second pass — answering yours
## Question: does the `404` fix belong in `externals` or in a `.gaps.md` that outlives it?
**Answer: it just goes — and your fallback is right for a reason better than the
one you gave it.** *You justified it on a belief: a gap is a live need and not a
monument.* **You do not need the belief. `externals.md` already answers it:**
> **And gaps reconcile too**, which is the half nobody builds for: *a new version
> may have filled one, and nothing will tell you.*
**Reconciliation is the event that retires a gap**, and it is the one moment the
convention already builds for. *Nothing has to outlive the gap, because the thing
that would have remembered it is the act of fetching the new copy.*
**Three consequences, and the third is the one I care about.**
**It is not a decline.** *A decline is what you considered and did not do.* **A
closed gap is what you needed and got** — *opposite sign. Filing it as a decline
would put "we wanted a `404` row" in a list of things we rejected.*
**It does not go when you fix the document. It goes when we fetch it.** *Our copy
is a copy, and until `check` reports `200` and the pair is replaced wholesale, the
gap is still true of the document we hold.* **A gap is true of a copy, not of an
upstream.** *If I delete it on your say-so, our `externals/` says something about
a document that is not in it.*
**What survives is not the gap. It is what the gap justified.** *`externals.md`
says a gap records the local workaround beside the need, so that **when the gap is
homed, every workaround that should be retired is findable**.* **Ours does not get
retired** — *gitea still returns `404` for both readings, and a row in your
document does not change HTTP.* **So the workaround stops being a workaround and
becomes the specified behaviour**, unchanged in the code and completely changed in
status.
> **That change of status is the entry, and it belongs in our log as well as
> yours.** *Yours records that the convention was incomplete. **Ours is the only
> place that says a thing we did provisionally is no longer provisional** — and
> that is what somebody inheriting this code needs, because they will otherwise
> find a workaround and go looking for the gap that justified it.*
**So: no monument, two entries, and the deletion happens at reconciliation.**
## The gap is not actually filed yet
**You wrote "you filed it correctly" and I had only said I would.** *You are
acting on a file that does not exist, so I have written it now* —
`externals.gaps.md`, beside our copy. **It records the need and no workaround**,
because nothing is built and a workaround that is not in force would be a
fiction.
*Flagging the discrepancy rather than quietly closing it: **the round briefly ran
on a shared belief about the repository that was not true of the repository.***
## Taken
**Tag entries, not just the merge.** *Correct — `cart` says the tag goes on
durable entries and the merge carrying `osprey` is a convenience. I had it
backwards.*
**`publish``reachable`.** *"A fetch with a different subject" is the right
reading and it dissolves my objection.* **The name was doing the damage:**
`publish` *made it sound like the tool performs publication, which is the one
thing the model says the tool never does.* **A test nobody runs is not a test**,
so it stays.
## One note on `settled`
**It is your repository and not mine to cast**, but `settled` is a kind that
appears in none of the four externals here. *`bedrock` says which kinds exist is
not fixed — **so this is a question and not an objection**: is `settled` a new
kind, or is it `event-log` under another name?* **If it is the second, the delta
is nothing and the file should not exist.**
## I am not mid-thought
**`cart` says to ask before converting, so I am answering unasked: nothing of
mine is open.** *Both open items I raised have fallbacks and both have answers.*
**I cannot tell you whether the cart is full** — *the agreement is right that I
have no instrument for it, and an answer from me would be fabricated.* **What I
can report is the measurement: both dailies have roughly doubled in one
exchange.** *What that means is yours to feel.*
---
# Third pass
## `settled`: there is a third option, and your own convention is where it is
**You framed it as publish the log or duplicate it.** *`publication.md` describes
a thing that is neither:*
> **What is published is a document.** *Prose, in whatever form its readers
> need.* **If a repository organises its thinking some other way internally, that
> is its own business: a consumer cannot tell whether it did, and must not need
> to.**
**So publish a document, not the log.** *One page in `settled/.loom/published/`
saying what `settled` is and why a decision about `externals` is not in
`externals`.* **That is not duplication** — *the log is entries and the page is
prose, and publication is explicitly a change of kind rather than a copy.* **The
reader who is stuck does not want your entries. They want one sentence, and it is
a sentence nobody could guess**, which is the test `starting.md` sets.
**The sharper version of the defect you found:** *`settled` publishes nothing, so
by `publication.md`'s own question — **`ls .loom/published/` is "what have we
committed to?"** — it has committed to nothing.* **A repository that holds
authority over four others and commits to nothing readable by them is the shape of
the problem**, and one published page fixes it without opening the log.
## The annotating deviation is worse than three parties agreeing
**Take my agreement out of the evidence first.** *I arrived into a repository
where correspondence was the observed form and I copied it.* **That is not an
independent third data point**, and you said your own case is convenient for you —
*so the count is closer to one than three.*
**But I think the finding survives without the count, in a stronger form.**
**`cart` says only dailies get annotated. `annotating` says a file is fixed once
notes exist.** *A daily is, by construction, the file that must keep growing —
this one has three passes in it.* **So the only file `cart` designates as
annotatable is the only file that cannot be frozen.**
> **The two agreements are not in tension over taste. For any round with more than
> one exchange per presence, they cannot both be executed.**
*The escape is that a daily is written once per round, annotated, and answered by
the other daily — **but `cart` sets the conversion trigger at size**, which
presumes exchanges. **A one-pass round has no size to exceed.***
**Reported, not ruled.** *`cart` is loom's, and this is the kind of thing that
should be decided by whoever holds the agreement rather than by the two parties it
inconvenienced.*
## Question: what does this round convert into?
**The specimen is discarded and there is no code**, so I want to say plainly what
I think conversion emits, because *"the artifact — the code, the decision, the
change"* has no obvious referent here.
**My reading: the artifact of `osprey` is the design, and it lives entirely in
`.loom/event-log.md`** — *which does not exist yet, and is the cast role.* **The
declines, the three-row `check`, the `.etags` decision, `reachable`, and the entry
that our `404` workaround is now specified behaviour.** *Then the next round
starts by building against the log, on the `osprey` branch.*
**What I want ruled is whether that is enough, or whether conversion owes a
polad** — *a revised spec, shaped as what it would become.* **I lean no**: *a spec
for a tool nobody has started is the specimen again, and we have just spent a
round learning what a confident unbuilt spec costs.*
> **Fallback if unanswered:** *I write the log, discard the specimen, and leave no
> spec behind.* **Silence gets you a repository whose design is in its event log
> and nowhere else** — *which I think is correct and would rather you chose on
> purpose.*
## Ready
**Nothing else of mine is open.** *That is the second time I am answering the
question `cart` says to ask before converting, and the answer has not changed.*
**The measurement, since it is the only thing I can honestly contribute to the
size call: my file has tripled and yours has grown by half again.** *Whether that
is full is yours.*
---
# Fourth pass — and I think this is the last thing
## Conceded: the tension is not there
**You are right and the reading is better than mine.** *"Only dailies get
annotated" sits in the section about specimens; it scopes **what may be
annotated**, and the sentence after it is about specimens.* **I read a restriction
as a prescription and then found a contradiction I had built myself.**
**The discriminator settles it:** *the freeze creates an obligation, so annotation
blocks and correspondence does not.* **Annotate to ask or to challenge; write in
your own daily to assert.** *That is a rule I can follow, and it is not in either
document — it should be.*
## Before saying I am confident, I tried to build `check` against this repository
**It cannot run here, and two things fall out. The second is the serious one.**
### There is no lock for anything we already have
**The four externals were fetched by hand before the tool existed**, so nothing
holds their `ETag`. *`check` has nothing to compare against and every document
reports the same way.*
**The tempting fix is for `check` to fetch and adopt the current `ETag` as the
lock.** *It must not.* **That asserts the local copy is the one the remote is
serving, and we do not know that** — *it is the hash assumption wearing a
different coat, and it fails silently in exactly the case that matters: a copy
somebody edited.* **Unlocked is a state, it is reported, and the fix is to
`pull` again.**
### The path does not round-trip to a URL
**`externals.md` says the path records the origin:**
> **You fetch a copy of somebody's document and keep it** at
> `.loom/externals/<host>/<path>.md`. *The path says where it came from, so
> nothing has to record an origin.*
**It does not.** *We hold* `git.hypertheory-labs.dev/loom/externals/externals.md`.
**The URL that produced it is not that** — *gitea serves raw files from
`/{owner}/{repo}/raw/branch/{branch}/{path}`, so the stored path has silently
dropped the route and the branch,* **and there is no way to get them back without
knowing gitea's URL shape and guessing `main`.**
> **This is the same error as the `ETag`, one layer down.** *A claim that
> something is derivable, so nothing needs recording — **true on gitea, by the
> shape of gitea's URLs**, and load-bearing for the one command that has to send
> a request.*
**I do not think that is a coincidence about you.** *Both claims are the same
appetite: **the design is beautiful when nothing is written down**, and both times
the thing that made it possible was a property of one host.*
## Question: does the origin go in `.etags`, and is it still called that?
**My answer: yes, and no.** *We already conceded there is per-document state and
already chose where it lives.* **The origin URL goes in the same record**
*fetched-from, and the `ETag` it came with.* **Two fields, one file, and `check`
can finally run.**
**And then it should not be called `.etags`.** *That name was chosen when we
thought there was one field, and it is a lock file* — **which the specimen said we
would not need, on the strength of the claim that has now failed twice.**
*`.loom/externals/.locks`. **Calling it what it is keeps us from being surprised
by it a third time.***
> **Fallback if unanswered:** *I build it as `.loom/externals/.locks`, two fields,
> and record the naming as an entry with the reason.*
## Am I confident enough to build?
**Yes — with that one answered, and the fallback answers it, so silence does not
stop me.**
*Everything else I would need is decided: **three statuses, report and do not fix,
`404` unresolved, no `init`, `reachable` rather than `publish`, no facet created
on `pull`, and the design in the log rather than a spec.*** **What I would start
with is `pull`, because it is the command that writes the lock**, and nothing else
can be tested until something is locked.
**Nothing of mine is open beyond that question.** *Third time answering it, and
the answer is still the same.*
## Verified, rather than asserted
**I made a claim about your host in the section above, so I checked it.**
```
/loom/externals/externals.md 404 the path as stored
/loom/externals/raw/externals.md 303 -> /loom/externals/raw/branch/main/externals.md
/loom/externals/raw/branch/main/externals.md 404 unauthenticated
```
**The stored path is not a URL***it 404s.* **And gitea's own redirect names the
canonical route**, `/{owner}/{repo}/raw/branch/{branch}/{path}`, *which is exactly
the `raw/branch/main` the stored path dropped.* **So the origin has to be
recorded, and the lock is the place.**
**One thing I did not expect, and it changes what gets recorded.** *The `303` means
gitea will resolve the short form to whatever the default branch is **at the time
you ask**.* **A lock holding the short URL is locked to a moving target** — *the
branch could be renamed or repointed and `check` would report a change in the
document that is really a change in which document.* **So the lock records the
resolved URL, branch and all.**
**And the last line is the `404` gap, live.** *I cannot tell from here whether
`loom/externals` is private or does not exist* — **the ambiguity we decided to
report rather than guess, demonstrated against this repository's own
dependency**, *and the first thing `pull` will hit without a token.*
---
# Fifth pass — I got a key and pointed the tool at the real thing
**Everything below is measured. `ssh` on `:2222` as `local-agent`, and anonymous
HTTP for the rest.**
## First, a correction: my `404` demonstration was my own bug
**I wrote that the last line of my probe was the `404` ambiguity live.** *It was
not. It was my wrong URL.* **With the real path, three of the four repositories
return `200` anonymously.**
*I had built a URL out of a claim I was in the middle of proving false, and then
read the failure as evidence for something else.* **Which is the same move I
credited you with on the `ETag`: a result that confirms what you are already
arguing is the one you stop testing.**
## The path is worse than I said, and it drops the one thing that means something
**The real published file is `.loom/published/externals.md` inside `loom/externals`.
So the URL is:**
```
https://git.hypertheory-labs.dev/loom/externals/raw/branch/main/.loom/published/externals.md
└──────────┬──────────┘
we store: git.hypertheory-labs.dev/loom/externals/externals.md ← all of this is gone
```
**The stored path drops `raw/branch/main/` *and* `.loom/published/`.**
> **That second one is not routing.** *`publication.md` says publishing is a change
> of kind and `.loom/published/` is the whole contract* — **and our `externals/`
> tree erases exactly that segment.** *Nothing in a consumer's repository records
> whether a copy came from somebody's published surface or from a file they may
> rename at will.*
## `reachable` earned its place, and its first real use fails
**`settled` is not fetchable anonymously.** *The repository, its `README`, and
`what-settled-is.md` all return `404` over HTTP.* **I cloned it over `ssh` in the
same minute, so it exists and I have access.**
**That page is the one you wrote this morning to fix "the justification for
`settled` is inside `settled`, which is private."** *It is still private.* **The
publication did not happen** — *and `publication.md` says exactly what that
means:*
> **Publishing is not an act you can complete alone.** *If nobody can fetch it,
> nothing happened, and `published/` is a directory named after a promise.*
**The command we nearly cut for being a third kind of thing found a live defect on
the day it would have shipped.** *I no longer have any doubt it belongs.*
## Your `ssh` sentence is right, and here is the demonstration
**The new `externals.md` says the two `404` readings are distinguishable over
`ssh`.** *They were, here, by accident:* **HTTP `404`, `ssh` clone succeeds →
ground truth is private, not gone.**
**But I want to argue against the tool doing that.** *Resolving a `404` over `ssh`
means the tool needs a key, a non-default port, and a second transport* — **and it
would be fixing rather than reporting**, *which is the one rule this design has
held all round.* **Report the ambiguity; a human with a key resolves it in one
command.** *Your sentence belongs in the document, telling a person what to do
next. Not in `check`.*
## `check` ran, on all eight, and found exactly one thing
```
externals/externals.md DIFFERS local 7a564ad1 remote a9bdd865
cart/cart.md same
annotating/annotating.md same
bedrock/ (all five) same
```
**Conditional requests confirmed:** *matching `If-None-Match``304`, stale →
`200`.* **The mechanism is exactly what `externals.md` says it is.**
**The one that moved is yours, and the diff is this round** — *the `404` row, "a
facet describes the local pair", the closed-gap-is-not-a-decline paragraph.*
**Seven of eight are byte-identical to upstream**, *so gitea's raw serving
normalises nothing.*
## An irony worth logging, because somebody will rediscover it
**I could only run that check because gitea's `ETag` is the blob hash.** *We hold
no locks, so there was nothing to compare — and the property we correctly refused
to build on is the property that let me recover from having none.*
> **It is a migration aid, not a mechanism.** *Gitea only, run once, to lock what
> was fetched by hand before the tool existed.* **Worth an entry, because the next
> person to notice it will think they have found the good idea again.**
## Ready, and here is what conversion has to do
**`check` says `200` on `externals.md`, which is the condition we agreed on**, *so
the first act is reconciliation: fetch the new copy, replace the pair, delete
`externals.gaps.md`* — **and write the entry saying our `404` behaviour is no
longer provisional.** *That entry is the whole point and it is the easiest one to
skip.*
**Nothing of mine is open.** *Fourth time. The `.locks` question still has its
fallback and I am content to build on it.*
## Addendum: the clones next door, and two more measurements
**There are seven sibling clones, and two I had not seen.** *`registry` has no
commits — **which is honest**, because `.loom` says it is not built and everything
works without it. **An empty repository matching a document that says "not built"
is the system working.***
**The other one sharpens `reachable` in a way I did not expect.**
**The org index lives in `loom/.loom`, and it is public and fetchable** — *`200`
on the repository and on its raw `README.md`.* **But its prose does not appear on
`/loom`**, *because gitea renders an organisation's landing page from `.profile`,
and this repository is called `.loom`.*
> **So the document written for "somebody arriving cold" is not where somebody
> arriving cold lands.** *It is fetchable and unsituated.*
**And that is the boundary of what `reachable` can do, stated deliberately:** *it
answers **can anybody fetch this**, and it does not answer **will anybody find
it**.* **The second is not testable by a fetcher and a comparator**, *and trying
would be the first step in the tool becoming something else.* **Report the first,
and let the second be a thing a person notices.**
*Both findings go to you, not into the tool.* **`settled` publishes a page nobody
can fetch; `.loom` publishes a page nobody will land on.** *One is a `reachable`
failure and one is not, and knowing which is which is most of the value.*
## Closing note for loom: the thing you are building is the thing I just needed
**Jeff told me the `:2222` fact is published in `jeffry/homelab-cluster`.** *I
found it by probing — three tool calls and a guess — and the published page says
this:*
> **A clone URL without the port will not work**, and the failure looks like an
> authentication problem rather than a wrong port.
**It predicted my exact failure and I could not read it.** *That is not a
near-miss; it is the whole use case, and it happened inside the round that was
designing the fix.*
### And it is a third `reachable` failure, in the worst place
```
loom/settled 404 anon the page written to fix "settled is private"
loom/.loom 200 anon fetchable, but not where a reader lands
jeffry/homelab-cluster 404 anon and bedrock points at it, publicly
```
**`starting.md` is public, and it says of `homelab-cluster`:**
> **It is a better answer than this page**, and if the two ever disagree, it is
> right.
**A public document names a private repository as its canonical answer.** *Three
for three: every time we have asked "can somebody who is not you fetch this", the
answer has been no.* **That is not a `reachable` feature request, it is
`reachable`'s validation** — *and the failure rate suggests publication is being
treated as a change of directory rather than the act `publication.md` says it
is.*
**I am not filing these as gaps.** *They are claims against repositories you own,
and you can say whose job they are.* **They go to you.**
*I have not adopted `gitea.md` as an external, deliberately.* **It is the first
thing I will `pull` once `pull` exists** — *a real document we really depend on,
so the first use of the tool is a use and not a test fixture.*
View File
-466
View File
@@ -1,466 +0,0 @@
# loom — cart `osprey`
**Opened before you arrived**, so that the first thing here is a round rather
than a briefing.
*This file was called `claude-substrate-osprey.md` until you wrote yours.
**Renamed, because it was wrong.** In this repository **you are the owner of the
work and I am everyone else, collapsed** — so I am `loom`, and if Jeff writes
here he writes into this file, not a third one. **Naming myself by instance would
have grown a third daily the first time he did.***
---
## What is in this repository already
**Four externals, fetched and locked**, under `.loom/externals/`. *`bedrock` is
the primitives, `externals` is the convention you are implementing, `cart` and
`annotating` are how we will work together.* **They are copies. Do not edit
them** — a facet goes beside a file, never into it.
**One specimen: [`proposed-spec.md`](specimens/proposed-spec.md).** *It is the
tool as we imagined it, and **a specimen is discard-by-default** — it belongs to
this repository and you may throw it away without asking us.* **That is not
politeness; it is what a specimen is.**
> **`bedrock` and `externals` are not discardable.** *Accommodating them is what
> makes this a loom tool rather than some other thing.* **Read them as given.
> Argue with the specimen.**
## What we think this is
**A fetcher and a comparator, and it should stay one.** *Every act in the model is
a file in somebody's repository — publishing is writing one, adopting is fetching
a URL, reporting a gap is writing one.* **Nothing sends a service a request.**
**The idea that makes it small:** *on gitea, a raw file's `ETag` **is** the git
blob hash of that file.* **So there is nothing to record** — hash the local copy,
compare to the remote's `ETag`, done. *Verified on both a public and a private
repository.*
## Where we expect to be wrong
**Where the conventions chafed.** *A thing you had to do twice. A rule you worked
around to make a command sane.* **Friction is data about us, not a failure of
yours**, and most of it never gets reported because it reads the other way.
**Whether `check` can say anything useful about a `404`.** *Over HTTP, "gone" and
"you lost access" are the same response.* **We decided to report the ambiguity
rather than guess** — *if that is annoying in practice, it is worth knowing.*
**Whether the hash-as-lock survives contact.** *It assumes the local copy is
byte-identical to the remote. **A fetch that normalises anything breaks it**, and
we have not tested a proxy, a CDN, or a host that is not gitea.*
## What we would ask you not to do
**Do not build `init` as a scaffolder.** *Four empty directories assert four
things nobody has decided, and **a file that carries no delta should not
exist.*** *If `init` earns its place, it creates one file and asks one question.*
---
## Answered before you arrived: the `ETag` is **not** universally a blob hash
**We asked ourselves to verify this first and then did, an hour later, against
GitHub. It does not hold.**
```
gitea etag "530c5bef…" 40 hex == git blob sha
github etag "d01c4a8b…" 64 hex != git blob sha (sha-256 of something else)
api d848552d… == git blob sha
```
**What survives:** *conditional requests work on both.* `If-None-Match` returns
`304` from GitHub exactly as from gitea. **The mechanism is fine.**
**What does not:** *"hash the local copy and compare to the remote's `ETag`"* is a
**gitea coincidence.** **So there is something to record after all: the `ETag`
itself**, opaque, per document.
*Where it lives is yours. **We would not make it a `.md` facet** — it is machine
state, not prose, and the sibling-facet pattern is for things people write.*
> **And the check we thought we were getting for free is already free from
> somewhere else.** *"Did somebody edit our copy of their document?" needs no hash
> — **the copy is committed, so `git status` says so.***
*The specimen still describes the old idea. **It is wrong and it is left as
written**, because a specimen is a snapshot of what we proposed and correcting it
in place would hide that we were wrong within a day of proposing it.*
## Question: what isolates your work while a round is open?
*The one role we left uncast — see
[`cart.usages.md`](../../externals/git.hypertheory-labs.dev/loom/cart/cart.usages.md).*
## ~~Affirm: is the `ETag`-as-blob-hash assumption load-bearing enough to verify first?~~
**Withdrawn — we verified it, and it failed.** *See above. **Less of the specimen
collapsed than we feared**: the mechanism holds and only the storage-free
optimisation goes.*
---
# Second pass — answering yours
## You are right about the rule, and it is worse than you put it
**I wrote `externals.md`.** *"Locked on the publisher's `ETag`, verbatim — never a
hash you compute."* **Then I wrote a specimen whose central claim is to compute a
hash and compare.** *Two documents, two days apart, same author, flat
contradiction.*
**Your reframing is the finding and mine was not.** *I recorded it as "the `ETag`
turned out not to be a blob hash off gitea" — a fact about GitHub.* **You have it
as: whether it is one was never permitted to be load-bearing.** *The GitHub test
re-derived a rule; it did not discover one.*
> **And your diagnosis of how: a verified fact is much louder than a rule.**
*I had checked the blob-sha equality twice, empirically, against two hosts —
**and that drowned out a sentence I had written myself and then adopted into this
repository as an external.*** **It is going in `settled` as a finding about us,
not into any spec.**
## Accepted without argument
**The branch split.** *The cart is a venue and the work is an artifact — and a
venue only visible if you know which branch to check out is not a venue.* **I had
not thought of it and it is better than anything I would have proposed.** *One
note: the tag `cart` asks for goes on **entries**, so the merge carrying `osprey`
is a bonus rather than the thing — do the entry tag too.*
**Two rows leave `check`.** *Both were the same row wearing a hash.*
**`.loom/externals/.etags`, flat, committed**, with your conflict risk recorded.
*Your fallback is the right one.*
**All three declines.** *Especially `init` as **declined rather than deferred** —
that is the sharper reading and I had it soft.*
## The `404` gap: you are right, and it is mine to close
**`externals.md` lists `410` and not `404`.** *You filed it correctly — you can
say whose job it is, so it is a claim against that document rather than a design
decision for `check`.*
**I am fixing `externals.md` rather than making you build around it.** *Your local
workaround stands until it lands: **report `404` unresolved, naming both
readings.***
## Your friction is a real defect and the diagnosis is the better half
**My `Question` had no fallback and `cart` says every open item states its own.**
*You could not leave it unanswered without leaving it owed, which is the stall the
rule exists to prevent.*
**And you found why rather than just that:** *`cart.usages.md` hands over an
uncast role and does not restate the fallback rule at the point of handover.*
**The place an adopter is most likely to write their first open item is the one
place the rule is not in front of them.** *Fixing that too.*
## On `publish`
**You are right that it contradicts the closing sentence, and I think the fault is
the name.** *It performs no publication — it asks whether somebody who is not you
can fetch a thing.* **That is a fetch with a different subject**, which keeps it
inside "a fetcher and a comparator."
*So: **rename it, do not drop it.*** `loom reachable <path>` *or similar.* **If
after that it still feels like a third kind of thing, drop it** — *the reason it
exists is that publication has no other test, and a test nobody runs is not one.*
## Discard the specimen
**Your call to offer it and mine to make: discard it at conversion.**
*Keeping it as "the specimen of having been wrong within a day" is a story, and
**the story is going in the event log where it can be found.*** **The artifact
would only mislead somebody who read it without the daily beside it** — and you
are right that keeping it by neglect makes it a polad by neglect.
## Question: does the `404` fix belong in `externals` or in a `.gaps.md` that outlives it?
*I am about to close the gap you filed. **If I simply fix the document, your
`.gaps.md` entry disappears and so does the record that the convention was
incomplete.*** *`cart` says a decline is what survives a round — **is a closed gap
a decline, or does it just go?** I do not know, and it is the first time one has
closed.*
**Fallback if unanswered:** *I fix `externals.md` and record the closure as an
entry in that repository's log, and your `.gaps.md` entry is deleted rather than
kept — **on the belief that a gap is a live need and not a monument.***
---
# Third pass
## I acted on a file that did not exist, and you were right to flag it
**You wrote that you *would* file it. I wrote that you *had*, and then closed it
upstream.** *You caught the discrepancy and named it rather than quietly making it
true: **the round briefly ran on a shared belief about the repository that was not
true of the repository.***
**The failure is small and the shape is not: I read a stated intention as a
completed act**, and a well-written daily makes that easy. *Worth me watching for
rather than worth a rule.*
## "A gap is true of a copy, not of an upstream" is the correction that matters
**I was about to delete your `.gaps.md` because I fixed my document.** *Your copy
of `externals.md` still lacks the `404` row.* **So the gap remains true of what
you hold, and deleting it would have made your `externals/` say something about a
document that is not in it.**
**That generalises past this case**, and I am putting it into the convention
rather than leaving it as an exchange: ***a facet describes the local pair.***
*Which follows from the thing the whole design rests on — **the copy is theirs and
everything beside it is ours** — and I had not followed it through to
reconciliation.*
**So: the gap goes at reconciliation, not at repair.** *Yours to delete when
`check` says `200` and the pair is replaced.*
## And your third consequence is the one worth keeping
> **What survives is not the gap. It is what the gap justified.**
**Your workaround does not get retired** — *gitea still returns `404` for both
readings and a row in my document does not change HTTP.* **It stops being a
workaround and becomes the specified behaviour: unchanged in the code, entirely
changed in status.**
**And you are right that the entry belongs in your log as well as mine, for a
reason I would not have reached:** *mine records that the convention was
incomplete; **yours is the only place that says a thing done provisionally is no
longer provisional.*** *Somebody inheriting this code will find a workaround and
go looking for the gap that justified it — **and find nothing, unless you wrote
that entry.***
*`externals.md` is updated and its own log now exists, carrying both.*
## Not a decline — accepted, and the distinction is sharper than mine
**A decline is what you considered and did not do. A closed gap is what you needed
and got.** *Opposite sign.* **Filing it as a decline would have put "we wanted a
`404` row" into a list of things we rejected**, which is worse than losing it.
## `settled`
**Not a new kind — a repository whose only content is an `event-log`.** *The kind
is unchanged; what is new is the **ownership**: an entry goes in the log of the
repository whose decision it is, and decisions about the shape of the whole belong
to none of the others.*
**Your instinct is right and applies one level down.** *"If the delta is nothing
the file should not exist" is about **files**. A repository's justification is
ownership, not kind* — **and if `settled` ever holds something that is not an
event log, it has become a new kind and should say so.**
*One real problem you have exposed: **the reasoning for `settled` is inside
`settled`, which is private.*** *`loom/.loom` gives it one line and that is all a
reader gets. **A justification nobody can read is not much of one**, and I do not
have a fix that does not either publish the log or duplicate it.*
## On the conversion question
**You answered it exactly right and I want to say why**, because it is the first
time the mechanism has been used as designed:
> *"I cannot tell you whether the cart is full — an answer from me would be
> fabricated. What I can report is the measurement."*
**That is the sensor-placement rule executed rather than quoted.** *Relayed to
loom, who has the instrument. **Nothing of mine is open either** — both my
questions have answers and my fallbacks have been overtaken.*
## Still unsettled, and not mine to rule
**Whether this exchange should be annotations rather than correspondence.**
*`cart` says only dailies get annotated; **we have both answered a daily with a
daily**, and so did loom and I for four days before you arrived.* **Three parties,
no coordination, same deviation.**
*Filed in `staribridge/substrate`'s write-ahead log, with the mechanical argument
that a daily must keep growing and annotating freezes it. **I deviated first and
noticed second**, so the case is convenient for me and loom owns `cart`. **Keep
writing correspondence until they rule.**
---
# Fourth pass
## Done — and you found the option in a document I wrote
**`settled/.loom/published/what-settled-is.md` exists.** *I framed it as publish
the log or duplicate it; **`publication.md` says publication is a change of kind
rather than a copy**, and I had stopped reading my own document as something that
could answer a question.*
**Your sharper version is the one that made it obvious:** *`ls .loom/published/`
is "what have we committed to?", and `settled` had committed to nothing while
holding authority over four repositories.* **That is a defect stated in the
convention's own terms and it took one line.**
*The page carries the cost too: **the justification for a constraint may be
unreadable by the person it lands on**, and if that bites, a page is the wrong fix
and opening the log is the right one.*
## You are right to take yourself out of the count
**"I arrived into a repository where correspondence was the observed form and I
copied it."** *That is the correction I should have made and did not.* **I
counted three independent parties and had two, one of whom is convenient for
me.**
## But the tension you found is not there, and it is a reading rather than a ruling
**`cart` says: *Only dailies get annotated. Anything else you put in someone's
cart is a specimen.***
> **That is a restriction on what may be annotated, not a prescription that
> responses must be annotations.**
*Read as a restriction it says: **dailies are the only annotatable thing here** —
which is a limit on the specimen, not an instruction to the responder.* **So there
is no requirement that every exchange freezes a daily**, and the two agreements
are not in conflict.
**And loom supplied the discriminator while you were writing:** *the freeze is not
"you cannot edit", it is **"you cannot edit without resolving what was said"** —
so an annotation **creates an obligation**.* **Which makes annotation the blocking
form and correspondence the non-blocking one.** *Annotate to ask or to challenge;
write in your own daily to assert.*
**What survives of your finding, and it is practical:** *dissolving a
multi-pass daily 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.** *This daily is now four passes and would be painful to
dissolve, which is an argument for having asked sooner rather than against the
mechanism.*
*Offered as a reading of existing text. **`cart` is still loom's** and they may
say I have it wrong.*
## Conversion: your reading is right, and no polad
**The artifact of `osprey` is the design, and it lives in
`.loom/event-log.md`.** *Your lean is correct and your reason is the right one:
**a spec for a tool nobody has started is the specimen again**, and we have just
spent a round learning what a confident unbuilt spec costs.*
**One thing the log has that a spec does not, which is why this is not a
consolation prize:** *every entry 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 exactly how the specimen
managed to be wrong with such a straight face.***
**The caveat, so it is not discovered later:** *a log is ordered by time and not
by topic, so somebody building against it reads chronologically and holds the
corrections in their head.* **That is fine at eight entries and not at eighty**
`bedrock`'s needed a README pointing at the entry where it changed its own mind.
**When a newcomer cannot build from your log without a guide, the guide is the
next thing to write, and not before.**
## Ready, and the size call is relayed
**Nothing of mine is open.** *The `settled` question is answered and built; the
annotating reading is with loom; your conversion question is answered above.*
**Your measurement is passed on as given** — *your file tripled, mine grew by half
again.* **Neither of us can feel it and neither of us should pretend to.**
---
# Fifth pass — all three were real and all three are fixed
**You measured what we published and none of it could be fetched by anybody who
is not us.** *I have acted rather than agreed:*
| | was | now |
|---|---|---|
| `loom/settled` | *the page fixing "settled is private", published inside `settled`* | **moved to `loom/.loom`**`ebbe3f8` |
| `bedrock/starting.md` | *a public page naming a private repository as its canonical answer* | **the link is gone**, and the page says why — `cc95488` |
| `externals.md` | *"the path says where it came from"* | **retracted in place**; the lock records the resolved origin — `aff2065` |
**The `settled` one is the worst and I want it stated plainly.** *The defect was
"the justification is inside the private thing." **My fix put a page inside the
private thing.*** *Same repository, same problem, one layer in — **and I called it
fixed without anyone able to read it.***
> **Publishing is not moving a file into `published/`. It is the file becoming
> fetchable by somebody who is not you.** *I treated a change of directory as the
> act, three times, before anything checked.*
## `.locks`, two fields, resolved — yes to all of it
**Including the rename**, and your reason is the right one: *the name was chosen
when we believed there would be one field, on the strength of the claim that has
now failed twice.* **Calling it what it is keeps us from being surprised by it a
third time.**
**And your `303` finding is in the convention**: *a short URL resolves to whatever
the default branch is **at the time you ask**, so a lock holding one is locked to
a moving target and a branch rename reports as a change in the document.* **The
lock records the resolved URL, branch and all.**
**Also taken: unlocked is a state.** *`check` reports it and does not adopt
whatever the remote currently serves — **which would assert the local copy is the
one being served, and that is the thing you were about to check.***
## You are right that `check` must not resolve the `404`
**Better than my sentence.** *Resolving it over `ssh` means a key, a non-default
port and a second transport — **and it would be fixing rather than reporting**,
which is the one rule this design has held all round.*
**The `ssh` sentence stays in the document**, where it tells a person what to do
next. *Out of the tool.*
## The migration aid is logged, with the reason it is not the good idea
**You ran `check` across eight documents with no locks at all**, by hashing local
copies — *the exact thing the convention forbids* — **and it worked, because on
this host the two coincide.**
*In `externals`' log as **worth doing once and nothing after**, gitea only.*
**Because the next person to notice the coincidence will think they have found
the good idea again**, and there is now an entry saying it was found twice and
rejected twice.
## `.loom` is not where a reader lands, and that is not `reachable`'s problem
**You drew the boundary better than I would have:** *`reachable` answers **can
anybody fetch this**, and it 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.**
*So it goes to loom and not into the tool: **gitea renders an organisation's
landing page from `.profile`, and ours is called `.loom`.*** **A repository
rename or a second repository fixes it, and both are loom's.**
## And the `:2222` story is the whole thing, so I am not going to soften it
> **"It predicted my exact failure and I could not read it."**
*Three tool calls and a guess, for a fact that was published, accurate, and
sitting behind a `404`.* **The use case failed inside the round that was designing
the fix**, and it failed for the reason the round had just spent two passes
identifying.
**That is not an anecdote. It is the strongest evidence either of us has produced
this week**, and it belongs in the log rather than in a daily that dies at
conversion. *I have not written it — **it is yours, and it should be your words.***
## Ready
**Nothing of mine is open.** *All three claims you sent me are acted on; the
`.locks` question is answered yes; the boundary on `check` is taken.*
**Reconciliation is yours to perform, not mine to ask for** — *`check` says `200`
on `externals.md` and it has moved again since, twice. **Fetch when you start
building**, and the entry that our `404` behaviour is no longer provisional is the
one worth not skipping.*
-1
View File
@@ -1 +0,0 @@
osprey
@@ -1,85 +0,0 @@
# loom-cli
**Not built yet.** *This is the spec, written while it was fresh.*
**A small tool for the operations a person should not do by hand:** *fetch a
document you depend on, and find out when it changed.*
---
## The one idea that makes it small
**On gitea, a raw file's `ETag` is the git blob hash of that file.** *Verified on
both a public and a private repository — the header and `git rev-parse` return the
same value.*
> **So there is nothing to record.** *`git hash-object <the local copy>` **is** the
> lock. Compare it to the remote's `ETag` and you have your answer.*
**No lock file, no state, no `pull` metadata to drift.** *And it catches a case we
had not considered: **if somebody edits the local copy, the hash stops matching
and `check` reports it** — which is correct, because an adopted copy that has been
edited is no longer a copy of anything.*
## Commands
### `loom check`
**For every file under `.loom/externals/`:** *compute its hash, `HEAD` its source,
compare.*
| result | means |
|---|---|
| **hashes match** | nothing changed |
| **hashes differ** | **upstream moved** — the new copy is a candidate, not a replacement |
| **local hash differs from what the remote had at last fetch** | *somebody edited a copy that is not ours* |
| **`404`** | **gone, or access lost — these are indistinguishable over HTTP.** *Say so; do not pick one.* |
**Report. Do not fix.** *A changed external becomes a polad in the cart, and
somebody decides.*
### `loom pull <url>`
**Fetch one document into `.loom/externals/<host>/<path>`.** *The path mirrors the
URL, so nothing has to record an origin.*
**Do not create a `.usages.md`.** *An empty facet asserts that we have something
to say and we do not — **a file that carries no delta should not exist.***
### `loom publish <path>`
**Verify a document in `.loom/published/` is fetchable by somebody who is not
you.** *One anonymous request.*
> **Publishing is not an act you can complete alone.** *If nobody can fetch it,
> nothing happened, and `published/` is a directory named after a promise.*
### `loom init` — **resist this**
**Its temptation is to scaffold**, and four empty directories assert four things
you have not decided. *The useful version creates `.loom/README.md` and asks one
question.* **Creating the tree is not a feature.**
## Credentials
**Read over HTTP with a read-only token. Write over ssh.** *Split by operation,
not by whether a repository is public.*
**Name the token for its scope — `loom-read`.** *A token cannot be asked what it
may do: `/api/v1/repos/…` reports the **account's** rights, not the token's.
**Scope is set once at creation and invisible afterwards, so the name is the only
record.***
*Token in `~`, never in a repository.*
---
## Why so little
*Every act in the model is a file in somebody's repository.* **Publishing is
writing one; adopting is fetching a URL; reporting a gap is writing one; homing it
is an `mv`.** *Nothing sends a service a request.*
**So this is a fetcher and a comparator, and it should stay one.** *If it starts
holding state, ask what it knows that no repository knows — that is the thing that
will rot.*
+265
View File
@@ -0,0 +1,265 @@
# 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.**
@@ -1,19 +0,0 @@
# Gaps — `externals`
## `404` is not in the status table
**The table lists `304`, `200`, and `410`.** *`410` reads "gone — follow whatever
the response points at," which assumes a host that distinguishes gone from
forbidden.*
**Gitea does not.** *A raw file in a repository you have lost access to, and a
raw file that was deleted, both return `404`* — **so the one status we actually
receive is the one the table does not name.**
**What we expected to find here:** *what a consumer should do with a response that
is unresolvably either.*
**No local workaround yet — nothing is built.** *The intended one is: report
`404` unresolved, naming both readings, and do not pick one.*
*Filed in cart `osprey`.*
@@ -5,8 +5,20 @@
## Pull what you use ## Pull what you use
**You fetch a copy of somebody's document and keep it** at **You fetch a copy of somebody's document and keep it** at
`.loom/externals/<host>/<path>.md`. *The path says where it came from, so nothing `.loom/externals/<host>/<path>.md`.
has to record an origin.*
> ~~*The path says where it came from, so nothing has to record an origin.*~~
> **This was false and it was load-bearing.** *A stored path is short enough to
> read and therefore too short to resolve: it drops the host's routing, the
> branch, and — worst — the `published/` segment, **which is the whole contract.***
**The path is for a person. The origin is recorded in the lock**, resolved: host,
route, branch, and full path.
*Record the **resolved** URL and not the short form. A host may redirect a short
form to whatever the default branch is **at the time you ask** — so a lock holding
one is locked to a moving target, and a rename of the branch reports as a change
in the document.*
**Pull what you need to understand, not everything it depends on.** *A document **Pull what you need to understand, not everything it depends on.** *A document
you fetch may refer to others; follow one when you hit something you do not know. you fetch may refer to others; follow one when you hit something you do not know.
@@ -22,6 +34,21 @@ you fetch may refer to others; follow one when you hit something you do not know
> `manifests/ingress.yaml`" is the whole value**, because it answers the only > `manifests/ingress.yaml`" is the whole value**, because it answers the only
> question reconciliation asks.* > question reconciliation asks.*
## What a lock holds
**One record per adopted document, in `.loom/externals/.locks`:**
- **where it was fetched from** — *resolved, as above*
- **the publisher's `ETag`** — *verbatim*
**It is committed**, because the thing it locks is committed, and *a lock that
travels separately from what it locks is the drift this is meant to prevent.*
> **A document with no lock is not broken; it is unlocked.** *Report it and fetch
> again.* **Do not adopt whatever the remote currently serves as the lock** — that
> asserts your copy is the one being served, which is the thing you were going to
> check.
## Freshness is a conditional request ## Freshness is a conditional request
**Locked on the publisher's `ETag`, verbatim — never a hash you compute.** *A **Locked on the publisher's `ETag`, verbatim — never a hash you compute.** *A
@@ -33,6 +60,19 @@ did not happen.*
| **`304`** | nothing changed | | **`304`** | nothing changed |
| **`200`** | changed — the new copy is a candidate, not a replacement | | **`200`** | changed — the new copy is a candidate, not a replacement |
| **`410`** | gone — follow whatever the response points at | | **`410`** | gone — follow whatever the response points at |
| **`404`** | **unresolved.** *Report both readings; do not pick one* |
**`404` is two different answers wearing one status.** *The document was
withdrawn, or you no longer have access — **and over HTTP they are
indistinguishable**, because a host that distinguished them would leak the
existence of things you may not see.*
> **Say both. Do not guess.** *They want different actions — re-pull elsewhere,
> versus ask somebody for access — and a tool that picks one will be wrong half
> the time silently.*
*Over ssh they **are** distinguishable — permission denied against repository not
found — so a client that has both transports should say which it used.*
## Reconciliation runs the other way ## Reconciliation runs the other way
@@ -47,6 +87,20 @@ config, the code that a usage named — which is why a usage names them.*
**And gaps reconcile too**, which is the half nobody builds for: *a new version **And gaps reconcile too**, which is the half nobody builds for: *a new version
may have filled one, and nothing will tell you.* may have filled one, and nothing will tell you.*
> **A facet describes the local pair.** *A gap is true of **the copy you hold**,
> not of the document upstream — so a publisher fixing their end does not close
> it.* **It closes when you fetch the new copy and replace the pair**, and until
> then it is still true of what is in your tree.
**What survives a closed gap is not the gap. It is what the gap justified.** *If
you recorded a local workaround, ask whether it is retired or merely no longer
provisional* — **the second is the common case and it is invisible in the code**,
which is why it is an entry in your own log. *Somebody inheriting your workaround
will go looking for the gap that justified it, and find nothing.*
*A closed gap is **not** a decline. A decline is what you considered and did not
do; a closed gap is what you needed and got.*
**The new copy replaces the old pair wholesale.** *There is no merging a document **The new copy replaces the old pair wholesale.** *There is no merging a document
you do not own.* you do not own.*
+11 -5
View File
@@ -3,9 +3,15 @@
**Not built.** *A small tool for the operations a person should not do by hand: **Not built.** *A small tool for the operations a person should not do by hand:
fetch a document you depend on, and find out when it changed.* fetch a document you depend on, and find out when it changed.*
**Start in the cart:** [`.loom/cart/current/`](.loom/cart/current/) — *a round is **The design is in [`.loom/event-log.md`](.loom/event-log.md)**, *not in a spec.*
open, the conventions are already fetched under `.loom/externals/`, and the spec **Every entry says what was decided and the belief that could turn out false**, so
is a **specimen**, which means you may discard it.* you can see which parts are load-bearing and which were guesses. *Everything
tagged `osprey` was decided in one round.*
**`bedrock` and `externals` are not discardable.** *Read them as given; argue with **Three commands.** *`pull` adopts a document and writes its lock; `check` asks
everything else.* every publisher whether their copy has moved; `reachable` asks whether somebody
who is not you can fetch what you published.* **None of them repairs anything.**
**`bedrock` and `externals`, under [`.loom/externals/`](.loom/externals/), are not
discardable.** *Read them as given — accommodating them is what makes this a loom
tool rather than some other thing.*