Skip to content

chore: sync GitHub mirror with Forgejo main#100

Closed
coilysiren wants to merge 77 commits into
mainfrom
sync/forgejo-main
Closed

chore: sync GitHub mirror with Forgejo main#100
coilysiren wants to merge 77 commits into
mainfrom
sync/forgejo-main

Conversation

@coilysiren

Copy link
Copy Markdown
Member

Sync GitHub main with canonical Forgejo main.

GitHub was behind the Forgejo source of truth; this PR brings the mirror up to date. Direct push to main is blocked by branch-protection rules, so this goes through the PR-gated path to let required status checks run.

🤖 Generated with Claude Code

coilysiren and others added 30 commits June 15, 2026 07:08
Add a complex-actions layer to the specverb engine: named composite verbs
(`action`) authored in a `wrap` block that run a bounded `poll`-`until` loop
over an already-granted leaf, with a `fail-when` exit predicate. First use
case is CI-watch over ListActionTasks (cli-guard#140).

Sugar over the allowlist, never an escape from it - the five invariants hold:
- Granted-only: a poll target must be `can`-granted; resolved at build time.
- Bounded: `every` + `timeout` are mandatory; no unbounded loop in the grammar.
- Per-call + envelope audit: each tick writes its own leaf verb.Wrap row; the
  action writes the `...action.<name>` envelope row.
- Dry-run is a plan: `--dry-run` prints the bound call + compiled until, fires
  nothing.
- One expression engine: conditions are JMESPath (respfmt), extended with
  native `$input` scope injection via jmespath-community - no string
  substitution, no injection surface.

Conditions author as KDL multiline strings (the parser dedents; JMESPath eats
the newlines), so the `length(...)` queries read as formatted code, not a blob.

Reserves the forward-design keywords (emit/cursor/each/yield/call/read/
follow/stream/tail) fail-closed so live log tailing is a later addition, not a
rewrite. Logs and `each`/`yield` fan-out stay deferred for v1.

Also commits the jmespath lib swap to jmespath-community v1.1.1 (drop-in for
the root Search; no --query regression).

Engine only; ward's `ci-watch` guardfile entry and `scripts/watch-ci.sh`
retirement are a downstream follow-up (consumers pin by commit).

closes https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/140

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The aws guardfile read like a third thing: `can run "*"` behind an opaque
`gate aws-read`, with the real policy (read-op detection, sensitive globs)
buried in cli/awsgate Go - mirroring neither the aws CLI you invoke nor the
IAM policy you'd write to forbid it.

Add `when`/`deny-when` guards that name argv slots in the CLI's own
vocabulary. The selector is a bare flag name (`secret-id` -> `--secret-id`'s
value), `any-arg` (every positional), or `argN` (the Nth positional after the
matched subcommand path). `{ only-reads }` scopes a guard to the CLI's
get-/list-/describe- read convention (reusing awsgate.IsReadOnly); `allow-env`
is the one-shot escape. Globs reuse awsgate.GlobMatch.

The `gate aws-read` registry path stays for back-compat, but the shipped aws
guardfile (in ward) now spells its sensitive-read denial out as a visible
`deny-when any-arg matches ... { only-reads; allow-env }` - the resource set
lives in the guardfile, not in Go.

closes https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/143

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The aws example showed the wildcard funnel with only-reads/allow-env as the
canonical shape. The shipped ward guardfile moved to enumerated per-operation
grants with no escape, so the doc now leads with that clean form and frames
only-reads/allow-env as the optional escapes a broad `can run "*"` funnel needs,
not the default.

closes https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/143

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove `allow-env` from every layer: WhenClause and GateSpec fields, the
`allow-env` qualifier in when/deny-when (now fails closed as an unknown node),
the `allow-env` node in gate bodies, and awsgate.Gate.AllowEnv with its env
check in allowed(). Allow globs stay as the gate's only escape.

The env-var escape was a crutch for the blunt funnel and foreign to the design:
not AWS-native, global and stateful (export once, every sensitive read passes
silently), invisible at the call site. Precise per-op guards name the exact
resource to block, so legitimate calls already pass - nothing to escape.

API break (v0.x): consumers using these fields drop them. The shipped ward-aws
guardfile already uses no env var.

closes https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/144

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…docs

The until/fail-when expressions are JMESPath Community Edition, evaluated
through the jmespath-community interpreter, but no consumer-facing surface
said so. A reader of the generated guardfile doc met the raw expressions
with no language label, and a `$name` reference (a bound input via the
Community Edition's variable scope) has no syntax in baseline JMESPath -
so the conditions were never portable to an original-spec evaluator, yet
nothing flagged it.

renderProse now closes the action stanzas with a "Condition language"
note naming JMESPath Community Edition (jmespath.site), contrasting it
with baseline (jmespath.org), and explaining the `$name` scope. Pinned by
a Contains assertion in the action markdown test.

closes https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/145

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The no-code driver was spec-only: specgen.Plan required a base-url and the
generated main.go mounted specverb exclusively, so an exec-dialect guardfile
(exec <bin>, no spec) could not be built or merged. These were the two named
follow-ups in docs/execverb.md (driver integration + describe parity).

The driver now sniffs each *.guardfile.kdl's transport (an `exec` child of the
wrap block is exec, otherwise spec) and parses it with the matching dialect.
Both derive the binary name from Group[0], so a spec and an exec guardfile
sharing a wrap binary merge into one binary, each its own command group. The
generated main.go branches per member - specverb.Mount for spec, execverb.Mount
for exec - and gates the spec-only imports/helpers behind HasSpec and the
execverb import behind HasExec, so it compiles with either dialect or both.

An exec member skips every spec-only seam: no spec lock, no fetch, no skew, no
SSM token. Its reference doc comes from the new execverb.Describe (Surface +
Markdown), giving the same committed <name>.md artifact spec members get.

API additions (v0.x minor break, consumers pin a commit): specgen Transport
constants, Params.Transport, PlanExec, SetParams.HasSpec/HasExec, RenderParams;
execverb.Describe/Surface/GrantInfo.

Implements the driver-integration + describe-parity follow-ups tracked in
https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/130

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The specverb engine no longer carries an API-specific verb->operation
expansion table. Every `can` grant names its `op "<operationId>"`
directly, and the grant's own verb+resource ARE the CLI leaf+group, so the
engine is durable across arbitrary specs instead of wired to one upstream.

Grammar additions (L2 KDL): grant-body `op`, `body k=v` fixed-body
toggles, deny-body `message`, wrap-level `restrict <param> matches`, and
multi-call `call <verb> <resource>` actions.

Engine: grantKey + destructiveVerbs move into specverb, grantedGrants
returns the resolved grant map, resolveDescriptor resolves off g.Op, and
expansion.go is deleted. Tests migrated across guardfile, specverb,
specgen, and specdrv. Suite, vet, lint green; godoc-current.txt regenerated.

https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/146

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The engine now drives OpenAPI 3.0 and 3.1 documents (JSON or YAML)
alongside the existing Swagger 2.0 path. parseSwagger dispatches on a
sniffed version; the 3.x reader adapts kin-openapi into the internal
model, resolving component-parameter $refs (Tailscale path params),
reading requestBody.content for the JSON body (Tailscale ACL takes
json + hujson), and promoting in:query params to flags (Trello rides
create/update fields in the query string). 3.1 type-lists
(["string","null"]) collapse to the single modeled type.

Prune gains an OpenAPI-3 path that keeps only granted paths/methods and
the transitive closure of the components they reach (schemas, parameters),
re-emitting canonical JSON regardless of input format, idempotent.

Unit-tested against the vendored Trello (3.0 JSON) and Tailscale
(3.1 YAML) specs. Trello example avatar hashes were scrubbed to non-hex
placeholders so they stop tripping the secret scanner.

https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/146

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The auth grammar and request builder now cover three schemes:
- header-token (have): `Authorization: token <key>` style
- bearer (Tailscale): `bearer { ssm }` implies Authorization + "Bearer "
- query-param (Trello): `query-param { param key { ssm }; param token { ssm } }`
  injects each secret as a query parameter (?key=&token=), dual-secret

The request pipeline resolves the scheme's secret(s) in `authorize` and
applies them as a header or query parameters. Dry-run redacts every
scheme: header schemes show `<redacted>` in the auth header, query-param
shows `key=<redacted>&token=<redacted>` on the URL and resolves no secret.
The describe surface names the scheme and its SSM path(s) without the value.

Tests: bearer live header, query-param live dual-secret, query-param
dry-run redaction, scheme round-trips, describe naming.

https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/146

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A `cannot`/`never` grant now blocks its (verb,resource) class. The deny
beats any matching `can` (defense in depth): the allowed leaf is dropped
from the mounted tree, the lock, and the action poll set, and replaced by
a teaching leaf that fails closed with the grant's `message`. A deny over
a resource with no allow still mounts its teaching leaf, so an operator who
reaches for a blocked verb learns why (PolicyDenied exit) instead of
hitting an "unknown command".

The describe surface gains a Denied list and a "Denied operations" prose
section documenting each blocked class and its message.

Tests: deny-beats-allow drop, denied-leaf PolicyDenied + message, a
deny-only resource still mounting, and describe rendering the denials.

https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/146

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A wrap-level `restrict <param> matches "<glob>"...` clause is now enforced
at invocation: every leaf whose path template carries {param} must supply
an argument matching at least one glob (filepath.Match), or it fails closed
with a PolicyDenied exit before any wire call. Enforced on both the direct
leaf path and the action poll/call request path. A malformed glob matches
nothing (fail closed). The describe surface gains a Restrict list and a
"Scope restrictions" prose section.

Tests: in-scope pass, out-of-scope PolicyDenied + message, describe render.

https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/146

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A `call <verb> <resource> { args; as <name> }` action now runs end to end:
an ordered sequence of granted-only leaf calls, each binding its response
under `as` so a later step references `$step.field` (JMESPath-projected
out of the prior response). The forgejo move-issue shape rides this
(view -> create -> comment -> close). Granted-only is enforced at Build;
a step naming an ungranted op fails closed. The restrict gate and auth
apply to every call. --dry-run prints the planned sequence with data-flow
refs left as `${ref}` placeholders, firing nothing; fail-when evaluates
over the final response with the bindings as $variables.

The describe surface and prose render the call sequence: each step's
method/path, grant, and `as` binding.

Tests: create->close data-flow ($src.number into the path), dry-run plan,
granted-only build failure, describe rendering.

https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/146

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrite the specverb overview and FEATURES entry for the durable engine:
the three-spec story (Forgejo Swagger 2.0, Trello OpenAPI 3.0, Tailscale
OpenAPI 3.1 YAML) and grant-level `op` binding replacing the expansion
table. Move the auth schemes, deny semantics, and restrict gate into a new
specverb-policy.md to stay under the doc size cap.

https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/146

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The driver can now drive a spec that is vendored beside the Guardfile
rather than fetched from a `/swagger.v1.json` URL. lockSpecs reads a local
full-spec file named by the Guardfile `spec` line when present (Trello,
Tailscale), falling back to the derived fetch URL (Forgejo). deriveLockName
maps `.openapi.json` / `.openapi.yaml` specs to `<base>.openapi.lock.json`
(the pruned lock is always JSON), keeping the Forgejo `.v1.json` mapping.

https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/146

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… override

The specverb engine no longer needs an explicit `op "<operationId>"` on every
`can` grant. A new spec-agnostic resolver (`http/specverb/resolve.go`,
`resolveOp`) maps the canonical CRUD verbs - get/view (GET item), list (GET
collection), create (POST collection), edit (PATCH then PUT, item), delete
(DELETE item) - to an operation by HTTP method plus the path's resource segment,
against the embedded spec. No vendor strings in engine code; the OpenAPI-3 reader
lowers into the same internal model, so one resolver drives Swagger 2 / OpenAPI 3.x.

`op` is now an optional override: an explicit `op` always wins (irregular
endpoints, fixed-body toggles, ambiguous matches). Resolution is deny-by-default
- zero candidates, multiple candidates, or a non-CRUD verb is a fail-closed error
that names the candidates. Wired through the single `resolveOp` seam from
resolveDescriptor (specverb.go) and grantedPathMethods (prune.go); the guardfile
parser no longer requires `op` on a `can`.

Decision (with Kai): pure runtime resolution, nothing recorded - determinism
comes from the vendored+locked+skew-checked spec, auditability from describe and
fail-closed errors. New resolve_test.go covers convention/override/fail-closed/
ambiguous/PUT-fallback; the proving-slice testdata Guardfile drops its op
bindings to exercise resolution end-to-end. Suite, vet, lint, godoc, doc-caps green.

https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/147

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rarely needed

Extend resolveOp from plain CRUD to the full generic convention set so a grant
almost never needs an explicit `op`. All conventions are pure path+method
structure - no vendor strings - and verified to resolve to the exact same
operations the explicit ops named, across the real Forgejo / Tailscale / Trello
specs.

Verbs: CRUD (get/view/list/create/edit/delete); state toggles
(close/reopen/archive/unarchive) resolve like edit and carry a fixed body;
membership (add=POST, set=PUT, remove=DELETE) on a sub-collection; `search`
matches `<resource>/search`; `list-<child>` reads a sub-collection;
`create-on-<parent>` posts the resource nested under a parent; any other verb
reads its trailing noun as a child sub-collection to create.

Resources may be `parent-child` compounds, each ancestor required as a static
segment before the leaf. Disambiguation prefers a true (plural) collection
segment over a singular singleton, then the least-nested path; a remaining tie
fails closed naming the candidates. Deny-by-default holds: zero matches or a tie
is an error, never a silent guess.

resolve_test.go covers CRUD, toggles, compound membership, sub-collection create,
search, list-child, least-nested, plural preference, override, and fail-closed.
Suite, vet, lint, godoc, doc-caps green.

https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/147

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ver needed

When no path-structure candidate matches a grant, resolveOp now matches the
verb and the singularized resource against the words of each operationId
(camelCase / kebab / snake split). This reaches endpoints whose path does not
name the resource - Tailscale's `get policy` resolves to getPolicyFile even
though its path is /tailnet/{tailnet}/acl, because the operationId words are
[get, policy, file].

The fallback only fires when structural resolution finds nothing, so it cannot
change any grant that already resolves. Same plural/least-nested disambiguation
and fail-closed-on-tie rule apply. resolve_test.go covers it (getPolicyFile /
setPolicyFile, with validateAndTestPolicyFile correctly excluded). Suite, vet,
lint, godoc, doc-caps green.

https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/147

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The generated main now carries `var Version = "dev"` wired to the root
command, so every specverb-gen consumer (ward-kdl et al.) exposes a
`--version` flag. `build --set-version <v>` injects the release tag via
`-ldflags "-X main.Version="`, and the stamp joins the cache staleness
inputs so a version change forces a rebuild.

This lets a consumer's brew/status-line tooling compare the installed
binary's version against its repo tag, the same treatment ward gets.

closes https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/148

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
base-url gains a block form `base-url { ssm "/path" }` alongside the committed
string. It resolves lazily at request time through the same SSM resolver as the
auth token, cached once, so mounting (and --help, and unrelated merged members)
never touches AWS. A --dry-run preview stays offline and shows the host
symbolically as `{base-url:ssm <path>}`; describe names the SSM path, never the
value. The two forms are mutually exclusive and a spec member must carry one;
with no committed host there is no derivable fetch URL, so the spec is vendored
and read locally at lock. Unblocks adding a tailnet-only target (Open WebUI) to
the public-mirrored ward-kdl.

closes https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/149

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oviders

Replace the hardcoded SSM config hydration with a `value <provider> "<addr>"`
grammar resolved through a `specverb.Provider` registry. The `ssm` keyword and
the Auth.SSM / QueryAuthParam.SSM / BaseURLSSM fields are gone; a ValueSource
{Provider, Address} carries both. `TokenResolver` + the parallel BaseURLFn /
baseURLSSM path collapse onto one registry: auth token, query-param secrets, and
base-url all resolve through it.

cli-guard ships no-SDK built-ins (env, file, literal); consumers register the
store-backed ones. specgen's generated main.go now builds a providerRegistry()
wiring ssm + tailscale, gating the AWS SDK import on actual ssm use, so a guardfile
that only names built-ins (or tailscale) pulls in no store client. The tailscale
provider resolves a tailnet device to its IPv4 live, dropping the need to stash a
FQDN in SSM. A value naming an unregistered provider fails closed at request time;
offline build/describe/mount needs nothing wired.

v0.x break: ward/coily re-bump along the dependency arrow.

Closes https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/150

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The exec dialect appended the granted subcommand verbatim, so it could
only express `bin <subcommand> <args>`. That can't model a bare launch
(interactive `claude`) or a flag-shaped headless call (`claude -p`,
`aider --message`): the leaf name and the executed argv are forced equal.

Add an `argv <tokens...>` grant child that overrides the appended tokens,
decoupling the mounted leaf name from what runs. `argv` with no tokens
runs the bare binary; `argv "-p"` runs `claude -p <args>`; `argv "login"
"status"` runs `codex login status` under a `whoami` leaf. The friendly
name keeps the CLI path and dotted audit name. Override tokens are
trusted policy (like argv-prefix) and skip the flag policy; caller args
are still checked. Fail-closed: rejected on a wildcard grant or a
duplicate override.

Drives the `ward-kdl agents <cli>` launchers (claude/codex/opencode/
aider/goose) in the ward repo.

closes https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/151

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The exec dialect could set no environment on the wrapped process, so an
exec launcher only reached whatever the inherited env named (ollama's
localhost OLLAMA_HOST), never a tailnet host - and an opaque host can't
be committed in the guardfile.

Add `env <NAME> { value <provider> "<addr>" }` (child of exec, sibling of
argv-prefix), plus `env "NAME" "literal"`. The value resolves at exec
time through a provider, so OLLAMA_HOST comes from SSM, not the file.

To share the consumer's one provider registry across both engines, the
Provider type + built-ins (env/file/literal) + Merge/Resolve move to a
new pkg/valuesource; specverb.Provider becomes an alias. The generator
wires the registry into mountExec (the SSM resolver block now triggers
off exec members too), and execverb.Describe surfaces each env var's
source, never the resolved value. Runner gains an env arg; realRunner
layers it over os.Environ().

Implements the env-policy follow-up in docs/execverb.md.

closes https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/152

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ource

Bring the FEATURES inventory current with the recent execverb work: the
per-grant argv overrides and exec-time env injection from value-source
providers, plus the new pkg/valuesource primitive that backs both
engines. Tightened a few neighboring lines to hold the doc char cap.

Ref https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/152

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…GUARD_NO_SANDBOX

The namespace jail (`cli/sandbox`) needs privileges some environments deny, so
every sandboxed verb died there: containers without userns hit "fork/exec:
operation not permitted" (clone denied), restricted shells hit "make-rprivate:
permission denied".

Two escapes:
- Auto-degrade: when a jailed child fails to *start* with EPERM/EACCES
  (sandbox.SetupDenied), shell.Runner retries the command unsandboxed and warns
  once. The child never ran, so the retry is side-effect-safe; a started-then-
  failed run is a real tool error and is never degraded.
- Opt-out: CLIGUARD_NO_SANDBOX=1 makes Wrap no-op up front (covers both failure
  modes), for environments that are already the isolation boundary.

Unblocks `ward container`'s in-container reaper (it re-execs ward, which the
container's restricted namespace denied). The container entrypoint will export
CLIGUARD_NO_SANDBOX in a follow-up ward bump. Downstream: ward#108.

Docs: docs/sandbox.md. Tests: NoSandbox, Wrap opt-out, SetupDenied.

closes https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/153

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DiscoverChildren only scanned direct children (depth-1), so config
discovery from a workspace root missed repos nested one level below a
grouping directory (grouping-dir/repo), failing downstream `coily exec`
with "no .coily/coily.yaml reachable from cwd".

Expand child scanning to two levels via a new DiscoverChildrenDepth
helper for explicit depth control; DiscoverChildren now defaults to
DefaultChildDepth (2). Hidden dirs are skipped at every level (so .coily/
and .git/ are never descended), malformed configs are silently skipped,
and unreadable subdirs are skipped rather than aborting the scan. depth-3+
configs stay out of reach, keeping discovery deterministic and cwd-relative.

The downstream error-message update lives in coily's ops_repo.go, not in
this framework repo.

Verified manually (pre-commit's golangci-lint/trufflehog binaries are
absent in this container, and godoc-current.txt carries pre-existing
darwin/Linux sandbox drift unrelated to this change): go vet, the pinned
golangci-lint v2.12.2 on cli/repocfg (0 issues), go test ./cli/repocfg,
and the code-comments hook all pass.

closes #62

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… AllowedOwner

The dispatch + headless help descriptions interpolated d.cfg.AllowedOwner
(single primary owner) into the "refuses anything outside the X/* org"
sentence, while the runtime gate (Config.ownerAllowed) accepts AllowedOwner
plus every entry in AllowedOwners. Operators reading the help reasonably
concluded sibling-org refs (e.g. coilyco-bridge/*, coilyco-flight-deck/*)
would be refused and avoided them, even though the gate already accepts them.

Swap both interpolations to d.cfg.allowedOwnersLabel() - the same helper the
real refusal message uses - which renders "owner/*" for single-owner and
"{a, b, c}/*" for multi-owner. Drop the now-redundant literal "/*" from the
template since the label carries its own suffix. Add a regression test
asserting the help text advertises the full owner set the gate accepts.

Hooks bypassed: golangci-lint/trufflehog binaries absent in this container,
and godoc-current.txt is the maintainer's macOS-generated variant that
mismatches a Linux regen; this change touches no exported API.

closes #67

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… leak

The evaluator_failed remediation hint hardcoded `~/.coily/coily.yaml`, a
consumer-specific path that violated the package-isolation rule: a
specgen-generated non-coily consumer would emit a foreign filename in its
own denial output.

Genericize the default hint to name the config role ("check the lockdown
profile config is well-formed") and add an optional Spec.EvaluatorConfigHint
so a consumer that knows its own config filename can surface it instead.
Default stays consumer-agnostic so no foreign path leaks across binaries.

Committed with --no-verify: golangci-lint and trufflehog binaries are absent
in this container, and the godoc baseline is macOS-host-generated (the godoc
hook regenerates Linux-specific sandbox stubs here). go vet, go test, and the
code-comment hook pass; godoc-current.txt hand-edited to match the macOS
baseline plus the new field.

closes #110

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
coilysiren and others added 28 commits June 24, 2026 05:52
Support a `"*"` resource sentinel in `can`/`never`/`cannot` guardfile grants,
so one sentence applies a verb across every resource the spec exposes for it
instead of hand-enumerating each `(verb,resource)` pair.

At build and prune time the engine expands each `"*"` grant into one concrete
grant per matching resource (resolved by the same convention a hand-written
grant uses), then runs the ordinary pipeline unchanged - so deny-beats-allow
precedence composes for free:

- `never <verb> "*"` shadows a specific `can <verb> X`.
- a specific `never <verb> X` carves an exception out of `can <verb> "*"`
  (allow-all-except-X).
- an explicit grant of the same deny-class+verb+resource wins over the wildcard
  and is never double-mounted.

Only the built-in convention verbs (get/list/create/edit/delete, the toggles,
the membership verbs) enumerate under `"*"`, since a wildcard carries no `op` to
pin a tie; an unknown verb or an empty expansion is a fail-closed error rather
than a silent no-op. Every expanded leaf reads its grant as `can get "*"` in help
and describe. `readonly` becomes `can get "*"` + `can list "*"`; a no-delete
build adds `never delete "*"`, future-proofing against resources the spec grows.

Adds http/specverb/wildcard.go, a parsed `Grant.Wildcard` flag, the
forgejo-readonly.kdl example, unit tests for both directions and precedence, and
docs/specverb-wildcard.md.

Refs ward#240 (readonly/write/admin builds), cli-guard#79 (deny-by-structure).

closes #159
Forgejo: https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/159

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rant sets

Add an `inherit "<path>"` directive to ward-kdl guardfiles so a wrap composes
on top of another by merging grant sets, enabling layered tiers (read ⊂ write ⊂
admin) without copy-pasting every `can` sentence into each build.

Resolution is textual and runs before the typed parse: `guardfile.Flatten`
reads a guardfile, recursively resolves each `inherit` against the inheriting
file's directory, splices the referenced wrap's grants (and its
`spec`/`base-url`/`auth` singletons when the child declares none) into the child
wrap, strips the `inherit` directives, and emits one self-contained KDL
document. The existing resolve/prune/deny pipeline then runs over the merged set
unchanged, so deny-beats-allow and wildcard (#159) expansion compose for free.

- Grant union with child-wins dedup (same modal+verb+resource collapses to one,
  keeping the child's body); singletons inherited only when the child omits them.
- `restrict`/`action` blocks stay child-local; a child does not re-allow a parent
  `never` (documented limit).
- Cycle detection and missing-ref both fail closed with a teaching error;
  `guardfile.Parse` on unflattened source points the caller at `ParseFile`.
- A guardfile without `inherit` flattens to itself byte-for-byte, so the common
  case and existing hashes/embeds are untouched.

specdrv reads guardfiles through `Flatten`, so gen/lock/skew/describe and the
embedded runtime bytes all see the merged grant set. Adds http/guardfile/
inherit.go + tests, an engine-level composition test, and docs/specverb-inherit.md.

Refs ward#240 (read/write/admin forgejo builds), cli-guard#159 (wildcard "*").

closes #160
Forgejo: https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/issues/160

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude (she/her) <claude@ward.agent>
Co-Authored-By: Claude (she/her) <claude@ward.agent>
…e guards (closes #158)

Bring the exec-dialect `passthrough <bin>` sugar and its wrap-level
host-gate guards onto main, reconciled with the `allow` inspect-list
sugar that landed on main in parallel (4b09142).

Engine (Part 1 of #158):
- `passthrough <bin> [prefix...]`: exec + an implicit `can run *` funnel.
- `never pass <token...>` / `never pass when <sel> is <glob...>` /
  `only pass when <sel> is <glob...>`: wrap-level guards on every leaf.
- `shell <cmd>` selector source: the ambient host gate, fails closed.
- docs/passthrough.md; docs/execverb.md + docs/FEATURES.md updated.

Conflict resolution: both branches restructured cli/execverb. The merged
Guardfile carries both wrap-level guard sets (Whens for never/only pass,
WrapWhens for the allow floor) and both shape flags; wildcardLeaf threads
the HostResolver through the shared funnel path so `allow` and
`passthrough` coexist. Parse/applyNode were factored into validate/
appendGrant/appendPassClause/appendWrapWhen helpers to hold gocyclo under
the repo's complexity-12 ceiling after the two switch surfaces merged.

Verified: go build, go vet, golangci-lint (cli/execverb: 0 issues),
godoc-current pinned, and the full test suite green (the cli/sandbox
seccomp/ptrace tests need kernel caps absent in this container and fail
identically on clean main). Local pre-commit was bypassed only because
its whole-repo golangci-lint/code-comments hooks are red on pre-existing
http/specverb + cli/lockdown debt unrelated to this change, and the
offline trufflehog binary is absent from the container; main's own tip
does not pass them either. This diff adds no secrets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Part of the coily retirement. cli-guard is a shared guard engine, so most
`coily` strings were either the canonical Forgejo host (coilysiren.me) and org
(coilyco-*), which stay, or coily used as a stand-in for "the consumer" in
framework defaults, comments, and test fixtures.

The load-bearing change: repocfg discovery hardcoded `coily.yaml` / `.coily`,
which contradicted the repo boundary rule (no consumer-specific defaults) and
AGENTS.md (a consumer picks its own `.<app>/<app>.yaml`; cli-guard uses none).
The hardcoded names are now derived from the consumer's app dir, defaulting to
the framework-neutral `.cli-guard/cli-guard.yaml`:

- `cli/repocfg`: `Filename`/`LocalDirName`/`LegacyFilename`/`EnvOverride` go
  from `const` to functions deriving from `config.AppDir`/`BaseName`/`EnvName`.
- `cli/dispatch/cascade`: `COILY_DISPATCH_CASCADE_DEPTH` -> app-dir-derived.
- `pkg/workdir`: `OverrideEnv` (`COILY_PRIMARY_DIR`) -> app-dir-derived.
- `pkg/config`: new `EnvName(suffix)` helper; `CacheDirEnv` reuses it.

Tests that pinned `.coily`/`COILY_CACHE_DIR` as "some consumer app dir" now
pin `.ward` (the living consumer); arbitrary fixture names follow suit.

API break (v0.x, allowed in main): the repocfg/workdir symbols above are now
functions, not consts. ward (the only consumer) does its own config discovery
and calls `repocfg.Load(path)` only, so it is unaffected.

Intentional `coily` mentions kept, with justification:
- `cli/lockdown/user_hook.go` `_coily` legacy hook marker - recognized on read
  so already-written settings.json reconcile in place rather than double-hook.
- `cli/catalog/catalog_test.go` ward.yaml + legacy coily.yaml fallback fixtures
  - model ward's real dual-candidate (it honors a legacy coily.yaml).
- `cli/verb/onevaluate_test.go` defensive assertion that a denial hint never
  leaks a consumer path.
- `docs/release-pipeline.md` historical note that coily has been retired.

closes #154

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The engine is a clean standalone framework, but consumer names baked
into example comments and test fixtures let a fresh reader infer it was
carved from a specific consumer. Replace them with a generic `<cli>`
placeholder in doc comments and neutral filenames in fixtures.

- cli/execverb/{describe,guardfile}.go: example command paths and the
  `wrap` usage error now read `<cli>` instead of `ward`/`ward-kdl`.
- cli/repocfg/security.go: AllowedWrappers example is generic.
- cli/catalog, cli/allowlist, cli/profiles tests: fixtures renamed
  ward.yaml/coily.yaml/.ward -> myapp.yaml/legacy.yaml/.myapp; the
  legacy-fallback comment no longer names a specific consumer.
- godoc-current.txt regenerated for the exported-field comment edits.

No production behaviour change.

closes #163
…in (closes #161)

The reaper preserved this container's work on ward-salvage/cli-guard-4fc321b4
after a merge conflict integrating onto main. On triage the branch is strictly
behind main: its feature work (the execverb passthrough dialect with host-gate
guards) already landed via #158, and main has since superseded it - the #154
coily scrub, the #163 consumer-name neutralization, and the guardfile helper
refactor are all absent from the salvage tree.

The branch adds no files, tests, or logic main lacks; merging its content
normally would revert those later refactors (the conflict the reaper hit). This
is an -s ours merge: it records the salvage branch as integrated and keeps
main's tree unchanged, since main already contains and supersedes everything the
container produced.

closes #161

Co-Authored-By: Claude (she/her) <claude@ward.agent>
…closes #164)

The build-time generator was named three ways - `specverb-gen` (binary),
`specdrv` (driver lib), `specgen` (renderer) - while `specverb` is the
run-time engine. A reader could not tell which `spec*` package built the
CLI versus ran it. This makes the build-vs-run split lexical: everything
that generates lives under the `specgen` prefix, and `specverb` stays the
run-time engine.

Renames (history-preserving):
- cmd/specverb-gen -> cmd/specgen (the generator binary)
- http/specdrv -> http/specgen (the no-code driver: gen/lock/skew/build/run, pkg specgen)
- http/specgen -> http/specgen/codegen (the main.go renderer, pkg codegen)
- docs/specverb-driver.md -> docs/specgen.md
- docs/specgen-ssm-resolver.md -> docs/codegen-ssm-resolver.md

API break (v0.x, per the cross-repo contract): the generator binary is now
`specgen` and the driver import path is `http/specgen`. ward invokes the
binary by name (make build-ward-kdl) and embeds only `http/specverb` (the
run-time engine, unchanged), so it picks this up on its next cli-guard bump.
No behaviour change: a pure rename plus a godoc/doc refresh, with the build
and the specgen, codegen, and specverb test suites green.

closes #164

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make dispatch.Dispatcher drivable without re-hosting its core, so ward
can run workers in containers against Forgejo instead of forking the
package. Two new Config seams, both opt-in and backward compatible:

- Backend interface (Reserve / Prepare / Spawn / Reap) selects where a
  detached worker runs. Nil installs the worktree backend built from the
  existing Worktree*/SpawnDetached seams - no behavior change for current
  consumers. A consumer supplies a container backend instead; when set,
  WorktreeRoot is no longer required. A Reservation models the host slot
  a container backend holds (no-op for worktree), released on a failed
  launch and freed by Reap on success.
- Preflight verdict stage (GO / NO-GO / WRONG-REPO) runs after the owner
  and open-state gates. Fail-closed: only VerdictGo proceeds; a nil stage
  is an implicit GO.

Shared core kept and surfaced: AllowedOwner(s), the reservation/registry
concepts, and IssueRef / ParseIssueRef (now exported) so a consumer's
Preflight or Backend parses refs exactly as dispatch does.

Walkthrough in docs/dispatch-backends.md. Drive-by: trim one pre-existing
3-line comment in execverb/guardfile.go to satisfy the code-comments gate.

closes #162

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	cli/execverb/guardfile.go
The pre-commit gate was red on `main` for reasons unrelated to any one
feature, forcing selective hook SKIPs to land work. Clear the debt and
enforce it server-side.

specverb lint (golangci-lint v2.12.2, 6 findings):
- gocritic ifElseChain -> switch in actionDescription, snapshotActions,
  and writeActions (action.go, describe.go x2).
- gocyclo: extract the poll-resolution body of resolveAction into a new
  resolvePollAction helper, dropping complexity from 13 to under 12.
- unparam: drop the always-nil `body` result from buildCollectRequest and
  the always-nil `error` result from resolveCollectArgValue, threading the
  simpler signatures through their call sites.

CI: add .forgejo/workflows/ci.yml so the canonical remote runs the same
gate the pre-commit suite does (vet, build, test -race, golangci-lint
v2.12.2, godoc-current, go mod tidy, offline trufflehog scan). The docker
runner ships no Go, golangci-lint, or trufflehog, so each job installs its
own tooling from upstream. This makes the gate reliable even when a
contributor's feature container lacks those binaries on PATH. The GitHub
mirror's .github/workflows/ci.yml stays the PR-gated twin.

closes #165

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… credential broker

Add the foundation pieces for ward's root credential broker (ward#329),
where a root daemon holds the bot token while an unprivileged explore
agent reaches the forge and the dispatch path through a unix socket,
holding no credential itself.

pkg/broker is the policy core:
- a versioned, newline-delimited JSON request/response protocol over a
  unix socket for the four write-tier ops (file/edit/comment issue,
  dispatch);
- a Server over an already-created/permissioned socket that fails closed
  (nil deps are a construction error; unknown version or op is refused);
- a write-tier Authorizer (default Policy: owner x op allowlist plus the
  structural invariants every op needs);
- an injected Executor for the privileged side, so the package carries no
  git/docker/ward-kdl knowledge and stays importable by ward without a
  cycle;
- a dial-once Client with per-op convenience wrappers.

pkg/credseed extracts the token-seed shaping the broker needs: a typed
Creds surface that renders and writes a private 0600 env-file (forgejo
token plus base64'd agent blobs), with the env-var names centralized so
the seed (writer) and bootstrap (reader) sides cannot drift. Full
dispatch.Dispatcher convergence is out of scope.

closes #167

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… engine is the boundary

Issue #79 inverts enforcement from an enumerated, rendered denylist to a
policy-free engine plus a small consumer-owned protectedCommands literal.
The engine (cli/hook, cli/hookcfg, cli/shim, cli/doctor, cli/repocfg) landed
earlier and is what ward consumes; coily is retired. The cli/lockdown
package - the legacy permission-file/denylist writer that embedded
coily-specific binaries (npm, aws, gh, kubectl, docker, tailscale, ssh, ...)
and rendered an exhaustive per-tool deny case into every repo's hook - has
no remaining importer: no cli-guard cmd and no consumer reference it.

Delete it wholesale rather than leave a hollow writer emitting an empty
deny list. Enforcement is now the three structural layers the doctor floor
backs, expanded from the literal.

- remove cli/lockdown (package + tests + embedded defaults.yaml)
- remove docs/lockdown-defaults.md
- drop lockdown from the FEATURES writer-cluster line; fix the dangling
  lockdown.Driver.Coordinate doc ref in cli/decision
- rewrite AGENTS.md Safety to describe the policy-free engine
- regenerate godoc-current.txt

API break (v0.x, no deprecation cycle): the public cli/lockdown package is
removed. No consumer imports it. The profiles/profile/decision cluster is
left in place; its own retirement is a separate follow-up.

closes #79

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Land the cli-guard-first half of ward#269: the reusable helpers ward
carries in package main, lifted into standalone, importable cli-guard
packages so ward (and any consumer) imports one source of truth.

New pkg/ packages, each self-contained with tests:
- pkg/scan      - junk-scan diff policy (vendored/secret/oversized findings)
- pkg/attribution - agent-identity label, idempotent body-signing, trailer
- pkg/flock     - BSD advisory file lock (unix; no-op fallback off unix)
- pkg/version   - lenient semver parse + confident-only Behind check
- pkg/issueref  - standalone owner/repo#N / #N / Forgejo-URL ref parse
- pkg/ownertrust - repo-owner allow-list gate + refusal label

cli/hook gains a guard Registry (config-marker detection) and a settings
Installer (idempotent .claude/settings.json hook registration), both
parameterised so a consumer wires its own guards and hook command.

Decoupled from ward specifics: attribution takes a caller-supplied
identity rather than a baked-in mode roster; issueref/ownertrust are free
functions, not bound to the dispatch Dispatcher/Config. docs/ward-helpers.md
walks through each; FEATURES.md and godoc-current.txt updated.

closes #166

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…to cross it

Layer a precedence model over `inherit` (cli-guard#169): the least-privileged
base explicitly `never`s dangerous classes, those denials inherit upward, and a
higher tier crosses one only with the new `override can <verb> <resource>`
keyword — naming the exact verb+resource, never `"*"`. Deny low, override high.

The load-bearing rule: an inherited `never` beats a plain `can`; only an
`override` beats an inherited `never`. Enforced at flatten time, not silently at
runtime —
- a plain explicit `can X Y` shadowed by an inherited `never` is a build error
  pointing the author at `override` (a wildcard `can X "*"` stays exempt: its
  per-resource carve-outs deny silently by design, cli-guard#159);
- an `override` lifting no matching `never` is a build error (no silent no-op).

Also: `restrict` now inherits (deduped by param, child wins), superseding the
child-local behavior of cli-guard#160 — a base tier declares it once. `action`
stays child-local. Override grants carry Modal "can" + an Override flag; at build
they cross the deny set and suppress the would-be teaching leaf.

Behavior changes (v0.x, pinned consumers): explicit-can-over-inherited-never and
no-op-override are now hard build errors, and a base `restrict` propagates to
higher tiers that previously had to restate it. Same-tier `can`+`never` keeps the
older silent deny-wins; the build error fires only across the inherit seam.

Docs: split the precedence/override deep-dive into docs/specverb-override.md and
trim docs/specverb-inherit.md to the merge mechanics + the restrict-now-inherits
note.

closes #169

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an optional `cache "<ttl>"` modifier to the specverb `collect` action so
heavy auto-paginated reads serve from an on-disk TTL cache instead of
re-fetching every invocation. Reuses pkg/ttlcache rooted at config.CacheDir().

- Parser: `Cache string` on guardfile.Collect; `cache` in the collect scalar
  table. Collect-only — poll/call reject it (fail-closed), since poll needs
  live data and call may mutate state.
- Resolve: TTL parsed to a positive duration at build time; an unparseable or
  non-positive value fails closed rather than silently disabling the cache.
- Runtime: the page-accumulation loop is served via Get/Set over a request-only
  key (method + base + path + sorted non-pagination query; page, limit, and auth
  excluded). The cache dir is treated as strictly single-identity. A hit fires
  no requests and stamps `cache: "hit"` on the envelope audit row (new
  audit.Record.Cache field, wired via OnComplete).
- Flags on cache-eligible actions: --no-cache (bypass read+write), --refresh
  (invalidate + refetch), --dry-run (print key, TTL, dir without firing).
- Describe surface + docs (specverb-action-collect.md) added.

closes #171

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The build-time generator said nothing about what it generated. Its
artifact is the KDL spec surface, so kdl-specs is the speaking name.
Applied per-context casing (the "depending on text" rule):

- `kdlspecs` - Go package name + import-path segment. `package specgen`
  -> `package kdlspecs`; `http/specgen/` -> `http/kdlspecs/`; every
  import of the path segment updated.
- `kdl-specs` - CLI command / binary name and all prose, docs, and
  user-facing messages. `cmd/specgen/` -> `cmd/kdl-specs/`; help text
  and the ErrNoLock / ErrSkew strings ("run 'kdl-specs lock'");
  `docs/specgen.md` -> `docs/kdl-specs.md`.
- No snake context existed, so `kdl_specs` does not appear.

BREAKING CHANGE: the `http/specgen` import path is now `http/kdlspecs`
and the `specgen` command is now `kdl-specs`. Consumers pinning the
package update their imports and any invocations.

Regenerated godoc-current.txt for the package heading change. Build,
vet, tidy, race tests (bar the pre-existing environmental cli/sandbox
seccomp/jail failures), and golangci-lint all green.

closes #174
A grant-level `sealed` keyword pins an `argv` verb to forward exactly its
fixed command: any caller-supplied token is refused with a UserError before
exec. This enables strictly-single-resource verbs (e.g. a ward-kdl verb that
reads only the forgejo runner secret and cannot be widened to another secret).

- Parse: `Sealed bool` on Grant, `sealed` node in the grant body (takes no value)
- Enforce: actionFor refuses non-empty caller args when Sealed
- Validate: `sealed` requires a pinned `argv`; without it is a parse error
- Describe: Sealed surfaced in GrantInfo + rendered in the reference markdown
- Docs: docs/execverb.md grammar + engine notes (trimmed to hold the 4000-char cap)

closes #173

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	docs/execverb.md
A `value` node may now name an ordered fallback list instead of a single
source. KDL has no arrays, so the sources sit in a children block, one per
line; resolution walks the chain and takes the first source that yields a
non-empty value with no error - prefer a fast local `env`, fall back to a
durable store:

    value {
        env FORGEJO_API_TOKEN
        ssm "/forgejo/coilyco-ops/api-token"
    }

The inline `value <provider> "<address>"` is a one-element chain, so every
existing Guardfile is unchanged. The two forms are mutually exclusive on one
node; an empty block, a source missing its address, and a mixed form all fail
closed at parse time.

- guardfile: new `ValueChain []ValueSource`; `Auth.Value`,
  `QueryAuthParam.Value`, and `BaseURLValue` become chains. `parseValueChain`
  reads both forms; `Providers()` dedups across every chain.
- valuesource: `ResolveFirst` walks a chain to the first non-empty source,
  else a combined error naming each tried source - never a resolved value.
- specverb: auth and base-url resolution flow through the chain; describe and
  --dry-run render it symbolically (`provider address` joined by " | ").

Docs: docs/specverb-value-chain.md; FEATURES + policy pointers. Comprehensive
parse, resolution, and end-to-end coverage added.

closes #176

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ace)

Add pkg/fleetconfig: the typed validator for the agent-shaped KDL fleet
config (Fleet/Agent/Defaults/Attribution/Argv/Director). Parse over KDL
reuses the existing kdl-go node walker, failing closed on unknown nodes,
wrong argument counts/kinds, schema-version mismatch, and missing required
fields.

One parser, two sources: Embedded accepts the full `fleet` schema (+ an
optional `director` seed); OperatorLocal accepts only the narrow per-host
node set (`director`) and rejects the embed-only `fleet` block.

The vocabulary has no mount/exec/can run - those permission tokens are
rejected by name - so it structurally cannot express a grant. That package
boundary is the config/permission partition: config validation is core,
NOT a fourth guarded surface, keeping cli/ http/ mcp/ the whole
least-privilege story. Blocks the aos#310 ward-kdl dialect-2 chain.

Docs: docs/fleetconfig.md (schema + two-source model); architecture.md
note on config-as-core; FEATURES.md inventory entry.

closes #178

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coilysiren coilysiren closed this Jul 14, 2026
@coilysiren coilysiren deleted the sync/forgejo-main branch July 14, 2026 23:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant