From 86091f1f4eca068d758d2cff41c8199f1407e09f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D7=A0=CF=85=CE=B1=CE=B7=20=D7=A0=CF=85=CE=B1=CE=B7=D1=95?= =?UTF-8?q?=CF=83=CE=B7?= Date: Thu, 1 Oct 2026 10:35:08 -0700 Subject: [PATCH] docs: route the skill by subject, not by requirement number Every routing table in add-a-domain cited FR numbers. The components pages have carried headings rather than requirement numbers since the spec-kit removal, so all 58 citations pointed at something that no longer existed. An agent following the skill hit a dead pointer at step one of every layer. Each row now links a heading, and the links and anchors are checked: all 52 resolve. Three things came out with them. The SDK rules routed to domains.md when sdk.md states them, and the CLI rows had nowhere to point until cli.md was written. Two passages were archaeology, narrating what an earlier version of the file claimed and that the claim was wrong. The constitution forbids that for the same reason it forbids restating a rule: a reader cannot tell a record of a correction from the thing being corrected. What binds is stated plainly instead. The README justified relative links by saying scripts/validate-skills.py checks them and `just skill-lint` fails when one breaks. Both were removed earlier; the reason for relative links is now the one that is still true. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FuKUsHFG1EqZXamffh9M2c --- .claude/skills/add-a-domain/README.md | 13 +++--- .../skills/add-a-domain/references/agent.md | 26 ++++++------ .claude/skills/add-a-domain/references/api.md | 35 ++++++++-------- .claude/skills/add-a-domain/references/cli.md | 16 +++---- .../skills/add-a-domain/references/docs.md | 14 +++---- .../add-a-domain/references/provider.md | 42 +++++++++---------- .claude/skills/add-a-domain/references/sdk.md | 29 ++++++------- 7 files changed, 86 insertions(+), 89 deletions(-) diff --git a/.claude/skills/add-a-domain/README.md b/.claude/skills/add-a-domain/README.md index c5cd64a..1d78f3c 100644 --- a/.claude/skills/add-a-domain/README.md +++ b/.claude/skills/add-a-domain/README.md @@ -64,16 +64,15 @@ table row naming the requirement, not a sentence pointing at a document: | Rule | Stated in | | --- | --- | -| A provider returns a typed result, never a formatted string | [FR-004](../../../components/osapi/providers.md) | +| A provider returns a typed result, never a formatted string | [what an operation returns](../../../components/osapi/providers.md#what-an-operation-returns) | Three things make that a citation rather than a link: -- **It is relative.** `scripts/validate-skills.py` resolves relative links from - the file's own directory, so `just skill-lint` fails when the target is gone. - An absolute URL is checked by nothing, which makes it a restatement with extra - steps. -- **It names a requirement.** "See the provider contract" is a pointer; `FR-004` - tells a reader whether what they are looking for is there. +- **It is relative.** A relative link breaks visibly when the target moves. An + absolute URL to the same file goes on resolving to a page that no longer says + what the row claims. +- **It names the section.** "See the provider contract" is a pointer; a link to + the heading tells a reader whether what they are looking for is there. - **It does not restate the rule.** The row names the rule and where it lives. Two statements of one rule is what citing exists to prevent, because the copy an agent happens to load wins. diff --git a/.claude/skills/add-a-domain/references/agent.md b/.claude/skills/add-a-domain/references/agent.md index b296a34..f87144e 100644 --- a/.claude/skills/add-a-domain/references/agent.md +++ b/.claude/skills/add-a-domain/references/agent.md @@ -7,8 +7,14 @@ dispatch and facts wiring. | What you need to know | Where | | --- | --- | -| Two files connect a provider, and what does **not** change: `agent/types.go`, `agent/agent.go`, the `JobClient` interface | [FR-009](../../../../components/osapi/domains.md) | -| The `FactsAware` obligation: embed it, add the compile-time `FactsSetter` check | [FR-010](../../../../components/osapi/domains.md) | +| Delivery is at-least-once, and what the agent owes because of it | [delivery is at-least-once](../../../../components/osapi/job-system.md#delivery-is-at-least-once-and-the-agent-owes-idempotency) | +| What `_any`, `_all` and a label selector resolve to | [routing and targeting](../../../../components/osapi/job-system.md#routing-and-targeting) | +| Which failures terminate a message, and which redeliver | [which failures terminate](../../../../components/osapi/job-system.md#which-failures-terminate-a-message) | +| What happens when the response cannot be written | [when the response cannot be written](../../../../components/osapi/job-system.md#when-the-response-cannot-be-written) | +| The three limits, and what each one bounds | [three limits](../../../../components/osapi/job-system.md#three-limits-bounding-different-things) | +| The four result statuses, and why a row might not say `ok` | [why a row might not say ok](../../../../components/osapi/job-system.md#why-a-row-might-not-say-ok) | +| How a provider is selected for the host's platform | [platform selection](../../../../components/osapi/providers.md#platform-selection-happens-outside-the-provider) | +| How the unsupported outcome becomes a skip | [unsupported is not failure](../../../../components/osapi/providers.md#unsupported-is-not-failure-and-not-no-change) | This file holds the shapes and the file names. The rules above are stated once, in the corpus, so a change to either is a change in one place. @@ -102,13 +108,11 @@ in full. What matters when adding an operation, and where each rule is stated: | What you are relying on | Stated in | | --- | --- | -| Delivery is at-least-once, and the agent checks for a recorded response before executing | [FR-009](../../../../components/osapi/job-system.md) | -| Which operations make that check load-bearing rather than theoretical | [FR-010](../../../../components/osapi/job-system.md) | -| A job that has run is terminal, a failure is reported, not retried by redelivery | [FR-011](../../../../components/osapi/job-system.md) | -| What happens when the response cannot be written after the work ran | [FR-012](../../../../components/osapi/job-system.md) | -| Which failures terminate a message instead of redelivering it | [FR-013](../../../../components/osapi/job-system.md) | -| The consumer's delivery settings, as defaults a deployment may override | [FR-014](../../../../components/osapi/job-system.md) | -| A long operation is kept alive while it runs, so it is not redelivered mid-flight | [FR-015](../../../../components/osapi/job-system.md) | +| Delivery is at-least-once, and the agent checks for a recorded response before executing | [delivery is at-least-once](../../../../components/osapi/job-system.md#delivery-is-at-least-once-and-the-agent-owes-idempotency) | +| A job that has run is terminal, a failure is reported rather than retried by redelivery | [which failures terminate](../../../../components/osapi/job-system.md#which-failures-terminate-a-message) | +| What happens when the response cannot be written after the work ran | [when the response cannot be written](../../../../components/osapi/job-system.md#when-the-response-cannot-be-written) | +| The consumer's delivery settings, as defaults a deployment may override | [consumer defaults](../../../../components/osapi/job-system.md#consumer-defaults) | +| A long operation is kept alive while it runs, so it is not redelivered mid-flight | [three limits](../../../../components/osapi/job-system.md#three-limits-bounding-different-things) | Two consequences for a new operation, which are yours rather than the system's: @@ -122,9 +126,7 @@ Two consequences for a new operation, which are yours rather than the system's: `provider.WireProviderFacts(a.GetFacts, registry.AllProviders()...)` injects facts into every registered provider, one call, in `internal/agent/agent.go`. A provider registered through the registry is covered; one constructed and passed -somewhere else is not. The obligation on the provider struct itself is -[FR-010](../../../../components/osapi/domains.md), and what a provider does with facts is -[001](../../../../components/osapi/providers.md) FR-008. +somewhere else is not. What a provider does with facts is [facts](../../../../components/osapi/providers.md#facts). ## Tests diff --git a/.claude/skills/add-a-domain/references/api.md b/.claude/skills/add-a-domain/references/api.md index b425860..4e8eed8 100644 --- a/.claude/skills/add-a-domain/references/api.md +++ b/.claude/skills/add-a-domain/references/api.md @@ -21,20 +21,23 @@ exists to end. | What you need to know | Where | | --- | --- | -| The spec is the source of truth for validation, and the three places a tag goes | FR-011 | -| Path parameters are the trap, and what actually validates one | FR-012 | -| Separate verbs for create and update, and why a combined upsert is forbidden | FR-013 | -| The six API design guidelines, including path versus query parameters | FR-014 | -| What `{hostname}` accepts, a literal, `_any`, `_all`, a label selector | FR-015 | -| Broadcast is mandatory, and both paths return the same collection shape | FR-016 | -| The job client has four methods, and an operation adds none | FR-017 | -| `Handler()` returns route closures, and the `Server` struct does not change | FR-018 | -| What verifies a finished domain, and what Step 8 alone misses | FR-024 | +| Node-targeted or controller-only, and which shape applies | [node-targeted or controller-only](../../../../components/osapi/domains.md#node-targeted-or-controller-only) | +| The spec is the source of truth for validation, and the three places a tag goes | [validation](../../../../components/osapi/domains.md#validation) | +| Path parameters are the trap, and what actually validates one | [validation](../../../../components/osapi/domains.md#validation) | +| Separate verbs for create and update, and why a combined upsert is forbidden | [verbs](../../../../components/osapi/domains.md#verbs) | +| The six API design guidelines, including path versus query parameters | [design guidelines](../../../../components/osapi/domains.md#design-guidelines) | +| Broadcast is mandatory, and both paths return the same collection shape | [broadcast is not optional](../../../../components/osapi/domains.md#broadcast-is-not-optional) | +| What `{hostname}` accepts, a literal, `_any`, `_all`, a label selector | [routing and targeting](../../../../components/osapi/job-system.md#routing-and-targeting) | +| The job client has four methods, and an operation adds none | [the job client](../../../../components/osapi/domains.md#the-job-client-needs-nothing-added) | +| `Handler()` returns route closures, and the `Server` struct does not change | [wiring](../../../../components/osapi/domains.md#wiring) | +| The permission a new endpoint needs, and where it is declared | [adding a permission](../../../../components/osapi/permissions.md#adding-a-permission) | +| A domain appears everywhere an existing domain appears | [every layer, or not done](../../../../components/osapi/domains.md#a-domain-is-in-every-layer-or-it-is-not-done) | What a caller sees when an agent does not answer is the job system's, not this layer's: -[004-job-system](../../../../components/osapi/job-system.md) -FR-019 for the four per-host statuses and FR-016 for the two clocks. Domain code +[what a caller gets back](../../../../components/osapi/job-system.md#what-a-caller-gets-back) for the +four per-host statuses, and [three limits](../../../../components/osapi/job-system.md#three-limits-bounding-different-things) +for the clocks. Domain code does not handle the timeout; CLI and SDK output must not imply the operation ran. Read the reference domain's package alongside the specification. The @@ -89,18 +92,16 @@ handlers = append(handlers, ## Three rules the corpus does not yet hold Stated here because they are real and nothing else states them. Both belong in -the corpus and neither is there, which is recorded rather than left to be -discovered, the same treatment FR-019 gives the absent `sdk-standards` -capability. +the corpus and neither is there yet. **A custom validation rule is a registered validator.** It belongs in `internal/validation` with a hint in `customHints`, so the 400 says what shape was expected rather than naming the tag. `sysctl_key` and `cron_schedule` are the pattern. -**`IsBroadcastTarget` has one implementation and never a second.** FR-015 cites -it at `internal/job/subjects.go:306`, so the corpus names where it lives; what the -corpus does not say is that a domain must not write its own target parser. +**`IsBroadcastTarget` has one implementation and never a second.** It lives in +`internal/job/subjects.go`. What the corpus does not say is that a domain must +not write its own target parser. **A permission is chosen by blast radius, not by endpoint group.** Two operations that differ in how much damage they can do want two permissions however similar diff --git a/.claude/skills/add-a-domain/references/cli.md b/.claude/skills/add-a-domain/references/cli.md index 2ee3d99..d7619b3 100644 --- a/.claude/skills/add-a-domain/references/cli.md +++ b/.claude/skills/add-a-domain/references/cli.md @@ -10,12 +10,13 @@ The obligations this layer carries are stated in | What you need to know | Where | | --- | --- | -| One parent command per domain, one subcommand per endpoint | FR-021 | -| `--json` on every command, and flags rather than positional arguments for IDs | FR-021 | -| `cli.PrintKV` for a single resource, `cli.PrintCompactTable` for rows | FR-021 | -| Every response code the spec declares handled in the status switch | FR-021 | -| What `--target` accepts, a literal, `_any`, `_all`, a label selector | FR-015 | -| What verifies a finished domain, and what Step 8 alone misses | FR-024 | +| One parent command per domain, one subcommand per endpoint | [one command per endpoint](../../../../components/osapi/cli.md#one-command-per-endpoint) | +| Flags rather than positional arguments for identifiers | [values arrive as flags](../../../../components/osapi/cli.md#values-arrive-as-flags-never-as-positional-arguments) | +| What `--target` accepts, and why no domain redeclares it | [two inherited flags](../../../../components/osapi/cli.md#two-flags-every-command-inherits) | +| `--json` returning raw bytes before anything is formatted | [json returns first](../../../../components/osapi/cli.md#json-returns-before-anything-is-formatted) | +| `cli.PrintKV` for a single resource, `cli.PrintCompactTable` for rows | [four shared helpers](../../../../components/osapi/cli.md#rendering-is-four-shared-helpers-not-per-command-formatting) | +| Every response code the spec declares handled the same way | [one error handler](../../../../components/osapi/cli.md#errors-go-through-one-handler) | +| A domain appears everywhere an existing domain appears | [every layer, or not done](../../../../components/osapi/domains.md#a-domain-is-in-every-layer-or-it-is-not-done) | ## Files @@ -51,8 +52,7 @@ operators read these side by side. Stated here because they are real and nothing else states them, not the corpus, not osapi's `CONTRIBUTING.md`. Recorded as unstated rather than left to be -discovered, the same treatment FR-019 gives the absent `sdk-standards` -capability. +discovered. **A command whose remote work failed exits non-zero.** For `command exec` and `command shell` the exit code is the remote command's, through diff --git a/.claude/skills/add-a-domain/references/docs.md b/.claude/skills/add-a-domain/references/docs.md index d47f512..0a4372d 100644 --- a/.claude/skills/add-a-domain/references/docs.md +++ b/.claude/skills/add-a-domain/references/docs.md @@ -14,10 +14,10 @@ The obligation is stated in | What you need to know | Where | | --- | --- | -| A domain appears everywhere an existing domain appears, and the check is to pick one and search for it | FR-001 | -| What verifies a finished domain, and why the documentation gate is not in Step 8's commands | FR-024 | +| The permission a new domain needs, and the tables that name it | [adding a permission](../../../../components/osapi/permissions.md#adding-a-permission) | +| A domain appears everywhere an existing domain appears | [every layer, or not done](../../../../components/osapi/domains.md#a-domain-is-in-every-layer-or-it-is-not-done) | -FR-024 is the one to read before running anything: the documentation below is +Read this before running anything: the documentation below is checked by `docusaurus-fmt-check` and `docusaurus-build`, which run in `just test` and **not** in `just ready`. Writing these pages and then running only the build-and-unit commands hands in work that fails continuous integration on the @@ -53,10 +53,10 @@ A new permission appears in four places: the spec, the role expansion in code, `authentication.md`, and `configuration.md`. Missing the last two means operators cannot discover it. -**`architecture/api-guidelines.md` is not on this list any more.** It was, and the -row said to add a new path pattern to its table. The page no longer exists: its six -guidelines are FR-014 and FR-015, and its address redirects. A new path pattern is -now a question of whether it obeys FR-014, not of whether a table lists it. +**There is no API guidelines page to update.** A new path pattern is a question +of whether it obeys the +[design guidelines](../../../../components/osapi/domains.md#design-guidelines), not of whether a table +lists it. ## Writing diff --git a/.claude/skills/add-a-domain/references/provider.md b/.claude/skills/add-a-domain/references/provider.md index cd1f3a7..9ea4745 100644 --- a/.claude/skills/add-a-domain/references/provider.md +++ b/.claude/skills/add-a-domain/references/provider.md @@ -15,25 +15,24 @@ Read it before writing one. This file holds only what that specification does not: where the files go, what they are called, and the scaffolding to start from. -That split is deliberate, and the specification requires it (FR-016). A rule -restated here would drift from the one in the corpus, and the copy an agent -happened to load would win. - -| What you need to know | Where | -| --------------------------------------------------------- | -------- | -| A provider is the operations layer, and what it returns | FR-001-3 | -| The idempotency contract, as a table of operation outcomes | FR-004 | -| `ErrUnsupported` is a fourth outcome, not a failure | FR-005 | -| The four implementation patterns, and how to choose | FR-006 | -| Platform variants, and how the agent selects one | FR-007 | -| How a provider obtains host facts | FR-008 | -| Why the provider validates what the API already validated | FR-009 | -| Secrets reach a command without appearing in it | FR-010 | -| A caller's value never becomes an option | FR-011 | -| Filesystem access, and why not the `os` package | FR-012 | -| A file is not written in place | FR-013 | -| The testing obligations that belong to the provider | FR-014 | -| What a provider does not touch | FR-015 | +That split is deliberate. A rule restated here would drift from the one in the +corpus, and the copy an agent happened to load would win. + +| What you need to know | Where | +| --- | --- | +| A provider is the operations layer, and what it returns | [what an operation returns](../../../../components/osapi/providers.md#what-an-operation-returns) | +| The idempotency contract, as a table of operation outcomes | [the idempotency rule](../../../../components/osapi/providers.md#the-idempotency-rule) | +| Somebody edited the host by hand, and the next run overwrites it | [osapi owns the state](../../../../components/osapi/providers.md#osapi-owns-the-state-so-drift-gets-overwritten) | +| `ErrUnsupported` is a fourth outcome, not a failure | [unsupported is not failure](../../../../components/osapi/providers.md#unsupported-is-not-failure-and-not-no-change) | +| The four implementation patterns, and how to choose | [the four patterns](../../../../components/osapi/providers.md#the-four-implementation-patterns) | +| Platform variants, and how the agent selects one | [platform selection](../../../../components/osapi/providers.md#platform-selection-happens-outside-the-provider) | +| How a provider obtains host facts | [facts](../../../../components/osapi/providers.md#facts) | +| Why the provider validates what the API already validated | [validation does not discharge](../../../../components/osapi/providers.md#validation-on-the-request-path-does-not-discharge-the-providers) | +| Secrets reach a command without appearing in it | [secrets are not arguments](../../../../components/osapi/providers.md#a-secret-never-appears-in-a-commands-arguments) | +| A caller's value never becomes an option | [never parsed as an option](../../../../components/osapi/providers.md#a-callers-value-is-never-parsed-as-an-option) | +| Filesystem access, the exec manager, and why a file is not written in place | [what it goes through](../../../../components/osapi/providers.md#what-a-provider-goes-through-not-around) | +| What a provider does not touch | [what it does not touch](../../../../components/osapi/providers.md#what-a-provider-does-not-touch) | +| The testing obligations that belong to the provider | [what its tests owe](../../../../components/osapi/providers.md#what-its-tests-owe) | Read the reference domain's provider package alongside it. The specification says what must hold; an existing domain shows it holding. @@ -71,8 +70,9 @@ stands alone and reads `/etc/resolv.conf` directly. ## Scaffolding -The shapes to start from. What they have to satisfy is FR-002, FR-003 and -FR-008. +The shapes to start from. What they have to satisfy is +[what an operation returns](../../../../components/osapi/providers.md#what-an-operation-returns) and +[facts](../../../../components/osapi/providers.md#facts). ```go // types.go, package {domain} diff --git a/.claude/skills/add-a-domain/references/sdk.md b/.claude/skills/add-a-domain/references/sdk.md index b2ba49d..a1b3a47 100644 --- a/.claude/skills/add-a-domain/references/sdk.md +++ b/.claude/skills/add-a-domain/references/sdk.md @@ -7,23 +7,18 @@ combined spec as the server, so `just generate` covers it. | What you need to know | Where | | --- | --- | -| Four files per service, a `Client` field, an example, a doc page, the navbar entry | [FR-019](../../../../components/osapi/domains.md) | -| No `gen` type in a public signature; JSON tags on every result type; errors wrapped with context; one service per file | [FR-020](../../../../components/osapi/domains.md) | -| What verifies a finished domain, and what Step 8 alone misses | [FR-024](../../../../components/osapi/domains.md) | - -**There is no `sdk-standards` capability.** Earlier versions of this file said the -binding rules were "the `sdk-standards` capability in this repository", that they -bound `osapi-orchestrator` too, and that the capability won any disagreement. -Nothing of the sort has been written. The claim was also on osapi's -`adding-an-api-domain.md`, so two documents deferred to a specification that reads -as settled and does not exist, recorded as -[FR-019](../../../../components/osapi/domains.md)'s gap rather than repeated here. - -What that means in practice: the conventions below and in FR-019 and FR-020 are -what actually binds, because they are what can be checked against -`pkg/sdk/client/`. If a cross-repository SDK standard is wanted, it is a feature -of its own, and inventing one inside a citation file is how a rule comes to exist -that nobody agreed. +| What a service owes, and the four files per service | [what a service owes](../../../../components/osapi/sdk.md#what-a-service-owes) | +| Method naming, and the seven methods that break it | [method naming](../../../../components/osapi/sdk.md#method-naming) | +| How the package is laid out | [package layout](../../../../components/osapi/sdk.md#how-the-package-is-laid-out) | +| No `gen` type in a public signature, and the envelope every method returns | [the envelope](../../../../components/osapi/sdk.md#every-method-returns-the-same-envelope) | +| JSON tags, wrapped errors, one service per file | [the rest of the conventions](../../../../components/osapi/sdk.md#the-rest-of-the-conventions) | +| A domain appears everywhere an existing domain appears | [every layer, or not done](../../../../components/osapi/domains.md#a-domain-is-in-every-layer-or-it-is-not-done) | + +There is no cross-repository SDK standard. The conventions in the corpus and +below are what binds, because they are what can be checked against +`pkg/sdk/client/`. If a shared standard with `osapi-orchestrator` is wanted it is +a feature of its own, and inventing one inside a reference file is how a rule +comes to exist that nobody agreed. ## Files, one service per domain