@r1z4x/tezgah 1.6.0 → 1.7.0
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.
- package/CHANGELOG.md +80 -1
- package/MANIFEST +5 -0
- package/README.md +6 -3
- package/bin/tezgah-docs +18 -16
- package/bin/tezgah-doctor +152 -0
- package/bin/tezgah-research +234 -10
- package/bin/tezgah-route +48 -1
- package/bin/tezgah-setup +26 -13
- package/bin/tezgah-task +589 -17
- package/bin/tezgah-taste +48 -10
- package/docs/architecture.md +7 -4
- package/docs/clarity-baseline.json +2 -2
- package/docs/contract.md +108 -24
- package/docs/evidence.md +201 -28
- package/docs/feature-audit.md +1 -1
- package/docs/gate.md +60 -22
- package/docs/glossary.md +4 -4
- package/docs/hosts.md +8 -8
- package/docs/index.json +11 -5
- package/docs/judge.md +9 -3
- package/docs/models.md +35 -2
- package/docs/operations.md +103 -3
- package/docs/research.md +346 -13
- package/docs/skills.md +1 -1
- package/docs/status-line.md +5 -5
- package/docs/testing.md +2 -2
- package/docs/vision.md +12 -4
- package/hooks/hooks.json +2 -2
- package/hooks/lang/en.json +323 -0
- package/hooks/lang/tr.json +344 -0
- package/hooks/projects-posttooluse.py +24 -5
- package/hooks/projects-pretooluse.py +9 -4
- package/hooks/projects-stop.py +28 -16
- package/hooks/tezgah_agents.py +29 -10
- package/hooks/tezgah_clarity.py +7 -3
- package/hooks/tezgah_context.py +425 -406
- package/hooks/tezgah_gate.py +583 -33
- package/hooks/tezgah_integrity.py +1480 -197
- package/hooks/tezgah_models.py +337 -15
- package/hooks/tezgah_paths.py +103 -10
- package/hooks/tezgah_policy.py +258 -126
- package/hooks/tezgah_rank.py +29 -27
- package/hooks/tezgah_replay.py +200 -30
- package/hooks/tezgah_research.py +1294 -9
- package/hooks/tezgah_stopspec.py +7 -0
- package/hooks/tezgah_store.py +74 -27
- package/hooks/tezgah_task.py +915 -6
- package/hosts/codex/hook.py +9 -10
- package/hosts/cursor/hook.py +21 -14
- package/hosts/dsh/hooks.json +2 -2
- package/hosts/omp/hook.py +44 -23
- package/hosts/omp/tezgah-hook.ts.in +93 -7
- package/hosts/opencode/plugins/tezgah.js +102 -2
- package/output-styles/tezgah.md +76 -42
- package/package.json +1 -1
- package/skills/i-have-adhd/EVALS.md +22 -2
- package/skills/i-have-adhd/SKILL.md +12 -5
- package/skills/plan-add/SKILL.md +5 -3
- package/skills/plan-sync/SKILL.md +5 -3
- package/skills/ponytail/EVALS.md +21 -2
- package/skills/ponytail/SKILL.md +4 -4
- package/skills/research/SKILL.md +89 -1
- package/skills/tezgah-contract/EVALS.md +52 -7
- package/skills/tezgah-contract/SKILL.md +151 -56
package/hooks/tezgah_context.py
CHANGED
|
@@ -19,20 +19,21 @@ import tezgah_orca
|
|
|
19
19
|
import tezgah_research
|
|
20
20
|
import tezgah_store
|
|
21
21
|
from tezgah_guard import import_crash_key
|
|
22
|
-
from tezgah_integrity import (STEP_KINDS, _heredocs, _path as _ledger_path,
|
|
22
|
+
from tezgah_integrity import (GRANT_KIND, STEP_KINDS, _heredocs, _path as _ledger_path,
|
|
23
23
|
_shell_segments, bind_session,
|
|
24
24
|
changed_files, cut, last_check, note,
|
|
25
|
-
note_compaction, note_turn,
|
|
26
|
-
UNTRUSTED_CHANNEL)
|
|
25
|
+
note_compaction, note_turn, outward_classes, redact,
|
|
26
|
+
scratch_evidence, UNTRUSTED_CHANNEL)
|
|
27
27
|
from tezgah_lessons import (lesson_key, lines as lesson_lines,
|
|
28
28
|
tainted as tainted_lessons)
|
|
29
29
|
from tezgah_policy import (CONDITIONAL_KEYS, CORE, FANOUT_ACTION, POINTERS,
|
|
30
|
-
PROMPT_REMINDER,
|
|
30
|
+
PROMPT_REMINDER, REPLY_ANY, open_lines_note,
|
|
31
31
|
pony_level_line)
|
|
32
|
-
from tezgah_paths import (CACHE, REPO_MARKS, SWITCHES,
|
|
32
|
+
from tezgah_paths import (CACHE, CONTEXT_NUDGE_TOKENS, REPO_MARKS, SWITCHES,
|
|
33
|
+
ai_research_dir, alts, armed,
|
|
33
34
|
cache_dir, codegraph_bin, consult_options,
|
|
34
|
-
ensure_workspace, fallback_cache, have_judge_key,
|
|
35
|
-
orx_bin,
|
|
35
|
+
ensure_workspace, fallback_cache, have_judge_key, language,
|
|
36
|
+
off, orx_bin, packs, pony_level, root_for, roots, say, tool,
|
|
36
37
|
workspace, workspace_from_repo, worktrees, writable_dir)
|
|
37
38
|
|
|
38
39
|
try: # The task record is the active plan's frontmatter (see tezgah_task), read
|
|
@@ -55,325 +56,95 @@ except ImportError: # pragma: no cover - only where the module has not landed
|
|
|
55
56
|
|
|
56
57
|
# A prompt that matches one of these arms the matching conditional rule for that
|
|
57
58
|
# turn only. Kept as (key, compiled regex) so the arming is one pass and the
|
|
58
|
-
# patterns are reviewable.
|
|
59
|
-
#
|
|
59
|
+
# patterns are reviewable. The words are data: each language pack
|
|
60
|
+
# (hooks/lang/<code>.json, `hints.<key>`) lists its own half, and `_hints`
|
|
61
|
+
# joins every pack's into one pattern per key, so a prompt in any shipped
|
|
62
|
+
# language arms the same rules. Word-ish boundaries keep "deploy" from firing
|
|
63
|
+
# inside an identifier.
|
|
60
64
|
#
|
|
61
|
-
# Turkish is agglutinative and
|
|
62
|
-
# trailing \b would kill the most natural phrasing (düzgün
|
|
63
|
-
# test et): a
|
|
64
|
-
# which \b then closes. \w is Unicode-aware
|
|
65
|
-
#
|
|
65
|
+
# Turkish is agglutinative and its hints are verb/noun stems, so an
|
|
66
|
+
# English-style trailing \b would kill the most natural phrasing (düzgün
|
|
67
|
+
# çalışsın, hipotezi test et): such a fragment carries \w* where a suffix can
|
|
68
|
+
# land, which \b then closes. \w is Unicode-aware. A whole word keeps its plain
|
|
69
|
+
# boundary, so "deney" still does not fire on "deneyim".
|
|
66
70
|
#
|
|
67
|
-
# The craft words in the spec class that are ALSO ordinary
|
|
68
|
-
#
|
|
69
|
-
# paragraph on asks that had no UI in them at all.
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"""`word` as a UI ask: a UI object beside it, or a quality word after it.
|
|
88
|
-
|
|
89
|
-
The gap is bounded and stops at a sentence's end, so the qualification is
|
|
90
|
-
about the phrase the word sits in and not about the paragraph it is in."""
|
|
91
|
-
obj = r"\b(?:%s)\b" % UI_OBJECTS
|
|
71
|
+
# The craft words in the spec class that are ALSO ordinary nouns are qualified
|
|
72
|
+
# by a UI object rather than matched bare (a pack fragment `{"ui_ask": word}`):
|
|
73
|
+
# the bare word armed the paragraph on asks that had no UI in them at all.
|
|
74
|
+
# `_ui_ask` expands one in the pack's own word order - a `verb_first` pack (en)
|
|
75
|
+
# takes a UI object beside the word or a quality word after it, an
|
|
76
|
+
# `object_first` pack (tr: "adım akışını sadeleştir" is a UI ask, "bu
|
|
77
|
+
# fonksiyonu sadeleştir" a refactor) only an object before it - and both halves
|
|
78
|
+
# are pinned in tests/test_hint_coverage.py. Every object is word-bounded, so
|
|
79
|
+
# `app` cannot match inside `happen`.
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _ui_ask(word, pack):
|
|
83
|
+
"""`word` as a UI ask in `pack`'s word order. The gap is bounded and stops
|
|
84
|
+
at a sentence's end, so the qualification is about the phrase the word sits
|
|
85
|
+
in and not about the paragraph it is in."""
|
|
86
|
+
objects = "|".join(pack["ui_objects"]) or "(?!)"
|
|
87
|
+
if pack["word_order"] == "object_first":
|
|
88
|
+
# the object takes its case suffix through \w*
|
|
89
|
+
return r"(?:\b(?:%s)\w*[^.!?\n]{0,40}\b(?:%s))" % (objects, word)
|
|
90
|
+
obj = r"\b(?:%s)\b" % objects
|
|
92
91
|
return (r"(?:%s[^.!?\n]{0,40}\b(?:%s)\b"
|
|
93
92
|
r"|\b(?:%s)\b(?=[^.!?\n]{0,40}(?:%s|\b(?:%s))))"
|
|
94
|
-
% (obj, word, word, obj,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def
|
|
105
|
-
"""
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
("
|
|
127
|
-
r"
|
|
128
|
-
r"
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
# game", "focus the terminal window" - and the same held for the six
|
|
150
|
-
# siblings below: "our margins are down this quarter", "the theme of
|
|
151
|
-
# the meeting", "the badges in the README", "scaffold the skeleton of
|
|
152
|
-
# the parser", "cluttered imports in the module", "the data center".
|
|
153
|
-
# The UI asks still arm it through `component\w*`, `ui`, `layout`,
|
|
154
|
-
# `spacing`, `hover`, `focus durum\w*`, `hizala\w*`, `renk\w*` and
|
|
155
|
-
# the rest (tests/test_hint_coverage.py holds both halves).
|
|
156
|
-
r"refactor\w* (the |this |our )?(\w+ ){0,2}(components?|ui|ux|css|"
|
|
157
|
-
r"layout|screens?|pages?|styles?|theme|forms?|fields?|inputs?|"
|
|
158
|
-
r"views?|widgets?|buttons?|modals?|dialogs?)|"
|
|
159
|
-
r"ui|ux|css|tailwind|tasarım\w*|layout|responsive|"
|
|
160
|
-
r"hover|focus[- ](?:state|ring|outline|visible|indicator|style|"
|
|
161
|
-
r"durum\w*)|spacing|tipografi\w*|typography|component\w*|"
|
|
162
|
-
r"bileşen\w*|dark mode|dark theme|light theme|"
|
|
163
|
-
+ _ui_ask(r"theme\w*") + r"|tema\w*|animasyon\w*|animations?|"
|
|
164
|
-
r"erişilebilir\w*|accessib\w*|wireframe|mockup|prototip\w*|"
|
|
165
|
-
# The craft, and the surfaces a person names without the word
|
|
166
|
-
# component. Kept off the product class's surface nouns on purpose
|
|
167
|
-
# (tablo, filtre, ekran stay there) so an ordinary surface ask still
|
|
168
|
-
# pays one paragraph, and off bare `table`/`filter` so that pin holds.
|
|
169
|
-
# `align`/`alignment`/`misalign` are one family with the property
|
|
170
|
-
# spellings: `align-?items` allowed only a hyphen between the two
|
|
171
|
-
# words, so "align items in the header" armed nothing and neither
|
|
172
|
-
# adjective form was a hint at all. The nouns read bare the way
|
|
173
|
-
# `spacing` does - qualifying them by a UI object would drop "the
|
|
174
|
-
# alignment is off", the ask that named this defect - and the
|
|
175
|
-
# non-UI phrasings stay dead in the corpus ("align the two arrays",
|
|
176
|
-
# "align the teams on the roadmap").
|
|
177
|
-
r"align[- ]?(?:items|self|content)|text-align|alignment\w*|"
|
|
178
|
-
r"misalign\w*|hizala\w*|hiza\w*|"
|
|
179
|
-
r"padding|" + _ui_ask(r"cent(er|re)\w*") + r"|"
|
|
180
|
-
+ _ui_ask(r"margins?") + r"|"
|
|
181
|
-
r"gutter\w*|boşluk\w*|satır aral\w*|line-?height|letter-?spacing|"
|
|
182
|
-
r"drop-?shadow|box-?shadow|border-?radius|corner radius|gölge\w*|"
|
|
183
|
-
r"kenarlık\w*|fonts?[- ](?:size|family|weight|face|stack|scale)\w*|"
|
|
184
|
-
r"yazı tipi\w*|punto\w*|type ?scale|"
|
|
185
|
-
r"colou?r[- ](?:role|scheme|palette|token|contrast)|palette|"
|
|
186
|
-
r"renk\w*|palet\w*|"
|
|
187
|
-
r"cards?[- ](?:component\w*|layout|grid|list|view|title|body|"
|
|
188
|
-
r"footer|header)|sidebars?|toolbars?|modals?|dialogs?|dropdowns?|"
|
|
189
|
-
r"popovers?|accordions?|tooltips?|toasts?|avatars?|"
|
|
190
|
-
+ _ui_ask(r"badges?") + r"|"
|
|
191
|
-
# pagination is qualified like the craft nouns above: "add
|
|
192
|
-
# pagination to the API" is an endpoint, "the data table pagination
|
|
193
|
-
# is wrong" a rendered surface
|
|
194
|
-
r"breadcrumbs?|" + _ui_ask(r"pagination") + r"|tab ?bar|steppers?|"
|
|
195
|
-
r"spinners?|"
|
|
196
|
-
+ _ui_ask(r"skeletons?") + r"|"
|
|
197
|
-
r"disabled\w*|devre dışı\w*|pressed|active state|selected state|"
|
|
198
|
-
r"empty state|boş durum\w*|breakpoints?\w*|kırılma nokta\w*|"
|
|
199
|
-
r"viewport|media quer\w*|mobil (uyumlu|görünüm)|karanlık mod\w*|"
|
|
200
|
-
r"aydınlık mod\w*|renk şema\w*|design (system|token|guide|library|"
|
|
201
|
-
r"language)\w*|style guides?|stil rehber\w*|tasarım sistemi|"
|
|
202
|
-
r"tasarım token\w*|wcag\w*|a11y|aria-?\w*|screen readers?|"
|
|
203
|
-
r"ekran okuyucu\w*|tab order|keyboard naviga\w*|kontrast\w*|"
|
|
204
|
-
r"contrast (ratio|level|check|issue)|screenshots?|"
|
|
205
|
-
r"ekran görüntü\w*|visual (review|regression|diff|check)|"
|
|
206
|
-
r"görsel (incele|kontrol|karşılaştır)\w*|redesign\w*|revamp\w*|"
|
|
207
|
-
r"restyle\w*|makeover|" + _ui_ask(r"cluttered")
|
|
208
|
-
+ r"|outdated|cohesive)\b"),
|
|
209
|
-
("consult", r"\b(architect(ure|ural)|root cause|migrat(e|ion)|deploy|"
|
|
210
|
-
r"security|trade-?off|which approach|design decision|"
|
|
211
|
-
r"irreversible|rollback|schema change|mimari\w*|kök neden\w*|"
|
|
212
|
-
r"geri dönüşü olmayan|"
|
|
213
|
-
# The durable-structure asks the rule's own trigger list names,
|
|
214
|
-
# which reached no rule: a rewrite of a unit, a contract others
|
|
215
|
-
# inherit, a live-data move, a budget or a hard-to-undo call.
|
|
216
|
-
# `refactor` is qualified by its object rather than left bare, so
|
|
217
|
-
# a UI refactor still pays the spec paragraph alone; `güvenlik`,
|
|
218
|
-
# `şema` and `göç` are the Turkish halves of security/schema/
|
|
219
|
-
# migration, which the rule text already named in English only.
|
|
220
|
-
r"re-?architect\w*|restructur\w*|rewrite\w*|"
|
|
221
|
-
r"(refactor\w*|rewrite\w*) (the |this |our )?(\w+ ){0,2}"
|
|
222
|
-
r"(module|subsystem|service|system|layer|engine|architecture|"
|
|
223
|
-
r"data ?model|code ?base|ingest\w*)|"
|
|
224
|
-
r"breaking change\w*|backwards?[- ]incompatib\w*|"
|
|
225
|
-
r"kırıcı değişiklik\w*|geriye dönük uyum\w*|"
|
|
226
|
-
r"api version\w*|version(ing)? the api|api sürüm\w*|"
|
|
227
|
-
r"sürümleme\w*|versiyonlama\w*|backfill\w*|şema\w*|göç\w*|"
|
|
228
|
-
r"migrasyon\w*|veri (taşı|göç)\w*|revert\w*|rollback|"
|
|
229
|
-
r"geri al\w*|geri (alınamaz|dönüşü (yok|olmaz|olmayan))|"
|
|
230
|
-
r"performance budget|perf budget|latency budget|hot ?path|"
|
|
231
|
-
r"performans bütçe\w*|gecikme bütçe\w*|threat model\w*|"
|
|
232
|
-
r"security review\w*|güvenlik\w*|zafiyet\w*|cve\b|"
|
|
233
|
-
r"threat model\w*|bağımlılık(ları)? güncelle\w*|"
|
|
234
|
-
r"paket güncelle\w*|sürüm yükselt\w*|tedarik zincir\w*|"
|
|
235
|
-
r"concurren\w*|race condition\w*|deadlock\w*|eş ?zamanlı\w*|"
|
|
236
|
-
r"yarış (durumu|koşulu)\w*|capacit\w* (plan|budget|test|limit)\w*|"
|
|
237
|
-
r"scal(e|ing) (plan|strategy|limit|test)\w*|load test\w*|"
|
|
238
|
-
r"kapasite (plan|hesab|test|sınır)\w*|ölçeklen\w*|yük test\w*|"
|
|
239
|
-
r"adrs?\b|architecture decision record\w*|design doc\w*|"
|
|
240
|
-
r"mimari karar\w*|tasarım karar\w*|politika değiş\w*|"
|
|
241
|
-
r"sözleşme (değiş|yenile)\w*|yayına al\w*|canlıya al\w*|"
|
|
242
|
-
r"sürüm çıkar\w*|rollout|ödün (ver|vermey)\w*)\b"),
|
|
243
|
-
("research", r"\b(research|literature|hypothes(is|es)|experiment(al)?|"
|
|
244
|
-
r"ablation|hyperparameter|benchmark|survey|paper|dataset|"
|
|
245
|
-
r"araştır\w*|literatür\w*|hipotez\w*|deney|"
|
|
246
|
-
# The study asks the rule's own opening names - a comparison of
|
|
247
|
-
# variants under a metric, a study with people, a claim that
|
|
248
|
-
# needs a number - which reached no rule. `compar` is qualified
|
|
249
|
-
# nowhere on purpose: a file diff asks the same question of the
|
|
250
|
-
# evidence, so the turn pays the paragraph and says so.
|
|
251
|
-
r"compar\w*|karşılaştır\w*|karşılaştırmalı analiz|kıyaslama\w*|"
|
|
252
|
-
r"varyant\w*|(a/?b|ab) tests?|a/b testi|"
|
|
253
|
-
r"(user|kullanıcı|müşteri) (interview\w*|görüşme\w*|mülakat\w*)|"
|
|
254
|
-
r"anket\w*|root[ -]?cause (analys|investigat)\w*|"
|
|
255
|
-
r"kök neden (analiz|araştır)\w*|reference review|"
|
|
256
|
-
r"lit(erature)?[ -]review|kaynak tarama\w*|referans tarama\w*|"
|
|
257
|
-
r"evaluat(e|ing) (whether|the (librar|technique|approach|tool|"
|
|
258
|
-
r"option|alternative))|(yöntem|yaklaşım|kütüphane|varyant|"
|
|
259
|
-
r"seçenek)\w* karşılaştır\w*|"
|
|
260
|
-
# a timing ask is not a study: "measure how long the hook
|
|
261
|
-
# takes" is a stopwatch, "measure the effect of X" a study
|
|
262
|
-
r"measure(?! how (?:long|fast|much time)| the (?:time|duration|"
|
|
263
|
-
r"latency|speed|runtime))\w*|ölçüm\w*|deneysel\w*|"
|
|
264
|
-
r"deneyler\w*|makale\w*|veri (seti|kümesi)|post[ -]?mortem|"
|
|
265
|
-
r"error budget|incident (review|report)|olay sonrası (analiz|"
|
|
266
|
-
r"değerlendirme)\w*|hata bütçe\w*|is (this|that|it) (actually )?"
|
|
267
|
-
r"(true|right)|does (this|that) (claim )?hold|gerçekten (doğru|"
|
|
268
|
-
r"öyle) mu|kanıt\w*|test kapsam\w*|kapsam oran\w*|"
|
|
269
|
-
r"kapsamı (artır|yükselt)\w*|doküman(tasyon)?\w* "
|
|
270
|
-
r"(yetersiz|eksik|kötü|zayıf))\b"),
|
|
271
|
-
# A product question reached no rule at all before this: the four above are
|
|
272
|
-
# about code, a UI adjective or a study, so "ürünümü nasıl iyileştiririz"
|
|
273
|
-
# armed nothing and the answer came from priors. `product` excludes
|
|
274
|
-
# production/productivity/productive explicitly - those are code words that
|
|
275
|
-
# merely share the prefix, and matching them would arm product analysis on a
|
|
276
|
-
# deploy question.
|
|
277
|
-
# The generic stems are qualified, never dropped (decision 006): each keeps
|
|
278
|
-
# its product sense and loses the code sense a lookahead names - a feature
|
|
279
|
-
# flag, a segment fault, a tier list, writing to the screen, a SQL table, a
|
|
280
|
-
# page number - so the frozen corpus keeps its rows.
|
|
281
|
-
("product", r"\b(ürün\w*|product(?!ion|ivity|ive)\w*|"
|
|
282
|
-
r"feature(?!s?[ -](?:flags?|toggles?|gates?|branch\w*))\w*|roadmap|"
|
|
283
|
-
r"yol harita\w*|backlog|prd|north star|kuzey yıldız\w*|jtbd|"
|
|
284
|
-
r"retention|churn|onboarding|aktivasyon\w*|cohort|funnel|"
|
|
285
|
-
r"dönüşüm\w*|conversion rate|pricing|fiyatlandır\w*|"
|
|
286
|
-
r"prioriti[sz]\w*|önceliklendir\w*|user research|"
|
|
287
|
-
r"user interview\w*|kullanıcı araştırma\w*|ürün keşf\w*|"
|
|
288
|
-
r"müşteri geri bildirim\w*|ab test|a/b test|"
|
|
289
|
-
# The product vocabulary the rule's five axes name and the table
|
|
290
|
-
# never carried: the HEART signals as people say them, the
|
|
291
|
-
# packaging and positioning words, the segmentation input, and
|
|
292
|
-
# the plural forms the bare singulars' trailing \b rejected.
|
|
293
|
-
r"activations?\b|churn(ed|ing|s)?|funnels?|cohorts?|drop-?off|"
|
|
294
|
-
r"roadmaps?|priorit(y|ies)|adoption|benimsen\w*|"
|
|
295
|
-
r"conversion (rate|funnel|drop\w*)|packaging|monetiz\w*|"
|
|
296
|
-
r"price\w*|tier(?!s?[ -]lists?)\w*|positioning|value proposition|"
|
|
297
|
-
r"konumlandır\w*|\bicp\b|ideal customer profile|persona\w*|"
|
|
298
|
-
r"segment(?!(?:ation)?[ -]faults?)\w*|segmentasyon\w*|hedef kitle\w*|"
|
|
299
|
-
r"(customer|user|product|problem|kullanıcı|müşteri) discovery|"
|
|
300
|
-
r"keşif (görüşme|çalışma)\w*|opportunity (tree|solution|space|"
|
|
301
|
-
r"score)|fırsat\w*|\bnps\b|net promoter|\bcsat\b|satisfaction|"
|
|
302
|
-
r"memnuniyet\w*|anket\w*|(customer|user|kullanıcı|müşteri)"
|
|
303
|
-
r"[ -]?(feedback|geri bildirim)\w*|support ticket\w*|"
|
|
304
|
-
r"şikayet\w*|competitor\w*|competitive (analysis|landscape|"
|
|
305
|
-
r"teardown|benchmark)\w*|rakip\w*|rekabet\w*|pazar pay\w*|"
|
|
306
|
-
r"terk oran\w*|elde tutma\w*|abonelik\w*|gelir model\w*|"
|
|
307
|
-
r"özellik\w*|sayfa(?! numara)\w*|"
|
|
308
|
-
# A single feature said by its surface: an admin screen, a table,
|
|
309
|
-
# a filter, a form, a step flow. These armed nothing before, so a
|
|
310
|
-
# feature-level audit got a screen-level answer. The lookaheads
|
|
311
|
-
# keep the code senses out: "ekran kartı" is a GPU, "adım sayısı"
|
|
312
|
-
# is a count, "format" is not a form.
|
|
313
|
-
r"ekran(?! kart)(?!a\b[^.!?\n]{0,40}\b(?:yaz|bas)\w*)\w*|arayüz\w*|"
|
|
314
|
-
r"arama kutu\w*|filtre\w*|"
|
|
315
|
-
r"tablo(?![^.!?\n]{0,60}\b(?:sütun|kolon|sql|index|indeks)\w*)\w*|"
|
|
316
|
-
r"wizard|adım(?! sayı)\w*|crud|kullanıcı liste\w*|"
|
|
317
|
-
r"form(u|un|da|daki|lar|ları|unu)\w*|"
|
|
318
|
-
r"form (validation|field|error|label)|data table|step flow|"
|
|
319
|
-
r"search (dropdown|box)|form validation|user management|"
|
|
320
|
-
r"(admin|users?) (panel|screen|page|list)|"
|
|
321
|
-
+ _ui_ask(r"(?:admin|users?) table") + r")\b"),
|
|
322
|
-
("graph", r"\b(who calls|callers?|call sites?|who uses|what breaks|"
|
|
323
|
-
r"blast radius|where is|where's|definition of|who invokes|"
|
|
324
|
-
r"kim çağır\w*|çağrı yerleri|nerede tanımlı|nasıl bağlan\w*|"
|
|
325
|
-
r"etkilenir\w*|hangi dosyalar etkilen\w*|"
|
|
326
|
-
# The structural questions the graph answers and the table never
|
|
327
|
-
# asked: impact in the active voice, direction and layering, the
|
|
328
|
-
# import edge, reachability, cycles, entry points, and a rename's
|
|
329
|
-
# blast radius. The Turkish who-uses form is anchored to a code noun,
|
|
330
|
-
# because bare "kim kullanıyor" is user research, not a caller.
|
|
331
|
-
r"impact (analysis|assessment|of (changing|renaming|removing|"
|
|
332
|
-
r"deleting|editing))|change impact|ripple effect|"
|
|
333
|
-
r"(who|what|anything)( else)? (depends on|uses)|dependents? of|"
|
|
334
|
-
r"dependenc(y|ies) (graph|direction|inversion|cycle)|"
|
|
335
|
-
r"layer (violation|boundar\w*|direction)|layering|who imports|"
|
|
336
|
-
r"which (files|modules) import|call (graph|chain|path|tree|"
|
|
337
|
-
r"hierarchy)|transitive callers?|callers? of|usages? of|"
|
|
338
|
-
r"find usages|references? to|is \w+ (still )?(used|referenced|"
|
|
339
|
-
r"called)|never called|dead code|unused (code|function|method|"
|
|
340
|
-
r"module|export|class|variable|import|param\w*)|unreachable code|"
|
|
341
|
-
r"orphan(ed)? (module|file|code)|circular (import|dependenc\w*)|"
|
|
342
|
-
r"(import|dependenc\w*) cycle|cycle (between|detection)|"
|
|
343
|
-
r"(code|app|program|service|main|server|cli|boot) entry ?points?|"
|
|
344
|
-
r"boot (path|sequence|order)|startup (path|sequence|order)|"
|
|
345
|
-
r"how is (this|it|\w+) wired|wired up|rename\w*|"
|
|
346
|
-
r"etki (analiz|alan)\w*|neyi etkile\w*|neleri etkile\w*|"
|
|
347
|
-
r"etkilen\w*|bağımlı\w*|kullanılmayan\w*|kullanılmıyor|ölü kod|"
|
|
348
|
-
r"döngüsel (bağımlılık|import)\w*|giriş nokta\w*|"
|
|
349
|
-
r"uygulama (nerede|nereden) başl\w*|kim import ed\w*|"
|
|
350
|
-
r"(fonksiyon|metod|sınıf|modül|api)\w* (kim|nerede) kullan\w*|"
|
|
351
|
-
r"nerede (kullanıl|çağrıl)\w*|tanım\w* nerede|kullanım yerleri|"
|
|
352
|
-
r"çağrı (zinciri|grafiği)|bağımlılık (grafiği|ağacı)|"
|
|
353
|
-
r"yeniden adlandır\w*)\b"),
|
|
354
|
-
# Two or more independent items: a numbered or bulleted list of at least
|
|
355
|
-
# two entries on their own lines, or the explicit delegation ask. The owner
|
|
356
|
-
# had to say "do everything with subagents and in parallel" on every task
|
|
357
|
-
# because nothing armed the fan-out rule from the shape of the request.
|
|
358
|
-
# `parallelize` alone is code work, so only the delegation phrasings count.
|
|
359
|
-
# The owner's own shapes armed nothing until 2026-10-09: `1 - `, `1- `,
|
|
360
|
-
# `1:`, `a)`, `+` items, an inline `1. ... 2. ...`, "subagentlerle", three
|
|
361
|
-
# files named as one list, "First, ... Second, ...", and three imperative
|
|
362
|
-
# clauses in one sentence (clause-initial in English, clause-final in
|
|
363
|
-
# Turkish). A timestamp, a range and a traceback stay out (the corpus).
|
|
364
|
-
("fanout", r"(?ms:^[ \t]*" + _FAN_ITEM + r".*?^[ \t]*" + _FAN_ITEM + r")|"
|
|
365
|
-
r"(?<![\w.])1[ \t]*[.)-][ \t]+\S[^\n]*?(?<![\w.])2[ \t]*[.)-][ \t]+\S|"
|
|
366
|
-
+ _FAN_FILE + r"(?:[ \t]*(?:,[ \t]*(?:(?:and|ve)[ \t]+)?|"
|
|
367
|
-
r"(?:and|ve|&)[ \t]+)" + _FAN_FILE + r"){2,}|"
|
|
368
|
-
r"(?s:\bfirst(?:ly)?[,:].*?\bsecond(?:ly)?[,:])|"
|
|
369
|
-
r"(?s:\bbirinci(?:si)?[,:].*?\bikinci(?:si)?[,:])|"
|
|
370
|
-
r"(?m:(?:(?:^|[,;]|\band\b)[ \t]*(?:and[ \t]+)?" + _FAN_VERB_EN
|
|
371
|
-
+ r"\b[^\n]*?){3})|"
|
|
372
|
-
r"(?m:(?:\b" + _FAN_VERB_TR + r"\w{0,4}[ \t]*(?:[,;.]|\bve\b|$)"
|
|
373
|
-
r"[^\n]*?){3})|"
|
|
374
|
-
r"\b(?:in parallel|paralel\w*|sub-?agent\w*|alt ?ajan\w*|"
|
|
375
|
-
r"worktrees?|fan[- ]?out|orchestrat\w*|orkestra\w*)\b"),
|
|
376
|
-
)
|
|
93
|
+
% (obj, word, word, obj, "|".join(pack["ui_quality"]) or "(?!)"))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _hints(key):
|
|
97
|
+
"""Every pack's `hints.<key>` fragments as one `\\b(...)\\b` pattern."""
|
|
98
|
+
return r"\b(%s)\b" % "|".join(
|
|
99
|
+
frag if isinstance(frag, str) else _ui_ask(frag["ui_ask"], pack)
|
|
100
|
+
for pack in packs().values() for frag in pack["hints"][key])
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _fanout():
|
|
104
|
+
"""The fan-out hint: two or more independent items - a numbered or bulleted
|
|
105
|
+
list of at least two entries on their own lines, or the explicit delegation
|
|
106
|
+
ask. The owner had to say "do everything with subagents and in parallel" on
|
|
107
|
+
every task because nothing armed the fan-out rule from the shape of the
|
|
108
|
+
request. `parallelize` alone is code work, so only the delegation phrasings
|
|
109
|
+
count. The owner's own shapes armed nothing until 2026-10-09: `1 - `, `1- `,
|
|
110
|
+
`1:`, `a)`, `+` items, an inline `1. ... 2. ...`, "subagentlerle", three
|
|
111
|
+
files named as one list, "First, ... Second, ...", and three imperative
|
|
112
|
+
clauses in one sentence (clause-initial in a `verb_first` pack, clause-final
|
|
113
|
+
in an `object_first` one). A timestamp, a range and a traceback stay out
|
|
114
|
+
(the corpus)."""
|
|
115
|
+
# A list item's marker: `1.`, `1)`, `1 -`, `1:`, `a)`, or a `-`/`*`/`+`/`•`
|
|
116
|
+
# bullet, then its text.
|
|
117
|
+
item = r"(?:\d+[ \t]*[.):\u2013-]|[a-z]\)|[-*+\u2022])[ \t]+\S"
|
|
118
|
+
# A file a prompt names: a path with a directory, or a name with an
|
|
119
|
+
# extension. Anchored at a token start: unanchored, a long run of word
|
|
120
|
+
# characters made every position a start and the search quadratic (100 KB
|
|
121
|
+
# took minutes).
|
|
122
|
+
file = (r"(?<![\w./-])[`'\"]?(?:(?:[\w.-]+/)+[\w.-]*\w|"
|
|
123
|
+
r"\w[\w-]*\.[a-z]{1,4}\b)[`'\"]?")
|
|
124
|
+
join = "|".join(w for p in packs().values() for w in p["fan_join"])
|
|
125
|
+
parts = [r"(?ms:^[ \t]*" + item + r".*?^[ \t]*" + item + r")",
|
|
126
|
+
r"(?<![\w.])1[ \t]*[.)-][ \t]+\S[^\n]*?(?<![\w.])2[ \t]*[.)-][ \t]+\S",
|
|
127
|
+
file + r"(?:[ \t]*(?:,[ \t]*(?:(?:%s)[ \t]+)?|(?:%s|&)[ \t]+)" % (join, join)
|
|
128
|
+
+ file + r"){2,}"]
|
|
129
|
+
for pack in packs().values():
|
|
130
|
+
first, second = pack["fan_ordinals"]
|
|
131
|
+
parts.append(r"(?s:\b%s[,:].*?\b%s[,:])" % (first, second))
|
|
132
|
+
for pack in packs().values():
|
|
133
|
+
verbs, joins = "|".join(pack["fan_verbs"]), "|".join(pack["fan_join"])
|
|
134
|
+
if pack["word_order"] == "object_first":
|
|
135
|
+
parts.append(r"(?m:(?:\b(?:%s)\w{0,4}[ \t]*(?:[,;.]|\b(?:%s)\b|$)"
|
|
136
|
+
r"[^\n]*?){3})" % (verbs, joins))
|
|
137
|
+
else:
|
|
138
|
+
parts.append(r"(?m:(?:(?:^|[,;]|\b(?:%s)\b)[ \t]*(?:(?:%s)[ \t]+)?(?:%s)"
|
|
139
|
+
r"\b[^\n]*?){3})" % (joins, joins, verbs))
|
|
140
|
+
parts.append(r"\b(?:%s)\b" % "|".join(
|
|
141
|
+
d for p in packs().values() for d in p["fan_directives"]))
|
|
142
|
+
return "|".join(parts)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
PROMPT_HINTS = tuple((key, _hints(key)) for key in
|
|
146
|
+
("spec", "consult", "research", "product", "graph")) + (
|
|
147
|
+
("fanout", _fanout()),)
|
|
377
148
|
|
|
378
149
|
# the detached auto-index worker (lock-guarded, retrying); same dir as this file
|
|
379
150
|
INDEX_WORKER = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
@@ -390,7 +161,7 @@ CORE_RULES = (
|
|
|
390
161
|
("ponytail", "**Ponytail (minimal code).**"),
|
|
391
162
|
("adhd", "**Output shape: ADHD-friendly.**"),
|
|
392
163
|
("fidelity", "**Deliver the whole ask; never the shortcut.**"),
|
|
393
|
-
("integrity", '**Integrity: evidence, or
|
|
164
|
+
("integrity", '**Integrity: evidence, or a named blocker.**'),
|
|
394
165
|
("loop", "**Loop discipline.**"),
|
|
395
166
|
("scope", "**Session scope: the user's repo, not tezgah.**"),
|
|
396
167
|
("spec", "**Spec before building.**"),
|
|
@@ -417,11 +188,12 @@ CORE_RULES = (
|
|
|
417
188
|
REMINDER_CLAUSES = (
|
|
418
189
|
("exec", "{REPLY_SHORT}, BLUF, "),
|
|
419
190
|
("adhd", "answer first - no recap, no closer, at most five ranked items; "),
|
|
420
|
-
("ponytail", "code minimal per ponytail (code first
|
|
191
|
+
("ponytail", "code minimal per ponytail (code first; a note only for a "
|
|
192
|
+
"requested part you cut); "),
|
|
421
193
|
("lessons", ".tezgah/lessons.md lines are standing constraints; "),
|
|
422
194
|
("consult", "consult before irreversible calls; "),
|
|
423
195
|
("integrity", re.compile(r'done/tested claims need observed evidence -> .*?'
|
|
424
|
-
r'unverified "done"; ')),
|
|
196
|
+
r'unverified "done"; .*?under "\{BLOCKERS_HEADING\}"; ')),
|
|
425
197
|
)
|
|
426
198
|
|
|
427
199
|
|
|
@@ -448,12 +220,17 @@ def prompt_reminder(drop=()):
|
|
|
448
220
|
|
|
449
221
|
|
|
450
222
|
def render(text, root=""):
|
|
451
|
-
"""Fill the path placeholders with stable, existing paths, and the
|
|
452
|
-
|
|
223
|
+
"""Fill the path placeholders with stable, existing paths, and the language
|
|
224
|
+
placeholders - the reply language and the contract's own tokens, `{UNVERIFIED}`,
|
|
225
|
+
`{BLOCKERS_HEADING}`, `{DECISION}`... - with the configured language's words
|
|
226
|
+
(`tezgah_paths.say`; REPLY_ANY's reply clause under `any`)."""
|
|
453
227
|
if not text:
|
|
454
228
|
return text
|
|
455
|
-
|
|
456
|
-
|
|
229
|
+
words = say()
|
|
230
|
+
if language() == "any":
|
|
231
|
+
words.update(REPLY_ANY)
|
|
232
|
+
for key, word in words.items():
|
|
233
|
+
text = text.replace("{%s}" % key.upper(), word)
|
|
457
234
|
return (text.replace("{CONSULT_BIN}", tool("consult"))
|
|
458
235
|
.replace("{CODEGEN_BIN}", tool("codegen"))
|
|
459
236
|
.replace("{ORX_BIN}", orx_bin() or "orx")
|
|
@@ -822,6 +599,13 @@ RESUME_PLAN_CHARS = 40
|
|
|
822
599
|
RESUME_COMMIT_CHARS = 38
|
|
823
600
|
RESUME_CHECK_CHARS = 40
|
|
824
601
|
RESUME_FILES = 3
|
|
602
|
+
# The slice journal's lines (plan 081): the newest three stopped or open slices
|
|
603
|
+
# and background jobs, each already bounded by the journal and cut again here.
|
|
604
|
+
# They ride the resume block and, because an omp `/clear` fires no
|
|
605
|
+
# session_start, every prompt's `slices` part while any is pending.
|
|
606
|
+
SLICE_LINES = 3
|
|
607
|
+
SLICE_CHARS = 160
|
|
608
|
+
SLICES_HEAD = "Slices left stopped or open (the journal, `tezgah-task slice reconcile`):"
|
|
825
609
|
|
|
826
610
|
|
|
827
611
|
def _plan_facts(path):
|
|
@@ -876,13 +660,33 @@ def _active_plan(root):
|
|
|
876
660
|
return paths[0], False
|
|
877
661
|
|
|
878
662
|
|
|
663
|
+
def slice_lines(root, session_id=None, others=False):
|
|
664
|
+
"""This session's pending slices and jobs (`tezgah_task.slice_lines`): at
|
|
665
|
+
most SLICE_LINES lines of at most SLICE_CHARS each, newest first, or []
|
|
666
|
+
with no session, under `slices-off`, without the journal, or when it
|
|
667
|
+
raises - a broken journal costs the lines, never the injection. Another
|
|
668
|
+
session's slices are never listed: they are that session's to resume (omp's
|
|
669
|
+
`--resume` keeps the session id). With `others` - the resume block, read at
|
|
670
|
+
session start and after a compaction - one line counts them, so a session
|
|
671
|
+
that came back under a new id still learns they exist."""
|
|
672
|
+
reader = getattr(tezgah_task, "slice_lines", None)
|
|
673
|
+
if not reader or not session_id or off("slices-off"):
|
|
674
|
+
return []
|
|
675
|
+
try:
|
|
676
|
+
lines = reader(root, limit=SLICE_LINES, session=session_id, others=others) or []
|
|
677
|
+
except Exception:
|
|
678
|
+
return []
|
|
679
|
+
return [cut(str(ln), SLICE_CHARS) for ln in lines[:SLICE_LINES + 1]]
|
|
680
|
+
|
|
681
|
+
|
|
879
682
|
def resume_state(root, session_id):
|
|
880
683
|
"""The live turn state as a context block, or "" when every part is empty.
|
|
881
684
|
|
|
882
|
-
The
|
|
685
|
+
The five parts, in order: the active plan's State and Next, the branch's last
|
|
883
686
|
RESUME_LOG commits, the newest `verify_ok`/`verify_fail` the ledger holds,
|
|
884
|
-
|
|
885
|
-
|
|
687
|
+
the files this turn's writes changed, and the journal's stopped or open
|
|
688
|
+
slices (`slice_lines`). Read by session_start and the post-compaction path;
|
|
689
|
+
see the note above for why both."""
|
|
886
690
|
lines = []
|
|
887
691
|
plan, mine = _active_plan(root)
|
|
888
692
|
if plan:
|
|
@@ -909,7 +713,7 @@ def resume_state(root, session_id):
|
|
|
909
713
|
# lessons, pointer - for a session whose ledger holds one bad line. So a
|
|
910
714
|
# damaged ledger costs these two bullets and nothing else.
|
|
911
715
|
try:
|
|
912
|
-
row = last_check(session_id) if session_id else None
|
|
716
|
+
row = last_check(session_id, root) if session_id else None
|
|
913
717
|
files = sorted(changed_files(session_id)) if session_id else []
|
|
914
718
|
except ValueError:
|
|
915
719
|
row, files = None, []
|
|
@@ -919,6 +723,7 @@ def resume_state(root, session_id):
|
|
|
919
723
|
RESUME_CHECK_CHARS)))
|
|
920
724
|
if files:
|
|
921
725
|
lines.append("- changed this turn: %s" % ", ".join(files[:RESUME_FILES]))
|
|
726
|
+
lines += slice_lines(root, session_id, others=True)
|
|
922
727
|
if not lines:
|
|
923
728
|
return ""
|
|
924
729
|
return "## Session so far\n" + "\n".join(lines)
|
|
@@ -1194,35 +999,8 @@ def constraint_notice(cwd, session_id):
|
|
|
1194
999
|
# So the ask is judged per clause: it arms `product` only when no code marker -
|
|
1195
1000
|
# a file name, an identifier, SQL, a build, test or runtime word - shares its
|
|
1196
1001
|
# clause (decision 006: qualify the stem, never drop it).
|
|
1197
|
-
DATA_ASK = re.compile(
|
|
1198
|
-
|
|
1199
|
-
r"(?:dashboard|charts?|visuali[sz]ation)|"
|
|
1200
|
-
r"(?:metri[ck]s?|kpis?|data|sales|revenue|veri\w*|satış\w*|gelir\w*) "
|
|
1201
|
-
r"(?:\w+ ){0,2}(?:dashboard|pano)|"
|
|
1202
|
-
r"(?:dashboard|gösterge panel)\w* (?:\w+ ){0,4}(?:tasarla|oluştur|hazırla|"
|
|
1203
|
-
r"yap|göster)\w*|"
|
|
1204
|
-
r"(?:what|which) (?:\w+ ){0,3}(?:metri[ck]s|kpis|charts?) (?:\w+ ){0,3}"
|
|
1205
|
-
r"(?:should|to|do|matter|would)|"
|
|
1206
|
-
r"what should (?:\w+ ){0,3}dashboard show|"
|
|
1207
|
-
r"(?:choose|pick|define|track|design)(?:s|ing)? (?:\w+ ){0,3}(?:metri[ck]s|kpis)|"
|
|
1208
|
-
r"(?:metri[ck]s|kpis) (?:should|to|do|would) (?:\w+ ){0,2}"
|
|
1209
|
-
r"(?:track|measure|use|watch)|success metrics?|"
|
|
1210
|
-
r"(?:suggest|recommend)\w* (?:\w+ ){0,3}charts?|öner\w* (?:\w+ ){0,3}grafi\w*|"
|
|
1211
|
-
r"hangi (?:metri[ck]\w*|kpi\w*|grafi\w*|göstergeleri?)|"
|
|
1212
|
-
r"(?:metri[ck]\w*|kpi\w*) (?:\w+ ){0,3}(?:tasarla|seç|tanımla|belirle|takip|izle)\w*|"
|
|
1213
|
-
r"gösterge panel\w*|visuali[sz]\w* (?:\w+ ){0,3}(?:data|numbers|veri\w*)|"
|
|
1214
|
-
r"data visuali[sz]ation|veri\w* (?:\w+ ){0,3}görselleştir\w*)")
|
|
1215
|
-
CODE_SENSE = re.compile(
|
|
1216
|
-
r"\w\.(?:py|tsx?|jsx?|json|go|rs|ya?ml|sql|java|rb|css|html)\b|\b\w+_\w+\b|"
|
|
1217
|
-
r"/metrics\b|\bselect\b.{0,200}?\bfrom\b|\b(?:tables?|columns?|schema|sql|queries|"
|
|
1218
|
-
r"query|bundle|webpack|vite|tests?|ci|counters?|registry|pipeline|backend|"
|
|
1219
|
-
r"handlers?|structs?|endpoints?|exporters?|export\w*|servers?|librar\w*|"
|
|
1220
|
-
r"packages?|modules?|database|index\w*|indeks\w*|"
|
|
1221
|
-
r"middleware|debugger|data (?:structure|flow)s?|clipboard|spinner|threads?|"
|
|
1222
|
-
r"commit|git|functions?|class|component|prop|hooks?|prometheus|grafana|"
|
|
1223
|
-
r"kibana|statsd|opentelemetry|config\w*|provision\w*|api|routes?|snake_case|"
|
|
1224
|
-
r"veri yapı\w*|fonksiyon\w*|bileşen\w*|modül\w*|dosya\w*|kopyala\w*|"
|
|
1225
|
-
r"seçici\w*|yükleme|ilerleme|paket\w*)\b")
|
|
1002
|
+
DATA_ASK = re.compile(r"\b(?:%s)" % alts("data_ask"))
|
|
1003
|
+
CODE_SENSE = re.compile(alts("code_sense"))
|
|
1226
1004
|
|
|
1227
1005
|
|
|
1228
1006
|
def _data_ask(low):
|
|
@@ -1458,11 +1236,22 @@ def session_of(payload):
|
|
|
1458
1236
|
# ADHD rules inline from the first turn (+1042 B), the fan-out pointer (+112 B)
|
|
1459
1237
|
# and the `tezgah-skill` line (+113 B) took the healthy fixture's session_start
|
|
1460
1238
|
# from 10579 B to 12059 B, so at 12000 B a healthy repo gave up its lessons
|
|
1461
|
-
# and sibling checkouts to pay for core rules; 13000 B restores the headroom.
|
|
1239
|
+
# and sibling checkouts to pay for core rules; 13000 B restores the headroom.
|
|
1240
|
+
# Moved again on 2026-10-11 (plan 080): the integrity paragraph carries the
|
|
1241
|
+
# residual rule (the four blockers and the "Engeller" format) and the reminder
|
|
1242
|
+
# its one-line form, +1473 B of core and +151 B of reminder; at 13000 B the
|
|
1243
|
+
# healthy fixture (13588 B) gave up its lessons, plans and steer blocks (950 B;
|
|
1244
|
+
# 1002 B where a subagents note rides along), and at 6000 B the busiest prompt
|
|
1245
|
+
# turn its active-task line (255 B), so
|
|
1246
|
+
# the session budgets move to 14500 B and the prompt budget to 6500 B.
|
|
1247
|
+
# Moved again on 2026-10-12 (plan 086, S3): the integrity paragraph carries
|
|
1248
|
+
# the Ask policy (+913 B of core), and at 14500 B the healthy fixture gave up
|
|
1249
|
+
# its lessons, plans or knowledge block (235-338 B), so the session budgets
|
|
1250
|
+
# move to 15500 B. A
|
|
1462
1251
|
# budget in bytes, not tokens: this file has no tokenizer and a wrong estimate
|
|
1463
1252
|
# would be worse than a bound.
|
|
1464
|
-
CONTEXT_BUDGET = {"session_start":
|
|
1465
|
-
"subagent_start": 5500, "user_prompt":
|
|
1253
|
+
CONTEXT_BUDGET = {"session_start": 15500, "post_compact": 15500,
|
|
1254
|
+
"subagent_start": 5500, "user_prompt": 6500}
|
|
1466
1255
|
DEFAULT_BUDGET = 13000
|
|
1467
1256
|
# The blocks in the order they are given up when the budget is exceeded, lowest
|
|
1468
1257
|
# value first: text another surface already carries (the plan table lives in the
|
|
@@ -1477,8 +1266,10 @@ DEFAULT_BUDGET = 13000
|
|
|
1477
1266
|
# block, which outlives every status and availability line because it is the only
|
|
1478
1267
|
# one that says what THIS session was doing (a `git log` and a ledger read are
|
|
1479
1268
|
# turns the session would otherwise spend) but still yields to a rule - then the
|
|
1480
|
-
#
|
|
1481
|
-
#
|
|
1269
|
+
# journal's stopped or open slices, which outlive the resume block because after
|
|
1270
|
+
# an omp `/clear` they are the one pointer left to delegated work in flight - then
|
|
1271
|
+
# the scratch-path warning, which is about evidence the turn may already have
|
|
1272
|
+
# claimed - then the active task's phase, which outlives both because a phase is what
|
|
1482
1273
|
# stops a refused write before it happens - then the delta, and the skill pointer
|
|
1483
1274
|
# last. A key absent from this tuple is never dropped: the always-on core, the
|
|
1484
1275
|
# per-turn reminder and an armed paragraph ARE the rules, and a budget that could
|
|
@@ -1487,7 +1278,8 @@ DEFAULT_BUDGET = 13000
|
|
|
1487
1278
|
DROP_ORDER = ("knowledge", "worktrees", "lessons", "skill", "lessons_turn",
|
|
1488
1279
|
"taste", "plans", "subagents", "steer", "orca", "consult", "research",
|
|
1489
1280
|
"research_broken", "graph", "offnote", "orchestrate",
|
|
1490
|
-
"index", "resume", "scratch", "task", "delta",
|
|
1281
|
+
"index", "resume", "slices", "context", "scratch", "task", "delta",
|
|
1282
|
+
"pointer")
|
|
1491
1283
|
|
|
1492
1284
|
|
|
1493
1285
|
def _first_item(text):
|
|
@@ -1609,7 +1401,8 @@ SCRATCH_CHARS = 120
|
|
|
1609
1401
|
# while the tool hooks never run - an unloaded plugin, a matcher the host
|
|
1610
1402
|
# renamed, a hook path that moved. The gate is then off and nothing says so.
|
|
1611
1403
|
# What the prompt hook can observe is the host's own transcript (Claude and
|
|
1612
|
-
# Codex hand `transcript_path` to every hook
|
|
1404
|
+
# Codex hand `transcript_path` to every hook, omp's bridge hands its session
|
|
1405
|
+
# file to the prompt hook) and this session's ledger: gated
|
|
1613
1406
|
# tool calls in the transcript since the session's first turn marker, and not
|
|
1614
1407
|
# one row from the gate over the same span, is a gate that is not running. Only
|
|
1615
1408
|
# a bounded tail of the transcript is read, the ledger is scanned as bytes, and
|
|
@@ -1618,7 +1411,8 @@ GATE_TAIL = 262144
|
|
|
1618
1411
|
GATE_MIN_CALLS = 3
|
|
1619
1412
|
# The tool names whose every call leaves a PreToolUse row (`began`,
|
|
1620
1413
|
# tezgah_gate.decision's write and shell branch) and every refused one a `deny`:
|
|
1621
|
-
# Claude's write and shell tools
|
|
1414
|
+
# Claude's write and shell tools, omp's `bash`/`edit`/`write` and Codex's
|
|
1415
|
+
# `exec_command`/`apply_patch`. A
|
|
1622
1416
|
# tool the gate may pass without a row (Read, Grep, Task, WebFetch, an MCP
|
|
1623
1417
|
# call) is not counted, so its absence from the ledger is never read as a
|
|
1624
1418
|
# disarmed gate.
|
|
@@ -1639,25 +1433,32 @@ def _iso_epoch(value):
|
|
|
1639
1433
|
return None
|
|
1640
1434
|
|
|
1641
1435
|
|
|
1642
|
-
def
|
|
1643
|
-
"""
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
"payload": {"type": "function_call", "name"}}`; anything else counts 0."""
|
|
1436
|
+
def _tail_lines(path):
|
|
1437
|
+
"""The raw lines of a `.jsonl` transcript's last GATE_TAIL bytes, the cut
|
|
1438
|
+
first line dropped. [] for any other path or an unreadable file, so a host
|
|
1439
|
+
that hands no transcript pays nothing."""
|
|
1647
1440
|
if not isinstance(path, str) or not path.endswith(".jsonl"):
|
|
1648
|
-
return
|
|
1441
|
+
return []
|
|
1649
1442
|
try:
|
|
1650
1443
|
with open(path, "rb") as fh:
|
|
1651
1444
|
size = fh.seek(0, 2)
|
|
1652
1445
|
fh.seek(max(0, size - GATE_TAIL))
|
|
1653
1446
|
lines = fh.read().split(b"\n")
|
|
1654
1447
|
except OSError:
|
|
1655
|
-
return
|
|
1656
|
-
if size > GATE_TAIL
|
|
1657
|
-
|
|
1448
|
+
return []
|
|
1449
|
+
return lines[1:] if size > GATE_TAIL else lines
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
def _transcript_calls(path, since):
|
|
1453
|
+
"""Gated tool calls in the transcript's last GATE_TAIL bytes stamped at or
|
|
1454
|
+
after `since` (epoch seconds). Claude writes `{"timestamp", "message":
|
|
1455
|
+
{"content": [{"type": "tool_use", "name"}]}}`, omp the same with
|
|
1456
|
+
`"toolCall"` blocks, Codex `{"timestamp", "payload": {"type":
|
|
1457
|
+
"function_call", "name"}}`. Anything else counts 0."""
|
|
1658
1458
|
calls = 0
|
|
1659
|
-
for raw in
|
|
1660
|
-
if b"tool_use" not in raw and b"
|
|
1459
|
+
for raw in _tail_lines(path):
|
|
1460
|
+
if (b"tool_use" not in raw and b"toolCall" not in raw
|
|
1461
|
+
and b"function_call" not in raw):
|
|
1661
1462
|
continue
|
|
1662
1463
|
try:
|
|
1663
1464
|
row = json.loads(raw)
|
|
@@ -1670,7 +1471,7 @@ def _transcript_calls(path, since):
|
|
|
1670
1471
|
msg = row.get("message")
|
|
1671
1472
|
if isinstance(msg, dict) and isinstance(msg.get("content"), list):
|
|
1672
1473
|
names = [b.get("name") for b in msg["content"]
|
|
1673
|
-
if isinstance(b, dict) and b.get("type")
|
|
1474
|
+
if isinstance(b, dict) and b.get("type") in ("tool_use", "toolCall")]
|
|
1674
1475
|
pay = row.get("payload")
|
|
1675
1476
|
if isinstance(pay, dict) and pay.get("type") == "function_call":
|
|
1676
1477
|
names.append(pay.get("name"))
|
|
@@ -1678,10 +1479,72 @@ def _transcript_calls(path, since):
|
|
|
1678
1479
|
return calls
|
|
1679
1480
|
|
|
1680
1481
|
|
|
1482
|
+
# The usage keys that make up one step's input side: Claude's names, then omp's.
|
|
1483
|
+
# Codex reports the cached share inside `input_tokens`.
|
|
1484
|
+
INPUT_KEYS = ("input_tokens", "cache_creation_input_tokens",
|
|
1485
|
+
"cache_read_input_tokens", "input", "cacheRead", "cacheWrite")
|
|
1486
|
+
CONTEXT_NUDGE = (
|
|
1487
|
+
"Router context %dk tokens: push heavy reads and check output through a "
|
|
1488
|
+
"slice that returns the exit code and the failing lines. Hand the open "
|
|
1489
|
+
"work to a slice or run `/compact` before the next heavy step.")
|
|
1490
|
+
# The row a compaction leaves: omp's `{"type": "compaction"}`, Claude's
|
|
1491
|
+
# `compact_boundary` system row. A step before it measured the old context.
|
|
1492
|
+
COMPACTED = re.compile(rb'"(?:type"\s*:\s*"compaction|subtype"\s*:\s*"compact_boundary)"')
|
|
1493
|
+
|
|
1494
|
+
|
|
1495
|
+
def context_tokens(path):
|
|
1496
|
+
"""Input-side tokens of the newest main-thread assistant step in the
|
|
1497
|
+
transcript tail, or None. Claude and omp put `usage` on the assistant
|
|
1498
|
+
message (a Claude `isSidechain` row is a subagent's), Codex on a
|
|
1499
|
+
`token_count` event's `last_token_usage`. A compaction row met first means
|
|
1500
|
+
no step since the compaction, so None."""
|
|
1501
|
+
for raw in reversed(_tail_lines(path)):
|
|
1502
|
+
if COMPACTED.search(raw):
|
|
1503
|
+
return None
|
|
1504
|
+
if b"usage" not in raw:
|
|
1505
|
+
continue
|
|
1506
|
+
try:
|
|
1507
|
+
row = json.loads(raw)
|
|
1508
|
+
except ValueError:
|
|
1509
|
+
continue
|
|
1510
|
+
if not isinstance(row, dict) or row.get("isSidechain"):
|
|
1511
|
+
continue
|
|
1512
|
+
msg, pay = row.get("message"), row.get("payload")
|
|
1513
|
+
usage = None
|
|
1514
|
+
if isinstance(msg, dict) and msg.get("role") == "assistant":
|
|
1515
|
+
usage = msg.get("usage")
|
|
1516
|
+
elif isinstance(pay, dict) and pay.get("type") == "token_count":
|
|
1517
|
+
usage = (pay.get("info") or {}).get("last_token_usage")
|
|
1518
|
+
if isinstance(usage, dict):
|
|
1519
|
+
n = sum(v for k, v in usage.items() if k in INPUT_KEYS and isinstance(v, int))
|
|
1520
|
+
if n:
|
|
1521
|
+
return n
|
|
1522
|
+
return None
|
|
1523
|
+
|
|
1524
|
+
|
|
1525
|
+
def context_key(session_id):
|
|
1526
|
+
"""The router-context line's key in the `nudged` mark store."""
|
|
1527
|
+
return "context:" + slug(str(session_id))
|
|
1528
|
+
|
|
1529
|
+
|
|
1530
|
+
def context_nudge(payload, session_id):
|
|
1531
|
+
"""The one line asking the router to delegate heavy reading, or "" below
|
|
1532
|
+
CONTEXT_NUDGE_TOKENS, without a transcript, under `orchestrate-off`, or
|
|
1533
|
+
once `context_for` latched it for this session (cleared by a compaction)."""
|
|
1534
|
+
path = payload.get("transcript_path") if isinstance(payload, dict) else None
|
|
1535
|
+
if not path or off("orchestrate-off") or tezgah_store.marked(
|
|
1536
|
+
"nudged", context_key(session_id)):
|
|
1537
|
+
return ""
|
|
1538
|
+
tokens = context_tokens(path)
|
|
1539
|
+
return CONTEXT_NUDGE % (tokens // 1000) if (
|
|
1540
|
+
tokens or 0) >= CONTEXT_NUDGE_TOKENS else ""
|
|
1541
|
+
|
|
1542
|
+
|
|
1681
1543
|
# The ledger kinds a hook other than the tool hooks writes: the prompt hook's
|
|
1682
1544
|
# turn marker, judge row, lesson rows and disarm row, the Stop hook's claim,
|
|
1683
1545
|
# refusal, after_block, shape and stop_spec rows, the subagent-end hook's subagent_end row,
|
|
1684
|
-
# compaction, the subagent mark (SubagentStart writes it too), the
|
|
1546
|
+
# compaction, the subagent mark (SubagentStart writes it too), the omp fork's
|
|
1547
|
+
# `forked` row, the guard's crash
|
|
1685
1548
|
# row (any hook), the damage row any reader writes and the session start's
|
|
1686
1549
|
# attest row. Every other kind - deny, nudge, began, snapshot, ... - can only
|
|
1687
1550
|
# come from a tool hook.
|
|
@@ -1691,7 +1554,7 @@ def _transcript_calls(path, since):
|
|
|
1691
1554
|
NOT_TOOL_HOOK = frozenset((b"turn", b"judge", b"claim", b"refusal",
|
|
1692
1555
|
b"after_block", b"subagent_end", b"stop_spec",
|
|
1693
1556
|
b"shape", b"compact", b"orch", b"crash", b"route",
|
|
1694
|
-
b"spawned", b"lesson", b"disarm", b"ledger_damage",
|
|
1557
|
+
b"spawned", b"forked", b"lesson", b"disarm", b"ledger_damage",
|
|
1695
1558
|
b"attest"))
|
|
1696
1559
|
# The rows PostToolUse writes (`note_tool`): they prove the host ran the call,
|
|
1697
1560
|
# not that the gate saw it - a PostToolUse hook keeps writing them while a
|
|
@@ -1828,15 +1691,31 @@ POINTER_NEEDLE = "tezgah-contract"
|
|
|
1828
1691
|
# constraint that the user never meant is a rule the session did not get from
|
|
1829
1692
|
# the user. tests/constraint-fixtures.md holds the real prompts it was measured
|
|
1830
1693
|
# on, its false positives listed.
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
r"(?
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1694
|
+
def _constraint_shapes():
|
|
1695
|
+
"""Every pack's `constraint.dont` and `constraint.ask` shapes in one
|
|
1696
|
+
pattern, each named group suffixed with its pack's code (`obj_en`,
|
|
1697
|
+
`ask_tr`), so `_shape` can tell which language's shape matched."""
|
|
1698
|
+
return re.compile(r"(?i)\b(?:%s)" % "|".join(
|
|
1699
|
+
frag.replace("(?P<obj>", "(?P<obj_%s>" % code).replace(
|
|
1700
|
+
"(?P<ask>", "(?P<ask_%s>" % code)
|
|
1701
|
+
for code, pack in packs().items() for kind in ("dont", "ask")
|
|
1702
|
+
for frag in pack["constraint"][kind]))
|
|
1703
|
+
|
|
1704
|
+
|
|
1705
|
+
CONSTRAINT_SHAPES = _constraint_shapes()
|
|
1706
|
+
|
|
1707
|
+
|
|
1708
|
+
def _shape(m):
|
|
1709
|
+
"""(kind, group, pack) of a CONSTRAINT_SHAPES match: `obj` for a
|
|
1710
|
+
prohibition's object, `ask` for an ask-first clause's verb. Read off the
|
|
1711
|
+
one named group that matched (`groupdict`), so a pack fragment's own
|
|
1712
|
+
groups cannot stand in for it (tests/test_lang_packs.py holds each
|
|
1713
|
+
fragment to its one named group)."""
|
|
1714
|
+
group = next(k for k, v in m.groupdict().items() if v is not None)
|
|
1715
|
+
kind, code = group.split("_", 1)
|
|
1716
|
+
return kind, group, packs()[code]
|
|
1717
|
+
|
|
1718
|
+
|
|
1840
1719
|
# Pasted material is not the user's constraint: a shape that starts inside a
|
|
1841
1720
|
# quoted span, inline code, a fenced block or a `>` quote line is skipped (a
|
|
1842
1721
|
# quoted object after an unquoted shape stays the user's), and a prompt
|
|
@@ -1846,11 +1725,12 @@ CONSTRAINT_SHAPES = re.compile(
|
|
|
1846
1725
|
# log alone matched six clauses of another agent's reasoning. ponytail: a long
|
|
1847
1726
|
# prompt the user did write loses its constraints - the cost of a length rule.
|
|
1848
1727
|
CONSTRAINT_PROMPT_MAX = 12000
|
|
1728
|
+
# Single quotes count only off a word ("main'e" is no quote), with ‘…’ and «…».
|
|
1849
1729
|
QUOTED = re.compile(r"```.*?```|`[^`\n]*`|\"[^\"\n]{0,200}\"|“[^”\n]{0,200}”"
|
|
1730
|
+
r"|(?<!\w)'[^'\n]{1,200}'(?!\w)|‘[^’\n]{0,200}’|«[^»\n]{0,200}»"
|
|
1850
1731
|
r"|^\s*>.*$", re.S | re.M)
|
|
1851
1732
|
SENTENCE_END = re.compile(r"[.!?;:](?=\s|$)|\n")
|
|
1852
|
-
NEEDLE_STOP = frozenset(
|
|
1853
|
-
"before after from into of but ve veya ile".split())
|
|
1733
|
+
NEEDLE_STOP = frozenset(w for p in packs().values() for w in p["needle_stop"])
|
|
1854
1734
|
QUOTE_MARKS = re.compile(r"[`\"“”]")
|
|
1855
1735
|
NEEDLE_WORDS = 3
|
|
1856
1736
|
CONSTRAINT_CHARS = 120
|
|
@@ -1860,8 +1740,8 @@ CONSTRAINTS_MAX = 5
|
|
|
1860
1740
|
def user_constraints(prompt):
|
|
1861
1741
|
"""The constraint clauses a prompt issues, redacted and cut, in order and
|
|
1862
1742
|
once each. A clause runs from the shape to the end of its sentence - from the
|
|
1863
|
-
sentence's start for
|
|
1864
|
-
keeps the clause, never the prompt."""
|
|
1743
|
+
sentence's start for an `object_first` pack's prohibition (Turkish), whose
|
|
1744
|
+
object comes first. The stamp keeps the clause, never the prompt."""
|
|
1865
1745
|
text = prompt or ""
|
|
1866
1746
|
if len(text) > CONSTRAINT_PROMPT_MAX:
|
|
1867
1747
|
return []
|
|
@@ -1870,8 +1750,11 @@ def user_constraints(prompt):
|
|
|
1870
1750
|
for m in CONSTRAINT_SHAPES.finditer(text):
|
|
1871
1751
|
if any(a <= m.start() < b for a, b in quoted):
|
|
1872
1752
|
continue
|
|
1753
|
+
kind, _group, pack = _shape(m)
|
|
1873
1754
|
start = (max((e.end() for e in SENTENCE_END.finditer(text, 0, m.start())),
|
|
1874
|
-
default=0)
|
|
1755
|
+
default=0)
|
|
1756
|
+
if kind == "obj" and pack["word_order"] == "object_first"
|
|
1757
|
+
else m.start())
|
|
1875
1758
|
end = SENTENCE_END.search(text, m.end())
|
|
1876
1759
|
clause = " ".join(text[start:end.start() if end else len(text)].split())
|
|
1877
1760
|
clause = cut(redact(clause.strip("-*• ")), CONSTRAINT_CHARS)
|
|
@@ -1879,6 +1762,97 @@ def user_constraints(prompt):
|
|
|
1879
1762
|
out.append(clause)
|
|
1880
1763
|
return out
|
|
1881
1764
|
|
|
1765
|
+
# A grant is the positive half of the same reader (plan 086, S3): "bana sormadan
|
|
1766
|
+
# pushla", "without asking", "onay veriyorum" lets the session take an outward
|
|
1767
|
+
# step without asking again; "artık bana sor", "ask me again" or a later
|
|
1768
|
+
# prohibition that names the step ("bana sormadan push etme", "don't push")
|
|
1769
|
+
# lifts it. Each is one `grant` row in the session's ledger, scoped to the
|
|
1770
|
+
# OUTWARD class the sentence names, `all` when it names none; the Stop rule's
|
|
1771
|
+
# `needless ask` and the ask-tool warning read them back
|
|
1772
|
+
# (`tezgah_integrity.grants_in_force`). Never a class-F step: a grant lowers
|
|
1773
|
+
# asks, it never licenses a force push, a delete or a spend over the cap.
|
|
1774
|
+
LIFT_SHAPES = re.compile(r"(?i)\b(?:%s)" % alts("constraint", "lift"))
|
|
1775
|
+
# A negated outward step ("Don't push.", "şimdilik push yapma") lifts only the
|
|
1776
|
+
# class its own words name, never `all`.
|
|
1777
|
+
LIFT_OUTWARD = re.compile(r"(?i)\b(?:%s)" % alts("constraint", "lift_outward"))
|
|
1778
|
+
# A grant shape in a sentence that forbids, complains or asks is no grant:
|
|
1779
|
+
# "bana sormadan hiçbir şey yapma", "neden bana sormadan pushladın?", "Never
|
|
1780
|
+
# tag without asking me". Each pack's rules read only the sentences its own
|
|
1781
|
+
# grant shape matched, so "theme" is no Turkish -me: `grant_not` anywhere in
|
|
1782
|
+
# the sentence with the grant shape blanked ("don't ask me again" stays a
|
|
1783
|
+
# grant), the pack's question particle, and `grant_not_verb` only on the word
|
|
1784
|
+
# right after the grant shape or the sentence's last word - one of
|
|
1785
|
+
# `grant_not_soft` ("durma", "unutma") only after an outward word. ponytail: a
|
|
1786
|
+
# Turkish noun ending in -ma/-me in those two places also reads as a
|
|
1787
|
+
# prohibition - a missed grant costs one question, a false one an unasked push.
|
|
1788
|
+
|
|
1789
|
+
|
|
1790
|
+
def _any(frags):
|
|
1791
|
+
return re.compile(r"(?i)(?:%s)" % ("|".join(frags) or "(?!)"))
|
|
1792
|
+
|
|
1793
|
+
|
|
1794
|
+
GRANT_PACKS = [(re.compile(r"(?i)\b(?:%s)" % "|".join(p["constraint"]["grant"])),
|
|
1795
|
+
_any(p["constraint"]["grant_not"]
|
|
1796
|
+
+ [r"\b(?:%s)\b" % q for q in p["question_end"]]),
|
|
1797
|
+
_any(p["constraint"]["grant_not_verb"]),
|
|
1798
|
+
_any(p["constraint"]["grant_not_soft"]))
|
|
1799
|
+
for p in packs().values() if p["constraint"]["grant"]]
|
|
1800
|
+
WORD = re.compile(r"[^\W\d_]+")
|
|
1801
|
+
|
|
1802
|
+
|
|
1803
|
+
def _refuses(text, start, at, to, stop, nots, verb, soft):
|
|
1804
|
+
"""True when the sentence `text[start:stop]` takes back the grant shape
|
|
1805
|
+
at `text[at:to]` (GRANT_PACKS' rules of the pack that matched it)."""
|
|
1806
|
+
if text[stop:stop + 1] == "?" or nots.search(text[start:at] + " " + text[to:stop]):
|
|
1807
|
+
return True
|
|
1808
|
+
words = [m for m in WORD.finditer(text, start, stop) if not at <= m.start() < to]
|
|
1809
|
+
near = [m for m in words if m.start() >= to][:1] + words[-1:]
|
|
1810
|
+
return any(verb.fullmatch(m.group(0)) and not (
|
|
1811
|
+
soft.fullmatch(m.group(0)) and not outward_classes(text[start:m.start()]))
|
|
1812
|
+
for m in near)
|
|
1813
|
+
|
|
1814
|
+
|
|
1815
|
+
def user_grants(prompt):
|
|
1816
|
+
"""[(clause, target)] for every grant and lift a prompt issues, in order:
|
|
1817
|
+
`target` is an OUTWARD class or `all`, `lift:` before it for a lift. A
|
|
1818
|
+
sentence is read whole for the class it names, since a Turkish grant often
|
|
1819
|
+
names the step before "sormadan"; a grant its sentence takes back
|
|
1820
|
+
(`_refuses`) is none, a lift shape inside a grant shape ("don't ask me
|
|
1821
|
+
again") is the grant's, and a LIFT_OUTWARD match lifts only the classes it
|
|
1822
|
+
names. Pasted material and an over-long prompt are skipped as
|
|
1823
|
+
`user_constraints` skips them."""
|
|
1824
|
+
text = prompt or ""
|
|
1825
|
+
if len(text) > CONSTRAINT_PROMPT_MAX:
|
|
1826
|
+
return []
|
|
1827
|
+
quoted = [q.span() for q in QUOTED.finditer(text)]
|
|
1828
|
+
found = [(m.start(), m.end(), rules) for shapes, *rules in GRANT_PACKS
|
|
1829
|
+
for m in shapes.finditer(text)]
|
|
1830
|
+
grants = [(a, b) for a, b, _r in found]
|
|
1831
|
+
found += [(m.start(), m.end(), kind) for kind, shapes in
|
|
1832
|
+
(("lift", LIFT_SHAPES), ("outward", LIFT_OUTWARD))
|
|
1833
|
+
for m in shapes.finditer(text)
|
|
1834
|
+
if not any(a <= m.start() < b for a, b in grants)]
|
|
1835
|
+
out = []
|
|
1836
|
+
for at, to, rules in sorted(found, key=lambda f: f[:2]):
|
|
1837
|
+
if any(a <= at < b for a, b in quoted):
|
|
1838
|
+
continue
|
|
1839
|
+
lift = isinstance(rules, str)
|
|
1840
|
+
start = max((e.end() for e in SENTENCE_END.finditer(text, 0, at)), default=0)
|
|
1841
|
+
end = SENTENCE_END.search(text, to)
|
|
1842
|
+
stop = end.start() if end else len(text)
|
|
1843
|
+
if not lift and _refuses(text, start, at, to, stop, *rules):
|
|
1844
|
+
continue
|
|
1845
|
+
sentence = " ".join(text[start:stop].split())
|
|
1846
|
+
clause = cut(redact(sentence.strip("-*• ")), CONSTRAINT_CHARS)
|
|
1847
|
+
classes = (sorted(outward_classes(text[at:to])) if rules == "outward"
|
|
1848
|
+
else sorted(outward_classes(sentence)) or ["all"])
|
|
1849
|
+
for cls in classes:
|
|
1850
|
+
out.append((clause, ("lift:" if lift else "") + cls))
|
|
1851
|
+
for clause in user_constraints(text):
|
|
1852
|
+
out += [(clause, "lift:" + cls) for cls in sorted(outward_classes(clause))]
|
|
1853
|
+
return list(dict.fromkeys(out))
|
|
1854
|
+
|
|
1855
|
+
|
|
1882
1856
|
|
|
1883
1857
|
def constraint_needle(clause):
|
|
1884
1858
|
"""The fragment of a constraint a summary keeps when it kept the constraint:
|
|
@@ -1900,13 +1874,14 @@ def constraint_needle(clause):
|
|
|
1900
1874
|
if len(kept) == NEEDLE_WORDS or bare != word:
|
|
1901
1875
|
break
|
|
1902
1876
|
return kept
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1877
|
+
kind, group, pack = _shape(m)
|
|
1878
|
+
if kind == "ask":
|
|
1879
|
+
needle = m.group(group)
|
|
1880
|
+
elif pack["word_order"] == "object_first":
|
|
1881
|
+
before = phrase(reversed(clause[:m.end(group)].split()))
|
|
1907
1882
|
needle = " ".join(reversed(before)).split("'")[0]
|
|
1908
1883
|
else:
|
|
1909
|
-
needle = m.group
|
|
1884
|
+
needle = " ".join(phrase(clause[m.start(group):].split()))
|
|
1910
1885
|
return needle or clause
|
|
1911
1886
|
|
|
1912
1887
|
|
|
@@ -1993,6 +1968,8 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
1993
1968
|
if event == "post_compact" or (event == "session_start" and isinstance(
|
|
1994
1969
|
payload, dict) and payload.get("source") == "compact"):
|
|
1995
1970
|
forget_seen(session_of(payload))
|
|
1971
|
+
# the context line may fire again on the compacted context
|
|
1972
|
+
tezgah_store.drop("nudged", context_key(session_of(payload)))
|
|
1996
1973
|
# The switch latch: every off() below answers for this session, and a
|
|
1997
1974
|
# prompt's turn marker (with its `authorized` row) is written first, so a
|
|
1998
1975
|
# switch the user's prompt names is honored from this very turn.
|
|
@@ -2023,6 +2000,13 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2023
2000
|
# even when the per-turn text is off. Never in DROP_ORDER, so no budget
|
|
2024
2001
|
# gives it up - it says the rules below are not being enforced.
|
|
2025
2002
|
gate = gate_inactive(session_id, payload)
|
|
2003
|
+
# The user's grants and lifts (`user_grants`), before the reminder
|
|
2004
|
+
# switch too: the Stop rule reads them whether or not this turn's text
|
|
2005
|
+
# is shown. A subagent's prompt is its parent's brief, never the
|
|
2006
|
+
# user's grant (omp's bridge sends `subagent`).
|
|
2007
|
+
for clause, target in ([] if isinstance(payload, dict) and payload.get("subagent")
|
|
2008
|
+
else user_grants(prompt)):
|
|
2009
|
+
note(session_id, GRANT_KIND, clause, target=target)
|
|
2026
2010
|
# per-turn nudge: openers decay over long sessions. Kept short because
|
|
2027
2011
|
# it is paid every turn, and on Claude the output style already carries
|
|
2028
2012
|
# the same rules on every response. The conditional rules ride along
|
|
@@ -2118,6 +2102,26 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2118
2102
|
if scratch:
|
|
2119
2103
|
command = str(scratch.get("detail") or "").strip()
|
|
2120
2104
|
parts.append(("scratch", SCRATCH_REMINDER % command[:SCRATCH_CHARS]))
|
|
2105
|
+
# The journal's stopped or open slices and background jobs: an omp
|
|
2106
|
+
# `/clear` fires no session_start, so the resume block never reaches the
|
|
2107
|
+
# next turn and this part is the one that names the work left in flight.
|
|
2108
|
+
pending = slice_lines(root, session_id)
|
|
2109
|
+
if pending:
|
|
2110
|
+
parts.append(("slices", "\n".join([SLICES_HEAD] + pending)))
|
|
2111
|
+
# The router's own context: above the threshold, one line asks it to
|
|
2112
|
+
# push heavy reading through a slice (orchestrator-flow-audit S2 F1).
|
|
2113
|
+
heavy = context_nudge(payload, session_id)
|
|
2114
|
+
if heavy:
|
|
2115
|
+
parts.append(("context", heavy))
|
|
2116
|
+
# a long session outlives a provider's reset: the same due check as
|
|
2117
|
+
# session start (one small JSON read), so a family rejoins omp's chain
|
|
2118
|
+
# without waiting for the next session
|
|
2119
|
+
try:
|
|
2120
|
+
import tezgah_models
|
|
2121
|
+
if tezgah_models.health_due():
|
|
2122
|
+
tezgah_models.start_health()
|
|
2123
|
+
except Exception:
|
|
2124
|
+
pass
|
|
2121
2125
|
stale = index_notice(cwd)
|
|
2122
2126
|
if stale:
|
|
2123
2127
|
parts.append(("index", stale))
|
|
@@ -2125,6 +2129,12 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2125
2129
|
parts.append(("offnote", "(off this session: %s)"
|
|
2126
2130
|
% ", ".join(disabled)))
|
|
2127
2131
|
text = budgeted(event, parts)
|
|
2132
|
+
# latched only when the budget kept it, so a dropped line comes back
|
|
2133
|
+
if heavy and heavy in text:
|
|
2134
|
+
try:
|
|
2135
|
+
tezgah_store.mark("nudged", context_key(session_id))
|
|
2136
|
+
except tezgah_store.ERRORS:
|
|
2137
|
+
pass # fail open: an unwritable store shows the line again
|
|
2128
2138
|
items = [ln for ln in relevant.split("\n") if ln.startswith("- ")]
|
|
2129
2139
|
shown = ([k for k, ln in zip(keys, items) if ln in text] if items
|
|
2130
2140
|
else keys if relevant and relevant in text else [])
|
|
@@ -2189,10 +2199,19 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2189
2199
|
"research to a host subagent and say the tooling is "
|
|
2190
2200
|
"unavailable; do not improvise its protocol."))
|
|
2191
2201
|
if event == "session_start":
|
|
2192
|
-
|
|
2193
|
-
if
|
|
2202
|
+
synced = sync_agents(root)
|
|
2203
|
+
if synced:
|
|
2194
2204
|
parts.append(("subagents",
|
|
2195
|
-
"Subagents (this repo, generated): %s" %
|
|
2205
|
+
"Subagents (this repo, generated): %s" % synced))
|
|
2206
|
+
# the provider health behind omp's subagent chains: re-probed in the
|
|
2207
|
+
# background at most every few hours or once a recorded reset passes, so
|
|
2208
|
+
# a family at its limit leaves the chain and rejoins it after the reset
|
|
2209
|
+
try:
|
|
2210
|
+
import tezgah_models
|
|
2211
|
+
if tezgah_models.health_due():
|
|
2212
|
+
tezgah_models.start_health()
|
|
2213
|
+
except Exception:
|
|
2214
|
+
pass
|
|
2196
2215
|
# orchestrate-off says "do not delegate", so no line may name delegates
|
|
2197
2216
|
steer = (None if off("orchestrate-off")
|
|
2198
2217
|
else steering(root, (payload or {}).get("host")))
|