openfactory 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.
Files changed (270) hide show
  1. openfactory/__init__.py +7 -0
  2. openfactory/actions/__init__.py +344 -0
  3. openfactory/actions/base.py +334 -0
  4. openfactory/actions/catalog.py +4872 -0
  5. openfactory/actions/floor_intents.py +431 -0
  6. openfactory/adapters/__init__.py +0 -0
  7. openfactory/adapters/agent/__init__.py +71 -0
  8. openfactory/adapters/agent/base.py +358 -0
  9. openfactory/adapters/agent/claude_code.py +1023 -0
  10. openfactory/adapters/agent/codex.py +350 -0
  11. openfactory/adapters/agent/kimi.py +291 -0
  12. openfactory/adapters/agent/opencode.py +432 -0
  13. openfactory/adapters/agent/registry.py +485 -0
  14. openfactory/adapters/agent/roles.py +263 -0
  15. openfactory/adapters/agent/routes.py +255 -0
  16. openfactory/adapters/agent/session_store.py +221 -0
  17. openfactory/adapters/agent/stream.py +367 -0
  18. openfactory/adapters/agent/techlead.py +108 -0
  19. openfactory/adapters/agent/token_pool.py +67 -0
  20. openfactory/adapters/azure_devops.py +329 -0
  21. openfactory/adapters/board/__init__.py +10 -0
  22. openfactory/adapters/board/azure_devops.py +748 -0
  23. openfactory/adapters/board/base.py +193 -0
  24. openfactory/adapters/board/columns.py +68 -0
  25. openfactory/adapters/board/factory.py +218 -0
  26. openfactory/adapters/board/jira.py +252 -0
  27. openfactory/adapters/board/local.py +187 -0
  28. openfactory/adapters/board_db.py +276 -0
  29. openfactory/adapters/board_setup/__init__.py +8 -0
  30. openfactory/adapters/board_setup/base.py +49 -0
  31. openfactory/adapters/board_setup/local.py +63 -0
  32. openfactory/adapters/board_setup/registry.py +41 -0
  33. openfactory/adapters/channel/__init__.py +4 -0
  34. openfactory/adapters/channel/base.py +100 -0
  35. openfactory/adapters/channel/panel.py +107 -0
  36. openfactory/adapters/channel/registry.py +112 -0
  37. openfactory/adapters/credential/__init__.py +8 -0
  38. openfactory/adapters/credential/registry.py +164 -0
  39. openfactory/adapters/environment/__init__.py +30 -0
  40. openfactory/adapters/environment/azure_pipelines.py +412 -0
  41. openfactory/adapters/environment/base.py +35 -0
  42. openfactory/adapters/environment/github_actions.py +97 -0
  43. openfactory/adapters/environment/none.py +53 -0
  44. openfactory/adapters/environment/registry.py +160 -0
  45. openfactory/adapters/forge/__init__.py +30 -0
  46. openfactory/adapters/forge/azure_devops.py +1528 -0
  47. openfactory/adapters/forge/base.py +515 -0
  48. openfactory/adapters/forge/github.py +779 -0
  49. openfactory/adapters/forge/local.py +660 -0
  50. openfactory/adapters/forge/registry.py +210 -0
  51. openfactory/adapters/github_app.py +70 -0
  52. openfactory/adapters/notify/__init__.py +12 -0
  53. openfactory/adapters/notify/base.py +45 -0
  54. openfactory/adapters/notify/panel.py +43 -0
  55. openfactory/adapters/notify/registry.py +301 -0
  56. openfactory/adapters/reviewer/__init__.py +30 -0
  57. openfactory/adapters/reviewer/base.py +34 -0
  58. openfactory/adapters/reviewer/claude_code.py +140 -0
  59. openfactory/adapters/reviewer/harness.py +154 -0
  60. openfactory/adapters/sandbox/__init__.py +31 -0
  61. openfactory/adapters/sandbox/base.py +410 -0
  62. openfactory/adapters/sandbox/container.py +550 -0
  63. openfactory/adapters/sandbox/registry.py +490 -0
  64. openfactory/adapters/sandbox/timeouts.py +76 -0
  65. openfactory/adapters/sandbox/worktree.py +381 -0
  66. openfactory/adapters/tracker/__init__.py +30 -0
  67. openfactory/adapters/tracker/azure_devops.py +904 -0
  68. openfactory/adapters/tracker/base.py +549 -0
  69. openfactory/adapters/tracker/github.py +529 -0
  70. openfactory/adapters/tracker/github_board_setup.py +216 -0
  71. openfactory/adapters/tracker/github_project.py +733 -0
  72. openfactory/adapters/tracker/jira.py +575 -0
  73. openfactory/adapters/tracker/local.py +379 -0
  74. openfactory/adapters/tracker/parse.py +231 -0
  75. openfactory/adapters/tracker/registry.py +200 -0
  76. openfactory/after_merge.py +51 -0
  77. openfactory/api/__init__.py +4 -0
  78. openfactory/api/app.py +2627 -0
  79. openfactory/api/metrics_view.py +337 -0
  80. openfactory/api/panel.html +3419 -0
  81. openfactory/approvals.py +86 -0
  82. openfactory/box_prove.py +1458 -0
  83. openfactory/cli.py +3298 -0
  84. openfactory/cli_refusals.py +169 -0
  85. openfactory/conformance/__init__.py +19 -0
  86. openfactory/conformance/adapters.py +622 -0
  87. openfactory/contracts/__init__.py +65 -0
  88. openfactory/contracts/bot.py +42 -0
  89. openfactory/contracts/commands.py +183 -0
  90. openfactory/contracts/decision.py +286 -0
  91. openfactory/contracts/manifest.py +713 -0
  92. openfactory/contracts/product.py +95 -0
  93. openfactory/contracts/profile.py +214 -0
  94. openfactory/contracts/project.py +353 -0
  95. openfactory/contracts/refs.py +155 -0
  96. openfactory/contracts/review.py +52 -0
  97. openfactory/contracts/run.py +326 -0
  98. openfactory/contracts/state.py +64 -0
  99. openfactory/contracts/ticket.py +113 -0
  100. openfactory/credentials.py +356 -0
  101. openfactory/doctor.py +1577 -0
  102. openfactory/doors.py +238 -0
  103. openfactory/environ.py +251 -0
  104. openfactory/factory.py +460 -0
  105. openfactory/floor/__init__.py +31 -0
  106. openfactory/floor/ladder.py +655 -0
  107. openfactory/floor/reading.py +302 -0
  108. openfactory/identity/__init__.py +14 -0
  109. openfactory/identity/base.py +95 -0
  110. openfactory/identity/local.py +186 -0
  111. openfactory/identity/oidc.py +633 -0
  112. openfactory/identity/people.py +352 -0
  113. openfactory/identity/registry.py +91 -0
  114. openfactory/knowledge/__init__.py +76 -0
  115. openfactory/knowledge/bundle.py +219 -0
  116. openfactory/knowledge/check.py +167 -0
  117. openfactory/knowledge/contracts.py +407 -0
  118. openfactory/knowledge/experiment.py +118 -0
  119. openfactory/knowledge/gaps.py +118 -0
  120. openfactory/knowledge/gate.py +330 -0
  121. openfactory/knowledge/gather.py +156 -0
  122. openfactory/knowledge/generator.py +822 -0
  123. openfactory/knowledge/inventory.py +557 -0
  124. openfactory/knowledge/okf.py +369 -0
  125. openfactory/knowledge/pipeline.py +333 -0
  126. openfactory/knowledge/render.py +146 -0
  127. openfactory/knowledge/service.py +53 -0
  128. openfactory/knowledge/staleness.py +112 -0
  129. openfactory/language/__init__.py +15 -0
  130. openfactory/language/assent.py +130 -0
  131. openfactory/loader.py +156 -0
  132. openfactory/memory/__init__.py +29 -0
  133. openfactory/memory/ledger.py +215 -0
  134. openfactory/memory/messages.py +406 -0
  135. openfactory/memory/recall.py +303 -0
  136. openfactory/memory/store.py +157 -0
  137. openfactory/memory/transcript.py +267 -0
  138. openfactory/namespace.py +258 -0
  139. openfactory/observability/__init__.py +23 -0
  140. openfactory/observability/events.py +188 -0
  141. openfactory/observability/metrics.py +191 -0
  142. openfactory/observability/query.py +80 -0
  143. openfactory/observability/registry.py +253 -0
  144. openfactory/observability/sqlite_metrics.py +203 -0
  145. openfactory/observability/trajectory.py +243 -0
  146. openfactory/onboarding/__init__.py +64 -0
  147. openfactory/onboarding/concepts.py +450 -0
  148. openfactory/onboarding/context.py +2399 -0
  149. openfactory/onboarding/cover.py +97 -0
  150. openfactory/onboarding/deployment.py +807 -0
  151. openfactory/onboarding/firstrun.py +1637 -0
  152. openfactory/onboarding/history.py +363 -0
  153. openfactory/onboarding/infer.py +2736 -0
  154. openfactory/onboarding/live_ci.py +162 -0
  155. openfactory/onboarding/onboard.py +1070 -0
  156. openfactory/onboarding/propose_manifest.py +349 -0
  157. openfactory/onboarding/questions.py +137 -0
  158. openfactory/onboarding/readiness.py +1073 -0
  159. openfactory/onboarding/renew.py +239 -0
  160. openfactory/onboarding/spend.py +82 -0
  161. openfactory/ops/__init__.py +6 -0
  162. openfactory/ops/impediment.py +288 -0
  163. openfactory/orchestrator/__init__.py +26 -0
  164. openfactory/orchestrator/context.py +231 -0
  165. openfactory/orchestrator/errors.py +27 -0
  166. openfactory/orchestrator/machine.py +2617 -0
  167. openfactory/orchestrator/merge_policy.py +203 -0
  168. openfactory/orchestrator/promotion.py +249 -0
  169. openfactory/orchestrator/risk.py +191 -0
  170. openfactory/orchestrator/validation.py +172 -0
  171. openfactory/org_defaults/engineering.md +75 -0
  172. openfactory/org_defaults/floor.yaml +182 -0
  173. openfactory/org_defaults/profiles/prototype.yaml +16 -0
  174. openfactory/org_defaults/profiles/regulated.yaml +35 -0
  175. openfactory/org_defaults/requirements/0000-template.md +39 -0
  176. openfactory/org_defaults/roles/coordinator.md +48 -0
  177. openfactory/org_defaults/roles/executor.md +92 -0
  178. openfactory/org_defaults/roles/planner.md +117 -0
  179. openfactory/org_defaults/roles/product.md +71 -0
  180. openfactory/org_defaults/roles/recovery.md +33 -0
  181. openfactory/org_defaults/roles/sizer.md +62 -0
  182. openfactory/org_defaults/roles/techlead.md +61 -0
  183. openfactory/org_defaults/tdd.md +10 -0
  184. openfactory/own_work.py +41 -0
  185. openfactory/paths.py +117 -0
  186. openfactory/plugins.py +208 -0
  187. openfactory/policy/__init__.py +10 -0
  188. openfactory/policy/authz.py +105 -0
  189. openfactory/policy/census.py +161 -0
  190. openfactory/policy/conformance.py +300 -0
  191. openfactory/policy/floor.py +42 -0
  192. openfactory/policy/presets.py +172 -0
  193. openfactory/policy/profiles.py +275 -0
  194. openfactory/policy/protected.py +169 -0
  195. openfactory/policy/test_work.py +70 -0
  196. openfactory/preflight.py +577 -0
  197. openfactory/presets/node.yaml +6 -0
  198. openfactory/presets/python.yaml +9 -0
  199. openfactory/presets/security-oss.yaml +26 -0
  200. openfactory/presets/terraform.yaml +8 -0
  201. openfactory/product/__init__.py +56 -0
  202. openfactory/product/asked.py +172 -0
  203. openfactory/product/authoring.py +1791 -0
  204. openfactory/product/board.py +484 -0
  205. openfactory/product/brownfield.py +210 -0
  206. openfactory/product/case.py +409 -0
  207. openfactory/product/channel.py +1437 -0
  208. openfactory/product/config.py +364 -0
  209. openfactory/product/confirm.py +801 -0
  210. openfactory/product/conversation.py +49 -0
  211. openfactory/product/corpus.py +462 -0
  212. openfactory/product/domain.py +242 -0
  213. openfactory/product/facts.py +209 -0
  214. openfactory/product/followup.py +647 -0
  215. openfactory/product/intents.py +641 -0
  216. openfactory/product/loader.py +198 -0
  217. openfactory/product/module.py +3100 -0
  218. openfactory/product/needs_action.py +198 -0
  219. openfactory/product/onboard.py +331 -0
  220. openfactory/product/queue.py +273 -0
  221. openfactory/product/reading.py +130 -0
  222. openfactory/product/release.py +153 -0
  223. openfactory/product/requester.py +55 -0
  224. openfactory/product/role.py +1356 -0
  225. openfactory/product/staging.py +477 -0
  226. openfactory/product/triage.py +277 -0
  227. openfactory/product/voice.py +2080 -0
  228. openfactory/product/workspace.py +182 -0
  229. openfactory/registry.py +526 -0
  230. openfactory/review/__init__.py +11 -0
  231. openfactory/review/verdict.py +199 -0
  232. openfactory/runtime/__init__.py +0 -0
  233. openfactory/runtime/boxed_job.py +399 -0
  234. openfactory/runtime/card_repo.py +146 -0
  235. openfactory/runtime/host.py +91 -0
  236. openfactory/runtime/repo_cache.py +296 -0
  237. openfactory/runtime/temporal/__init__.py +26 -0
  238. openfactory/runtime/temporal/activities.py +5081 -0
  239. openfactory/runtime/temporal/connection.py +89 -0
  240. openfactory/runtime/temporal/io.py +639 -0
  241. openfactory/runtime/temporal/poller.py +185 -0
  242. openfactory/runtime/temporal/schedule.py +465 -0
  243. openfactory/runtime/temporal/starter.py +54 -0
  244. openfactory/runtime/temporal/view.py +884 -0
  245. openfactory/runtime/temporal/worker.py +325 -0
  246. openfactory/runtime/temporal/workflow.py +2459 -0
  247. openfactory/runtime/toolbox.py +163 -0
  248. openfactory/scheduler.py +75 -0
  249. openfactory/semver.py +35 -0
  250. openfactory/techlead/__init__.py +38 -0
  251. openfactory/techlead/classify.py +427 -0
  252. openfactory/techlead/conversation.py +1401 -0
  253. openfactory/techlead/diagnosis.py +251 -0
  254. openfactory/techlead/memory.py +264 -0
  255. openfactory/techlead/pack.py +169 -0
  256. openfactory/techlead/voice.py +679 -0
  257. openfactory/techlead/watch.py +638 -0
  258. openfactory/testing/__init__.py +1 -0
  259. openfactory/testing/local_flow.py +272 -0
  260. openfactory/util/__init__.py +0 -0
  261. openfactory/util/bounded.py +77 -0
  262. openfactory/util/causes.py +125 -0
  263. openfactory/util/scratch.py +80 -0
  264. openfactory-0.2.0.dist-info/METADATA +41 -0
  265. openfactory-0.2.0.dist-info/RECORD +270 -0
  266. openfactory-0.2.0.dist-info/WHEEL +5 -0
  267. openfactory-0.2.0.dist-info/entry_points.txt +2 -0
  268. openfactory-0.2.0.dist-info/licenses/LICENSE +202 -0
  269. openfactory-0.2.0.dist-info/licenses/NOTICE +22 -0
  270. openfactory-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,431 @@
