agentforge-framework 0.2.0__py3-none-any.whl
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.
- agentforge_framework/.claude-plugin/plugin.json +4 -0
- agentforge_framework/__init__.py +3 -0
- agentforge_framework/agents/__init__.py +92 -0
- agentforge_framework/agents/architect.py +146 -0
- agentforge_framework/agents/implementer.py +162 -0
- agentforge_framework/agents/orchestrator.py +588 -0
- agentforge_framework/agents/reviewer.py +335 -0
- agentforge_framework/agents/security.py +138 -0
- agentforge_framework/agents/tester.py +125 -0
- agentforge_framework/cli.py +461 -0
- agentforge_framework/context/__init__.py +1 -0
- agentforge_framework/context/extractors/__init__.py +76 -0
- agentforge_framework/context/extractors/base.py +47 -0
- agentforge_framework/context/extractors/python.py +65 -0
- agentforge_framework/context/extractors/sql.py +121 -0
- agentforge_framework/context/extractors/yaml.py +59 -0
- agentforge_framework/context/prompt.py +104 -0
- agentforge_framework/context/resolver.py +185 -0
- agentforge_framework/core/__init__.py +1 -0
- agentforge_framework/core/commands.py +170 -0
- agentforge_framework/core/config.py +90 -0
- agentforge_framework/core/contracts.py +875 -0
- agentforge_framework/core/gates.py +333 -0
- agentforge_framework/core/issues.py +697 -0
- agentforge_framework/core/plan_format.py +272 -0
- agentforge_framework/core/process.py +141 -0
- agentforge_framework/core/project.py +262 -0
- agentforge_framework/core/registry.py +455 -0
- agentforge_framework/core/repo.py +185 -0
- agentforge_framework/core/router.py +1 -0
- agentforge_framework/core/runtime.py +639 -0
- agentforge_framework/core/skills.py +255 -0
- agentforge_framework/core/workflow.py +215 -0
- agentforge_framework/plugins/__init__.py +35 -0
- agentforge_framework/plugins/databricks/__init__.py +86 -0
- agentforge_framework/plugins/pyspark/__init__.py +57 -0
- agentforge_framework/plugins/python/__init__.py +45 -0
- agentforge_framework/plugins/sql/__init__.py +377 -0
- agentforge_framework/providers/__init__.py +48 -0
- agentforge_framework/providers/base.py +248 -0
- agentforge_framework/providers/claude.py +159 -0
- agentforge_framework/providers/codex.py +139 -0
- agentforge_framework/skills/MANIFEST.yaml +157 -0
- agentforge_framework/skills/NOTICE +49 -0
- agentforge_framework/skills/domain-modeling/ADR-FORMAT.md +47 -0
- agentforge_framework/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- agentforge_framework/skills/domain-modeling/SKILL.md +74 -0
- agentforge_framework/skills/domain-modeling/agents/openai.yaml +3 -0
- agentforge_framework/skills/grill-with-docs/SKILL.md +76 -0
- agentforge_framework/skills/grilling/SKILL.md +28 -0
- agentforge_framework/skills/grilling/agents/openai.yaml +3 -0
- agentforge_framework/skills/to-spec/SKILL.md +75 -0
- agentforge_framework/skills/to-spec/agents/openai.yaml +5 -0
- agentforge_framework/skills/to-tickets/SKILL.md +105 -0
- agentforge_framework/skills/to-tickets/agents/openai.yaml +5 -0
- agentforge_framework/skills/unslop/SKILL.md +131 -0
- agentforge_framework/skills/unslop/evals/fixtures/silhouette/human_reference.json +66 -0
- agentforge_framework/skills/unslop/scripts/_lang.py +106 -0
- agentforge_framework/skills/unslop/scripts/banned_phrase_scan.py +784 -0
- agentforge_framework/skills/unslop/scripts/calibrate_pairs.py +580 -0
- agentforge_framework/skills/unslop/scripts/calibrate_score.py +273 -0
- agentforge_framework/skills/unslop/scripts/check_packs.py +80 -0
- agentforge_framework/skills/unslop/scripts/check_suggestions.py +225 -0
- agentforge_framework/skills/unslop/scripts/contribute.py +373 -0
- agentforge_framework/skills/unslop/scripts/diff_check.py +139 -0
- agentforge_framework/skills/unslop/scripts/extract_constraints.py +201 -0
- agentforge_framework/skills/unslop/scripts/harvest_classify.py +223 -0
- agentforge_framework/skills/unslop/scripts/harvest_samples.py +534 -0
- agentforge_framework/skills/unslop/scripts/readability_metrics.py +295 -0
- agentforge_framework/skills/unslop/scripts/refresh_status.py +154 -0
- agentforge_framework/skills/unslop/scripts/silhouette_scan.py +390 -0
- agentforge_framework/skills/unslop/scripts/structure_scan.py +322 -0
- agentforge_framework/skills/unslop/scripts/suggest.py +211 -0
- agentforge_framework/skills/unslop/scripts/validate_preservation.py +409 -0
- agentforge_framework/skills/unslop/scripts/voice_card.py +496 -0
- agentforge_framework/skills/unslop/scripts/voice_profile.py +194 -0
- agentforge_framework/skills/unslop/scripts/voice_score.py +271 -0
- agentforge_framework/skills/unslop/scripts/wiki_sync.py +479 -0
- agentforge_framework/skills/write-plainly/SKILL.md +94 -0
- agentforge_framework/workflows/bugfix.yaml +8 -0
- agentforge_framework/workflows/feature.yaml +16 -0
- agentforge_framework/workflows/review.yaml +10 -0
- agentforge_framework-0.2.0.dist-info/METADATA +321 -0
- agentforge_framework-0.2.0.dist-info/RECORD +89 -0
- agentforge_framework-0.2.0.dist-info/WHEEL +5 -0
- agentforge_framework-0.2.0.dist-info/entry_points.txt +3 -0
- agentforge_framework-0.2.0.dist-info/licenses/LICENSE +202 -0
- agentforge_framework-0.2.0.dist-info/licenses/src/agentforge_framework/skills/NOTICE +49 -0
- agentforge_framework-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""Gates: what holds a Run between two Steps, as predicates in a registry.
|
|
2
|
+
|
|
3
|
+
A Gate is a predicate over Run State and the Run Log returning cleared, blocked,
|
|
4
|
+
or errored. Blocked suspends the Run — nothing is wrong, and the Gate can still
|
|
5
|
+
clear. Errored halts it: a Gate that cannot evaluate has nothing to clear, so
|
|
6
|
+
suspending one would invite a resume that suspends again forever.
|
|
7
|
+
|
|
8
|
+
The registry is the point. `GATES` maps a kind onto its predicate, the Workflow
|
|
9
|
+
parser validates against its keys, and the runtime looks a kind up rather than
|
|
10
|
+
knowing any. Adding a kind is an entry here and nothing in `runtime.py`, exactly
|
|
11
|
+
as a seventh Role is an entry in `RUNNERS`.
|
|
12
|
+
|
|
13
|
+
`GATES` is the floor rather than the whole table. An active Plugin contributes
|
|
14
|
+
Gate kinds of its own — a dbt parse, a dialect check — and `core.registry`
|
|
15
|
+
assembles the table one Run is validated and evaluated against, which is handed
|
|
16
|
+
to `parse_workflow` and to `evaluate_gate` rather than swapped in globally. The
|
|
17
|
+
shipped three cannot be redefined by a Plugin: see ADR-0018.
|
|
18
|
+
|
|
19
|
+
Every Gate is handed the same context, the Command Runner and the working tree
|
|
20
|
+
included, so that one whose verdict comes from executing something has what it
|
|
21
|
+
needs without the runtime knowing which one that is. Most predicates ignore both.
|
|
22
|
+
|
|
23
|
+
A Gate that draws its verdict from a Role's output names that Role in
|
|
24
|
+
`invalidates`, which un-retires the Step that produced it (ADR-0008). A Gate
|
|
25
|
+
that reads something else — a human, a suite it re-executes — names nobody, and
|
|
26
|
+
every Step behind it stays behind it.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from collections.abc import Callable, Mapping
|
|
32
|
+
from dataclasses import dataclass, replace
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
from ..agents.security import SECURITY
|
|
36
|
+
from .config import load_config
|
|
37
|
+
from .contracts import GateEntry, GateVerdict, RunState
|
|
38
|
+
from .process import CommandResult, CommandRunner, MissingBinary
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class GateContext:
|
|
43
|
+
"""What a Gate is asked about: the Run, and the Step it stands behind.
|
|
44
|
+
|
|
45
|
+
`role` is the Role of that Step, not the Role the Gate judges — a human Gate
|
|
46
|
+
stands behind a Step whose work it has no opinion of. A predicate that does
|
|
47
|
+
judge a Role's output names it in the verdict it returns.
|
|
48
|
+
|
|
49
|
+
`runner` and `root` are what a Gate acts through when its verdict comes from
|
|
50
|
+
running something rather than from reading the Run Log. They are on every
|
|
51
|
+
context rather than on the ones that need them, so that registering a Gate
|
|
52
|
+
stays the whole cost of adding one.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
state: RunState
|
|
56
|
+
kind: str
|
|
57
|
+
role: str
|
|
58
|
+
step: int
|
|
59
|
+
runner: CommandRunner
|
|
60
|
+
root: Path
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def verdicts(self) -> tuple[GateEntry, ...]:
|
|
64
|
+
"""What this Gate has already said, at this Step, in Run Log order.
|
|
65
|
+
|
|
66
|
+
Identity is the kind and the position: one Workflow may declare the same
|
|
67
|
+
kind of Gate twice, and the second one has not spoken because the first
|
|
68
|
+
one did.
|
|
69
|
+
"""
|
|
70
|
+
return tuple(
|
|
71
|
+
entry
|
|
72
|
+
for entry in self.state.gates
|
|
73
|
+
if entry.kind == self.kind and entry.step == self.step
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
GateCheck = Callable[[GateContext], GateEntry]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def human(context: GateContext) -> GateEntry:
|
|
81
|
+
"""A human Gate: the Run stops, and a human decides when it goes on.
|
|
82
|
+
|
|
83
|
+
It clears once the Run Log shows it has already blocked here. The human's
|
|
84
|
+
acknowledgement is running `agentforge implement` again — they were told the
|
|
85
|
+
Run stopped, they looked at the branch, and they came back. Nothing else
|
|
86
|
+
would be less ceremony: resuming is a command they have to type either way.
|
|
87
|
+
|
|
88
|
+
Read off the Run Log rather than held in memory, so the Run that resumes may
|
|
89
|
+
be on a different machine from the Run that suspended (ADR-0002).
|
|
90
|
+
"""
|
|
91
|
+
if any(entry.blocked for entry in context.verdicts):
|
|
92
|
+
return GateEntry(
|
|
93
|
+
kind="",
|
|
94
|
+
verdict=GateVerdict.CLEARED,
|
|
95
|
+
summary="a human was asked to look at this Step and has re-run the Run",
|
|
96
|
+
)
|
|
97
|
+
return GateEntry(
|
|
98
|
+
kind="",
|
|
99
|
+
verdict=GateVerdict.BLOCKED,
|
|
100
|
+
summary=(
|
|
101
|
+
f"a human Gate follows the {context.role} Step. Review the work on the "
|
|
102
|
+
"Run's branch, then re-run `agentforge implement` to carry on."
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
#: The exit status a test runner spends on "tests ran and some failed", which is
|
|
108
|
+
#: the one non-zero status that is a report on the code rather than on the run.
|
|
109
|
+
#: pytest spends 2 through 5 on interruption, internal error, bad usage, and
|
|
110
|
+
#: nothing collected; none of those say anything about the repository.
|
|
111
|
+
SUITE_FAILED = 1
|
|
112
|
+
|
|
113
|
+
#: How much of a failing suite reaches the Run Log. The end of it: a test runner
|
|
114
|
+
#: puts its summary last, and a comment nobody scrolls to the bottom of is a
|
|
115
|
+
#: comment nobody reads.
|
|
116
|
+
TAIL_LINES = 40
|
|
117
|
+
TAIL_CHARS = 2000
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def tests(context: GateContext) -> GateEntry:
|
|
121
|
+
"""The test-suite Gate: run the suite, and read the exit status.
|
|
122
|
+
|
|
123
|
+
It re-executes rather than reading what the Tester said about the suite. An
|
|
124
|
+
Agent Result is a Role's account of its own work, and a Gate that took one at
|
|
125
|
+
its word would be checking the report rather than the repository. So this
|
|
126
|
+
Gate names nobody in `invalidates`: it judged no Step's output, every Step
|
|
127
|
+
behind it stays behind it, and the Run that resumes runs the suite again
|
|
128
|
+
rather than reading this verdict back (ADR-0008). Naming the Tester here
|
|
129
|
+
would un-retire the Tester Step and deadlock the Run.
|
|
130
|
+
|
|
131
|
+
Cleared, blocked, and errored are three different things that happen when you
|
|
132
|
+
run a suite. It passed. It ran and reported failures — nothing is wrong with
|
|
133
|
+
the plan, and the next commit may well clear it, which is Suspended exactly.
|
|
134
|
+
Or it never reached a verdict, and a Gate with nothing to clear halts the Run
|
|
135
|
+
rather than inviting a resume that suspends again.
|
|
136
|
+
|
|
137
|
+
ADR-0007's default-deny governs what a Role may run, not what AgentForge
|
|
138
|
+
runs. A Gate is not an Agent: the suite is the one the project declared, its
|
|
139
|
+
exit status is read rather than interpreted, and no model chose either.
|
|
140
|
+
"""
|
|
141
|
+
suite = load_config(context.root).test_suite
|
|
142
|
+
rendered = " ".join(suite)
|
|
143
|
+
|
|
144
|
+
if not context.runner.has_binary(suite[0]):
|
|
145
|
+
return _cannot_run(rendered, f"{suite[0]!r} is not installed or not on PATH")
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
result = context.runner.run(suite, cwd=context.root)
|
|
149
|
+
except MissingBinary as exc:
|
|
150
|
+
# The tool resolved on PATH and then would not start: a Windows `npm.cmd`
|
|
151
|
+
# is the everyday way to arrive here. Ending the Run is the Gate's job
|
|
152
|
+
# either way — raising would crash it instead, and nothing a human could
|
|
153
|
+
# act on would reach the Issue.
|
|
154
|
+
return _cannot_run(rendered, str(exc))
|
|
155
|
+
|
|
156
|
+
if result.ok:
|
|
157
|
+
return GateEntry(
|
|
158
|
+
kind="",
|
|
159
|
+
verdict=GateVerdict.CLEARED,
|
|
160
|
+
summary=f"`{rendered}` passed.",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if result.returncode == SUITE_FAILED:
|
|
164
|
+
return GateEntry(
|
|
165
|
+
kind="",
|
|
166
|
+
verdict=GateVerdict.BLOCKED,
|
|
167
|
+
summary=(
|
|
168
|
+
f"`{rendered}` failed. The Run stops here rather than carrying a red "
|
|
169
|
+
f"suite to Sign-off.\n\n{command_tail(result)}"
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return GateEntry(
|
|
174
|
+
kind="",
|
|
175
|
+
verdict=GateVerdict.ERRORED,
|
|
176
|
+
summary=(
|
|
177
|
+
f"`{rendered}` exited {result.returncode}, which is not a report on the "
|
|
178
|
+
"code: the suite did not run to a verdict, so there is nothing here for a "
|
|
179
|
+
f"later Run to clear.\n\n{command_tail(result)}"
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _cannot_run(rendered: str, reason: str) -> GateEntry:
|
|
185
|
+
"""The suite never started, which is not a report on the code.
|
|
186
|
+
|
|
187
|
+
Errored rather than blocked: waiting clears nothing, and the thing to fix is
|
|
188
|
+
the machine or the declaration rather than the repository.
|
|
189
|
+
"""
|
|
190
|
+
return GateEntry(
|
|
191
|
+
kind="",
|
|
192
|
+
verdict=GateVerdict.ERRORED,
|
|
193
|
+
summary=(
|
|
194
|
+
f"the test-suite Gate cannot run `{rendered}`: {reason}. Name the suite this "
|
|
195
|
+
"repository runs under `gates.tests.suite` in `.agentforge/config.yaml`."
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def command_tail(result: CommandResult) -> str:
|
|
201
|
+
"""The end of what a command printed, fenced for the Issue.
|
|
202
|
+
|
|
203
|
+
Public because a Plugin's Gate reports what it ran the same way the shipped
|
|
204
|
+
ones do. A validator that shells out to a parser wants the last forty lines
|
|
205
|
+
of it in the Run Log, and writing that a second time would be two answers to
|
|
206
|
+
one question about how much of a failure a human reads.
|
|
207
|
+
|
|
208
|
+
Four backticks rather than three: a failing test in a repository like this
|
|
209
|
+
one prints fenced blocks of its own, and a fence closed early takes the rest
|
|
210
|
+
of the Run Log entry with it.
|
|
211
|
+
"""
|
|
212
|
+
text = "\n".join(part for part in (result.stdout.strip(), result.stderr.strip()) if part)
|
|
213
|
+
text = "\n".join(text.splitlines()[-TAIL_LINES:])[-TAIL_CHARS:].strip()
|
|
214
|
+
if not text:
|
|
215
|
+
return "It printed nothing."
|
|
216
|
+
return f"````text\n{text}\n````"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def security(context: GateContext) -> GateEntry:
|
|
220
|
+
"""The clean-pass Gate: the Security Agent's Findings, read off the Run Log.
|
|
221
|
+
|
|
222
|
+
The mirror image of the test-suite Gate above. This verdict is drawn from a
|
|
223
|
+
Role's own output, so it names that Role in `invalidates` (ADR-0008): a human
|
|
224
|
+
who fixes a finding needs the audit run again, and a Run that resumed past
|
|
225
|
+
this entry would re-read a finding about code that no longer exists — which
|
|
226
|
+
is the deadlock the ADR was written about, from the other side.
|
|
227
|
+
|
|
228
|
+
An audit that reported nothing clears it. That is why the Security Role is
|
|
229
|
+
told to escalate rather than report an empty list when it could not look:
|
|
230
|
+
"audited and clean" and "did not audit" are the same shape here, and only
|
|
231
|
+
the Role knows which one happened.
|
|
232
|
+
|
|
233
|
+
Security not having run at all errors rather than blocks. A Gate waits for
|
|
234
|
+
something that can still arrive, and a Step that is not in front of this Run
|
|
235
|
+
never will.
|
|
236
|
+
"""
|
|
237
|
+
audits = [
|
|
238
|
+
result
|
|
239
|
+
for result in context.state.results
|
|
240
|
+
if result.role == SECURITY.name and result.ok
|
|
241
|
+
]
|
|
242
|
+
if not audits:
|
|
243
|
+
return GateEntry(
|
|
244
|
+
kind="",
|
|
245
|
+
verdict=GateVerdict.ERRORED,
|
|
246
|
+
summary=(
|
|
247
|
+
f"a {SECURITY.name} Gate stands behind the {context.role} Step, and the "
|
|
248
|
+
f"{SECURITY.name} Role has not completed in this Run. There is no audit "
|
|
249
|
+
"to read, so there is nothing here to clear: put a `security` Step in "
|
|
250
|
+
"front of this Gate."
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
findings = audits[-1].findings
|
|
255
|
+
if not findings:
|
|
256
|
+
return GateEntry(
|
|
257
|
+
kind="",
|
|
258
|
+
verdict=GateVerdict.CLEARED,
|
|
259
|
+
summary=f"the {SECURITY.name} Agent audited the change and reported no findings",
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
listed = "\n".join(
|
|
263
|
+
f"- `{finding.location or 'no location reported'}` — "
|
|
264
|
+
f"{finding.risk.strip() or 'no risk described'}"
|
|
265
|
+
for finding in findings
|
|
266
|
+
)
|
|
267
|
+
return GateEntry(
|
|
268
|
+
kind="",
|
|
269
|
+
verdict=GateVerdict.BLOCKED,
|
|
270
|
+
invalidates=SECURITY.name,
|
|
271
|
+
# No closing instruction here: the comment this verdict travels in
|
|
272
|
+
# already tells the reader that the Security Step will run again, as
|
|
273
|
+
# every verdict naming a Role in `invalidates` does.
|
|
274
|
+
summary=(
|
|
275
|
+
f"the {SECURITY.name} Agent reported {len(findings)} "
|
|
276
|
+
f"finding{'' if len(findings) == 1 else 's'}, so the Run holds here rather "
|
|
277
|
+
f"than carrying them to Sign-off.\n\n{listed}"
|
|
278
|
+
),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
#: Gate kind to predicate. The Workflow parser validates names against these
|
|
283
|
+
#: keys, so a definition naming `vibes` is refused at load time.
|
|
284
|
+
GATES: dict[str, GateCheck] = {
|
|
285
|
+
"human": human,
|
|
286
|
+
"tests": tests,
|
|
287
|
+
"security": security,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def evaluate_gate(
|
|
292
|
+
kind: str, context: GateContext, gates: Mapping[str, GateCheck] | None = None
|
|
293
|
+
) -> GateEntry:
|
|
294
|
+
"""Ask one Gate, and stamp its answer with which Gate was asked.
|
|
295
|
+
|
|
296
|
+
`gates` is the table to look in, and `None` means the shipped three. A Run
|
|
297
|
+
with active Plugins passes the wider one `core.registry` assembles, the same
|
|
298
|
+
way it passes a wider extractor table to the resolver: this module knows
|
|
299
|
+
that a table can be widened and nothing about what widens it.
|
|
300
|
+
|
|
301
|
+
Total on purpose: an unregistered kind errors rather than raising, so the
|
|
302
|
+
runtime has one way of ending at a Gate rather than two. A Workflow cannot
|
|
303
|
+
reach here with an unknown kind — the parser refuses those, validated
|
|
304
|
+
against the same table — but a Workflow built in code can, and so can a Run
|
|
305
|
+
whose Plugin was skipped after the Workflow that names its Gate was written.
|
|
306
|
+
"""
|
|
307
|
+
table = GATES if gates is None else gates
|
|
308
|
+
check = table.get(kind)
|
|
309
|
+
if check is None:
|
|
310
|
+
entry = GateEntry(
|
|
311
|
+
kind=kind,
|
|
312
|
+
verdict=GateVerdict.ERRORED,
|
|
313
|
+
summary=(
|
|
314
|
+
f"no Gate of kind {kind!r} is registered in this Run; "
|
|
315
|
+
f"kinds are: {', '.join(sorted(table))}"
|
|
316
|
+
),
|
|
317
|
+
)
|
|
318
|
+
else:
|
|
319
|
+
entry = check(context)
|
|
320
|
+
|
|
321
|
+
return replace(entry, kind=kind, step=context.step)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
__all__ = [
|
|
325
|
+
"GATES",
|
|
326
|
+
"GateCheck",
|
|
327
|
+
"GateContext",
|
|
328
|
+
"command_tail",
|
|
329
|
+
"evaluate_gate",
|
|
330
|
+
"human",
|
|
331
|
+
"security",
|
|
332
|
+
"tests",
|
|
333
|
+
]
|