weft-command 2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,78 @@
1
+ # The donor checkout, reachable for reading and lifting only.
2
+ # Never a build input: see CLAUDE.md and docs/README.md.
3
+ /donor
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ .venv/
9
+ dist/
10
+ build/
11
+ *.egg-info/
12
+
13
+ # Tooling
14
+ .pytest_cache/
15
+ .ruff_cache/
16
+ .pyright/
17
+
18
+ # `uv.lock` was here, filed between two caches. It is not a cache: `ci.yml` runs
19
+ # `uv sync --frozen` in three of its four jobs, and every one of them died at that step
20
+ # the first time CI ever ran. It is tracked now — the resolution CI installs and the
21
+ # resolution a developer installs must be one artefact, which is the same argument
22
+ # `docs/README.md` makes about single-sourcing anything two readers can disagree about.
23
+ #
24
+ # It constrains nobody downstream. A lockfile is not a dependency bound; what a consumer
25
+ # resolves is decided by each distribution's `pyproject.toml`.
26
+
27
+ # Session-local attempt counter for the guard_quality_gates.py PreToolUse hook — per-session
28
+ # scratch, not a record anyone should read later. See the hook's module docstring.
29
+ .claude/.gate-attempts.json
30
+
31
+ # Secrets. `.env` holds live provider keys; only the documented, valueless
32
+ # example is tracked. Listed before any tooling rule so a stray `git add -A`
33
+ # cannot reach it.
34
+ .env
35
+ .env.*
36
+ !.env.example
37
+
38
+ # Corpus payload. Every subdirectory of /corpus is a materialised document set and
39
+ # is deliberately untracked; `corpus/manifest.toml` and `scripts/fetch_corpus.py`
40
+ # are the tracked artefact. `09` §4 V1 permits exactly this:
41
+ # a corpus is "either redistributable or fetched by a pinned, checksummed script",
42
+ # and the mRMR papers are published under publisher copyright, so committing them
43
+ # would be redistribution this repository has no right to perform. The manifest
44
+ # carries a sha256 per document, which is what makes the set reproducible without
45
+ # the bytes being here. The pattern is a directory glob rather than a list of names
46
+ # so that scaling the corpus up cannot silently start tracking a paper.
47
+ /corpus/*/
48
+
49
+ # Where a baseline run stages the corpus it indexes and writes the `weft.toml` it measures
50
+ # through (`eval/run_baseline.py`). The staged copies are the same untracked papers one
51
+ # directory over, and the configuration is reproduced by the harness rather than kept — what
52
+ # is tracked is the run it produced, under `eval/baselines/`.
53
+ /.baseline-run/
54
+
55
+ # Working artefacts of a build session — a generated map of the codebase and a design
56
+ # record produced while planning. Untracked on purpose: `docs/README.md` routes every
57
+ # document this project owns, and a design that matters belongs in the `docs/` file
58
+ # that owns its content, not in a root-level file nothing points at.
59
+ /.phase2-*.md
60
+ /.phase3-*.md
61
+ /.phase4-*.md
62
+
63
+ # Gate-session preparation: the Bring lists of `docs/05-grilling-sessions.md`, measured on the day
64
+ # a session is about to run. Untracked for the same reason — the session's outcome belongs in
65
+ # `docs/README.md`'s decision log and in the reference document the decision changes, never here.
66
+ /.gate-brief-*.md
67
+
68
+ # Claude Code local state
69
+ .claude/settings.local.json
70
+
71
+ .DS_Store
72
+
73
+ # The build harness driving Phase 2, alongside its brief, findings and design record.
74
+ /.phase2-build.js
75
+
76
+ # Transient: harvested subagent findings, promoted into docs/lessons.md by implement-ll.
77
+ # Never committed — its content belongs in the queue or nowhere.
78
+ .claude/lessons-spool.md
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adam Krysztopa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,24 @@
1
+ Weft
2
+ Copyright (c) 2026 Adam Krysztopa
3
+
4
+ This product is licensed under the MIT License. See the LICENSE file at the root
5
+ of this repository.
6
+
7
+ --------------------------------------------------------------------------------
8
+ Original work
9
+ --------------------------------------------------------------------------------
10
+
11
+ **Weft contains no source text from any other codebase.** Every line here is
12
+ written for this project, against this project's contracts.
13
+
14
+ This is a rule, not a description of the current state: no file may be copied or
15
+ adapted from another project's source, and no third-party source text may be
16
+ pasted into this repository. Where a prior system informed a design, what was
17
+ carried across is understanding — an approach, an ordering, a measurement, a
18
+ reason a guard exists — restated in this project's own words and implemented
19
+ fresh. Copyright does not reach any of that, and nothing in this repository
20
+ depends on a licence granted by anyone else.
21
+
22
+ `docs/04-donor-inventory.md` records what was learned from prior work and what
23
+ was deliberately not taken. It is a design record. Nothing in it authorises a
24
+ copy, because copying is not permitted here at all.
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.5
2
+ Name: weft-command
3
+ Version: 2.1.0
4
+ Summary: First-party command pack. Publishes the Command contract and PermissionClass.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ License-File: NOTICE
8
+ Requires-Python: >=3.12
9
+ Requires-Dist: weft-kernel<1.0.0,>=0.1.0
@@ -0,0 +1,20 @@
1
+ [project]
2
+ name = "weft-command"
3
+ version = "2.1.0"
4
+ description = "First-party command pack. Publishes the Command contract and PermissionClass."
5
+ requires-python = ">=3.12"
6
+ license = "MIT"
7
+ license-files = ["LICENSE", "NOTICE"]
8
+ dependencies = ["weft-kernel>=0.1.0,<1.0.0"]
9
+
10
+ # No `[project.entry-points."weft.packs"]` block — the identical reasoning
11
+ # `packages/weft-prompts/pyproject.toml` already carries: fitness function 2 asserts that
12
+ # every distribution declaring this entry point is active *and contributing*, and this
13
+ # distribution registers no `Command` of its own. `weft-cli`'s own built-in commands
14
+ # (`weft index`, `weft ask`, ...) stay on `weft_cli.cli.COMMANDS` until a later task moves
15
+ # them onto this contract; the first plugin-contributed `Command` adds the entry-point line
16
+ # in the same commit it lands in, exactly as `weft-retrieve` did at 2.13.
17
+
18
+ [build-system]
19
+ requires = ["hatchling"]
20
+ build-backend = "hatchling.build"
@@ -0,0 +1,43 @@
1
+ """First-party command pack. Publishes the `Command` contract and `PermissionClass`.
2
+
3
+ Task **3.1**: "a pack registers a command exactly as it registers a retriever, and a command
4
+ that declares no permission class fails to register while its author is standing there."
5
+ `weft_command.contract`'s own module docstring carries the full reasoning for why this
6
+ distribution exists at all — in particular why `Command` lives here rather than in
7
+ `weft-kernel` (G1: the kernel names no capability) or `weft-cli` (a pack implementing a
8
+ contract must not depend on the driving adapter that calls it).
9
+
10
+ **Registers nothing, and declares no `weft.packs` entry point**, on `packages/weft-prompts`'s
11
+ own precedent (see that distribution's module docstring for the identical reasoning): fitness
12
+ function 2 requires every distribution declaring that entry point to be active *and
13
+ contributing*, and every first-party command belongs to the pack whose behaviour it exposes —
14
+ none of which is this one's to own. `weft_cli.commands` carries every built-in command since
15
+ task 3.2, registered through `weft-cli`'s own entry point (see that module's docstring for why
16
+ `weft-cli` is the owner rather than a twelfth distribution); a third party's first
17
+ plugin-contributed `Command` adds the entry-point line to *its own* `pyproject.toml` in the same
18
+ commit it registers, exactly as `weft-retrieve` did at task 2.13.
19
+
20
+ There is therefore no `Settings` model and no `register` function here — a pack declaring no
21
+ entry point is never handed either, and declaring them unused would be two more things to keep
22
+ true. `packages/weft-generate` and `packages/weft-prompts` take the same shape for the same
23
+ reason.
24
+
25
+ **Task 6.20 (G13) adds `ExitCode` and `Rendered` to this namespace** — `weft_command.render`'s
26
+ own module docstring carries the reasoning in full: a renderer a third-party pack registers
27
+ through `weft_kernel.discovery.PackRegistrar.add_renderer` has to answer with a `Rendered`
28
+ carrying an `ExitCode`, and a pack implementing `Command` must not depend on `weft-cli`, the
29
+ driving adapter, for the vocabulary its own renderer speaks.
30
+ """
31
+
32
+ from weft_command.contract import COMMAND_CONTRACT_VERSION, Command, CommandResult
33
+ from weft_command.permission import PermissionClass
34
+ from weft_command.render import ExitCode, Rendered
35
+
36
+ __all__ = [
37
+ "COMMAND_CONTRACT_VERSION",
38
+ "Command",
39
+ "CommandResult",
40
+ "ExitCode",
41
+ "PermissionClass",
42
+ "Rendered",
43
+ ]
@@ -0,0 +1,221 @@
1
+ """The `Command` contract — published here, never by the kernel and never by `weft-cli`.
2
+
3
+ Task **3.1**: "a pack registers a command exactly as it registers a retriever, and a command
4
+ that declares no permission class fails to register while its author is standing there"
5
+ (`docs/build-ledger.md`). `docs/03-cli.md` → *Plugin-contributed commands*: "A pack registers
6
+ commands against the `Command` contract exactly as it registers a retriever."
7
+
8
+ **Why this distribution, and not `weft-kernel` or `weft-cli` — an architectural decision this
9
+ task took, which no prior document had settled.**
10
+
11
+ - **Not `weft-kernel`.** G1 draws the line exactly: the kernel expresses, loads and runs
12
+ contracts it knows nothing about, and publishes none of its own (`docs/02-extension-model.md`
13
+ §1 → *Who publishes a contract*). `Command` names a capability — "a CLI-invoked action" — the
14
+ same way `Extractor`, `Chunker` and `Prompt` do, and CLAUDE.md states the test plainly: "if
15
+ you cannot describe the kernel without naming a capability, it is too big."
16
+ - **Not `weft-cli`.** `weft-cli` is `docs/03-cli.md`'s **driving adapter** — "every operation it
17
+ performs is a library call a FastAPI route could make identically" — and a pack that
18
+ contributes a command is a **driven** side of that same call, exactly as a pack contributing a
19
+ retriever is. Publishing `Command` from `weft-cli` would mean a third-party command pack
20
+ depending on the adapter to implement the contract the adapter itself drives, which inverts
21
+ the dependency direction the whole extension model is organised around (`docs/02` §1: "a pack
22
+ depends on the pack that publishes the contract it implements, exactly as it would on any
23
+ third-party protocol" — never on the thing that *calls* it). Concretely, `weft-cli`'s own
24
+ `pyproject.toml` already names nine other distributions as dependencies
25
+ (`weft-kernel`, `weft-extract`, `weft-chunk`, `weft-embed`, `weft-store`, `weft-llm`,
26
+ `weft-prompts`, `weft-retrieve`, `weft-generate`); a command pack that had to depend on
27
+ `weft-cli` to get `Command` would inherit every one of those transitively for the sole purpose
28
+ of reading one Protocol.
29
+ - **The precedent is `weft-prompts`.** A distribution that publishes a contract and registers no
30
+ plugin of its own is not a new shape — `weft_prompts.contract.Prompt` lived exactly this way
31
+ from task 2.10 until the first prompt landed and added the entry-point line. `weft-command`
32
+ takes the identical position: it publishes `Command` and `PermissionClass`
33
+ (`weft_command.permission`), declares no `[project.entry-points."weft.packs"]` block (fitness
34
+ function 2 requires every distribution declaring that entry point to be active *and
35
+ contributing*, and this one contributes nothing of its own), and `weft-cli` gains it as a
36
+ ninth-turned-tenth dependency the same way it already depends on `weft-prompts` for `Prompt`.
37
+
38
+ **`Command` is not a pipeline position**, on `weft_prompts.contract.Prompt`'s own footing and
39
+ for the identical reason: `.phase2-design.md` §3 groups `Prompt` and `LLMProvider` as "named and
40
+ registered, but not pipeline positions", and a CLI command is invoked once per `weft` invocation
41
+ (or once per REPL turn), never composed into an ingest or query `StageSpec` list. It therefore
42
+ declares no `Stage` base — `weft_kernel.runner.Runner.resolve` has no reason to see it, and
43
+ `weft_cli.contract_reference.capability_siblings`' own `_is_stage_protocol` filter, built for
44
+ exactly this family of "one method, several unrelated contracts sharing its name", never needs a
45
+ special case for it.
46
+
47
+ **`args_model` and `result_model` are declared in the Protocol body, deliberately** — the
48
+ identical choice `weft_prompts.contract.Prompt` makes for `input_model`/`output_model`, and for
49
+ the same reason stated there: a command whose arguments are an untyped `argparse.Namespace` is
50
+ the shape this contract exists to replace, and a renderer needs `result_model` to introspect a
51
+ result's shape without having been written against any one command by name. Required for
52
+ `isinstance` is the cost, stated once so nobody rediscovers it: `issubclass` against a Protocol
53
+ with non-method members raises `TypeError`, so a future walk over registered `Command`s must use
54
+ `isinstance` against a constructed instance, never `issubclass` against the class.
55
+
56
+ **`run` takes `args` at the call, not at construction — `Prompt.render`'s shape, not `Stage`'s.**
57
+ A `Stage` plugin binds its `with:` configuration once, at construction, because one pipeline
58
+ position runs many payloads through the identical configuration. A `Command` is the opposite: the
59
+ same registered instance answers a different, freshly parsed `args` on every invocation — one
60
+ shot from the CLI, or turn after turn in the REPL (`docs/03-cli.md` → *Two modes, one
61
+ implementation*: "Both go through the same command objects.") — so binding `args` at construction
62
+ would mean constructing a fresh instance per invocation for no reason `Lifetime.PROCESS` could
63
+ ever amortise.
64
+
65
+ **The governing property this task builds into the signature, per `docs/03-cli.md` → *Two modes,
66
+ one implementation*:** "a `Command` returns a typed result and never writes to a stream." `run`
67
+ returns a decided `Outcome[CommandResult]` — `Produced` / `NothingToProduce` / `Failed`, `02` §1's
68
+ own vocabulary applied here as it is everywhere else a contract answers — never a printed line and
69
+ never a bare value. `weft_cli` renders `Produced.value` for a human, `--json` renders the
70
+ identical value for a script, and the REPL renders it again with the session's context around
71
+ it; none of that is buildable if this method had already chosen a renderer by printing.
72
+
73
+ **`permission_class` is mandatory and declared nowhere in this file** — the same asymmetry
74
+ `weft_chunk.contract.Chunker` has with `destroys`, and for the identical reason. `docs/03-cli.md`
75
+ → *Permissions*: "The class is a `ClassVar` on the `Command` contract, read at registration
76
+ alongside `lifetime`, `requires` and `provides`; a command that declares none fails to register,
77
+ loudly, while its author is standing right there. No default is safe." Declaring
78
+ `permission_class` directly in the Protocol body would make it a required `isinstance` member,
79
+ checked only once an *instance* exists — too late, since refusing at registration means refusing
80
+ before any instance is ever constructed. `Command.required_declarations = ("permission_class",)`
81
+ is instead read by `weft_kernel.registry`'s generalised mandatory-declaration check (task 3.1's
82
+ own repair to that module — see its docstring, *"`destroys` generalised into
83
+ `required_declarations`, not duplicated"*), the identical mechanism `Chunker.
84
+ publishes_property_vocabulary` already drives for `destroys`, applied to a second name without
85
+ the kernel ever learning the word `Command` or `permission`.
86
+
87
+ **What this task deliberately leaves to 3.2 and 3.4.** Namespacing a pack's commands under its
88
+ own name (`docs/03-cli.md`: "`weft graph build`") is a registration-*name* convention, not a
89
+ contract shape — `registrar.add(Command, "graph build", GraphBuildCommand)` registers under
90
+ `weft_kernel.registry` exactly as any other string name would, and turning that string into
91
+ argparse subcommand wiring, generated `--help` text and REPL completion is task 3.2's own
92
+ ("`weft --help` cannot drift from what is installed, because core has no command list to edit").
93
+ Likewise, rewiring `weft_cli.cli.COMMANDS`'s five built-in commands onto this contract, and
94
+ building the renderer that actually turns a `CommandResult` into text/JSON/REPL output, are 3.4's
95
+ ("the interactive session and the one-shot invocation are the same commands with a different
96
+ implementation") — `weft_cli.permissions.CliCommand` was expected to stay untouched a task
97
+ longer than it did: task 3.2 found the 3.2/3.4 split stated above too narrow (converting the
98
+ built-ins requires exactly the parser-generation and a minimal renderer it names, neither the
99
+ REPL nor the plural `--json`/REPL renderers 3.4 owns) and rewired all five onto this contract
100
+ immediately, deleting `CliCommand` and `weft_cli.permissions` as dead code — see
101
+ `weft_cli.commands`'s own module docstring for the full account.
102
+
103
+ **Task 3.2 adds `help` — the one piece of metadata 3.1 left this contract without, and the one a
104
+ generated `--help` cannot do without.** `docs/03-cli.md` → *Plugin-contributed commands*: "Core has
105
+ no list of commands to edit. The help text is generated from the registry" — which needs a
106
+ one-line string to generate *from*, and nothing on `Command` as 3.1 shipped it carried one.
107
+ `required_declarations` grows to `("permission_class", "help")` rather than gaining a second,
108
+ parallel mechanism: `help` is exactly as mandatory as `permission_class` and for the identical
109
+ reason — a command with no help text is not a smaller problem than one with no permission class,
110
+ it is `weft --help` printing a blank line or crashing outright for whichever plugin forgot it,
111
+ discovered by a stranger long after the author who could have been told at registration is gone.
112
+ Declared under `if TYPE_CHECKING:`, exactly like `version` and `required_declarations` themselves
113
+ — never a required `isinstance` member, for the same reason `permission_class` is not one: refusing
114
+ at registration means refusing before any instance exists to check, and `hasattr` on the class is
115
+ the only test available at that point.
116
+
117
+ **`COMMAND_CONTRACT_VERSION` was recorded as `"1.1.0"` here, and that was a mis-recorded major —
118
+ corrected to `"2.0.0"` at task **5.2a**, per `docs/README.md` → G9's settled two-audience rule
119
+ (`docs/09-release.md` §2.3).** Adding `help` to `required_declarations` is a minor change for a
120
+ *caller* — nothing that only ever reads a `Command` through this Protocol changes — but a **major**
121
+ change for an *implementer*: every existing `Command` that does not already declare `help` breaks
122
+ at registration, loudly, the instant this contract is depended on at the corrected range. G9's
123
+ table classifies exactly this row ("add a name to `required_declarations`") as `—` / **major** /
124
+ **major**, and the bump a Protocol change takes is the maximum of the two audiences, never the
125
+ caller's alone. At the time this docstring first recorded `"1.1.0"`, G9 had not yet settled that
126
+ rule, so the bump was chosen from the caller's side only; it is corrected here rather than left
127
+ mis-recorded once the rule existed to correct it against — the identical honesty G9's own settled
128
+ row applies to itself.
129
+
130
+ **Task 6.20 (G13) bumps `COMMAND_CONTRACT_VERSION` again, to `"2.1.0"` — this time minor, not
131
+ major, and the reasoning is stated at the time the bump is taken rather than left for a later
132
+ task to reconstruct.** `weft_command.render.Rendered` and `.ExitCode` are published from this
133
+ distribution and re-exported here (see that module's own docstring for why a renderer's
134
+ vocabulary belongs beside the contract that produces the result it formats), and
135
+ `weft_kernel.discovery.PackRegistrar` gains a further registration seam, `add_renderer` — but
136
+ nothing on the `Command` Protocol itself changed, `required_declarations` is untouched, and a
137
+ pack that registers no renderer still works exactly as it did before this task, because the
138
+ unregistered-result structured-dump fallback is the floor and stays the floor. Applying G9's
139
+ table honestly: additive for a *caller* (nothing that reads `Command` through this Protocol
140
+ changes) and additive for an *implementer* (no existing `Command` implementation breaks at
141
+ registration), and the maximum of the two is minor.
142
+ """
143
+
144
+ from typing import TYPE_CHECKING, ClassVar, Protocol, runtime_checkable
145
+
146
+ from pydantic import BaseModel, ConfigDict
147
+
148
+ from weft_command.render import ExitCode as ExitCode
149
+ from weft_command.render import Rendered as Rendered
150
+ from weft_kernel.context import Context
151
+ from weft_kernel.payload import Outcome
152
+
153
+ #: Fitness function 6's subject for this contract — see the module docstring's note on the
154
+ #: correction at task 5.2a: the 3.2 bump to "1.1.0" (when `help` joined `required_declarations`)
155
+ #: was a mis-recorded major, per G9's two-audience rule, corrected to "2.0.0".
156
+ #:
157
+ #: **Task 6.20 bumps this to "2.1.0" — additive for both audiences, so minor, not major.**
158
+ #: `docs/09-release.md` §2.3, G9's two-audience rule, applied at the time this bump is taken
159
+ #: rather than left for a later task to reconstruct: nothing on the `Command` Protocol itself
160
+ #: changed, `required_declarations` is untouched, and a pack that registers no renderer through
161
+ #: the new `PackRegistrar.add_renderer` seam still works exactly as before — the unregistered-
162
+ #: result structured-dump fallback is the floor, and stays the floor. Publishing `Rendered` and
163
+ #: `ExitCode` from this module (see `weft_command.render`) grows the surface without breaking
164
+ #: anything either audience already depended on, which is what "minor" means under G9's table:
165
+ #: the maximum of "additive for a caller" and "additive for an implementer" is still additive.
166
+ COMMAND_CONTRACT_VERSION = "2.1.0"
167
+
168
+
169
+ class CommandResult(BaseModel):
170
+ """The typed result a `Command` returns — never printed text, per the module docstring.
171
+
172
+ Frozen per CLAUDE.md ("Frozen where the value is a domain object"), and the base every
173
+ concrete command's own result model subclasses with its own fields, the same relationship
174
+ `weft_kernel.payload.outcome`'s `Produced[T]` has to whatever `T` a contract names: a
175
+ renderer built against this base type can accept any command's result without having been
176
+ written against that command by name, which is what lets `weft_cli` render a plugin's
177
+ command exactly as it renders a built-in one. Carries no fields of its own — a bare
178
+ `CommandResult()` states nothing, and every real command adds what it actually produced.
179
+ """
180
+
181
+ model_config = ConfigDict(frozen=True, extra="forbid")
182
+
183
+
184
+ @runtime_checkable
185
+ class Command(Protocol):
186
+ """One CLI-invoked action a pack contributes, registered exactly as it registers a retriever.
187
+
188
+ Not a pipeline position — see the module docstring's *"`Command` is not a pipeline
189
+ position"* paragraph. `run` accepts the parsed, already-validated arguments and returns a
190
+ decided `Outcome[CommandResult]`, never printed text — see the module docstring's paragraph
191
+ on *"The governing property this task builds into the signature."*
192
+ """
193
+
194
+ if TYPE_CHECKING:
195
+ #: See `weft_extract.contract`'s module docstring for the full `__protocol_attrs__`
196
+ #: reasoning behind the `if TYPE_CHECKING:` / assign-after-the-class-body split, which
197
+ #: applies here unchanged for both of these: neither may become a required `isinstance`
198
+ #: member, or a structurally-conforming `Command` that never restates them would fail a
199
+ #: capability check that has nothing to do with capability.
200
+ version: ClassVar[str]
201
+ #: See the module docstring's paragraph on `permission_class` — this names the class
202
+ #: attribute every implementation must carry; it does not carry the value itself.
203
+ required_declarations: ClassVar[tuple[str, ...]]
204
+ #: The one-line summary `weft --help` prints beside this command's name — see the
205
+ #: module docstring's *"Task 3.2 adds `help`"* paragraph. Mandatory via
206
+ #: `required_declarations`, not a required `isinstance` member, for the identical
207
+ #: reason `permission_class` is not one.
208
+ help: ClassVar[str]
209
+
210
+ #: The model `run` accepts. Required for `isinstance` — see the module docstring's
211
+ #: `args_model`/`result_model` paragraph.
212
+ args_model: ClassVar[type[BaseModel]]
213
+ #: The `CommandResult` subclass `run` produces on success. Required for `isinstance`, for
214
+ #: the identical reason `args_model` is.
215
+ result_model: ClassVar[type[CommandResult]]
216
+
217
+ async def run(self, args: BaseModel, ctx: Context) -> Outcome[CommandResult]: ...
218
+
219
+
220
+ Command.version = COMMAND_CONTRACT_VERSION
221
+ Command.required_declarations = ("permission_class", "help")
@@ -0,0 +1,44 @@
1
+ """`PermissionClass` — the five-member, closed vocabulary `docs/03-cli.md` → *Permissions* fixes.
2
+
3
+ Task **3.1** moves this out of `weft_cli.permissions`, where it lived as a stand-in until the
4
+ `Command` contract itself existed (`weft_cli.permissions`'s own former docstring: "the `Command`
5
+ *contract* ... is Phase 3's ... gated on G8"). G8 settled 2026-08-18 and this distribution is
6
+ that contract's home — see `weft_command.contract`'s module docstring for why `Command` lives
7
+ here rather than in `weft-kernel` or `weft-cli`. `PermissionClass` moves with it because a
8
+ permission class is meaningless without the contract that reads it: `Command.required_declarations`
9
+ names `permission_class` as mandatory (`weft_kernel.registry`'s generalised check, task 3.1), and
10
+ the vocabulary that field's values are drawn from belongs beside the contract, not beside one
11
+ particular renderer of it.
12
+
13
+ **`weft_cli.permissions` — the re-export this class briefly had — is gone as of task 3.2.** It
14
+ existed only to keep `weft_cli.cli.COMMANDS`'s hand-written table importing one name from two
15
+ places during the 3.1→3.2 transition; 3.2 deleted `COMMANDS` itself (every built-in command is
16
+ now a `weft_command.contract.Command` plugin in `weft_cli.commands`, which imports
17
+ `PermissionClass` directly from here, on the same footing any third-party command pack does), so
18
+ the stand-in had no remaining caller and staying would have been the exact kind of drift-prone
19
+ duplicate name this contract's own placement exists to avoid.
20
+
21
+ `Enum`, never `Literal[...]`, per CLAUDE.md. The five members are fixed by `docs/03-cli.md`'s own
22
+ table — `read`/`write` default to allow, `overwrite`/`destroy` default to ask, `network` defaults
23
+ to allow but stays configurable — and defaults are policy a renderer or `weft.toml` enforces,
24
+ not a fact this enum carries; see `docs/03-cli.md` → *Permissions* for the table in full.
25
+ """
26
+
27
+ from enum import StrEnum
28
+
29
+
30
+ class PermissionClass(StrEnum):
31
+ """The five classes `docs/03-cli.md` → *Permissions* names.
32
+
33
+ **No default anywhere a `Command` implementation reads this from.** A plugin-contributed
34
+ command must declare exactly one of these — `Command.required_declarations` makes silence a
35
+ registration refusal, never a fallback to `read` — because `read` silently under-protects and
36
+ `destroy` trains an operator to pass `--yes` reflexively, disarming the whole table
37
+ (`docs/03-cli.md` → *Permissions*).
38
+ """
39
+
40
+ READ = "read"
41
+ WRITE = "write"
42
+ OVERWRITE = "overwrite"
43
+ DESTROY = "destroy"
44
+ NETWORK = "network"
File without changes
@@ -0,0 +1,67 @@
1
+ """`ExitCode` and `Rendered` — a renderer's own vocabulary, published beside the `Command`
2
+ contract that produces the result a renderer formats.
3
+
4
+ Task **6.20**, G13's third repair (`docs/03-cli.md` → *Plugin-contributed commands*): "a
5
+ result type nobody outside the CLI can format is only half a contract." Both classes moved
6
+ here from `weft_cli` — `ExitCode` out of `weft_cli.exit_codes`, `Rendered` out of
7
+ `weft_cli.render` — because a renderer registered by a third-party pack (`weft_kernel.
8
+ discovery.PackRegistrar.add_renderer`) has to build one of these to answer with, and a pack
9
+ implementing `Command` must not depend on `weft-cli`, the driving adapter that calls it, for
10
+ the vocabulary its own renderer speaks — the identical reasoning `weft_command.contract`'s
11
+ own module docstring already gives for why `Command` itself lives here rather than in
12
+ `weft-cli`.
13
+
14
+ **`ExitCode` travels with `Rendered` rather than staying behind, and that is a deliberate
15
+ choice, not a convenience.** A renderer that cannot say the run failed is a renderer a
16
+ built-in could not have used: `weft delete` and `weft index` both compute their own exit
17
+ code from their own result's fields (a partial deletion, or an index run with failures, is
18
+ success at the process level and failure at the operation level — see `weft_cli.render`'s
19
+ own `_render_delete`/`_render_index`), so a third-party renderer needs the identical
20
+ five-value vocabulary to state the same kind of fact about its own result. `weft_command.
21
+ permission.PermissionClass` is the standing precedent for a `docs/03-cli.md` concept living
22
+ in this pack rather than in `weft-cli`: this pack already publishes a vocabulary that
23
+ document defines, because the contract is unusable without it.
24
+
25
+ **`CommandResult` still names no exit code — what moved is the *renderer's* vocabulary, not
26
+ the result's.** `weft_cli.render`'s retired docstring argued exactly this, and the argument
27
+ still holds unchanged now that `ExitCode` and `Rendered` live here instead: `CommandResult`
28
+ is capability-agnostic, and `IndexCommandResult.summary.failed > 0` still has to become exit
29
+ code `1` somewhere — that "somewhere" is still whichever renderer a pack registers through
30
+ `PackRegistrar.add_renderer`, computed from the typed fields a command actually returned,
31
+ never smuggled into the result itself.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from dataclasses import dataclass
37
+ from enum import IntEnum
38
+
39
+
40
+ class ExitCode(IntEnum):
41
+ """`docs/03-cli.md` → *Output*: "0 success, 1 operation failed, 2 bad usage, 3 refused for
42
+ permissions, 4 pipeline failed to resolve." `2` is never assigned by a renderer — `argparse`
43
+ itself calls `sys.exit(2)` on a usage error, before any command runs, so nothing downstream
44
+ of a `Command`'s own `run` ever needs to produce it.
45
+ """
46
+
47
+ SUCCESS = 0
48
+ OPERATION_FAILED = 1
49
+ BAD_USAGE = 2
50
+ POLICY_REFUSED = 3
51
+ RESOLUTION_FAILED = 4
52
+
53
+
54
+ @dataclass(frozen=True, slots=True)
55
+ class Rendered:
56
+ """What a renderer answers with — stdout, stderr, and the exit code the process should
57
+ report. `None` on either stream means nothing prints on it.
58
+
59
+ Every renderer registered through `weft_kernel.discovery.PackRegistrar.add_renderer`
60
+ returns one of these, whether it is one of `weft_cli`'s own eighteen built-in renderers
61
+ or a third party's — a `Rendered` built by a stranger's pack is indistinguishable from
62
+ one `weft_cli.render` built itself, which is the property task 6.20 exists to make true.
63
+ """
64
+
65
+ stdout: str | None
66
+ stderr: str | None
67
+ exit_code: ExitCode