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.
Files changed (137) hide show
  1. formwork_cli/__init__.py +326 -0
  2. formwork_cli/kit/COSTS.md +111 -0
  3. formwork_cli/kit/adapters/claude-code/README.md +53 -0
  4. formwork_cli/kit/adapters/claude-code/settings.json +46 -0
  5. formwork_cli/kit/adapters/codex/README.md +43 -0
  6. formwork_cli/kit/adapters/cursor/README.md +45 -0
  7. formwork_cli/kit/adapters/gemini-cli/README.md +47 -0
  8. formwork_cli/kit/build +410 -0
  9. formwork_cli/kit/check/checks/config-shape +123 -0
  10. formwork_cli/kit/check/checks/decision-ids +159 -0
  11. formwork_cli/kit/check/checks/doc-links +133 -0
  12. formwork_cli/kit/check/checks/generated-current +74 -0
  13. formwork_cli/kit/check/checks/guard-wired +139 -0
  14. formwork_cli/kit/check/checks/kit-integrity +199 -0
  15. formwork_cli/kit/check/checks/predictions-first +127 -0
  16. formwork_cli/kit/check/checks/role-shape +172 -0
  17. formwork_cli/kit/check/checks/rule-labels +135 -0
  18. formwork_cli/kit/check/fixtures/config-shape/must-fail/documents-a-section-that-does-not-exist/.formwork.toml +5 -0
  19. formwork_cli/kit/check/fixtures/config-shape/must-fail/documents-a-section-that-does-not-exist/formwork/guide.md +13 -0
  20. formwork_cli/kit/check/fixtures/config-shape/must-fail/rules-as-a-switchboard/.formwork.toml +8 -0
  21. formwork_cli/kit/check/fixtures/config-shape/must-pass/layers-kept-apart/.formwork.toml +5 -0
  22. formwork_cli/kit/check/fixtures/decision-ids/must-fail/a-placeholder-shipped/docs/decisions/0003-still-pending.md +7 -0
  23. formwork_cli/kit/check/fixtures/decision-ids/must-fail/superseded-by-nothing/docs/decisions/0002-old.md +6 -0
  24. formwork_cli/kit/check/fixtures/decision-ids/must-fail/two-decisions-one-number/docs/decisions/0007-first.md +6 -0
  25. formwork_cli/kit/check/fixtures/decision-ids/must-fail/two-decisions-one-number/docs/decisions/0007-second.md +6 -0
  26. formwork_cli/kit/check/fixtures/decision-ids/must-pass/clean-numbering/docs/decisions/0001-the-first.md +6 -0
  27. formwork_cli/kit/check/fixtures/decision-ids/must-pass/clean-numbering/docs/decisions/0002-the-second.md +6 -0
  28. formwork_cli/kit/check/fixtures/decision-ids/must-pass/clean-numbering/docs/decisions/0003-the-third.md +6 -0
  29. formwork_cli/kit/check/fixtures/decision-ids/must-pass/nothing-recorded-yet/docs/decisions/README.md +3 -0
  30. formwork_cli/kit/check/fixtures/doc-links/must-fail/never-written/index.md +7 -0
  31. formwork_cli/kit/check/fixtures/doc-links/must-fail/renamed-file/architecture-notes.md +3 -0
  32. formwork_cli/kit/check/fixtures/doc-links/must-fail/renamed-file/guide.md +8 -0
  33. formwork_cli/kit/check/fixtures/doc-links/must-pass/links-resolve/architecture-notes.md +1 -0
  34. formwork_cli/kit/check/fixtures/doc-links/must-pass/links-resolve/guide.md +5 -0
  35. formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.claude/agents/sample.md +22 -0
  36. formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.codex/agents/sample.toml +22 -0
  37. formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.formwork.toml +1 -0
  38. formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.gemini/agents/sample.md +23 -0
  39. formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/formwork/build +349 -0
  40. formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/formwork/roles/method/sample.md +18 -0
  41. formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.claude/agents/sample.md +20 -0
  42. formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.codex/agents/sample.toml +22 -0
  43. formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.formwork.toml +1 -0
  44. formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.gemini/agents/sample.md +23 -0
  45. formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/formwork/build +349 -0
  46. formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/formwork/roles/method/sample.md +18 -0
  47. formwork_cli/kit/check/fixtures/generated-current/must-pass/nothing-is-generated-here/README.md +3 -0
  48. formwork_cli/kit/check/fixtures/guard-wired/must-fail/declared-but-no-file/.formwork.toml +1 -0
  49. formwork_cli/kit/check/fixtures/guard-wired/must-fail/file-but-not-wired/.claude/settings.json +1 -0
  50. formwork_cli/kit/check/fixtures/guard-wired/must-fail/file-but-not-wired/.formwork.toml +1 -0
  51. formwork_cli/kit/check/fixtures/guard-wired/must-pass/declared-and-wired/.claude/settings.json +1 -0
  52. formwork_cli/kit/check/fixtures/guard-wired/must-pass/declared-and-wired/.formwork.toml +1 -0
  53. formwork_cli/kit/check/fixtures/guard-wired/must-pass/nothing-declared/README.md +1 -0
  54. formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-check-went-missing/formwork/check/checks/still-here +2 -0
  55. formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-check-went-missing/state/fingerprints.txt +2 -0
  56. formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-guard-was-altered/formwork/guard/git-boundary +3 -0
  57. formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-guard-was-altered/state/fingerprints.txt +1 -0
  58. formwork_cli/kit/check/fixtures/kit-integrity/must-pass/everything-matches/formwork/guard/git-boundary +2 -0
  59. formwork_cli/kit/check/fixtures/kit-integrity/must-pass/everything-matches/state/fingerprints.txt +1 -0
  60. formwork_cli/kit/check/fixtures/predictions-first/must-fail/argued-with-no-predictions/docs/rounds/0004-the-storage-question/architect.md +3 -0
  61. formwork_cli/kit/check/fixtures/predictions-first/must-fail/argued-with-no-predictions/docs/rounds/0004-the-storage-question/researcher.md +3 -0
  62. formwork_cli/kit/check/fixtures/predictions-first/must-fail/argued-with-no-predictions/docs/rounds/0004-the-storage-question/round.md +4 -0
  63. 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
  64. formwork_cli/kit/check/fixtures/predictions-first/must-pass/no-rounds-at-all/docs/README.md +3 -0
  65. formwork_cli/kit/check/fixtures/predictions-first/must-pass/predictions-on-record/docs/rounds/0005-the-shape-of-a-brief/architect.md +3 -0
  66. formwork_cli/kit/check/fixtures/predictions-first/must-pass/predictions-on-record/docs/rounds/0005-the-shape-of-a-brief/predictions.md +4 -0
  67. formwork_cli/kit/check/fixtures/predictions-first/must-pass/predictions-on-record/docs/rounds/0005-the-shape-of-a-brief/researcher.md +3 -0
  68. formwork_cli/kit/check/fixtures/role-shape/must-fail/claims-a-grant-binds-everywhere/formwork/roles/README.md +6 -0
  69. formwork_cli/kit/check/fixtures/role-shape/must-fail/claims-a-grant-binds-everywhere/formwork/roles/complete.md +18 -0
  70. formwork_cli/kit/check/fixtures/role-shape/must-fail/missing-a-section/formwork/roles/vague.md +16 -0
  71. formwork_cli/kit/check/fixtures/role-shape/must-fail/spawn-without-being-lead/formwork/roles/eager.md +18 -0
  72. formwork_cli/kit/check/fixtures/role-shape/must-fail/two-roles-one-job/formwork/roles/first.md +18 -0
  73. formwork_cli/kit/check/fixtures/role-shape/must-fail/two-roles-one-job/formwork/roles/second.md +18 -0
  74. formwork_cli/kit/check/fixtures/role-shape/must-pass/well-formed/formwork/roles/complete.md +18 -0
  75. formwork_cli/kit/check/fixtures/rule-labels/must-fail/claims-enforcement-that-does-not-exist/formwork/rules/core.md +9 -0
  76. formwork_cli/kit/check/fixtures/rule-labels/must-fail/no-catches/formwork/rules/core.md +9 -0
  77. formwork_cli/kit/check/fixtures/rule-labels/must-fail/unlabelled/formwork/rules/core.md +7 -0
  78. formwork_cli/kit/check/fixtures/rule-labels/must-pass/well-formed/formwork/rules/core.md +10 -0
  79. formwork_cli/kit/check/run +340 -0
  80. formwork_cli/kit/check/test_gate.py +222 -0
  81. formwork_cli/kit/first-run.md +204 -0
  82. formwork_cli/kit/fw +121 -0
  83. formwork_cli/kit/glossary.md +160 -0
  84. formwork_cli/kit/guard/git-boundary +627 -0
  85. formwork_cli/kit/guard/protected-files +748 -0
  86. formwork_cli/kit/guard/quality-gate +260 -0
  87. formwork_cli/kit/guard/test_boundary.py +273 -0
  88. formwork_cli/kit/guard/test_protection.py +254 -0
  89. formwork_cli/kit/guard/test_quality_gate.py +156 -0
  90. formwork_cli/kit/install +395 -0
  91. formwork_cli/kit/limits.md +141 -0
  92. formwork_cli/kit/loop.md +82 -0
  93. formwork_cli/kit/roles/HOW-TO-ADD-A-ROLE.md +105 -0
  94. formwork_cli/kit/roles/TEMPLATE.md +26 -0
  95. formwork_cli/kit/roles/method/architect.md +269 -0
  96. formwork_cli/kit/roles/method/challenger.md +243 -0
  97. formwork_cli/kit/roles/method/lead.md +280 -0
  98. formwork_cli/kit/roles/method/record-keeper.md +206 -0
  99. formwork_cli/kit/roles/method/researcher.md +246 -0
  100. formwork_cli/kit/roles/method/reviewer.md +207 -0
  101. formwork_cli/kit/roles/packs/accessibility.md +236 -0
  102. formwork_cli/kit/roles/packs/ai.md +248 -0
  103. formwork_cli/kit/roles/packs/analyst.md +233 -0
  104. formwork_cli/kit/roles/packs/backend.md +425 -0
  105. formwork_cli/kit/roles/packs/brainstormer.md +190 -0
  106. formwork_cli/kit/roles/packs/data.md +212 -0
  107. formwork_cli/kit/roles/packs/devops.md +203 -0
  108. formwork_cli/kit/roles/packs/frontend.md +224 -0
  109. formwork_cli/kit/roles/packs/integrations.md +215 -0
  110. formwork_cli/kit/roles/packs/legal.md +251 -0
  111. formwork_cli/kit/roles/packs/marketing.md +206 -0
  112. formwork_cli/kit/roles/packs/mobile.md +202 -0
  113. formwork_cli/kit/roles/packs/performance.md +192 -0
  114. formwork_cli/kit/roles/packs/product.md +217 -0
  115. formwork_cli/kit/roles/packs/security.md +267 -0
  116. formwork_cli/kit/roles/packs/sre.md +203 -0
  117. formwork_cli/kit/roles/packs/tester.md +246 -0
  118. formwork_cli/kit/roles/packs/user-researcher.md +218 -0
  119. formwork_cli/kit/roles/packs/ux.md +205 -0
  120. formwork_cli/kit/roles/packs/visual.md +199 -0
  121. formwork_cli/kit/roles/packs/writer.md +198 -0
  122. formwork_cli/kit/round.md +131 -0
  123. formwork_cli/kit/rules/core.md +195 -0
  124. formwork_cli/kit/rules/full.md +493 -0
  125. formwork_cli/kit/templates/brief.md +68 -0
  126. formwork_cli/kit/templates/decision.md +93 -0
  127. formwork_cli/kit/templates/predictions.md +54 -0
  128. formwork_cli/kit/templates/report.md +52 -0
  129. formwork_cli/kit/templates/round.md +77 -0
  130. formwork_cli/kit/test_install.py +165 -0
  131. formwork_cli/kit/troubleshooting.md +247 -0
  132. formwork_cli/kit-page/FORMWORK.md +182 -0
  133. formwork_kit-0.1.0.dist-info/METADATA +308 -0
  134. formwork_kit-0.1.0.dist-info/RECORD +137 -0
  135. formwork_kit-0.1.0.dist-info/WHEEL +4 -0
  136. formwork_kit-0.1.0.dist-info/entry_points.txt +2 -0
  137. formwork_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -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.