Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions .claude/skills/add-a-domain/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
26 changes: 14 additions & 12 deletions .claude/skills/add-a-domain/references/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:

Expand All @@ -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

Expand Down
35 changes: 18 additions & 17 deletions .claude/skills/add-a-domain/references/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
16 changes: 8 additions & 8 deletions .claude/skills/add-a-domain/references/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions .claude/skills/add-a-domain/references/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
42 changes: 21 additions & 21 deletions .claude/skills/add-a-domain/references/provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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}
Expand Down
Loading
Loading