1
+ """What a typed sentence on the FLOOR asks for — merge, discard, adjust (#120).
2
+
3
+ The pilot had `#87`'s PR open, the panel showing `View PR · Merge · Adjust… · Discard`, and typed
4
+ into the tech-lead chat:
5
+
6
+ pode fazer o merge
7
+
8
+ and got back *"I can't merge from here — a merge is a human action, outside what I execute."*
9
+ Half right, and the wrong half is the expensive one: `merge_policy: human` means the DECISION is
10
+ the human's — the EXECUTION has always been the factory's, and `actions/catalog.py` carries the
11
+ row the panel's own button posts to. A human who has decided in words has decided.
12
+
13
+ THIS IS A MATCHER AND NOTHING ELSE. It never performs anything: the caller routes what it returns
14
+ through `actions.perform` with the actor that came through the door, so a sentence can never reach
15
+ something its author's credential could not — the rule `_say_as_an_intent` states for the product
16
+ side, and the reason this file holds no dispatch of its own.
17
+
18
+ THE ASYMMETRY OF MISTAKES DECIDES THE PATTERNS. A miss costs a rephrase. A false positive merges a
19
+ pull request into somebody's main branch on a sentence that was a question — so the gestures are
20
+ imperative, the question test from the product matcher is reused rather than reinvented, and
21
+ anything that reads as wondering aloud falls through to the tech-lead, who answers in prose.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import re
27
+
28
+ from openfactory.product.intents import _asks_rather_than_tells
29
+
30
+ #: intent → the catalogue row it performs. The row is where the gate, the scope and the engine
31
+ #: check live; this table only says which door the sentence knocked on.
32
+ FLOOR_ROWS: dict[str, str] = {
33
+ "merge": "merge",
34
+ "discard": "discard",
35
+ "adjust": "adjust",
36
+ "review": "review",
37
+ "stop": "stop",
38
+ "resume": "resume",
39
+ "skip": "skip",
40
+ }
41
+
42
+ #: An optional ticket ref riding on the instruction — `merge #87`, `faz o merge do 90`,
43
+ #: `descarta o CONT-412`. A DIGIT IS REQUIRED somewhere in the token, so "merge it" cannot read
44
+ #: "it" as a ticket; the shape is otherwise the provider's own (C-05: never assume numeric).
45
+ #:
46
+ #: CAPTURED SO THE CALLER CAN OBEY IT, and the sweep found all three costs of not doing so
47
+ #: (2026-08-16): a named ref was silently DROPPED, so "merge #90" merged whatever happened to be
48
+ #: waiting; and with two PRs waiting, the refusal dictated the reply `merge #87` — a sentence this
49
+ #: very matcher then threw the ref away from, re-entering the same refusal for ever. An
50
+ #: instruction's scope is part of the instruction.
51
+ _REF = r"(?:\s+(?:o\s+|a\s+|do\s+|de\s+|the\s+)?[#!]?(?P<ref>(?=[\w.-]*\d)[\w][\w.-]*))?"
52
+
53
+ #: The same ref, REQUIRED — for a verb whose false positive cannot be undone (#127).
54
+ #:
55
+ #: SPELLED OUT RATHER THAN DERIVED FROM `_REF`. The first version built it by string-surgery on
56
+ #: the pattern above (`_REF.replace("(?:\\s+", …)`), which left the trailing `?` in place — so the
57
+ #: ref stayed optional and "mata o job" matched with no ticket at all, which is the shape that
58
+ #: terminates whichever job happens to be first. Editing a regex by pattern is the mistake this
59
+ #: repository has a rule against, and it does not stop being one because the regex is ours.
60
+ _REF_REQUIRED = r"\s*(?:o\s+|a\s+|do\s+|de\s+|the\s+)?[#!]?(?P<ref>(?=[\w.-]*\d)[\w][\w.-]*)"
61
+
62
+ #: What may stand between the start of a clause and the verb that COMMANDS it. Politeness and
63
+ #: conjunctions, in both languages — never a determiner, never a subject, never another verb. It is
64
+ #: deliberately not a domain list: nothing in it names a stack, a vendor or this deployment.
65
+ #: `pode`/`vai lá`/`go ahead` ARE POLITENESS, not subjects (#158). The pilot typed "vai la da o
66
+ #: merge por favor" — as plain an order as the language has — and the head rule refused it because
67
+ #: two words stood in front of the verb. `vai` ALONE is not here and must not be: in Portuguese it
68
+ #: is also the future auxiliary, so "vai descartar tudo" ("that will discard everything") would
69
+ #: become an order to close a pull request. The two-word imperative cannot mean that.
70
+ _LEADER_WORDS = (r"ok|okay|please|por\s+favor|favor|e|and|then|ent[aã]o|agora|now|also|"
71
+ r"tamb[eé]m|depois|a[ií]|beleza|blz|pode|podes|poderia|"
72
+ r"vai\s+l[aá]|v[aá]\s+l[aá]|go\s+ahead")
73
+ _LEADERS = re.compile(rf"^(?:\s*(?:{_LEADER_WORDS})\b[,]?)*\s*$", re.I)
74
+
75
+ #: The same words as a PREFIX, for the bare verb below. One list, two uses: `_BARE` used to allow
76
+ #: exactly "ok," and nothing else, so `please merge` — the button's own word, politely — was
77
+ #: conversation. A second hand-written politeness list is the drift this file is made of.
78
+ #:
79
+ #: NO LEADING `\s*` INSIDE THE LOOP, and this is not style. With `\s*` at BOTH ends of a repeated
80
+ #: group a run of spaces can be eaten by the tail of one iteration or the head of the next — two
81
+ #: ways per space, 2^n paths, and the engine walks all of them before reporting failure. Every
82
+ #: typed message on the panel goes through here, so that is a denial of service on a web-facing
83
+ #: path, in a file whose whole job is to be handed arbitrary text. The trailing `\s*` alone is
84
+ #: unambiguous: each iteration must begin with a word from the list, so nothing competes for the
85
+ #: spaces in front of it.
86
+ _LEAD = rf"(?:(?:{_LEADER_WORDS})\b[,.!]?\s*)*"
87
+
88
+ #: "You may go ahead" — THE PERMISSION FORM OF AN ORDER, in every language this matcher
89
+ #: catalogues (#161, measured on the pilot 2026-08-21).
90
+ #:
91
+ #: THE FILE'S OWN HISTORY IS THE ARGUMENT. The Portuguese half was added the day the pilot typed
92
+ #: it — #153, "pode DAR o merge", with the Merge button on screen — and the English half was not.
93
+ #: Four months later the same pilot typed "you can merge" at the same gate, on a project
94
+ #: configured `en`, and got the identical failure: the sentence fell through to the tech-lead,
95
+ #: which answered with prose and a button asking him to decide a second time. A vocabulary grown
96
+ #: one live failure at a time grows exactly one language at a time.
97
+ #:
98
+ #: THE SUBJECT IS SECOND PERSON, AND IN ENGLISH IT IS REQUIRED. Portuguese drops it, so `pode` is
99
+ #: unambiguous alone; English does not, and "I can merge it" is a person saying they will do it
100
+ #: THEMSELVES — a permission form that accepts a first-person subject reads a withdrawal as a
101
+ #: grant. `we` is out for the same reason: it includes the speaker.
102
+ _MAY = (r"(?:pode|podes|poderia"
103
+ r"|(?:you|y'?all)\s+(?:can|may|could)"
104
+ r")\s+(?:go\s+ahead\s+and\s+)?")
105
+
106
+ #: Portuguese and English, imperative, and deliberately narrow. `merge` is the verb an operator
107
+ #: reaches for in both languages; the polite Brazilian form ("pode fazer o merge") is an
108
+ #: instruction, not a question, and is matched as one — while "posso fazer o merge?" ends in a
109
+ #: question mark and is refused by `_asks_rather_than_tells`.
110
+ #:
111
+ #: THE BARE VERB IS NOT HERE. "merge" alone is the word on the button and still counts — but only
112
+ #: as `_BARE`, anchored to a whole clause, because searched loose it also matched every sentence
113
+ #: that mentions merging: *"o merge falhou ontem"* — a narration — read as an order to do it
114
+ #: again. These patterns are the shapes where the verb is doing the commanding.
115
+ _PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = (
116
+ ("adjust", re.compile(
117
+ # `(?!\()` — A CONVENTIONAL-COMMIT SUBJECT IS NOT AN ORDER (#152). Every ticket in this
118
+ # product is titled `fix(scope): …`, so quoting one back — which is what an operator does
119
+ # when saying WHICH job they mean — read as `adjust`, with the title's own words as the
120
+ # instruction. The verb that commands is followed by a space or a colon, never by a scope.
121
+ r"\b(?:ajust[ae]|corrig[ei]|refaz|refaça|adjust|fix|rework)\b(?!\()"
122
+ r"(?:\s*[#!](?P<ref>\w[\w.-]*))?" # scope only when marked: "ajusta 2 coisas" has no ref
123
+ r"\s*[:,]?\s*(?P<instruction>.+)",
124
+ re.I)),
125
+ # THE REPETITION IS THE VERB (#181). The bare word is not here for the reason `merge`'s is
126
+ # not, and more so: these channels are full of sentences ABOUT reviews — "the review rejected
127
+ # it", "a revisão está vencida", "review: OUT OF DATE" is a line this platform prints itself —
128
+ # and a loose `review` reads every one of them as an order to spend a model pass. What
129
+ # commands is asking for it AGAIN, in both languages, which is also exactly the gesture the
130
+ # gate exists for.
131
+ ("review", re.compile(
132
+ # THE PERMISSION FORM IN BOTH LANGUAGES. `_MAY` leads the clause, so every shape a person
133
+ # may put behind it has to be spelled here — an English "you can re-review it" reaching
134
+ # only the bare alternative below is refused by `_commands_its_clause`, which is how a
135
+ # gesture comes to exist in one language only.
136
+ r"\b(?:" + _MAY + r"(?:(?:revisar|rever|reler)\s+(?:de\s+novo|novamente|outra\s+vez)"
137
+ r"|re-?review|review\s+(?:it\s+|this\s+)?again|re-?run\s+(?:the\s+)?review)"
138
+ r"|re-?review(?:s|ed)?"
139
+ r"|re-?run\s+(?:the\s+|a\s+)?review"
140
+ r"|review\s+(?:it\s+|this\s+|the\s+(?:pr|diff|change)\s+)?again"
141
+ r"|(?:run|do|get|give\s+me)\s+(?:a\s+|one\s+)?(?:fresh|new|another)\s+review"
142
+ r"|(?:a\s+|one\s+)?(?:fresh|new|another)\s+review"
143
+ r"|revis[ae]r?\s+(?:isso\s+|o\s+pr\s+|a\s+mudan[çc]a\s+)?(?:de\s+novo|novamente|outra\s+vez)"
144
+ r"|(?:nova|outra)\s+revis[ãa]o|revis[ãa]o\s+nova"
145
+ r"|(?:roda|rodar|manda|mandar|passa|passar)\s+(?:a\s+)?revis[ãa]o\s+"
146
+ r"(?:de\s+novo|novamente|outra\s+vez)"
147
+ r")" + _REF,
148
+ re.I)),
149
+ ("discard", re.compile(
150
+ # THE PERMISSION FORM TOO, for the reason `_MAY` states: the two gate verbs are the ones a
151
+ # person answers WITH PERMISSION rather than with an imperative, because the platform has
152
+ # just asked them a question. "you can discard it" is the same gesture as "discard it".
153
+ r"\b(?:" + _MAY + r"(?:descartar?|discard)"
154
+ r"|descarta(?:r)?|joga fora|fecha sem (?:o )?merge|discard|drop it|close without)\b"
155
+ + _REF,
156
+ re.I)),
157
+ # STOP IS NARROWER THAN THE OTHERS, and deliberately (#127). It TERMINATES a running job and
158
+ # does not resume — so a false positive costs a run, where a false positive on `merge` costs a
159
+ # pull request somebody was going to land anyway. Only the imperative forms that can mean
160
+ # nothing else: no bare "stop", no "para" (which is the preposition "for" in Portuguese and
161
+ # appears in half of all sentences), and the object must be named.
162
+ ("stop", re.compile(
163
+ # `stop` IS IN THE LIST BECAUSE THE PLATFORM ITSELF TELLS PEOPLE TO TYPE IT. The
164
+ # tech-lead's wedged-job alarm ends with `stop #{ticket}`, and a message dictating a
165
+ # command no matcher accepts is the exact defect the merge gate paid for (#68/#120): the
166
+ # operator types what they were asked to type and nothing happens. Safe here only because
167
+ # the ref below is REQUIRED — bare "stop" stays conversation.
168
+ # `(?<![\w-])` AND NOT `\b`: a hyphen is a word boundary, so `\bstop` matched "non-stop 3"
169
+ # and read it as an order to terminate job 3. For the one verb here that cannot be undone,
170
+ # the left edge has to be a real gap.
171
+ r"(?<![\w-])(?:mata|matar|encerra(?:r)?|aborta(?:r)?|derruba(?:r)?"
172
+ r"|kill|terminate|abort|stop)\s+(?:o\s+|a\s+|the\s+)?(?:job|ticket|card|run)?"
173
+ + _REF_REQUIRED,
174
+ re.I)),
175
+ # RESUME AND SKIP CARRY A REQUIRED REF, like `stop` (#159): they act on a PARKED job, the
176
+ # diagnosis that dictates them always names the ticket, and a bare "resume" typed into a chat
177
+ # is as often a noun as an order. The park card's buttons stay the no-typing path.
178
+ ("resume", re.compile(
179
+ r"(?<![\w-])(?:resume|retom[ae]|retomar)\s+(?:o\s+|a\s+|the\s+)?"
180
+ r"(?:job\s+|ticket\s+|card\s+)?" + _REF_REQUIRED,
181
+ re.I)),
182
+ ("skip", re.compile(
183
+ r"(?<![\w-])(?:skip|pul[ae]|pular)\s+(?:o\s+|a\s+|the\s+)?"
184
+ r"(?:job\s+|ticket\s+|card\s+)?" + _REF_REQUIRED,
185
+ re.I)),
186
+ ("merge", re.compile(
187
+ # THE POLITE FORMS ARE THE COMMON ONES, and the list was one verb wide (#153). The pilot
188
+ # typed "pode DAR o merge" — a merge gate on screen, a decision made in words — and it fell
189
+ # through to the tech-lead, which answered with an opinion. That is #120's defect exactly,
190
+ # surviving in a verb nobody had thought of.
191
+ r"\b(?:" + _MAY + r"(?:fazer|dar|mandar|subir|integrar)?\s*(?:o\s+)?merge"
192
+ r"|(?:faz|faça|manda|sobe|integra|d[aá])\s+(?:o\s+)?merge"
193
+ r"|merge it|go ahead and merge|ship it|land it)"
194
+ # …but never when the sentence is ABOUT the merge machinery: "pode fazer o merge policy
195
+ # ser auto" carries no question mark and would otherwise land a pull request while
196
+ # somebody was discussing configuration.
197
+ r"(?!\s*[-_]?\s*(?:polic|gate|queue|commit|conflict|strateg|method))"
198
+ + _REF,
199
+ re.I)),
200
+ )
201
+
202
+ #: A clause that IS the verb — the word on the button, typed back: `merge`, `ok, merge #87`,
203
+ #: `mergeia`. Anchored to the whole clause, so a clause with anything else in it is conversation.
204
+ _BARE: tuple[tuple[str, re.Pattern[str]], ...] = (
205
+ ("discard", re.compile(
206
+ r"^\s*" + _LEAD + r"(?:descarta|discard)" + _REF + r"\s*[.!]?\s*$", re.I)),
207
+ ("merge", re.compile(
208
+ r"^\s*" + _LEAD + r"merge(?:ia|ar|a)?" + _REF + r"\s*[.!]?\s*$", re.I)),
209
+ )
210
+
211
+ #: What flips or holds an instruction. A NEGATED ORDER IS THE OPPOSITE ORDER (sweep, 2026-08-16):
212
+ #: "não faça o merge", "do not merge yet", "segura o merge" and "espera, não mergeia" all matched
213
+ #: `merge` and — with one PR waiting — would have MERGED, the human gate inverted by the exact
214
+ #: sentence that exercises it. The list holds negators and hold-verbs in both languages; it is
215
+ #: checked only within the verb's own clause, so "a espera acabou, faz o merge" still instructs.
216
+ _NEGATORS = re.compile(
217
+ r"\b(?:n[aã]o|nunca|jamais|nem|never|don'?t|do\s+not|hold(?:\s+off)?|wait|stop"
218
+ r"|segur[ae]|esper[ae]|aguard[ae]|adi[ae]|deixa)\b", re.I)
219
+
220
+ #: Clause boundaries. The comma is one on purpose: it is what separates "espera," from "não
221
+ #: mergeia" — and "ok," from "merge", which must keep working.
222
+ _CLAUSES = re.compile(r"[.!?;\n,]")
223
+
224
+
225
+
226
+
227
+ def _clause_start(body: str, start: int) -> int:
228
+ last = 0
229
+ for boundary in _CLAUSES.finditer(body, 0, start):
230
+ last = boundary.end()
231
+ return last
232
+
233
+
234
+ def _negated_before(body: str, start: int) -> bool:
235
+ """Is there a negator or hold-verb between the verb and the start of its own clause?"""
236
+ return bool(_NEGATORS.search(body, _clause_start(body, start), start))
237
+
238
+
239
+ #: A clause that is NOTHING BUT A HOLD — "wait", "hold off", "segura", "espera aí". It holds
240
+ #: whatever instruction sits in a later clause (product owner, 2026-08-21: *"hold e wait têm que
241
+ #: segurar"*).
242
+ #:
243
+ #: MEASURED, NOT SUSPECTED. `_NEGATORS` already carried `wait`, `hold`, `segura` and `espera` for
244
+ #: exactly this, and `_negated_before` searches only from the verb back to ITS OWN clause start —
245
+ #: so the comma that makes "hold off, merge it" two clauses put the hold out of reach:
246
+ #:
247
+ #: 'hold off, merge it' -> merge ← the gate inverted by the sentence
248
+ #: 'wait, discard it' -> discard that was typed to stop it
249
+ #: 'segura, pode fazer o merge' -> merge
250
+ #:
251
+ #: AND THE COUNTEREXAMPLE THE FILE ALREADY NAMES SURVIVES, which is why this is a shape and not a
252
+ #: word search: "a espera acabou, faz o merge" says the wait is OVER. Its first clause is not a
253
+ #: hold instruction — it is a sentence containing the noun — so it does not match, and the merge
254
+ #: still lands. A blanket search for the word would have refused it.
255
+ #:
256
+ #: WHAT THE CLAUSE OPENS WITH IS THE WHOLE TEST, and the tail is deliberately unbounded. The
257
+ #: first version stopped at a short list of particles ("wait a second", "espera aí") on the theory
258
+ #: that anything longer is prose ABOUT waiting — and "wait for CI to finish, then merge it" landed
259
+ #: the pull request. At a gate whose entire purpose is to hold, the safe direction is to hold: a
260
+ #: refusal costs one more sentence, a wrong merge costs the pull request.
261
+ #:
262
+ #: It still cannot swallow the counterexample, because that one does not OPEN with the verb: "a
263
+ #: espera acabou" starts with an article and "esperei o suficiente" is a different word. A clause
264
+ #: that begins by telling somebody to wait is telling them to wait.
265
+ _BARE_HOLD = re.compile(
266
+ r"^\s*(?:(?:" + _LEADER_WORDS + r")\b[,.!]?\s*)*"
267
+ r"(?:hold(?:\s+on|\s+off)?|wait|hang\s+on|hold\s+your\s+horses"
268
+ r"|segur[ae]|esper[ae]|aguard[ae]|calma|per[ae]\s*a[íi]|pera)\b"
269
+ r"(?:\s+\S.*)?$",
270
+ re.I | re.DOTALL)
271
+
272
+
273
+ #: A clause that is JUST a refusal — "não", "never", "nope". Same shape as the hold and the same
274
+ #: risk: it is the whole clause, so nothing inside the instruction's own clause carries it.
275
+ #: `no` is NOT here and cannot be: it is also the Portuguese contraction em+o, and the collision
276
+ #: discipline this codebase settled on (#157) says a word enters a language table only if, as a
277
+ #: complete message, it can mean nothing else in EVERY catalogued language.
278
+ _BARE_REFUSAL = re.compile(
279
+ r"^\s*(?:(?:" + _LEADER_WORDS + r")\b[,.!]?\s*)*"
280
+ r"(?:n[ãa]o|nunca|jamais|never|nope|nem pensar|de jeito nenhum)"
281
+ r"\s*[.!]?\s*$",
282
+ re.I)
283
+
284
+
285
+ def _held_by_another_clause(body: str, start: int, end: int) -> bool:
286
+ """Does a clause of its own tell this instruction to wait, or refuse it outright?
287
+
288
+ BOTH SIDES, because a person retracts in both directions: "wait, merge it" and "merge it —
289
+ wait" are the same change of mind. The hold has to OPEN its own clause; a hold verb arriving
290
+ later in a clause is prose about waiting ("a espera acabou" — the wait is over), and reading
291
+ that as a hold would refuse the sentence it was written to permit.
292
+ """
293
+ head = body[:_clause_start(body, start)]
294
+ tail = body[end:]
295
+ for chunk in (head, tail):
296
+ for clause in _CLAUSES.split(chunk):
297
+ if clause.strip() and (_BARE_HOLD.match(clause) or _BARE_REFUSAL.match(clause)):
298
+ return True
299
+ return False
300
+
301
+
302
+ def _commands_its_clause(body: str, start: int) -> bool:
303
+ """Is this verb DOING the commanding, or is it a word inside a sentence about it?
304
+
305
+ THE DEFECT THIS ENDS (#152, measured on the pilot). Every pattern here was `search`ed across
306
+ the whole message, so a verb anywhere in prose was an order. The operator pasted a 586-character
307
+ review instruction into the tech-lead chat; it contained the words "a real type fix" near the
308
+ end; that fired `adjust`, whose `(?P<instruction>.+)` then captured the twenty-six characters
309
+ that happened to follow — and the platform spent a full agent pass on them, silently. A sweep
310
+ of the other rows found the same shape everywhere: *"the discard button is confusing"* closed a
311
+ pull request, *"I would drop it if the review were worse"* closed one, and *"stop #101 was
312
+ suggested by the tech-lead earlier"* terminated a running job — a narration QUOTING the command
313
+ the tech-lead itself dictates.
314
+
315
+ An imperative opens its clause. That is the whole rule, and it is the one `_BARE` below already
316
+ states for the bare verb ("a clause with anything more in it is conversation"); it simply was
317
+ never applied to the verbs that carry an object.
318
+ """
319
+ return bool(_LEADERS.match(body[_clause_start(body, start):start]))
320
+
321
+
322
+ def _fills_its_clause(body: str, end: int) -> bool:
323
+ """Does the command leave its clause, or does the sentence carry on past it?
324
+
325
+ "descarta esse" is an instruction with a pronoun trailing it; "descartar seria um desperdício"
326
+ is somebody thinking out loud, and the difference a reader hears is that the sentence keeps
327
+ going. One trailing word is the allowance — enough for the demonstrative that follows a real
328
+ order, and not enough for a predicate.
329
+
330
+ IT COSTS `adjust` NOTHING, which is why there is no exemption for it: that pattern's
331
+ `(?P<instruction>.+)` runs to the end of the line, and a newline is a clause boundary — so
332
+ the match already fills its clause by construction. A special case here would have been a
333
+ branch that can never be taken, and the mutation proving it was one is what removed it.
334
+ """
335
+ clause_end = len(body)
336
+ match = _CLAUSES.search(body, end)
337
+ if match:
338
+ clause_end = match.start()
339
+ # POLITENESS TRAILS AS WELL AS LEADS (#158). "vai la da o merge POR FAVOR" is an order with two
340
+ # words of courtesy after it, and counting them refused the clearest instruction of the day.
341
+ rest = re.sub(rf"\b(?:{_LEADER_WORDS}|obrigad[oa]|thanks|thank\s+you)\b", " ",
342
+ body[end:clause_end], flags=re.I)
343
+ return len(rest.split()) <= 1
344
+
345
+
346
+ def _accept(name: str, m: re.Match[str], body: str) -> tuple[str, dict] | None:
347
+ if _asks_rather_than_tells(body, m.end()):
348
+ return None
349
+ if _negated_before(body, m.start()):
350
+ return None
351
+ if _held_by_another_clause(body, m.start(), m.end()):
352
+ return None
353
+ if not _commands_its_clause(body, m.start()):
354
+ return None
355
+ if not _fills_its_clause(body, m.end()):
356
+ return None
357
+ captures = {k: (v or "").strip() for k, v in (m.groupdict() or {}).items() if v}
358
+ if name == "adjust" and len(captures.get("instruction", "")) < 4:
359
+ return None # "ajusta" alone is not an instruction anybody can carry out
360
+ return name, captures
361
+
362
+
363
+ def is_affirmation(text: str) -> bool:
364
+ """Is this message a plain YES and nothing else? (#156)
365
+
366
+ MEASURED ON THE PILOT. The tech-lead ended an answer staging `merge #101`; the operator typed
367
+ "pode seguir"; nothing happened, because a staged proposal could only be accepted by PRESSING
368
+ it. From his side the factory asked a yes/no question, was told yes, and changed the subject.
369
+
370
+ THE VOCABULARY MOVED (#161) to `openfactory/language/assent.py`, and this is the mapping onto
371
+ it. It moved because three surfaces carried three hand-rolled yes-lists that had already
372
+ drifted — `certo` asserted approval on one, merely accompanied it on another, and was audited
373
+ out here. A table nobody can compare is a table that disagrees with itself.
374
+
375
+ THE QUESTION TEST STAYS HERE. "pode?" is asking permission, not granting it, and it is refused
376
+ by the same `_asks_rather_than_tells` every other gesture in this file goes through — a second
377
+ notion of "this is a question" is how two surfaces come to disagree about one sentence.
378
+ """
379
+ from openfactory.language import assent
380
+
381
+ body = (text or "").strip()
382
+ if not body or not assent.is_bare_assent(body):
383
+ return False
384
+ # FROM THE END OF THE ASSENT, not the end of the message: `_asks_rather_than_tells` reads the
385
+ # terminator that FOLLOWS the gesture, so handing it `len(body)` gave it nothing to read.
386
+ stripped = body.rstrip(".!?…").strip()
387
+ return not _asks_rather_than_tells(body, len(stripped))
388
+
389
+
390
+ def match_floor_intent(text: str) -> tuple[str, dict] | None:
391
+ """`(intent, captures)` for a sentence that INSTRUCTS, or None — and None is the common answer.
392
+
393
+ `captures` may carry `ref` — the ticket the sentence itself names — and the caller is bound by
394
+ it: acting on a different job than the one named is worse than refusing.
395
+
396
+ Ordered `adjust` first because it swallows the rest of the sentence into an instruction, and
397
+ an operator who writes "ajusta o merge para depois do CI" is asking for a repair pass, not a
398
+ merge. A message that is a question about any of these falls through untouched."""
399
+ body = text or ""
400
+ for name, pattern in _PATTERNS:
401
+ m = pattern.search(body)
402
+ if not m:
403
+ continue
404
+ hit = _accept(name, m, body)
405
+ if hit:
406
+ # A rejected candidate FALLS THROUGH to the next pattern rather than ending the
407
+ # search: "vale a pena mergear? descarta esse" is a question followed by an
408
+ # instruction, and reading the whole message would throw the instruction away with it
409
+ # (the rule the product matcher pays for by name).
410
+ return hit
411
+ # The bare verb, clause by clause: the word on the button typed back is an instruction, and a
412
+ # clause with anything more in it is conversation about one.
413
+ at = 0
414
+ for chunk in _CLAUSES.split(body):
415
+ for name, pattern in _BARE:
416
+ m = pattern.match(chunk)
417
+ if not m:
418
+ continue
419
+ # THE SAME FENCES AS THE ROWS ABOVE (2026-08-21). This path had only the question
420
+ # test, because a clause-anchored verb cannot carry a negator INSIDE its own clause —
421
+ # true, and beside the point: the hold lives in a DIFFERENT clause. Measured,
422
+ # "espera o CI ficar verde, ai mergeia" landed the pull request through here while
423
+ # the identical sentence with a carried object was correctly held one loop up.
424
+ if _asks_rather_than_tells(body, min(at + m.end(), len(body))):
425
+ continue
426
+ if _held_by_another_clause(body, at + m.start(), min(at + m.end(), len(body))):
427
+ continue
428
+ return name, {k: (v or "").strip()
429
+ for k, v in (m.groupdict() or {}).items() if v}
430
+ at += len(chunk) + 1
431
+ return None
File without changes
@@ -0,0 +1,71 @@
1
+ from openfactory.adapters.agent.base import (
2
+ AgentContext,
3
+ CodingAgentAdapter,
4
+ JudgmentAgentAdapter,
5
+ final_text,
6
+ )
7
+ from openfactory.adapters.agent.registry import (
8
+ DEFAULT_KIND,
9
+ HARNESSES,
10
+ ROLE_MODELS,
11
+ ROLES,
12
+ addon_role,
13
+ build_asker,
14
+ build_executor,
15
+ build_product,
16
+ build_reviewer,
17
+ build_techlead,
18
+ harness_kind,
19
+ known_roles,
20
+ model_for,
21
+ )
22
+ from openfactory.adapters.agent.roles import RoleSpec
23
+
24
+ __all__ = [
25
+ "final_text",
26
+ "DEFAULT_KIND",
27
+ "HARNESSES",
28
+ "ROLES",
29
+ "ROLE_MODELS",
30
+ "RoleSpec",
31
+ "addon_role",
32
+ "known_roles",
33
+ "model_for",
34
+ "AgentContext",
35
+ "ClaudeCodeAdapter",
36
+ "CodingAgentAdapter",
37
+ "JudgmentAgentAdapter",
38
+ "build_asker",
39
+ "build_executor",
40
+ "build_product",
41
+ "build_reviewer",
42
+ "build_techlead",
43
+ "harness_kind",
44
+ ]
45
+
46
+ _LAZY = {
47
+ 'ClaudeCodeAdapter': 'openfactory.adapters.agent.claude_code',
48
+ }
49
+
50
+ # LAZY, and this is a contract rather than a micro-optimisation.
51
+ #
52
+ # These lines used to be plain imports, so importing the PORT loaded every implementation of it —
53
+ # `from openfactory.adapters.notify.base import Notifier` pulled the Slack adapter and, through it,
54
+ # httpx.
55
+ # Seven of the nine port packages did this. It meant the Core could not be installed without every
56
+ # vendor library, a plugin could not load one provider without all of them, and ADR-0022's claim
57
+ # that "nothing outside a provider's package names a concrete class" was true of the call sites and
58
+ # false of the package that holds them.
59
+ #
60
+ # PEP 562: the names still resolve exactly as before, on first use rather than on import.
61
+ def __getattr__(name: str):
62
+ import importlib
63
+
64
+ module = _LAZY.get(name)
65
+ if module is None:
66
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
67
+ return getattr(importlib.import_module(module), name)
68
+
69
+
70
+ def __dir__():
71
+ return sorted(__all__)