formwork-kit 0.1.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.
- formwork_cli/__init__.py +326 -0
- formwork_cli/kit/COSTS.md +111 -0
- formwork_cli/kit/adapters/claude-code/README.md +53 -0
- formwork_cli/kit/adapters/claude-code/settings.json +46 -0
- formwork_cli/kit/adapters/codex/README.md +43 -0
- formwork_cli/kit/adapters/cursor/README.md +45 -0
- formwork_cli/kit/adapters/gemini-cli/README.md +47 -0
- formwork_cli/kit/build +410 -0
- formwork_cli/kit/check/checks/config-shape +123 -0
- formwork_cli/kit/check/checks/decision-ids +159 -0
- formwork_cli/kit/check/checks/doc-links +133 -0
- formwork_cli/kit/check/checks/generated-current +74 -0
- formwork_cli/kit/check/checks/guard-wired +139 -0
- formwork_cli/kit/check/checks/kit-integrity +199 -0
- formwork_cli/kit/check/checks/predictions-first +127 -0
- formwork_cli/kit/check/checks/role-shape +172 -0
- formwork_cli/kit/check/checks/rule-labels +135 -0
- formwork_cli/kit/check/fixtures/config-shape/must-fail/documents-a-section-that-does-not-exist/.formwork.toml +5 -0
- formwork_cli/kit/check/fixtures/config-shape/must-fail/documents-a-section-that-does-not-exist/formwork/guide.md +13 -0
- formwork_cli/kit/check/fixtures/config-shape/must-fail/rules-as-a-switchboard/.formwork.toml +8 -0
- formwork_cli/kit/check/fixtures/config-shape/must-pass/layers-kept-apart/.formwork.toml +5 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-fail/a-placeholder-shipped/docs/decisions/0003-still-pending.md +7 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-fail/superseded-by-nothing/docs/decisions/0002-old.md +6 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-fail/two-decisions-one-number/docs/decisions/0007-first.md +6 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-fail/two-decisions-one-number/docs/decisions/0007-second.md +6 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-pass/clean-numbering/docs/decisions/0001-the-first.md +6 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-pass/clean-numbering/docs/decisions/0002-the-second.md +6 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-pass/clean-numbering/docs/decisions/0003-the-third.md +6 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-pass/nothing-recorded-yet/docs/decisions/README.md +3 -0
- formwork_cli/kit/check/fixtures/doc-links/must-fail/never-written/index.md +7 -0
- formwork_cli/kit/check/fixtures/doc-links/must-fail/renamed-file/architecture-notes.md +3 -0
- formwork_cli/kit/check/fixtures/doc-links/must-fail/renamed-file/guide.md +8 -0
- formwork_cli/kit/check/fixtures/doc-links/must-pass/links-resolve/architecture-notes.md +1 -0
- formwork_cli/kit/check/fixtures/doc-links/must-pass/links-resolve/guide.md +5 -0
- formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.claude/agents/sample.md +22 -0
- formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.codex/agents/sample.toml +22 -0
- formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.formwork.toml +1 -0
- formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.gemini/agents/sample.md +23 -0
- formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/formwork/build +349 -0
- formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/formwork/roles/method/sample.md +18 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.claude/agents/sample.md +20 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.codex/agents/sample.toml +22 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.formwork.toml +1 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.gemini/agents/sample.md +23 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/formwork/build +349 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/formwork/roles/method/sample.md +18 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/nothing-is-generated-here/README.md +3 -0
- formwork_cli/kit/check/fixtures/guard-wired/must-fail/declared-but-no-file/.formwork.toml +1 -0
- formwork_cli/kit/check/fixtures/guard-wired/must-fail/file-but-not-wired/.claude/settings.json +1 -0
- formwork_cli/kit/check/fixtures/guard-wired/must-fail/file-but-not-wired/.formwork.toml +1 -0
- formwork_cli/kit/check/fixtures/guard-wired/must-pass/declared-and-wired/.claude/settings.json +1 -0
- formwork_cli/kit/check/fixtures/guard-wired/must-pass/declared-and-wired/.formwork.toml +1 -0
- formwork_cli/kit/check/fixtures/guard-wired/must-pass/nothing-declared/README.md +1 -0
- formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-check-went-missing/formwork/check/checks/still-here +2 -0
- formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-check-went-missing/state/fingerprints.txt +2 -0
- formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-guard-was-altered/formwork/guard/git-boundary +3 -0
- formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-guard-was-altered/state/fingerprints.txt +1 -0
- formwork_cli/kit/check/fixtures/kit-integrity/must-pass/everything-matches/formwork/guard/git-boundary +2 -0
- formwork_cli/kit/check/fixtures/kit-integrity/must-pass/everything-matches/state/fingerprints.txt +1 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-fail/argued-with-no-predictions/docs/rounds/0004-the-storage-question/architect.md +3 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-fail/argued-with-no-predictions/docs/rounds/0004-the-storage-question/researcher.md +3 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-fail/argued-with-no-predictions/docs/rounds/0004-the-storage-question/round.md +4 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-pass/a-round-that-has-not-argued-yet/docs/rounds/0006-not-started/round.md +3 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-pass/no-rounds-at-all/docs/README.md +3 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-pass/predictions-on-record/docs/rounds/0005-the-shape-of-a-brief/architect.md +3 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-pass/predictions-on-record/docs/rounds/0005-the-shape-of-a-brief/predictions.md +4 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-pass/predictions-on-record/docs/rounds/0005-the-shape-of-a-brief/researcher.md +3 -0
- formwork_cli/kit/check/fixtures/role-shape/must-fail/claims-a-grant-binds-everywhere/formwork/roles/README.md +6 -0
- formwork_cli/kit/check/fixtures/role-shape/must-fail/claims-a-grant-binds-everywhere/formwork/roles/complete.md +18 -0
- formwork_cli/kit/check/fixtures/role-shape/must-fail/missing-a-section/formwork/roles/vague.md +16 -0
- formwork_cli/kit/check/fixtures/role-shape/must-fail/spawn-without-being-lead/formwork/roles/eager.md +18 -0
- formwork_cli/kit/check/fixtures/role-shape/must-fail/two-roles-one-job/formwork/roles/first.md +18 -0
- formwork_cli/kit/check/fixtures/role-shape/must-fail/two-roles-one-job/formwork/roles/second.md +18 -0
- formwork_cli/kit/check/fixtures/role-shape/must-pass/well-formed/formwork/roles/complete.md +18 -0
- formwork_cli/kit/check/fixtures/rule-labels/must-fail/claims-enforcement-that-does-not-exist/formwork/rules/core.md +9 -0
- formwork_cli/kit/check/fixtures/rule-labels/must-fail/no-catches/formwork/rules/core.md +9 -0
- formwork_cli/kit/check/fixtures/rule-labels/must-fail/unlabelled/formwork/rules/core.md +7 -0
- formwork_cli/kit/check/fixtures/rule-labels/must-pass/well-formed/formwork/rules/core.md +10 -0
- formwork_cli/kit/check/run +340 -0
- formwork_cli/kit/check/test_gate.py +222 -0
- formwork_cli/kit/first-run.md +204 -0
- formwork_cli/kit/fw +121 -0
- formwork_cli/kit/glossary.md +160 -0
- formwork_cli/kit/guard/git-boundary +627 -0
- formwork_cli/kit/guard/protected-files +748 -0
- formwork_cli/kit/guard/quality-gate +260 -0
- formwork_cli/kit/guard/test_boundary.py +273 -0
- formwork_cli/kit/guard/test_protection.py +254 -0
- formwork_cli/kit/guard/test_quality_gate.py +156 -0
- formwork_cli/kit/install +395 -0
- formwork_cli/kit/limits.md +141 -0
- formwork_cli/kit/loop.md +82 -0
- formwork_cli/kit/roles/HOW-TO-ADD-A-ROLE.md +105 -0
- formwork_cli/kit/roles/TEMPLATE.md +26 -0
- formwork_cli/kit/roles/method/architect.md +269 -0
- formwork_cli/kit/roles/method/challenger.md +243 -0
- formwork_cli/kit/roles/method/lead.md +280 -0
- formwork_cli/kit/roles/method/record-keeper.md +206 -0
- formwork_cli/kit/roles/method/researcher.md +246 -0
- formwork_cli/kit/roles/method/reviewer.md +207 -0
- formwork_cli/kit/roles/packs/accessibility.md +236 -0
- formwork_cli/kit/roles/packs/ai.md +248 -0
- formwork_cli/kit/roles/packs/analyst.md +233 -0
- formwork_cli/kit/roles/packs/backend.md +425 -0
- formwork_cli/kit/roles/packs/brainstormer.md +190 -0
- formwork_cli/kit/roles/packs/data.md +212 -0
- formwork_cli/kit/roles/packs/devops.md +203 -0
- formwork_cli/kit/roles/packs/frontend.md +224 -0
- formwork_cli/kit/roles/packs/integrations.md +215 -0
- formwork_cli/kit/roles/packs/legal.md +251 -0
- formwork_cli/kit/roles/packs/marketing.md +206 -0
- formwork_cli/kit/roles/packs/mobile.md +202 -0
- formwork_cli/kit/roles/packs/performance.md +192 -0
- formwork_cli/kit/roles/packs/product.md +217 -0
- formwork_cli/kit/roles/packs/security.md +267 -0
- formwork_cli/kit/roles/packs/sre.md +203 -0
- formwork_cli/kit/roles/packs/tester.md +246 -0
- formwork_cli/kit/roles/packs/user-researcher.md +218 -0
- formwork_cli/kit/roles/packs/ux.md +205 -0
- formwork_cli/kit/roles/packs/visual.md +199 -0
- formwork_cli/kit/roles/packs/writer.md +198 -0
- formwork_cli/kit/round.md +131 -0
- formwork_cli/kit/rules/core.md +195 -0
- formwork_cli/kit/rules/full.md +493 -0
- formwork_cli/kit/templates/brief.md +68 -0
- formwork_cli/kit/templates/decision.md +93 -0
- formwork_cli/kit/templates/predictions.md +54 -0
- formwork_cli/kit/templates/report.md +52 -0
- formwork_cli/kit/templates/round.md +77 -0
- formwork_cli/kit/test_install.py +165 -0
- formwork_cli/kit/troubleshooting.md +247 -0
- formwork_cli/kit-page/FORMWORK.md +182 -0
- formwork_kit-0.1.0.dist-info/METADATA +308 -0
- formwork_kit-0.1.0.dist-info/RECORD +137 -0
- formwork_kit-0.1.0.dist-info/WHEEL +4 -0
- formwork_kit-0.1.0.dist-info/entry_points.txt +2 -0
- formwork_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
formwork_cli/__init__.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""The `formwork` command.
|
|
2
|
+
|
|
3
|
+
formwork init put the kit into this project
|
|
4
|
+
formwork install wire it up to your coding agent
|
|
5
|
+
formwork check run every check
|
|
6
|
+
formwork demo watch every check refuse a broken input
|
|
7
|
+
formwork roles rebuild the role files after editing one
|
|
8
|
+
formwork record write down what the kit looks like now
|
|
9
|
+
formwork test run every test in the kit
|
|
10
|
+
formwork where say which kit is being used
|
|
11
|
+
|
|
12
|
+
WHY THIS EXISTS
|
|
13
|
+
---------------
|
|
14
|
+
The kit is a folder of files that has to live inside your project, because
|
|
15
|
+
your agent's hooks call those files by path. That part cannot be global.
|
|
16
|
+
|
|
17
|
+
What can be global is the word you type. This module is the word.
|
|
18
|
+
|
|
19
|
+
HOW IT FINDS THE KIT
|
|
20
|
+
--------------------
|
|
21
|
+
It looks for a `formwork/` folder in the current directory, then each
|
|
22
|
+
directory above it. So the command works anywhere inside your project.
|
|
23
|
+
|
|
24
|
+
`formwork init` copies a fresh kit out of this package into the current
|
|
25
|
+
directory. That copy is what your project uses from then on. Upgrading the
|
|
26
|
+
command does not change a project you have already set up, which is
|
|
27
|
+
deliberate: a rule that changes under you without a commit is not a rule.
|
|
28
|
+
|
|
29
|
+
Python 3.8 or newer. No dependencies.
|
|
30
|
+
"""
|
|
31
|
+
import os
|
|
32
|
+
import re
|
|
33
|
+
import shutil
|
|
34
|
+
import stat
|
|
35
|
+
import subprocess
|
|
36
|
+
import sys
|
|
37
|
+
|
|
38
|
+
__all__ = ["main"]
|
|
39
|
+
|
|
40
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
41
|
+
BUNDLED_KIT = os.path.join(HERE, "kit")
|
|
42
|
+
BUNDLED_PAGE = os.path.join(HERE, "kit-page", "FORMWORK.md")
|
|
43
|
+
|
|
44
|
+
try: # Python 3.8 or newer
|
|
45
|
+
from importlib.metadata import version as _version, PackageNotFoundError
|
|
46
|
+
except ImportError: # pragma: no cover
|
|
47
|
+
_version = None
|
|
48
|
+
|
|
49
|
+
COMMANDS = [
|
|
50
|
+
("init", "put the kit into this project"),
|
|
51
|
+
("install", "wire it up to your agent. --runtime <name> to choose"),
|
|
52
|
+
("check", "run every check on this project"),
|
|
53
|
+
("demo", "watch every check refuse a broken input"),
|
|
54
|
+
("roles", "rebuild the role files after editing one"),
|
|
55
|
+
("test", "run every test in the kit"),
|
|
56
|
+
("record", "write down what the kit looks like now, after you changed it"),
|
|
57
|
+
("where", "say which kit is being used"),
|
|
58
|
+
("version", "which version of the command this is"),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
TESTS = [
|
|
62
|
+
os.path.join("guard", "test_boundary.py"),
|
|
63
|
+
os.path.join("guard", "test_protection.py"),
|
|
64
|
+
os.path.join("guard", "test_quality_gate.py"),
|
|
65
|
+
os.path.join("check", "test_gate.py"),
|
|
66
|
+
"test_install.py",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def find_kit(start=None, complete_only=True):
|
|
71
|
+
"""The nearest formwork/ folder at or above here, or None.
|
|
72
|
+
|
|
73
|
+
complete_only=False also returns a folder that looks like a kit but is
|
|
74
|
+
missing pieces. Without that, a damaged kit produced three different
|
|
75
|
+
answers from three commands and no way forward.
|
|
76
|
+
"""
|
|
77
|
+
d = os.path.abspath(start or os.getcwd())
|
|
78
|
+
while True:
|
|
79
|
+
candidate = os.path.join(d, "formwork")
|
|
80
|
+
looks_like = os.path.isdir(candidate) and (
|
|
81
|
+
os.path.isdir(os.path.join(candidate, "guard"))
|
|
82
|
+
or os.path.isdir(os.path.join(candidate, "check"))
|
|
83
|
+
or os.path.isdir(os.path.join(candidate, "roles")))
|
|
84
|
+
if os.path.isdir(candidate) and os.path.isfile(
|
|
85
|
+
os.path.join(candidate, "check", "run")):
|
|
86
|
+
return candidate
|
|
87
|
+
if looks_like and not complete_only:
|
|
88
|
+
return candidate
|
|
89
|
+
parent = os.path.dirname(d)
|
|
90
|
+
if parent == d:
|
|
91
|
+
return None
|
|
92
|
+
d = parent
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def make_runnable(root):
|
|
96
|
+
"""Give every program in the kit its execute bit back.
|
|
97
|
+
|
|
98
|
+
A wheel does not reliably carry file modes, and a check that cannot run is
|
|
99
|
+
treated by the gate as a failure rather than a pass. So this is not
|
|
100
|
+
cosmetic.
|
|
101
|
+
"""
|
|
102
|
+
fixed = 0
|
|
103
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
104
|
+
for fn in filenames:
|
|
105
|
+
p = os.path.join(dirpath, fn)
|
|
106
|
+
try:
|
|
107
|
+
with open(p, "rb") as fh:
|
|
108
|
+
if fh.read(2) != b"#!":
|
|
109
|
+
continue
|
|
110
|
+
except OSError:
|
|
111
|
+
continue
|
|
112
|
+
mode = os.stat(p).st_mode
|
|
113
|
+
os.chmod(p, mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
114
|
+
fixed += 1
|
|
115
|
+
return fixed
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def cmd_init(args):
|
|
119
|
+
if any(a.startswith("-") for a in args):
|
|
120
|
+
print("formwork init [directory]")
|
|
121
|
+
print("")
|
|
122
|
+
print(" Puts the kit into a directory. Defaults to this one.")
|
|
123
|
+
print(" `formwork init --help` used to create a folder called")
|
|
124
|
+
print(" --help, with a kit inside it.")
|
|
125
|
+
return 0
|
|
126
|
+
if not os.path.isdir(BUNDLED_KIT):
|
|
127
|
+
print("ERROR: this copy of the command has no kit bundled with it.",
|
|
128
|
+
file=sys.stderr)
|
|
129
|
+
print(" Clone the repository and copy formwork/ in by hand.",
|
|
130
|
+
file=sys.stderr)
|
|
131
|
+
return 2
|
|
132
|
+
|
|
133
|
+
target = os.path.abspath(args[0]) if args else os.getcwd()
|
|
134
|
+
dest = os.path.join(target, "formwork")
|
|
135
|
+
|
|
136
|
+
if os.path.exists(dest):
|
|
137
|
+
print("There is already a formwork/ folder here.")
|
|
138
|
+
print("Nothing was changed. Delete it first if you meant to start "
|
|
139
|
+
"again.")
|
|
140
|
+
return 1
|
|
141
|
+
|
|
142
|
+
# Without the filter, pip's own bytecode caches travel into the user's
|
|
143
|
+
# project and get committed by their first `git add .`
|
|
144
|
+
above = find_kit(os.path.dirname(target) if os.path.dirname(target) else None)
|
|
145
|
+
if above and os.path.dirname(os.path.dirname(above)) != target:
|
|
146
|
+
print("There is already a kit above this folder, at %s." % above)
|
|
147
|
+
print("Two kits in one tree is almost never what somebody wants.")
|
|
148
|
+
print("Nothing was changed. Pass a directory if you meant it:")
|
|
149
|
+
print(" formwork init /somewhere/else")
|
|
150
|
+
return 1
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
shutil.copytree(BUNDLED_KIT, dest,
|
|
154
|
+
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
|
|
155
|
+
except OSError as e:
|
|
156
|
+
print("ERROR: could not write into %s: %s" % (target, e),
|
|
157
|
+
file=sys.stderr)
|
|
158
|
+
return 2
|
|
159
|
+
n = make_runnable(dest)
|
|
160
|
+
|
|
161
|
+
page = os.path.join(target, "FORMWORK.md")
|
|
162
|
+
wrote_page = False
|
|
163
|
+
if os.path.isfile(BUNDLED_PAGE) and not os.path.exists(page):
|
|
164
|
+
shutil.copy2(BUNDLED_PAGE, page)
|
|
165
|
+
wrote_page = True
|
|
166
|
+
|
|
167
|
+
print("wrote formwork/ the kit, %d programs made runnable" % n)
|
|
168
|
+
if wrote_page:
|
|
169
|
+
print("wrote FORMWORK.md the method on one page")
|
|
170
|
+
print("")
|
|
171
|
+
print("Next: formwork install --runtime claude-code")
|
|
172
|
+
return 0
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def run_in_kit(rel, args):
|
|
176
|
+
kit = find_kit()
|
|
177
|
+
if kit is None:
|
|
178
|
+
broken = find_kit(complete_only=False)
|
|
179
|
+
if broken:
|
|
180
|
+
print("ERROR: there is a formwork/ folder at %s, and it is "
|
|
181
|
+
"incomplete." % broken, file=sys.stderr)
|
|
182
|
+
print(" formwork/check/run is missing. Delete the folder "
|
|
183
|
+
"and run `formwork init` again.", file=sys.stderr)
|
|
184
|
+
return 2
|
|
185
|
+
print("ERROR: no formwork/ folder here, or in any folder above this "
|
|
186
|
+
"one.", file=sys.stderr)
|
|
187
|
+
print(" Run `formwork init` first, or change to a project that "
|
|
188
|
+
"has one.", file=sys.stderr)
|
|
189
|
+
return 2
|
|
190
|
+
program = os.path.join(kit, rel)
|
|
191
|
+
if not os.path.isfile(program):
|
|
192
|
+
print("ERROR: %s is missing from the kit at %s" % (rel, kit),
|
|
193
|
+
file=sys.stderr)
|
|
194
|
+
return 2
|
|
195
|
+
if not os.access(program, os.X_OK):
|
|
196
|
+
make_runnable(kit)
|
|
197
|
+
project = os.path.dirname(kit)
|
|
198
|
+
try:
|
|
199
|
+
return subprocess.run([sys.executable, program] + list(args),
|
|
200
|
+
cwd=project).returncode
|
|
201
|
+
except OSError as e:
|
|
202
|
+
print("ERROR: could not run %s: %s" % (rel, e), file=sys.stderr)
|
|
203
|
+
return 2
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
TOTAL = re.compile(r"(\d+) of \1 behaved as specified|(\d+) passed, 0 failed")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def cmd_test():
|
|
210
|
+
kit = find_kit()
|
|
211
|
+
if kit is None:
|
|
212
|
+
print("ERROR: no formwork/ folder here.", file=sys.stderr)
|
|
213
|
+
return 2
|
|
214
|
+
# The tests exercise the guards with paths relative to the project root,
|
|
215
|
+
# so they must run from there. Running them from wherever the person
|
|
216
|
+
# happened to be standing failed 44 of them for the wrong reason.
|
|
217
|
+
project = os.path.dirname(kit)
|
|
218
|
+
failed = []
|
|
219
|
+
total = 0
|
|
220
|
+
for t in TESTS:
|
|
221
|
+
full = os.path.join(kit, t)
|
|
222
|
+
if not os.path.isfile(full):
|
|
223
|
+
continue
|
|
224
|
+
print("\n%s" % t, flush=True)
|
|
225
|
+
p = subprocess.run([sys.executable, full], cwd=project,
|
|
226
|
+
capture_output=True, text=True)
|
|
227
|
+
sys.stdout.write(p.stdout)
|
|
228
|
+
sys.stderr.write(p.stderr)
|
|
229
|
+
if p.returncode != 0:
|
|
230
|
+
failed.append(t)
|
|
231
|
+
# Add the suites up. The README states one number and this command
|
|
232
|
+
# used to end on the smallest of five, so a reader checking the claim
|
|
233
|
+
# concluded it was inflated twelvefold.
|
|
234
|
+
for m in TOTAL.finditer(p.stdout):
|
|
235
|
+
total += int(m.group(1) or m.group(2))
|
|
236
|
+
print("")
|
|
237
|
+
if failed:
|
|
238
|
+
print("FAILED: %s" % ", ".join(failed), file=sys.stderr)
|
|
239
|
+
return 1
|
|
240
|
+
print("%d tests, all passed" % total)
|
|
241
|
+
return 0
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def cmd_version():
|
|
245
|
+
"""Which version this is.
|
|
246
|
+
|
|
247
|
+
Every bug report worth having starts with this, and the first version of
|
|
248
|
+
this command did not have it.
|
|
249
|
+
"""
|
|
250
|
+
v = "unknown"
|
|
251
|
+
if _version is not None:
|
|
252
|
+
try:
|
|
253
|
+
v = _version("formwork-kit")
|
|
254
|
+
except Exception:
|
|
255
|
+
v = "not installed as a package"
|
|
256
|
+
print("formwork-kit %s" % v)
|
|
257
|
+
kit = find_kit()
|
|
258
|
+
if kit:
|
|
259
|
+
print("kit at %s" % kit)
|
|
260
|
+
return 0
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def cmd_where():
|
|
264
|
+
kit = find_kit()
|
|
265
|
+
if kit is None:
|
|
266
|
+
broken = find_kit(complete_only=False)
|
|
267
|
+
if broken:
|
|
268
|
+
print("An incomplete kit at %s. formwork/check/run is missing."
|
|
269
|
+
% broken)
|
|
270
|
+
print("Delete that folder and run `formwork init` again.")
|
|
271
|
+
return 1
|
|
272
|
+
print("No kit here. `formwork init` puts one in this folder.")
|
|
273
|
+
return 1
|
|
274
|
+
print("kit %s" % kit)
|
|
275
|
+
print("project %s" % os.path.dirname(kit))
|
|
276
|
+
print("command %s" % HERE)
|
|
277
|
+
return 0
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def usage():
|
|
281
|
+
print("formwork <command>")
|
|
282
|
+
print("")
|
|
283
|
+
for name, what in COMMANDS:
|
|
284
|
+
print(" %-8s %s" % (name, what))
|
|
285
|
+
print("")
|
|
286
|
+
print("Everything here runs a program inside your project's formwork/")
|
|
287
|
+
print("folder. You can always run those directly instead.")
|
|
288
|
+
return 0
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def main(argv=None):
|
|
292
|
+
argv = list(sys.argv if argv is None else argv)
|
|
293
|
+
if len(argv) >= 2 and argv[1] in ("--version", "-V"):
|
|
294
|
+
return cmd_version()
|
|
295
|
+
if len(argv) < 2 or argv[1] in ("help", "-h", "--help"):
|
|
296
|
+
return usage()
|
|
297
|
+
cmd, rest = argv[1], argv[2:]
|
|
298
|
+
if cmd == "init":
|
|
299
|
+
return cmd_init(rest)
|
|
300
|
+
if cmd == "install":
|
|
301
|
+
return run_in_kit("install", rest)
|
|
302
|
+
if cmd == "check":
|
|
303
|
+
return run_in_kit(os.path.join("check", "run"), rest)
|
|
304
|
+
if cmd == "demo":
|
|
305
|
+
return run_in_kit(os.path.join("check", "run"), ["--demo-fail"] + rest)
|
|
306
|
+
if cmd == "roles":
|
|
307
|
+
return run_in_kit("build", rest)
|
|
308
|
+
if cmd == "test":
|
|
309
|
+
return cmd_test()
|
|
310
|
+
if cmd == "where":
|
|
311
|
+
return cmd_where()
|
|
312
|
+
if cmd in ("version", "--version", "-V"):
|
|
313
|
+
return cmd_version()
|
|
314
|
+
if cmd == "record":
|
|
315
|
+
kit = find_kit()
|
|
316
|
+
target = os.path.dirname(kit) if kit else "."
|
|
317
|
+
return run_in_kit(os.path.join("check", "checks", "kit-integrity"),
|
|
318
|
+
["--record", target] + rest)
|
|
319
|
+
print("ERROR: no such command: %s" % cmd, file=sys.stderr)
|
|
320
|
+
print("", file=sys.stderr)
|
|
321
|
+
usage()
|
|
322
|
+
return 2
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
if __name__ == "__main__":
|
|
326
|
+
sys.exit(main())
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# What this costs to run
|
|
2
|
+
|
|
3
|
+
Two of the three numbers below are measured. The most important one is not.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## The gate — measured
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
$ time formwork check
|
|
11
|
+
real 1.89
|
|
12
|
+
real 2.27
|
|
13
|
+
real 2.20
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
About two seconds on this repository, over three runs. That is one run of all
|
|
17
|
+
nine checks over 129 files, including the fixtures each check is tried
|
|
18
|
+
against. It runs at the end of
|
|
19
|
+
every turn in which something changed.
|
|
20
|
+
|
|
21
|
+
**What would make that wrong:** it scales with how many files you have, and with
|
|
22
|
+
how many checks you add. On a large repository, measure it rather than trusting
|
|
23
|
+
this line.
|
|
24
|
+
|
|
25
|
+
## What the kit adds to your repository — measured
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
$ find formwork -type f | wc -l
|
|
29
|
+
129
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
129 files. Plain text and a few small programs. Python 3, standard library only,
|
|
33
|
+
no dependencies of any kind. It costs nothing to carry and nothing to install.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## A round — NOT ESTABLISHED
|
|
38
|
+
|
|
39
|
+
**Nobody has measured what a round costs in money. Not once.**
|
|
40
|
+
|
|
41
|
+
This is the most important number on this page and it is missing. Treat a round
|
|
42
|
+
as expensive until somebody produces the figure.
|
|
43
|
+
|
|
44
|
+
### What can be measured without spending anything
|
|
45
|
+
|
|
46
|
+
A round has a floor: the reading everybody has to do before a single thought
|
|
47
|
+
happens.
|
|
48
|
+
|
|
49
|
+
| | words |
|
|
50
|
+
|---|---|
|
|
51
|
+
| the core everybody reads — the page, the loop, the core rules | 2,276 |
|
|
52
|
+
| five role files, one per participant | 7,733 |
|
|
53
|
+
| **five participants, each reading the core plus their role** | **19,113** |
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
wc -w FORMWORK.md formwork/loop.md formwork/rules/core.md \
|
|
57
|
+
formwork/roles/method/{lead,challenger,architect,researcher,record-keeper}.md
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**What that number is not.** It is the floor, and only the floor. It excludes:
|
|
61
|
+
|
|
62
|
+
- your briefing, which is the largest single input and is different every time
|
|
63
|
+
- whatever each participant reads from your repository
|
|
64
|
+
- everything anybody writes
|
|
65
|
+
- every exchange after the first — the challenge, the reply, the record
|
|
66
|
+
|
|
67
|
+
A real round is a multiple of this, and **nobody knows the multiple.**
|
|
68
|
+
|
|
69
|
+
**And it will be out of date.** These figures have gone stale twice while this
|
|
70
|
+
page existed, because the roles keep growing. The commands are printed above for
|
|
71
|
+
that reason: **run them, do not trust the table.**
|
|
72
|
+
|
|
73
|
+
**What that number is also not:** it is words, not tokens. Models are billed in
|
|
74
|
+
tokens, and the conversion depends on the model. Converting it here would be
|
|
75
|
+
inventing precision.
|
|
76
|
+
|
|
77
|
+
### What would establish it
|
|
78
|
+
|
|
79
|
+
Run one round. Read the token usage the runtime itself reports. Multiply by the
|
|
80
|
+
published rate. Write down the size of round it came from.
|
|
81
|
+
|
|
82
|
+
That is an afternoon, and until somebody spends it this page keeps saying NOT
|
|
83
|
+
ESTABLISHED.
|
|
84
|
+
|
|
85
|
+
### There is no spend cap
|
|
86
|
+
|
|
87
|
+
Nothing in this kit stops a round costing more than you expected. A cap is on
|
|
88
|
+
the list of things not built, deliberately, because nobody has asked for one
|
|
89
|
+
yet.
|
|
90
|
+
|
|
91
|
+
**If you run rounds, set a limit with your provider.** That is outside this kit
|
|
92
|
+
and it is the only real protection available today.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## The cost that is certain, and is not money
|
|
97
|
+
|
|
98
|
+
**Every piece of work waits for you.**
|
|
99
|
+
|
|
100
|
+
That is what the version-control boundary means. The agent stops, reports, and
|
|
101
|
+
does nothing further until you say so.
|
|
102
|
+
|
|
103
|
+
On a busy day that is the bottleneck. It is also the entire point, and it is
|
|
104
|
+
the price of the whole method.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## What this page does not cover
|
|
109
|
+
|
|
110
|
+
Time. How long a brief takes to write, how long a review takes to read.
|
|
111
|
+
**Never measured**, and it would be a guess dressed as a figure.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Claude Code adapter
|
|
2
|
+
|
|
3
|
+
**Label: tested.** The boundary has been watched refusing a real command in
|
|
4
|
+
this runtime.
|
|
5
|
+
|
|
6
|
+
## What it wires
|
|
7
|
+
|
|
8
|
+
**Four hooks across three events**, not one. Copying only part of this file
|
|
9
|
+
leaves you with part of the protection.
|
|
10
|
+
|
|
11
|
+
| Event | What runs | What it does |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `PreToolUse` on `Bash` | `git-boundary` | refuses commits, pushes and merges |
|
|
14
|
+
| `PreToolUse` on `Bash` | `protected-files` | refuses changes to the kit's own files |
|
|
15
|
+
| `PreToolUse` on `Write\|Edit\|NotebookEdit` | `protected-files` | the same, for direct edits |
|
|
16
|
+
| `Stop` | `quality-gate` | refuses to end a turn while the gate is red |
|
|
17
|
+
|
|
18
|
+
Before any shell command runs, the guards read it and decide.
|
|
19
|
+
|
|
20
|
+
- exit `0` — the command runs
|
|
21
|
+
- exit `2` — the command is refused, and the reason goes back to the agent
|
|
22
|
+
|
|
23
|
+
Exit code 2 is the runtime's documented way to block a tool call.
|
|
24
|
+
|
|
25
|
+
## Installing it
|
|
26
|
+
|
|
27
|
+
Merge `settings.json` here into `.claude/settings.json` in your project. If you
|
|
28
|
+
have no hooks yet, copying the file is enough.
|
|
29
|
+
|
|
30
|
+
`$CLAUDE_PROJECT_DIR` is set by the runtime and points at your project root, so
|
|
31
|
+
the path works wherever the project lives.
|
|
32
|
+
|
|
33
|
+
## Checking it took
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
formwork check
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The `guard-wired` check fails if `.formwork.toml` names a runtime and that
|
|
40
|
+
runtime's wiring is missing. A boundary that is not installed is a boundary
|
|
41
|
+
that is not there, and nothing else would tell you.
|
|
42
|
+
|
|
43
|
+
## Turning it down
|
|
44
|
+
|
|
45
|
+
In `.formwork.toml`:
|
|
46
|
+
|
|
47
|
+
```toml
|
|
48
|
+
[strength]
|
|
49
|
+
git_boundary = "block" # block | warn | off
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`block` is the default and is the right setting for one person working alone.
|
|
53
|
+
Turning it down is a line you wrote, which is the point.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Bash",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/formwork/guard/git-boundary --format claude-code",
|
|
10
|
+
"timeout": 10,
|
|
11
|
+
"statusMessage": "version-control boundary"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"type": "command",
|
|
15
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/formwork/guard/protected-files --format claude-code",
|
|
16
|
+
"timeout": 10,
|
|
17
|
+
"statusMessage": "self-protection"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"matcher": "Write|Edit|NotebookEdit",
|
|
23
|
+
"hooks": [
|
|
24
|
+
{
|
|
25
|
+
"type": "command",
|
|
26
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/formwork/guard/protected-files --format claude-code",
|
|
27
|
+
"timeout": 10,
|
|
28
|
+
"statusMessage": "self-protection"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"Stop": [
|
|
34
|
+
{
|
|
35
|
+
"hooks": [
|
|
36
|
+
{
|
|
37
|
+
"type": "command",
|
|
38
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/formwork/guard/quality-gate --format claude-code",
|
|
39
|
+
"timeout": 300,
|
|
40
|
+
"statusMessage": "the aggregate"
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Codex adapter
|
|
2
|
+
|
|
3
|
+
**Label: untested.** The publisher documents everything below. Nobody has run
|
|
4
|
+
it. It stays untested until somebody watches it work.
|
|
5
|
+
|
|
6
|
+
## Blocking
|
|
7
|
+
|
|
8
|
+
Codex refuses a tool call on exit code 2, the same as the other three, so the
|
|
9
|
+
same guards serve it. Wire both to `preToolUse`, pointing at:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
formwork/guard/git-boundary --format codex
|
|
13
|
+
formwork/guard/protected-files --format codex
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Hooks go in `.codex/hooks.json`, or inline in `~/.codex/config.toml`. Its own
|
|
17
|
+
source carries `PreToolUseHookResult::Blocked`, so the mechanism is real even
|
|
18
|
+
though this wiring has not been exercised.
|
|
19
|
+
|
|
20
|
+
## Roles
|
|
21
|
+
|
|
22
|
+
`formwork roles --runtime codex` writes `.codex/agents/<name>.toml`.
|
|
23
|
+
|
|
24
|
+
**Of the four runtimes, this is the one that does not take markdown.** It takes TOML, with
|
|
25
|
+
`name`, `description` and `developer_instructions`.
|
|
26
|
+
|
|
27
|
+
## Where the tool grant goes
|
|
28
|
+
|
|
29
|
+
**Nowhere.** Codex has a sandbox mode, which is not a tool list. A role that
|
|
30
|
+
may not start other agents cannot say so here.
|
|
31
|
+
|
|
32
|
+
Each generated file states that in a comment rather than emitting a setting
|
|
33
|
+
that looks like a restriction and is not. **On Codex the grant is advice.**
|
|
34
|
+
|
|
35
|
+
## A defect worth knowing before you rely on this
|
|
36
|
+
|
|
37
|
+
Its issue tracker carries a report that agents in `.codex/agents/` cannot be
|
|
38
|
+
invoked by name from a tool-backed session — the runtime exposing only generic
|
|
39
|
+
spawning — and that the configuration has to be extracted and passed as
|
|
40
|
+
overrides instead.
|
|
41
|
+
|
|
42
|
+
If that holds, these generated files exist and cannot be used as documented.
|
|
43
|
+
**Not established here**, and worth checking before depending on it.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Cursor adapter
|
|
2
|
+
|
|
3
|
+
**Label: untested.** Documented by the publisher, never run.
|
|
4
|
+
|
|
5
|
+
## Roles — nothing is generated
|
|
6
|
+
|
|
7
|
+
Cursor's own documentation names `.claude/agents/` as a location it reads,
|
|
8
|
+
alongside `.cursor/agents/` and `.codex/agents/`.
|
|
9
|
+
|
|
10
|
+
So **the Claude Code output serves Cursor directly.** Generating a second,
|
|
11
|
+
identical tree would be duplication for its own sake, and duplication is what
|
|
12
|
+
the generator exists to remove.
|
|
13
|
+
|
|
14
|
+
Run `formwork roles --runtime claude-code` and Cursor finds them.
|
|
15
|
+
|
|
16
|
+
## Where the tool grant goes
|
|
17
|
+
|
|
18
|
+
Cursor has `readonly: true|false`. One bit.
|
|
19
|
+
|
|
20
|
+
That can say "this role may not write". It cannot say "this role may write and
|
|
21
|
+
may not start other agents", which is what the challenger needs.
|
|
22
|
+
|
|
23
|
+
Nothing approximate is emitted. **On Cursor the grant is advice.**
|
|
24
|
+
|
|
25
|
+
**And no file tells you that**, because nothing is generated for Cursor. The
|
|
26
|
+
files Cursor reads are the Claude Code ones, and they carry a Claude Code tool
|
|
27
|
+
list that Cursor cannot act on. This page is the only place that says so, which
|
|
28
|
+
is worth knowing before you rely on it.
|
|
29
|
+
|
|
30
|
+
## Blocking
|
|
31
|
+
|
|
32
|
+
Cursor refuses on exit code 2, and has more hook events than any of the four —
|
|
33
|
+
`preToolUse`, `beforeShellExecution`, `beforeMCPExecution`, `beforeReadFile`,
|
|
34
|
+
`subagentStart`.
|
|
35
|
+
|
|
36
|
+
Point `beforeShellExecution` at:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
formwork/guard/git-boundary --format cursor
|
|
40
|
+
formwork/guard/protected-files --format cursor
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`subagentStart` is worth knowing about: it can refuse the creation of a
|
|
44
|
+
subagent outright, which is the nearest thing Cursor has to the `spawn` grant.
|
|
45
|
+
Whether it is a workable substitute is **not established**.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Gemini CLI adapter
|
|
2
|
+
|
|
3
|
+
**Label: untested.** Documented by the publisher, never run.
|
|
4
|
+
|
|
5
|
+
## Blocking
|
|
6
|
+
|
|
7
|
+
`BeforeTool` refuses on exit code 2, with the reason taken from standard error.
|
|
8
|
+
Configure hooks in `.gemini/settings.json`, pointing at:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
formwork/guard/git-boundary --format gemini-cli
|
|
12
|
+
formwork/guard/protected-files --format gemini-cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Roles
|
|
16
|
+
|
|
17
|
+
`formwork roles --runtime gemini-cli` writes `.gemini/agents/<name>.md` —
|
|
18
|
+
markdown with YAML frontmatter, `kind: local`, and a `tools:` list.
|
|
19
|
+
|
|
20
|
+
## Where the tool grant goes — and the gap
|
|
21
|
+
|
|
22
|
+
Gemini CLI takes a named allowlist and enforces it, which is what the kit
|
|
23
|
+
wants. Two documented tool names are known and used:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
read → read_file, grep_search
|
|
27
|
+
run → run_shell_command
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**The name of the file-writing tool is not among the publisher's documented
|
|
31
|
+
examples.**
|
|
32
|
+
|
|
33
|
+
An allowlist is exact: writing one that omits a tool the role needs would
|
|
34
|
+
silently take that tool away. So the generator **writes no list at all** for
|
|
35
|
+
any role that needs `write`, and puts a line in the file saying why.
|
|
36
|
+
|
|
37
|
+
The consequence, stated plainly: **most roles get no enforced grant on Gemini
|
|
38
|
+
CLI today**, because most roles write. Read-only roles — the reviewer — get a
|
|
39
|
+
real one.
|
|
40
|
+
|
|
41
|
+
Closing this needs one fact: the documented name of the write tool. It is a
|
|
42
|
+
research task, not a design problem.
|
|
43
|
+
|
|
44
|
+
## One limit that costs this kit nothing
|
|
45
|
+
|
|
46
|
+
Subagents cannot start subagents, even with a wildcard grant. Only the lead
|
|
47
|
+
spawns, so nothing here is affected.
|