git archive --remote is out: gitea allows it, github 422s, gitlab 404s. Partial clone with --filter=blob:none --depth=1 --no-checkout is uniform across gitea, github and gitlab, and lists homelab-cluster's six published documents over ssh in under a second — the list I could not get this morning. REST would be three URL shapes, three JSON shapes and three auth schemes, and it 404s on the private repo, which is the case that matters. Corrects my claim that git collapses the auth section. It does not: list goes over git, but add and check still want HTTP, because a 304 is a few hundred bytes and a partial clone is 124KB, so checking eight externals over git costs a megabyte to learn nothing. Private raw HTTP needs a token. Two credentials per host, not one. The argument for git is consistency, not thrift, and that argument stands alone. Records the failure mode: --filter is a server capability and git's fallback is silent, so detect the warning rather than letting list become a full clone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UTxuSizozEA8yDitPuris
243 lines
10 KiB
Markdown
243 lines
10 KiB
Markdown
# claude — cart `marmalade`
|
|
|
|
**`osprey` converted, so this is a new round.** *Its name is meaningless and I
|
|
picked it arbitrarily; change it if you like.*
|
|
|
|
---
|
|
|
|
## Two mechanical things before the design
|
|
|
|
**Your text arrived as `jeff-osprey.md` and I have moved it, unedited, to
|
|
`loom-marmalade.md`.** *`cart` allows two files and gives the test:* **what
|
|
happens to this file when the person changes?** *`jeff-…` dies with Jeff, so it is
|
|
a person and not a stake* — **and loom renamed itself to `loom` last round for
|
|
exactly this reason**, *saying that if you wrote here you would write into that
|
|
file.* **Not a correction of you; the shape predicted it and the shape was right.**
|
|
|
|
**And it landed in a cart that had converted**, *which is fine — it opened the next
|
|
one.* **Nothing travelled silently:** *everything below is reintroduced with a
|
|
fresh reason, and the design that survived `osprey` is in
|
|
[`.loom/event-log.md`](../../event-log.md) rather than in a file I am carrying
|
|
forward.*
|
|
|
|
## `loomctl`, and the noun that is missing
|
|
|
|
**Subcommands, yes.** *And your grouping exposes something the log got wrong.*
|
|
|
|
**`osprey` named three commands — `pull`, `check`, `reachable` — as if they were
|
|
one family. They are two.**
|
|
|
|
```
|
|
loomctl external add | check things somebody else published, that we depend on
|
|
loomctl published check things we published, that somebody else depends on
|
|
```
|
|
|
|
> **`reachable` is not an `external` operation at all.** *It looks at
|
|
> `.loom/published/`, which is our surface and not anybody's dependency.* **Under
|
|
> your grouping it has no home, and that is the grouping doing its job.**
|
|
|
|
*`publication.md` already draws this line — **published is a change of kind** — and
|
|
the CLI having a noun for each side is the same distinction wearing a verb.*
|
|
|
|
**I would drop the word `reachable` and make it `loomctl published check`**, *so
|
|
that `check` means one thing in both nouns: **ask somebody else whether what we
|
|
believe is still true.*** *Against an external it asks the publisher; against our
|
|
own, it asks as a stranger.*
|
|
|
|
## `external list` is the first command that needs to know what a host is
|
|
|
|
**Everything in `osprey` is host-agnostic on purpose.** *A conditional `GET`
|
|
behaves identically on gitea and GitHub — we measured it.* **`list` is not like
|
|
that.** *Enumerating `.loom/published/` means asking a service what is in a
|
|
directory, and every host answers differently or not at all.*
|
|
|
|
**That is the real cost of the command, and I think it is worth paying**, because
|
|
of what it buys:
|
|
|
|
- **The origin path stops being parsed.** *You type a repository and a document
|
|
name; the tool builds the URL and writes it to the lock.* **The problem that ate
|
|
two passes of `osprey` disappears by never arising.**
|
|
- **It is discovery**, *which the log records as the thing a fetcher cannot do* —
|
|
**and your `gitea.md` story is exactly the failure it fixes.** *I could not have
|
|
pulled that document, because I did not know it existed.*
|
|
|
|
> **You said there is no registry yet, so you are the registry.** *`external list`
|
|
> is the smallest thing that stops that being true.*
|
|
|
|
## Select: what does `external list` talk to?
|
|
|
|
- **each host's REST API** — *gitea `/api/v1/repos/{o}/{r}/contents/.loom/published`,
|
|
GitHub `/repos/{o}/{r}/contents/…`, one adapter per host*
|
|
- **git itself** — *a treeless or shallow fetch, then `ls-tree` the published
|
|
directory; the same protocol everywhere, no adapters, no API tokens*
|
|
- **a published index** — *the convention grows a file listing the surface, and
|
|
`list` is a fetch like every other command*
|
|
|
|
*My lean is **git itself**, and I want to be honest that it is not obviously
|
|
right.* **git is the one API every one of these hosts already speaks**, *and we
|
|
have working ssh here* — **so `list` needs no host adapter and no second auth
|
|
story**, *which collapses most of your `auth` section.* **The costs are real:** *it
|
|
is heavier than one request, it needs git credentials rather than a read token,
|
|
and it fetches more than it needs to answer.*
|
|
|
|
**Fallback if unanswered:** *I build it against git, behind a small seam, and log
|
|
that the REST adapter is the fallback if the git path is too slow or too
|
|
credentialed for a public repository.*
|
|
|
|
## Auth: kubeconfig-shaped is right, and it is not only credentials
|
|
|
|
**Your instinct holds for a reason the specimen missed.** *`osprey` assumed one
|
|
token because it assumed one host.* **The moment there are several, the per-host
|
|
thing is not just a secret — it is *how you talk to this host at all***: *its API
|
|
shape, whether ssh is on a strange port, which transport `check` should use.*
|
|
|
|
**That is a config, and kubeconfig is the right silhouette:** *named contexts, one
|
|
current, everything addressable by name.*
|
|
|
|
**Two things I would carry forward from `osprey` into it:**
|
|
|
|
**A token cannot be asked what it may do** — *gitea's `/api/v1/repos/…` reports the
|
|
**account's** rights, not the token's* — **so the name is the only record of its
|
|
scope**, and the config should make naming unavoidable rather than optional.
|
|
|
|
**And it lives in `~`, never in a repository.** *A file that holds hosts and
|
|
tokens together will be tempting to commit the first time two people need the same
|
|
host list.* **Splitting it — a committed host list, a private credential file — is
|
|
the obvious answer and I have not thought it through**, *so I am naming it rather
|
|
than deciding it.*
|
|
|
|
## Affirm: subcommands now, plugins not yet?
|
|
|
|
**I would not build a plugin boundary before `beads` exists.**
|
|
|
|
*`kubectl` and `gh` both grew plugins after they had users and after somebody
|
|
outside wanted in.* **Asserting the seam now means guessing where it goes**, *and
|
|
`osprey` just spent a round on what a confident guess costs* — **it is the same
|
|
argument that killed `init`: a boundary nobody has felt asserts something nobody
|
|
decided.**
|
|
|
|
**Fallback if unanswered:** *subcommands only, with the internals arranged so that
|
|
`external` and `published` do not share state.* **If that stays true, the plugin
|
|
seam is discovered rather than designed.**
|
|
|
|
## Question: does `loomctl` document itself the way loom asks everyone else to?
|
|
|
|
**Your first requirement — legible to an AI or a human — is loom's own thesis
|
|
pointed at loom.** *So the test is available: **this repository has a
|
|
`.loom/published/`, and `loomctl published check` can be run against it.***
|
|
|
|
*I think that is the acceptance test for the whole tool, and it is nicer than any
|
|
unit test I would otherwise write.* **The tool's first real use is on itself, and
|
|
its second is `homelab-cluster`'s `gitea.md`** — *the document I needed and could
|
|
not read.*
|
|
|
|
**Fallback if unanswered:** *I write `.loom/published/` for `loomctl` as I build,
|
|
and treat `--help` as a rendering of it rather than a second source of truth.*
|
|
|
|
## Addendum: the third file was mine, not yours
|
|
|
|
**You were writing it before I converted.** *Which makes this the other rule in
|
|
`cart`, and it lands on me:*
|
|
|
|
> **Do not convert while someone is mid-thought. Ask whether they are ready. If
|
|
> you convert alone anyway, say so.**
|
|
|
|
**I did ask, and you said the plan was good, and you were mid-thought anyway.**
|
|
*So the check passed and was still wrong* — **approving a plan is not the same as
|
|
having nothing left to say**, and the question as `cart` phrases it invites the
|
|
first answer when it wants the second.
|
|
|
|
*Worth reporting upward: **"are you ready for me to convert?" and "is there
|
|
anything you are still writing?" are different questions**, and only the second one
|
|
would have caught this.*
|
|
|
|
**Nothing was lost** — *the shelf would have caught it and it did not need to,
|
|
because you simply wrote it again into the next round.* **Which is the mechanism
|
|
working.**
|
|
|
|
---
|
|
|
|
# Measured: git, and your instinct was right
|
|
|
|
**Taken: `published check`.** *`reachable` is gone as a word.*
|
|
|
|
## `git archive --remote` is out
|
|
|
|
```
|
|
gitea works (ssh)
|
|
github HTTP 422
|
|
gitlab HTTP 404
|
|
```
|
|
|
|
*One round trip and no clone, and only one of three hosts allows it.*
|
|
|
|
## Partial clone is uniform across all three
|
|
|
|
**`git clone --filter=blob:none --depth=1 --no-checkout`, then
|
|
`git ls-tree HEAD:.loom/published`:**
|
|
|
|
```
|
|
gitea filter ok .git=124KB
|
|
github filter ok .git=124KB
|
|
gitlab filter ok .git=144KB
|
|
```
|
|
|
|
**Same flags, same command, same output shape.** *And on the repository that
|
|
started all this:*
|
|
|
|
```
|
|
jeffry/homelab-cluster 0s 124KB
|
|
databases.md gitea.md private-access.md
|
|
public-access.md recovering-data.md storage.md
|
|
```
|
|
|
|
> **That is the list I could not get this morning**, *in one command, against a
|
|
> private repository, in under a second.*
|
|
|
|
## What REST would have cost
|
|
|
|
```
|
|
gitea /api/v1/repos/{o}/{r}/contents/{path}
|
|
github /repos/{o}/{r}/contents/{path} (api.github.com)
|
|
gitlab /api/v4/projects/{url-encoded}/repository/tree
|
|
```
|
|
|
|
**Three URL shapes, three JSON shapes, three auth schemes** — *and the private
|
|
gitea repository returns `404` anonymously, so REST fails on exactly the case that
|
|
matters.* **Your sense was right and it is now measured rather than felt.**
|
|
|
|
## But I overclaimed, and here is the correction
|
|
|
|
**I wrote that git "collapses most of your `auth` section." It does not.**
|
|
|
|
*`list` discovers names over git. **`add` and `check` still want HTTP***, because
|
|
freshness is a conditional request — *a `304` is a few hundred bytes and a partial
|
|
clone is 124KB, so checking eight externals over git would cost a megabyte to
|
|
learn nothing.*
|
|
|
|
**And raw HTTP against a private repository is `404` without a token.** *So the
|
|
honest account is:*
|
|
|
|
| | transport | credential |
|
|
|---|---|---|
|
|
| **`external list`** | git | *ssh key or https creds you already have* |
|
|
| **`external add`, `external check`** | HTTP | **a read token, for private repos** |
|
|
| **`published check`** | HTTP, anonymous | *none, deliberately* |
|
|
|
|
**Two credentials per host, not one.** *The argument for git is **consistency**,
|
|
not thrift* — **one mechanism against three hosts instead of three adapters** —
|
|
*and that argument stands on its own.* **The auth saving was me liking my answer.**
|
|
|
|
*Untested: whether a gitea read token actually works against raw HTTP on a private
|
|
repository.* **It is the specimen's assumption and nobody has run it.**
|
|
|
|
## The failure mode to build for
|
|
|
|
**`--filter` is a server capability.** *All three hosts here allow it; an older
|
|
self-hosted gitea or gitlab may not, and **git's fallback is silent** — it warns
|
|
`filtering not recognized by server` and downloads everything.*
|
|
|
|
**`--depth=1` bounds the damage to one commit**, *which is why both flags are
|
|
there and not just the filter.* **Detect the warning and say so**, rather than
|
|
letting `list` quietly become a full clone of somebody's monorepo.
|