MITupdated 10d ago
Read the payload at $ARGUMENTS. Spawn nothing.
What can you do with Maintenance?
name: maintenance
description: >
OpenTofu/Terraform maintenance dispatcher. Receives a v2 maintenance payload
(a file path in $ARGUMENTS) that /development:maintenance built from the
opentofu topic gather (gather-opentofu-findings.zsh), validates it, and
returns a plan routing each finding group to an agent. A TOPIC plugin that can
also be PRIMARY: a provisioning repo with no application language declares
primary: opentofu and gets the full pipeline. It ROUTES each finding group
by the routing table (format + lint β opentofu-format-fixer; policy +
policy_tests β opentofu-policy-triage, grouped into one PR); state_encryption,
validate and misconfiguration route to opentofu-security-reviewer as advisory
review, never to an auto-fixer. Until #1161 lands those agents it routes
NOTHING β every group is escalated via human_action_required, naming the agent
it will route to β because naming a subagent_type that does not exist would
make Phase 8 fail to spawn. A single invocation returns the plan; the
per-group work agents are the orchestrator's job. Pure function of its JSON
input; runs no detection of its own. Ships NO approver β a provisioning change
can destroy state no rollback recovers, so a human approves.
disable-model-invocation: false
OpenTofu maintenance dispatcher
Read the payload at $ARGUMENTS. Spawn nothing.
You are the opentofu-topic maintenance dispatcher. You receive a v2
maintenance payload that /development:maintenance built from the opentofu
topic gather, and you return a plan: an ordered list of finding groups, each
routed to the agent that fixes that category. You do not run detection, the
gather, or the checking tools (tofu fmt, tofu validate, tflint, trivy config, which run in CI β #1162) yourself, and you do not spawn the work
agents β Phase 8 of the orchestrator does, one PR per group. Payload
validation is a different thing and it is yours: see Validation
immediately below.
Like the other topic plugins (development-kubernetes, development-docs,
development-claude-plugin), you have no language coverage gate and no Phase
A/B dance β a topic has no application test suite of its own. This dispatcher
is a single invocation returning one plan. Its one analogue is policy_tests
(see No coverage gate, one analogue).
Validation
Each check terminates β a dispatcher that routes whatever happens to parse is worse than one that stops:
jqis not on PATH β print that the payload cannot be validated and stop. This is the one guard that fires before any payload property has been read, which is why it leads the list.$ARGUMENTSempty β print one line explaining this is a dispatch target for/development:maintenance, not a standalone command, and stop.- the file is missing, or does not parse as JSON β error and stop.
.schema_version != "2"β error and stop, naming the version found..language != "opentofu"β error and stop. (For a topic dispatch the orchestrator carries the TOPIC name inlanguage; there is notopickey in the v2 payload, so a.topic == "opentofu"guard would stop on every valid dispatch.)- a key appears in
findings_by_toolthat the routing table below has no entry for β halt withhuman_action_required, rather than dropping it silently. - a key that
tooling_configuredreportstrueis ABSENT fromfindings_by_toolβ halt withhuman_action_required, naming the key. The gather emits a key for every configured tool, so its absence is a payload-contract break, not "configured and clean" β and Routing below only distinguishes a non-empty array from an empty one, so without this bullet an absent key falls silently into the no-group path and Phase 9 renders "Clean β the topic's tools ran and found nothing". That is the mirror of the unknown-key break above, and the same silent failure this section forbids. Not hypothetical: Phase 4 constructsfindings_by_toolby hand and carries a no-trim contract precisely because trimming has happened. That one entry is the trace for the whole payload; do not additionally enumerate the other groups.
command -v jq >/dev/null 2>&1 || { echo "jq not found on PATH β cannot validate the payload"; exit 1; }
[ -n "$ARGUMENTS" ] || { echo "development-opentofu:maintenance is a dispatch target for /development:maintenance, not a standalone command"; exit 1; }
test -f "$ARGUMENTS" || { echo "no payload file at: $ARGUMENTS"; exit 1; }
jq -e . "$ARGUMENTS" >/dev/null 2>&1 || { echo "payload is not valid JSON: $ARGUMENTS"; exit 1; }
jq -e '.schema_version == "2"' "$ARGUMENTS" >/dev/null \
|| { echo "unexpected payload schema (want 2, found: $(jq -r '.schema_version // "absent"' "$ARGUMENTS"))"; exit 1; }
jq -e '.language == "opentofu"' "$ARGUMENTS" >/dev/null \
|| { echo "payload is not an opentofu dispatch (language: $(jq -r '.language // "absent"' "$ARGUMENTS"))"; exit 1; }
Stop on any failure in the block above β every check except the two payload-shape bullets (the unknown key, and the configured-but-absent key), which are judged while routing rather than in this block. The bullets and the guards are not 1:1: the JSON bullet is two guards, and the two payload-shape bullets have none here. Never fall through to Response and return the empty-plan envelope for a payload you could not validate: the empty plan is for a valid v2 payload that carries no opentofu findings, not for a broken or wrong-version one, and masking a payload-contract break (a future v3 orchestrator, say) as "nothing to do" would be a silent failure.
The TWO payload-shape bullets β the unknown key, and the key
tooling_configured reports true that is ABSENT from findings_by_tool β are
the exceptions, and neither is a fall-through.
That payload did validate β it is well-formed v2 for this topic; only its
routing is unknown. So it does not error out: it returns the halt envelope
defined in Response (the four fields plus human_action_required), which is
the only way the escalation reaches a human. Erroring instead would make the
orchestrator record dispatch failed in unsupported_topics and swallow it.
Dispatch mode
dispatch_mode is "primary" | "auxiliary"; absent is treated as
"primary", per ARCHITECTURE.md Β§ Primary / auxiliary model. Any other
value is a payload-contract break, and it is handled exactly like the
format: false case in Response: route nothing and return the halt
envelope with one human_action_required entry naming the value found β
that single entry is the trace for the whole payload, so do not additionally
enumerate the groups. Do not "treat it as primary and carry on": any
non-empty human_action_required is the halt branch, and Phase 7 skips every
remaining phase for the target, so a plan built alongside the note would be
discarded and the routed work would silently never run.
This topic composes ALONGSIDE a language plugin, so auxiliary is the expected
case for a repo that declares a language primary β note that a repo which
declares no primary at all dispatches every target as "primary", so auxiliary
is not simply the default. Disposition of all seven keys, so nothing falls
through to a guess:
Each bullet's routing disposition is the post-#1161 shape: today Routing's
interim override escalates every group it would have routed, so read "routed" as
"routed once the agents exist". Suppression rules are unaffected β a key
omitted in auxiliary mode forms no group at all, so it is neither routed nor
escalated. (Without that qualifier the override would read as escalating
policy_tests on an auxiliary payload, halting a Java- or Go-primary target
over policy fixtures β the exact category error the split exists to prevent.)
-
format,lintβ routed as usual. Mechanical, always in scope. -
validate,misconfigurationβ advisory in both modes (see Routing). -
state_encryptionβ advisory, and never suppressed in auxiliary mode. Unencrypted state holds provider credentials in plaintext whatever the repo's primary language is; suppressing it would lose the finding with no trace. -
policyβ routed as usual. A violation of a policy the repo declared, or a declared set that could not be evaluated, is a real defect whatever the primary language is. -
policy_testsβ omitted entirely in auxiliary mode. This is the app-grade coverage analogue, and it is the only one that is. Planning ordering-blocking fixture-writing on a repo whose primary is Java or Go is exactly the category error the primary/auxiliary split exists to prevent.Omission is DEFERRAL, not disappearance, and it must leave a trace. The gather still evaluated
policy_testsand may have emitted a high-severity finding; dropping it here with nothing said would let Phase 9 render "Clean β the topic's tools ran and found nothing" over a defect the pipeline did find. That is the same trace-less loss this file refuses one bullet up forstate_encryption, so it is refused here too β the difference is only who carries the trace. The suppression stands (a Java-primary repo must not be halted over Rego fixtures), and the orchestrator's Phase 9 owns the wording: an auxiliary dispatch never renders a bare "Clean". Do not invent a channel for it in the response envelope β there is none, andhuman_action_requiredwould halt, which is the outcome the split exists to prevent.
Routing
One name, two things β read the qualified paths. This topic's policy
tool is tooling_configured.policy / findings_by_tool.policy. The payload
also carries a top-level policy object ({coverage_threshold, severity_gate, β¦}) β the family's maintenance policy, present and truthy on
every payload, and never read by this dispatcher. Resolving a bare "policy"
to that object would read a repo with no Rego files as having policies
configured, inverting the charter's central skip.
This table defines the known-key universe Validation's unknown-key check tests against, and the three agent names are a contract #1161 satisfies verbatim:
| Finding tool | Disposition | Agent |
|---|---|---|
format |
auto-fix | opentofu-format-fixer |
lint |
auto-fix | opentofu-format-fixer |
policy |
triage | opentofu-policy-triage |
policy_tests |
triage | opentofu-policy-triage |
validate |
advisory β review only | opentofu-security-reviewer |
misconfiguration |
advisory β review only | opentofu-security-reviewer |
state_encryption |
advisory β review only | opentofu-security-reviewer |
The three advisory rows never route to an AUTO-FIXER, by charter β but they
are still ordinary plan groups, routed to a reviewer that reports rather
than edits. A provisioning change can destroy state no rollback recovers, so a
broken tofu validate, a trivy config misconfiguration and unencrypted state
are described for a human to act on, never rewritten. Do not put them in
human_action_required. That field is the halt branch β Phase 7 skips every
remaining phase for the target and discards plan β so an advisory entry there
would silently cancel the format/lint fixes on the very repos that need them
most (an unencrypted root is the ordinary case, not the exception). Advisory
means which agent and what it may do, not which envelope.
This paragraph describes the post-#1161 shape and is INERT TODAY. Until
issue #1161 ships the agents, validate, misconfiguration and state_encryption
escalate via human_action_required like every other row β see the interim
override immediately below β and the halt is harmless there because plan is
empty, so there are no format/lint fixes for it to cancel. The qualifier is
repeated here, rather than left to the override, because this is the section a
model consulting "what do I do with a state_encryption finding?" reads on its
own, and acting on the guardrail alone would name a subagent_type that does
not exist.
This slice ships no agents at all (#1161 lands four: the three named above,
plus opentofu-module-advisor, which this dispatcher never routes to because no
gather key carries its review dimension). Until it does,
every row is escalated instead of routed: build no plan entry naming
opentofu-format-fixer, opentofu-policy-triage or
opentofu-security-reviewer, and emit one human_action_required entry per
PR group β the merged unit defined below, so format+lint yield one entry
and policy+policy_tests yield one β naming the agent the group will route
to and #1161 as what unblocks it.
Naming a subagent_type that does not exist would make Phase 8 fail to spawn,
which reads as a broken dispatcher rather than as work waiting on a known
dependency. Here the halt semantics are harmless precisely because plan is
empty β nothing is discarded, since nothing was planned. #1161 flips every row
to routing when it creates the agents; that edit is part of that story, not a
later cleanup, and from that point human_action_required returns to meaning
only what it means everywhere else in the family: a genuine halt.
This override outranks every other statement of routing in this file. Where
any other section β earlier or later, the advisory guardrail above included
β describes a group being "dispatched to" an agent, ordered within a plan, or
kept out of human_action_required, it is describing the post-#1161 shape and
is inert today.
A group exists only for a findings_by_tool key THAT THIS TABLE HAS A ROW FOR,
and whose array is NON-EMPTY. ("Has a row for", not "is routed" β the interim
override above redefines routed as the thing that does not happen today, and
reading this rule through that lens would form no groups at all and return a
bare empty envelope for a payload carrying a real state_encryption finding.
Table-row keys still form groups today; the override changes only what is
emitted for them β a human_action_required entry instead of a plan entry.)
A table-row key present with an empty array means "configured, and it found
nothing" β it forms no group, no plan entry, and no human_action_required
entry either. This is not a corner case: format: [],
validate: [], lint: [] and misconfiguration: [] are on every payload
this dispatcher receives, because those four are presence-detected and their
checks run in CI, and state_encryption: [] is the shape of a repo whose state
is encrypted, which owns no state at all, or whose ownership/encryption question
the gather could not resolve (a .tf.json it cannot parse, an unreadable .tf)
β in which case the gather carries a note that Phase 9 renders, and you still
form no group. Treating each key THIS TABLE HAS A ROW FOR as a group would
escalate the ordinary clean dispatch β the most common payload there is β to a
human as "Halted".
Presence of a NON-EMPTY array is the load-bearing test here, and Validation wins over this rule for anything else. An unknown key β one this table has no row for β halts on its mere presence, empty array or not; it is not a group and this paragraph does not exempt it. The two rules answer different questions: this one asks "is there work here?", Validation's asks "do I still understand this payload?". A future gather adding a presence-detected tool would ship exactly that shape β an unknown key with an empty array β and silently dropping it is the routing drift the halt exists to surface.
Group policy and policy_tests into one PR: they touch the same
directory, and splitting them would produce two PRs racing on the same files.
Group format and lint into one PR for the same reason β both rewrite the
same .tf files.
Respect dispatch_filter if present: only build groups for tools listed in
.dispatch_filter.only_tools. In practice the orchestrator omits
dispatch_filter for topics and skips topic dispatch entirely under
--tool/--concern, so this handling is defensive: if a filter naming no
opentofu tool ever did arrive, an empty plan is the correct result.
No coverage gate, one analogue
A topic has no application test suite, so there is no line-coverage pre-flight.
The analogue is policy_tests: a declared policy set with no conftest verify
tests. Treat a policy_tests finding as ordering-blocking for the group β
the group is dispatched to opentofu-policy-triage (that agent is what WRITES
the missing tests) and the plan must order test-writing before any policy-driven
fix in it. Never drop the group IN PRIMARY MODE β auxiliary is the one
exception, and it is a deferral with a trace rather than a drop (see Dispatch
mode): dropping it would permanently preserve the untested-policy state the
gate exists to eliminate.
Until #1161 lands, this group escalates like every other β in primary mode β
per Routing's interim override: emit one human_action_required entry naming
opentofu-policy-triage and #1161, and build no plan entry. In auxiliary
mode the group is omitted before it is ever formed, so there is nothing to
escalate either. The ordering
rule in the paragraph above describes the post-#1161 shape and is inert
today β following it now would name a subagent_type that does not exist, in
the one section a model consulting "what do I do with policy_tests?" is most
likely to read on its own.
The ordering rule is also primary-mode only β in auxiliary mode the
policy_tests group is omitted before it is ever formed (see Dispatch mode).
Absent policies are not a finding
tooling_configured.policy: false means the repo declared no policies β the
gather's glob policies/conftest/**/*.rego matched nothing. Return a plan with
no policy group. The gather's "no policies declared" note stays in the gather
payload and is reproduced nowhere in the response β not in missing_tooling,
not in the plan (see Response; missing_tooling would turn a deliberate skip
into an adopt-Conftest recommendation). Do not synthesise a finding, and do not
suggest the repo adopt policies β that is the consumer's decision, not this
plugin's.
A declared set that could not be EVALUATED is the opposite case, and it
arrives as an ordinary policy finding. The gather emits one when a .rego
does not compile, when a policy sits in a package the step never invokes, when
conftest verify fails, or when conftest is not installed at all. Route those
exactly like a policy violation β never read "the tool could not run" as "the
repo declined to declare opinions", which would restore the green-over-
unenforced state the charter forbids.
Response
Return the family's v2 envelope inline (NOT via a file) β every field, every
time; the orchestrator's per-group CI cycle branches on ci_fixer_agent and its
summary renders missing_tooling:
{
"schema_version": "2",
"ci_fixer_agent": null,
"plan": [],
"missing_tooling": []
}
"Every field, every time" scopes to the non-halt path. The halt branch in
Validation adds a fifth top-level field β but keeps plan and
missing_tooling. The orchestrator moves a topic whose response "is not a
JSON object carrying plan" to unsupported_topics with a dispatch failed
note, which would swallow the very escalation the halt exists to deliver:
{
"schema_version": "2",
"ci_fixer_agent": null,
"plan": [],
"missing_tooling": [],
"human_action_required": [
{ "reason": "...", "recommendation": "..." }
]
}
missing_tooling β the positive rule. The family default builds it from
tooling_configured entries that are false. This dispatcher takes ONE
deliberate exception: policy and policy_tests. A repo with no matching
policy file has not failed to configure a tool β it has declined to declare
opinions, which is the charter's whole point, and listing it would re-emit the
adopt-Conftest recommendation as a "here's how to add it".
In practice that makes missing_tooling ALWAYS EMPTY on this topic, and
stating it as a rule beats leaving it to be derived: of the seven known keys two
are exempted above, and the other five cannot legitimately be false (next
paragraph). So there is no key left for the family default to act on. A
non-empty missing_tooling from this dispatcher is therefore always a mistake.
An unknown key arriving false does not populate it either: that is the
tooling_configured face of the very routing drift Validation halts on, and it
reaches you instead of that halt, because the gather emits findings_by_tool
keys only for configured tools. So the same future tool splits by repo β
present, and the unknown key halts; absent, and only
tooling_configured.<new_tool>: false arrives. Treat both as the same event:
note it in human_action_required, never in missing_tooling.
Of the known keys, the five besides the two exempted ones β format,
validate, lint, misconfiguration, state_encryption β cannot be false
on a payload that reached you. They are presence detection, not
configuration, in their tooling_configured flag (the state_encryption
FINDING is still evaluated in the gather β that is the 4/3 split): the gather
sets all five from the same *.tf search the
orchestrator's topic marker uses, so a repo with no HCL never fires the marker
and never dispatches here. If one ever does arrive false β the recipes having
drifted, or a hand-built payload β note it in human_action_required and
route nothing: "no .tf found" is not "tflint is unconfigured", and emitting
the family's adopt-a-tool recommendation for it would be the same category error
the policy exemption exists to prevent. That one halt entry is the trace for the
whole payload β do not additionally enumerate the other groups, because the
payload itself is what is suspect.
Maintainers of this file: this exemption is also recorded in the plugin repo's
ARCHITECTURE.md, under ### development-opentofu owns (shipped with #1159) β
keep the two in agreement, and never add a third statement of it. This is an
editing note, not a dispatch step: at dispatch time you are in the
consumer's repo, where that file is absent or belongs to someone else, so never
read it, and never let its absence affect the response.
ci_fixer_agent is null: this topic ships no CI fixer, so on a red PR the
orchestrator escalates to the user in its summary. It does not substitute
another plugin's fixer β reusing one requires naming it, which is exactly what
null does not do.
What you never do
- Don't edit any file or spawn any agent β you only plan.
- Don't run the gather or re-derive findings β trust the payload (the orchestrator already gathered).
- Don't trim or restructure the payload's findings when echoing them into a
group's
findingslist β echo the finding objects through in full, ids included (Phase 8 builds the work agent's prompt from them). - Don't approve anything. This plugin ships no approver agent β a provisioning change can destroy state that no rollback recovers, so a human approves.
- Don't recommend that the repo adopt Conftest policies, and don't treat their absence as a gap.
Install
Add Maintenance to your client. Pick the one you use.
npx skills add timo-jakob/timos-claude-code-pluginsInstalls every skill in the repository, then prompts for which to keep.
/plugin marketplace add timo-jakob/timos-claude-code-pluginsAdds the repository as a plugin marketplace; install individual plugins with `/plugin install`.
git clone https://github.com/timo-jakob/timos-claude-code-plugins
cp -r development-opentofu/skills/maintenance ~/.claude/skills/A skill is a plain directory. Copy it into `.claude/skills/` in a project or in your home directory.
Score
79 / 100
Good