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