@r1z4x/tezgah 1.5.1 → 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 +126 -1
- package/MANIFEST +11 -0
- package/README.md +6 -3
- package/bin/consult +13 -10
- package/bin/tezgah-docs +18 -16
- package/bin/tezgah-doctor +152 -0
- package/bin/tezgah-research +286 -12
- package/bin/tezgah-route +48 -1
- package/bin/tezgah-setup +27 -14
- package/bin/tezgah-task +771 -46
- package/bin/tezgah-taste +48 -10
- package/docs/architecture.md +7 -4
- package/docs/citations-baseline.json +1 -1
- package/docs/clarity-baseline.json +5 -5
- package/docs/contract.md +112 -25
- package/docs/evidence.md +201 -28
- package/docs/feature-audit.md +25 -4
- package/docs/gate.md +70 -33
- package/docs/glossary.md +4 -4
- package/docs/hosts.md +8 -8
- package/docs/index.json +30 -10
- package/docs/judge.md +9 -3
- package/docs/models.md +35 -2
- package/docs/operations.md +103 -3
- package/docs/research.md +392 -13
- package/docs/skills.md +24 -6
- 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 -9
- package/hooks/tezgah_clarity.py +7 -3
- package/hooks/tezgah_components.py +2 -1
- package/hooks/tezgah_context.py +460 -390
- package/hooks/tezgah_gate.py +640 -104
- 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 +286 -130
- package/hooks/tezgah_product.py +107 -0
- package/hooks/tezgah_rank.py +29 -27
- package/hooks/tezgah_replay.py +200 -30
- package/hooks/tezgah_research.py +1297 -12
- package/hooks/tezgah_skill_pick.py +58 -17
- package/hooks/tezgah_stopspec.py +7 -0
- package/hooks/tezgah_store.py +74 -27
- package/hooks/tezgah_task.py +917 -8
- 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 +119 -39
- package/output-styles/tezgah.md +76 -42
- package/package.json +1 -1
- package/skills/analyze-app/SKILL.md +57 -0
- package/skills/design-contract/EVALS.md +24 -1
- package/skills/design-contract/SKILL.md +17 -1
- package/skills/design-contract/data-views.md +86 -0
- package/skills/feature-audit/SKILL.md +32 -3
- package/skills/i-have-adhd/EVALS.md +22 -2
- package/skills/i-have-adhd/SKILL.md +12 -5
- package/skills/plan-add/EVALS.md +8 -8
- package/skills/plan-add/SKILL.md +22 -21
- package/skills/plan-sync/EVALS.md +2 -2
- package/skills/plan-sync/SKILL.md +14 -8
- package/skills/pm-frameworks/SKILL.md +6 -0
- package/skills/ponytail/EVALS.md +21 -2
- package/skills/ponytail/SKILL.md +4 -4
- package/skills/product-analysis/EVALS.md +23 -1
- package/skills/product-analysis/SKILL.md +50 -11
- package/skills/product-flow/EVALS.md +81 -0
- package/skills/product-flow/SKILL.md +91 -0
- package/skills/research/SKILL.md +99 -2
- package/skills/tezgah-contract/EVALS.md +52 -7
- package/skills/tezgah-contract/SKILL.md +204 -85
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")
|
|
@@ -787,16 +564,16 @@ def task_line(task):
|
|
|
787
564
|
and refuses a write the phase or the allowlist excludes, but a refusal
|
|
788
565
|
costs a turn - so the phase rides every user turn, and the refusal is never
|
|
789
566
|
the first the session hears of it. One line because it is paid every turn;
|
|
790
|
-
the allowlist is the globs as written, and the phase is stated as the
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
refuses is an invitation to a loop."""
|
|
567
|
+
the allowlist is the globs as written, and the phase is stated as the
|
|
568
|
+
orchestrator's own move: the session runs `tezgah-task` itself when the
|
|
569
|
+
work reaches the next phase and closes the plan when it is done, never
|
|
570
|
+
handing those steps to the user."""
|
|
795
571
|
paths = ", ".join(task.get("allowed_paths") or []) or "any path in the repo"
|
|
796
|
-
return ("Active task %s is in phase `%s`; writes allowed on: %s.
|
|
797
|
-
"
|
|
798
|
-
"
|
|
799
|
-
|
|
572
|
+
return ("Active task %s is in phase `%s`; writes allowed on: %s. Move the "
|
|
573
|
+
"phase yourself with `tezgah-task phase <next>` when the work reaches "
|
|
574
|
+
"it, and `tezgah-task close %s done` when it is merged - never hand "
|
|
575
|
+
"these steps to the user." % (task.get("id"), task.get("phase"),
|
|
576
|
+
paths, task.get("id")))
|
|
800
577
|
|
|
801
578
|
|
|
802
579
|
# --- the resume block: the live turn state a compact loses -------------------
|
|
@@ -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)
|
|
@@ -1186,10 +991,34 @@ def constraint_notice(cwd, session_id):
|
|
|
1186
991
|
return " ".join(render(subagent_core(core_for(cwd)[0])).split())
|
|
1187
992
|
|
|
1188
993
|
|
|
994
|
+
# A data ask said by its output: a dashboard, a chart, a KPI, a metric to choose.
|
|
995
|
+
# "Bu veriye uygun bir metric dashboard tasarla" armed nothing before plan 072.
|
|
996
|
+
# These words are code words as often as product words (`dashboard.json`, a
|
|
997
|
+
# metrics endpoint, a progress `gösterge`, the clipboard `pano`), and two rounds
|
|
998
|
+
# of noun denylists inside the product regex kept missing the next code sense.
|
|
999
|
+
# So the ask is judged per clause: it arms `product` only when no code marker -
|
|
1000
|
+
# a file name, an identifier, SQL, a build, test or runtime word - shares its
|
|
1001
|
+
# clause (decision 006: qualify the stem, never drop it).
|
|
1002
|
+
DATA_ASK = re.compile(r"\b(?:%s)" % alts("data_ask"))
|
|
1003
|
+
CODE_SENSE = re.compile(alts("code_sense"))
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
def _data_ask(low):
|
|
1007
|
+
"""Does one clause of the prompt ask for a data view or a metric choice with
|
|
1008
|
+
no code marker beside it? A clause ends at `!`, `?`, `;`, a newline or a full
|
|
1009
|
+
stop followed by a space, so `dashboard.json` stays one clause."""
|
|
1010
|
+
return any(DATA_ASK.search(clause) and not CODE_SENSE.search(clause)
|
|
1011
|
+
for clause in re.split(r"[!?;\n]|\.\s", low))
|
|
1012
|
+
|
|
1013
|
+
|
|
1189
1014
|
def classify_prompt(text):
|
|
1190
|
-
"""The conditional rule keys a prompt arms, from the shared hint table
|
|
1015
|
+
"""The conditional rule keys a prompt arms, from the shared hint table, plus
|
|
1016
|
+
`product` for a data ask (DATA_ASK, gated per clause by CODE_SENSE)."""
|
|
1191
1017
|
low = (text or "").lower()
|
|
1192
|
-
|
|
1018
|
+
keys = {key for key, pattern in PROMPT_HINTS if re.search(pattern, low)}
|
|
1019
|
+
if "product" not in keys and _data_ask(low):
|
|
1020
|
+
keys.add("product")
|
|
1021
|
+
return keys
|
|
1193
1022
|
|
|
1194
1023
|
|
|
1195
1024
|
def prompt_text(payload):
|
|
@@ -1407,11 +1236,22 @@ def session_of(payload):
|
|
|
1407
1236
|
# ADHD rules inline from the first turn (+1042 B), the fan-out pointer (+112 B)
|
|
1408
1237
|
# and the `tezgah-skill` line (+113 B) took the healthy fixture's session_start
|
|
1409
1238
|
# from 10579 B to 12059 B, so at 12000 B a healthy repo gave up its lessons
|
|
1410
|
-
# 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
|
|
1411
1251
|
# budget in bytes, not tokens: this file has no tokenizer and a wrong estimate
|
|
1412
1252
|
# would be worse than a bound.
|
|
1413
|
-
CONTEXT_BUDGET = {"session_start":
|
|
1414
|
-
"subagent_start": 5500, "user_prompt":
|
|
1253
|
+
CONTEXT_BUDGET = {"session_start": 15500, "post_compact": 15500,
|
|
1254
|
+
"subagent_start": 5500, "user_prompt": 6500}
|
|
1415
1255
|
DEFAULT_BUDGET = 13000
|
|
1416
1256
|
# The blocks in the order they are given up when the budget is exceeded, lowest
|
|
1417
1257
|
# value first: text another surface already carries (the plan table lives in the
|
|
@@ -1426,8 +1266,10 @@ DEFAULT_BUDGET = 13000
|
|
|
1426
1266
|
# block, which outlives every status and availability line because it is the only
|
|
1427
1267
|
# one that says what THIS session was doing (a `git log` and a ledger read are
|
|
1428
1268
|
# turns the session would otherwise spend) but still yields to a rule - then the
|
|
1429
|
-
#
|
|
1430
|
-
#
|
|
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
|
|
1431
1273
|
# stops a refused write before it happens - then the delta, and the skill pointer
|
|
1432
1274
|
# last. A key absent from this tuple is never dropped: the always-on core, the
|
|
1433
1275
|
# per-turn reminder and an armed paragraph ARE the rules, and a budget that could
|
|
@@ -1435,8 +1277,9 @@ DEFAULT_BUDGET = 13000
|
|
|
1435
1277
|
# exists to prevent, not one it may cause.
|
|
1436
1278
|
DROP_ORDER = ("knowledge", "worktrees", "lessons", "skill", "lessons_turn",
|
|
1437
1279
|
"taste", "plans", "subagents", "steer", "orca", "consult", "research",
|
|
1438
|
-
"research_broken", "graph", "offnote", "orchestrate",
|
|
1439
|
-
"resume", "scratch", "task", "delta",
|
|
1280
|
+
"research_broken", "graph", "offnote", "orchestrate",
|
|
1281
|
+
"index", "resume", "slices", "context", "scratch", "task", "delta",
|
|
1282
|
+
"pointer")
|
|
1440
1283
|
|
|
1441
1284
|
|
|
1442
1285
|
def _first_item(text):
|
|
@@ -1558,7 +1401,8 @@ SCRATCH_CHARS = 120
|
|
|
1558
1401
|
# while the tool hooks never run - an unloaded plugin, a matcher the host
|
|
1559
1402
|
# renamed, a hook path that moved. The gate is then off and nothing says so.
|
|
1560
1403
|
# What the prompt hook can observe is the host's own transcript (Claude and
|
|
1561
|
-
# 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
|
|
1562
1406
|
# tool calls in the transcript since the session's first turn marker, and not
|
|
1563
1407
|
# one row from the gate over the same span, is a gate that is not running. Only
|
|
1564
1408
|
# a bounded tail of the transcript is read, the ledger is scanned as bytes, and
|
|
@@ -1567,7 +1411,8 @@ GATE_TAIL = 262144
|
|
|
1567
1411
|
GATE_MIN_CALLS = 3
|
|
1568
1412
|
# The tool names whose every call leaves a PreToolUse row (`began`,
|
|
1569
1413
|
# tezgah_gate.decision's write and shell branch) and every refused one a `deny`:
|
|
1570
|
-
# 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
|
|
1571
1416
|
# tool the gate may pass without a row (Read, Grep, Task, WebFetch, an MCP
|
|
1572
1417
|
# call) is not counted, so its absence from the ledger is never read as a
|
|
1573
1418
|
# disarmed gate.
|
|
@@ -1577,7 +1422,7 @@ GATE_INACTIVE = (
|
|
|
1577
1422
|
"tezgah gate inactive on this host: %d gated tool call(s) since this "
|
|
1578
1423
|
"session's first turn and not one row from the gate, so the shortcut "
|
|
1579
1424
|
"denials and the Stop check are not running. Say so before claiming a check "
|
|
1580
|
-
"passed,
|
|
1425
|
+
"passed, run `tezgah-setup --report` yourself and quote its MISS rows.")
|
|
1581
1426
|
|
|
1582
1427
|
|
|
1583
1428
|
def _iso_epoch(value):
|
|
@@ -1588,25 +1433,32 @@ def _iso_epoch(value):
|
|
|
1588
1433
|
return None
|
|
1589
1434
|
|
|
1590
1435
|
|
|
1591
|
-
def
|
|
1592
|
-
"""
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
"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."""
|
|
1596
1440
|
if not isinstance(path, str) or not path.endswith(".jsonl"):
|
|
1597
|
-
return
|
|
1441
|
+
return []
|
|
1598
1442
|
try:
|
|
1599
1443
|
with open(path, "rb") as fh:
|
|
1600
1444
|
size = fh.seek(0, 2)
|
|
1601
1445
|
fh.seek(max(0, size - GATE_TAIL))
|
|
1602
1446
|
lines = fh.read().split(b"\n")
|
|
1603
1447
|
except OSError:
|
|
1604
|
-
return
|
|
1605
|
-
if size > GATE_TAIL
|
|
1606
|
-
|
|
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."""
|
|
1607
1458
|
calls = 0
|
|
1608
|
-
for raw in
|
|
1609
|
-
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):
|
|
1610
1462
|
continue
|
|
1611
1463
|
try:
|
|
1612
1464
|
row = json.loads(raw)
|
|
@@ -1619,7 +1471,7 @@ def _transcript_calls(path, since):
|
|
|
1619
1471
|
msg = row.get("message")
|
|
1620
1472
|
if isinstance(msg, dict) and isinstance(msg.get("content"), list):
|
|
1621
1473
|
names = [b.get("name") for b in msg["content"]
|
|
1622
|
-
if isinstance(b, dict) and b.get("type")
|
|
1474
|
+
if isinstance(b, dict) and b.get("type") in ("tool_use", "toolCall")]
|
|
1623
1475
|
pay = row.get("payload")
|
|
1624
1476
|
if isinstance(pay, dict) and pay.get("type") == "function_call":
|
|
1625
1477
|
names.append(pay.get("name"))
|
|
@@ -1627,10 +1479,72 @@ def _transcript_calls(path, since):
|
|
|
1627
1479
|
return calls
|
|
1628
1480
|
|
|
1629
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
|
+
|
|
1630
1543
|
# The ledger kinds a hook other than the tool hooks writes: the prompt hook's
|
|
1631
1544
|
# turn marker, judge row, lesson rows and disarm row, the Stop hook's claim,
|
|
1632
1545
|
# refusal, after_block, shape and stop_spec rows, the subagent-end hook's subagent_end row,
|
|
1633
|
-
# 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
|
|
1634
1548
|
# row (any hook), the damage row any reader writes and the session start's
|
|
1635
1549
|
# attest row. Every other kind - deny, nudge, began, snapshot, ... - can only
|
|
1636
1550
|
# come from a tool hook.
|
|
@@ -1640,7 +1554,7 @@ def _transcript_calls(path, since):
|
|
|
1640
1554
|
NOT_TOOL_HOOK = frozenset((b"turn", b"judge", b"claim", b"refusal",
|
|
1641
1555
|
b"after_block", b"subagent_end", b"stop_spec",
|
|
1642
1556
|
b"shape", b"compact", b"orch", b"crash", b"route",
|
|
1643
|
-
b"spawned", b"lesson", b"disarm", b"ledger_damage",
|
|
1557
|
+
b"spawned", b"forked", b"lesson", b"disarm", b"ledger_damage",
|
|
1644
1558
|
b"attest"))
|
|
1645
1559
|
# The rows PostToolUse writes (`note_tool`): they prove the host ran the call,
|
|
1646
1560
|
# not that the gate saw it - a PostToolUse hook keeps writing them while a
|
|
@@ -1777,15 +1691,31 @@ POINTER_NEEDLE = "tezgah-contract"
|
|
|
1777
1691
|
# constraint that the user never meant is a rule the session did not get from
|
|
1778
1692
|
# the user. tests/constraint-fixtures.md holds the real prompts it was measured
|
|
1779
1693
|
# on, its false positives listed.
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
r"(?
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
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
|
+
|
|
1789
1719
|
# Pasted material is not the user's constraint: a shape that starts inside a
|
|
1790
1720
|
# quoted span, inline code, a fenced block or a `>` quote line is skipped (a
|
|
1791
1721
|
# quoted object after an unquoted shape stays the user's), and a prompt
|
|
@@ -1795,11 +1725,12 @@ CONSTRAINT_SHAPES = re.compile(
|
|
|
1795
1725
|
# log alone matched six clauses of another agent's reasoning. ponytail: a long
|
|
1796
1726
|
# prompt the user did write loses its constraints - the cost of a length rule.
|
|
1797
1727
|
CONSTRAINT_PROMPT_MAX = 12000
|
|
1728
|
+
# Single quotes count only off a word ("main'e" is no quote), with ‘…’ and «…».
|
|
1798
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}»"
|
|
1799
1731
|
r"|^\s*>.*$", re.S | re.M)
|
|
1800
1732
|
SENTENCE_END = re.compile(r"[.!?;:](?=\s|$)|\n")
|
|
1801
|
-
NEEDLE_STOP = frozenset(
|
|
1802
|
-
"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"])
|
|
1803
1734
|
QUOTE_MARKS = re.compile(r"[`\"“”]")
|
|
1804
1735
|
NEEDLE_WORDS = 3
|
|
1805
1736
|
CONSTRAINT_CHARS = 120
|
|
@@ -1809,8 +1740,8 @@ CONSTRAINTS_MAX = 5
|
|
|
1809
1740
|
def user_constraints(prompt):
|
|
1810
1741
|
"""The constraint clauses a prompt issues, redacted and cut, in order and
|
|
1811
1742
|
once each. A clause runs from the shape to the end of its sentence - from the
|
|
1812
|
-
sentence's start for
|
|
1813
|
-
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."""
|
|
1814
1745
|
text = prompt or ""
|
|
1815
1746
|
if len(text) > CONSTRAINT_PROMPT_MAX:
|
|
1816
1747
|
return []
|
|
@@ -1819,8 +1750,11 @@ def user_constraints(prompt):
|
|
|
1819
1750
|
for m in CONSTRAINT_SHAPES.finditer(text):
|
|
1820
1751
|
if any(a <= m.start() < b for a, b in quoted):
|
|
1821
1752
|
continue
|
|
1753
|
+
kind, _group, pack = _shape(m)
|
|
1822
1754
|
start = (max((e.end() for e in SENTENCE_END.finditer(text, 0, m.start())),
|
|
1823
|
-
default=0)
|
|
1755
|
+
default=0)
|
|
1756
|
+
if kind == "obj" and pack["word_order"] == "object_first"
|
|
1757
|
+
else m.start())
|
|
1824
1758
|
end = SENTENCE_END.search(text, m.end())
|
|
1825
1759
|
clause = " ".join(text[start:end.start() if end else len(text)].split())
|
|
1826
1760
|
clause = cut(redact(clause.strip("-*• ")), CONSTRAINT_CHARS)
|
|
@@ -1828,6 +1762,97 @@ def user_constraints(prompt):
|
|
|
1828
1762
|
out.append(clause)
|
|
1829
1763
|
return out
|
|
1830
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
|
+
|
|
1831
1856
|
|
|
1832
1857
|
def constraint_needle(clause):
|
|
1833
1858
|
"""The fragment of a constraint a summary keeps when it kept the constraint:
|
|
@@ -1849,13 +1874,14 @@ def constraint_needle(clause):
|
|
|
1849
1874
|
if len(kept) == NEEDLE_WORDS or bare != word:
|
|
1850
1875
|
break
|
|
1851
1876
|
return kept
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
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()))
|
|
1856
1882
|
needle = " ".join(reversed(before)).split("'")[0]
|
|
1857
1883
|
else:
|
|
1858
|
-
needle = m.group
|
|
1884
|
+
needle = " ".join(phrase(clause[m.start(group):].split()))
|
|
1859
1885
|
return needle or clause
|
|
1860
1886
|
|
|
1861
1887
|
|
|
@@ -1942,6 +1968,8 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
1942
1968
|
if event == "post_compact" or (event == "session_start" and isinstance(
|
|
1943
1969
|
payload, dict) and payload.get("source") == "compact"):
|
|
1944
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)))
|
|
1945
1973
|
# The switch latch: every off() below answers for this session, and a
|
|
1946
1974
|
# prompt's turn marker (with its `authorized` row) is written first, so a
|
|
1947
1975
|
# switch the user's prompt names is honored from this very turn.
|
|
@@ -1972,6 +2000,13 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
1972
2000
|
# even when the per-turn text is off. Never in DROP_ORDER, so no budget
|
|
1973
2001
|
# gives it up - it says the rules below are not being enforced.
|
|
1974
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)
|
|
1975
2010
|
# per-turn nudge: openers decay over long sessions. Kept short because
|
|
1976
2011
|
# it is paid every turn, and on Claude the output style already carries
|
|
1977
2012
|
# the same rules on every response. The conditional rules ride along
|
|
@@ -2067,6 +2102,26 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2067
2102
|
if scratch:
|
|
2068
2103
|
command = str(scratch.get("detail") or "").strip()
|
|
2069
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
|
|
2070
2125
|
stale = index_notice(cwd)
|
|
2071
2126
|
if stale:
|
|
2072
2127
|
parts.append(("index", stale))
|
|
@@ -2074,6 +2129,12 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2074
2129
|
parts.append(("offnote", "(off this session: %s)"
|
|
2075
2130
|
% ", ".join(disabled)))
|
|
2076
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
|
|
2077
2138
|
items = [ln for ln in relevant.split("\n") if ln.startswith("- ")]
|
|
2078
2139
|
shown = ([k for k, ln in zip(keys, items) if ln in text] if items
|
|
2079
2140
|
else keys if relevant and relevant in text else [])
|
|
@@ -2138,10 +2199,19 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2138
2199
|
"research to a host subagent and say the tooling is "
|
|
2139
2200
|
"unavailable; do not improvise its protocol."))
|
|
2140
2201
|
if event == "session_start":
|
|
2141
|
-
|
|
2142
|
-
if
|
|
2202
|
+
synced = sync_agents(root)
|
|
2203
|
+
if synced:
|
|
2143
2204
|
parts.append(("subagents",
|
|
2144
|
-
"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
|
|
2145
2215
|
# orchestrate-off says "do not delegate", so no line may name delegates
|
|
2146
2216
|
steer = (None if off("orchestrate-off")
|
|
2147
2217
|
else steering(root, (payload or {}).get("host")))
|