@r1z4x/tezgah 1.6.0 → 1.8.2
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 +140 -1
- package/MANIFEST +7 -0
- package/README.md +6 -3
- package/bin/tezgah-decisions +6 -14
- package/bin/tezgah-design +21 -5
- package/bin/tezgah-docs +18 -16
- package/bin/tezgah-doctor +152 -0
- package/bin/tezgah-lessons +4 -1
- package/bin/tezgah-research +249 -17
- package/bin/tezgah-route +70 -8
- package/bin/tezgah-setup +26 -13
- package/bin/tezgah-task +877 -60
- package/bin/tezgah-taste +56 -15
- package/docs/architecture.md +28 -11
- package/docs/clarity-baseline.json +2 -2
- package/docs/contract.md +108 -24
- package/docs/environment.md +3 -1
- package/docs/evidence.md +209 -28
- package/docs/feature-audit.md +1 -1
- package/docs/gate.md +78 -23
- 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 +75 -14
- package/docs/operations.md +133 -3
- package/docs/research.md +349 -15
- package/docs/skills.md +1 -1
- package/docs/status-line.md +5 -5
- package/docs/testing.md +20 -13
- 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-auto-init.py +29 -1
- 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 +53 -17
- package/hooks/tezgah_clarity.py +7 -3
- package/hooks/tezgah_context.py +489 -438
- package/hooks/tezgah_gate.py +996 -80
- package/hooks/tezgah_integrity.py +1547 -197
- package/hooks/tezgah_models.py +427 -45
- package/hooks/tezgah_paths.py +147 -16
- package/hooks/tezgah_policy.py +261 -129
- package/hooks/tezgah_rank.py +29 -27
- package/hooks/tezgah_replay.py +200 -30
- package/hooks/tezgah_research.py +1303 -14
- package/hooks/tezgah_stopspec.py +7 -0
- package/hooks/tezgah_store.py +74 -27
- package/hooks/tezgah_task.py +1201 -40
- package/hooks/tezgah_taste.py +8 -4
- package/hosts/codex/hook.py +32 -13
- package/hosts/cursor/hook.py +32 -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 +273 -6
- 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 +27 -11
- package/skills/plan-add/render_table.py +8 -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 +154 -59
package/hooks/tezgah_context.py
CHANGED
|
@@ -19,20 +19,22 @@ 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
|
-
|
|
35
|
+
ensure_workspace, fallback_cache, have_judge_key, language,
|
|
36
|
+
off, orx_bin, packs, pony_level, project_top, root_for,
|
|
37
|
+
roots, say, tool,
|
|
36
38
|
workspace, workspace_from_repo, worktrees, writable_dir)
|
|
37
39
|
|
|
38
40
|
try: # The task record is the active plan's frontmatter (see tezgah_task), read
|
|
@@ -55,325 +57,95 @@ except ImportError: # pragma: no cover - only where the module has not landed
|
|
|
55
57
|
|
|
56
58
|
# A prompt that matches one of these arms the matching conditional rule for that
|
|
57
59
|
# turn only. Kept as (key, compiled regex) so the arming is one pass and the
|
|
58
|
-
# patterns are reviewable.
|
|
59
|
-
#
|
|
60
|
+
# patterns are reviewable. The words are data: each language pack
|
|
61
|
+
# (hooks/lang/<code>.json, `hints.<key>`) lists its own half, and `_hints`
|
|
62
|
+
# joins every pack's into one pattern per key, so a prompt in any shipped
|
|
63
|
+
# language arms the same rules. Word-ish boundaries keep "deploy" from firing
|
|
64
|
+
# inside an identifier.
|
|
60
65
|
#
|
|
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
|
-
#
|
|
66
|
+
# Turkish is agglutinative and its hints are verb/noun stems, so an
|
|
67
|
+
# English-style trailing \b would kill the most natural phrasing (düzgün
|
|
68
|
+
# çalışsın, hipotezi test et): such a fragment carries \w* where a suffix can
|
|
69
|
+
# land, which \b then closes. \w is Unicode-aware. A whole word keeps its plain
|
|
70
|
+
# boundary, so "deney" still does not fire on "deneyim".
|
|
66
71
|
#
|
|
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
|
|
72
|
+
# The craft words in the spec class that are ALSO ordinary nouns are qualified
|
|
73
|
+
# by a UI object rather than matched bare (a pack fragment `{"ui_ask": word}`):
|
|
74
|
+
# the bare word armed the paragraph on asks that had no UI in them at all.
|
|
75
|
+
# `_ui_ask` expands one in the pack's own word order - a `verb_first` pack (en)
|
|
76
|
+
# takes a UI object beside the word or a quality word after it, an
|
|
77
|
+
# `object_first` pack (tr: "adım akışını sadeleştir" is a UI ask, "bu
|
|
78
|
+
# fonksiyonu sadeleştir" a refactor) only an object before it - and both halves
|
|
79
|
+
# are pinned in tests/test_hint_coverage.py. Every object is word-bounded, so
|
|
80
|
+
# `app` cannot match inside `happen`.
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _ui_ask(word, pack):
|
|
84
|
+
"""`word` as a UI ask in `pack`'s word order. The gap is bounded and stops
|
|
85
|
+
at a sentence's end, so the qualification is about the phrase the word sits
|
|
86
|
+
in and not about the paragraph it is in."""
|
|
87
|
+
objects = "|".join(pack["ui_objects"]) or "(?!)"
|
|
88
|
+
if pack["word_order"] == "object_first":
|
|
89
|
+
# the object takes its case suffix through \w*
|
|
90
|
+
return r"(?:\b(?:%s)\w*[^.!?\n]{0,40}\b(?:%s))" % (objects, word)
|
|
91
|
+
obj = r"\b(?:%s)\b" % objects
|
|
92
92
|
return (r"(?:%s[^.!?\n]{0,40}\b(?:%s)\b"
|
|
93
93
|
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
|
-
)
|
|
94
|
+
% (obj, word, word, obj, "|".join(pack["ui_quality"]) or "(?!)"))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _hints(key):
|
|
98
|
+
"""Every pack's `hints.<key>` fragments as one `\\b(...)\\b` pattern."""
|
|
99
|
+
return r"\b(%s)\b" % "|".join(
|
|
100
|
+
frag if isinstance(frag, str) else _ui_ask(frag["ui_ask"], pack)
|
|
101
|
+
for pack in packs().values() for frag in pack["hints"][key])
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _fanout():
|
|
105
|
+
"""The fan-out hint: two or more independent items - a numbered or bulleted
|
|
106
|
+
list of at least two entries on their own lines, or the explicit delegation
|
|
107
|
+
ask. The owner had to say "do everything with subagents and in parallel" on
|
|
108
|
+
every task because nothing armed the fan-out rule from the shape of the
|
|
109
|
+
request. `parallelize` alone is code work, so only the delegation phrasings
|
|
110
|
+
count. The owner's own shapes armed nothing until 2026-10-09: `1 - `, `1- `,
|
|
111
|
+
`1:`, `a)`, `+` items, an inline `1. ... 2. ...`, "subagentlerle", three
|
|
112
|
+
files named as one list, "First, ... Second, ...", and three imperative
|
|
113
|
+
clauses in one sentence (clause-initial in a `verb_first` pack, clause-final
|
|
114
|
+
in an `object_first` one). A timestamp, a range and a traceback stay out
|
|
115
|
+
(the corpus)."""
|
|
116
|
+
# A list item's marker: `1.`, `1)`, `1 -`, `1:`, `a)`, or a `-`/`*`/`+`/`•`
|
|
117
|
+
# bullet, then its text.
|
|
118
|
+
item = r"(?:\d+[ \t]*[.):\u2013-]|[a-z]\)|[-*+\u2022])[ \t]+\S"
|
|
119
|
+
# A file a prompt names: a path with a directory, or a name with an
|
|
120
|
+
# extension. Anchored at a token start: unanchored, a long run of word
|
|
121
|
+
# characters made every position a start and the search quadratic (100 KB
|
|
122
|
+
# took minutes).
|
|
123
|
+
file = (r"(?<![\w./-])[`'\"]?(?:(?:[\w.-]+/)+[\w.-]*\w|"
|
|
124
|
+
r"\w[\w-]*\.[a-z]{1,4}\b)[`'\"]?")
|
|
125
|
+
join = "|".join(w for p in packs().values() for w in p["fan_join"])
|
|
126
|
+
parts = [r"(?ms:^[ \t]*" + item + r".*?^[ \t]*" + item + r")",
|
|
127
|
+
r"(?<![\w.])1[ \t]*[.)-][ \t]+\S[^\n]*?(?<![\w.])2[ \t]*[.)-][ \t]+\S",
|
|
128
|
+
file + r"(?:[ \t]*(?:,[ \t]*(?:(?:%s)[ \t]+)?|(?:%s|&)[ \t]+)" % (join, join)
|
|
129
|
+
+ file + r"){2,}"]
|
|
130
|
+
for pack in packs().values():
|
|
131
|
+
first, second = pack["fan_ordinals"]
|
|
132
|
+
parts.append(r"(?s:\b%s[,:].*?\b%s[,:])" % (first, second))
|
|
133
|
+
for pack in packs().values():
|
|
134
|
+
verbs, joins = "|".join(pack["fan_verbs"]), "|".join(pack["fan_join"])
|
|
135
|
+
if pack["word_order"] == "object_first":
|
|
136
|
+
parts.append(r"(?m:(?:\b(?:%s)\w{0,4}[ \t]*(?:[,;.]|\b(?:%s)\b|$)"
|
|
137
|
+
r"[^\n]*?){3})" % (verbs, joins))
|
|
138
|
+
else:
|
|
139
|
+
parts.append(r"(?m:(?:(?:^|[,;]|\b(?:%s)\b)[ \t]*(?:(?:%s)[ \t]+)?(?:%s)"
|
|
140
|
+
r"\b[^\n]*?){3})" % (joins, joins, verbs))
|
|
141
|
+
parts.append(r"\b(?:%s)\b" % "|".join(
|
|
142
|
+
d for p in packs().values() for d in p["fan_directives"]))
|
|
143
|
+
return "|".join(parts)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
PROMPT_HINTS = tuple((key, _hints(key)) for key in
|
|
147
|
+
("spec", "consult", "research", "product", "graph")) + (
|
|
148
|
+
("fanout", _fanout()),)
|
|
377
149
|
|
|
378
150
|
# the detached auto-index worker (lock-guarded, retrying); same dir as this file
|
|
379
151
|
INDEX_WORKER = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
@@ -390,7 +162,7 @@ CORE_RULES = (
|
|
|
390
162
|
("ponytail", "**Ponytail (minimal code).**"),
|
|
391
163
|
("adhd", "**Output shape: ADHD-friendly.**"),
|
|
392
164
|
("fidelity", "**Deliver the whole ask; never the shortcut.**"),
|
|
393
|
-
("integrity", '**Integrity: evidence, or
|
|
165
|
+
("integrity", '**Integrity: evidence, or a named blocker.**'),
|
|
394
166
|
("loop", "**Loop discipline.**"),
|
|
395
167
|
("scope", "**Session scope: the user's repo, not tezgah.**"),
|
|
396
168
|
("spec", "**Spec before building.**"),
|
|
@@ -417,11 +189,12 @@ CORE_RULES = (
|
|
|
417
189
|
REMINDER_CLAUSES = (
|
|
418
190
|
("exec", "{REPLY_SHORT}, BLUF, "),
|
|
419
191
|
("adhd", "answer first - no recap, no closer, at most five ranked items; "),
|
|
420
|
-
("ponytail", "code minimal per ponytail (code first
|
|
192
|
+
("ponytail", "code minimal per ponytail (code first; a note only for a "
|
|
193
|
+
"requested part you cut); "),
|
|
421
194
|
("lessons", ".tezgah/lessons.md lines are standing constraints; "),
|
|
422
195
|
("consult", "consult before irreversible calls; "),
|
|
423
196
|
("integrity", re.compile(r'done/tested claims need observed evidence -> .*?'
|
|
424
|
-
r'unverified "done"; ')),
|
|
197
|
+
r'unverified "done"; .*?under "\{BLOCKERS_HEADING\}"; ')),
|
|
425
198
|
)
|
|
426
199
|
|
|
427
200
|
|
|
@@ -448,12 +221,17 @@ def prompt_reminder(drop=()):
|
|
|
448
221
|
|
|
449
222
|
|
|
450
223
|
def render(text, root=""):
|
|
451
|
-
"""Fill the path placeholders with stable, existing paths, and the
|
|
452
|
-
|
|
224
|
+
"""Fill the path placeholders with stable, existing paths, and the language
|
|
225
|
+
placeholders - the reply language and the contract's own tokens, `{UNVERIFIED}`,
|
|
226
|
+
`{BLOCKERS_HEADING}`, `{DECISION}`... - with the configured language's words
|
|
227
|
+
(`tezgah_paths.say`; REPLY_ANY's reply clause under `any`)."""
|
|
453
228
|
if not text:
|
|
454
229
|
return text
|
|
455
|
-
|
|
456
|
-
|
|
230
|
+
words = say()
|
|
231
|
+
if language() == "any":
|
|
232
|
+
words.update(REPLY_ANY)
|
|
233
|
+
for key, word in words.items():
|
|
234
|
+
text = text.replace("{%s}" % key.upper(), word)
|
|
457
235
|
return (text.replace("{CONSULT_BIN}", tool("consult"))
|
|
458
236
|
.replace("{CODEGEN_BIN}", tool("codegen"))
|
|
459
237
|
.replace("{ORX_BIN}", orx_bin() or "orx")
|
|
@@ -606,10 +384,12 @@ def git(root, *args):
|
|
|
606
384
|
|
|
607
385
|
def repo_root(cwd):
|
|
608
386
|
"""The unit of work: the git top-level under a root, else the first path
|
|
609
|
-
component below the root (so a non-git project still gets one slug).
|
|
387
|
+
component below the root (so a non-git project still gets one slug). Inside
|
|
388
|
+
`.tezgah` git answers the workspace's own repository; the unit is the
|
|
389
|
+
project around it, or the session start sets the project up in `.tezgah`."""
|
|
610
390
|
top = git(cwd, "rev-parse", "--show-toplevel")
|
|
611
391
|
if top and under(top):
|
|
612
|
-
return os.path.realpath(top)
|
|
392
|
+
return project_top(os.path.realpath(top))
|
|
613
393
|
base = root_for(cwd)
|
|
614
394
|
if not base:
|
|
615
395
|
return os.path.realpath(cwd)
|
|
@@ -755,28 +535,49 @@ def open_plans(root):
|
|
|
755
535
|
|
|
756
536
|
|
|
757
537
|
def sibling_line(root):
|
|
758
|
-
"""One line naming every checkout of this repository
|
|
759
|
-
|
|
538
|
+
"""One line naming every checkout of this repository, or "" when it has one.
|
|
539
|
+
The main checkout's `.tezgah` is the repository's one workspace (plan 097),
|
|
540
|
+
named with its open plan and research line counts. A linked checkout whose
|
|
541
|
+
own `.tezgah` still holds data is named with what it holds, because `git
|
|
542
|
+
worktree remove` deletes it with the checkout. Fork-free (the pinned
|
|
760
543
|
session-start git budget): `worktrees` reads `.git/worktrees/*/gitdir`, and
|
|
761
|
-
the counts are directory listings.
|
|
762
|
-
this only makes the others visible."""
|
|
544
|
+
the counts are directory listings."""
|
|
763
545
|
checkouts = worktrees(root)
|
|
764
546
|
here = os.path.realpath(root)
|
|
765
547
|
if here not in checkouts:
|
|
766
548
|
checkouts.append(here)
|
|
767
549
|
if len(checkouts) < 2:
|
|
768
550
|
return ""
|
|
551
|
+
main = checkouts[0]
|
|
769
552
|
items = []
|
|
770
553
|
for checkout in checkouts:
|
|
771
554
|
ws = workspace(checkout)
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
555
|
+
if checkout == main:
|
|
556
|
+
state = ("no workspace yet" if not os.path.isdir(ws) else
|
|
557
|
+
"%d open plan(s), %d research line(s)"
|
|
558
|
+
% (len(glob.glob(os.path.join(ws, "plans", "open", "*.md"))),
|
|
559
|
+
len(tezgah_research.slugs(checkout))))
|
|
560
|
+
else:
|
|
561
|
+
try:
|
|
562
|
+
# a `.tezgah` that links to the main checkout's is that workspace
|
|
563
|
+
# (review of plan 097, F3): removing the worktree drops the link
|
|
564
|
+
held = ([] if os.path.realpath(ws) == os.path.realpath(workspace(main))
|
|
565
|
+
else sorted(set(os.listdir(ws)) - {".git"}))
|
|
566
|
+
except OSError:
|
|
567
|
+
held = []
|
|
568
|
+
lines = tezgah_research.slugs(checkout) if held else []
|
|
569
|
+
state = ("uses the main checkout's `.tezgah`" if not held else
|
|
570
|
+
"its own `.tezgah` still holds %s, which `git worktree remove` "
|
|
571
|
+
"deletes: move each research line with `%s import %s <slug>` "
|
|
572
|
+
"from the main checkout, and the rest by hand"
|
|
573
|
+
% (", ".join(held), tool("tezgah-research"), checkout))
|
|
574
|
+
if lines:
|
|
575
|
+
state += " (lines: %s)" % ", ".join(lines)
|
|
776
576
|
items.append("%s (%s%s)" % (checkout, "this checkout, " if checkout == here
|
|
777
577
|
else "", state))
|
|
778
|
-
return ("Worktrees: %d checkouts of this repository
|
|
779
|
-
"`.tezgah
|
|
578
|
+
return ("Worktrees: %d checkouts of this repository share the main checkout's "
|
|
579
|
+
"`.tezgah` - plans, lessons, research and analysis are written there, "
|
|
580
|
+
"never into a worktree's own: %s. Detail: `%s --all`."
|
|
780
581
|
% (len(checkouts), "; ".join(items), tool("tezgah-research")))
|
|
781
582
|
|
|
782
583
|
|
|
@@ -822,6 +623,13 @@ RESUME_PLAN_CHARS = 40
|
|
|
822
623
|
RESUME_COMMIT_CHARS = 38
|
|
823
624
|
RESUME_CHECK_CHARS = 40
|
|
824
625
|
RESUME_FILES = 3
|
|
626
|
+
# The slice journal's lines (plan 081): the newest three stopped or open slices
|
|
627
|
+
# and background jobs, each already bounded by the journal and cut again here.
|
|
628
|
+
# They ride the resume block and, because an omp `/clear` fires no
|
|
629
|
+
# session_start, every prompt's `slices` part while any is pending.
|
|
630
|
+
SLICE_LINES = 3
|
|
631
|
+
SLICE_CHARS = 160
|
|
632
|
+
SLICES_HEAD = "Slices left stopped or open (the journal, `tezgah-task slice reconcile`):"
|
|
825
633
|
|
|
826
634
|
|
|
827
635
|
def _plan_facts(path):
|
|
@@ -863,8 +671,10 @@ def _active_plan(root):
|
|
|
863
671
|
report of another plan's State/Next read as the active one is the block
|
|
864
672
|
inventing a task, which is worse than the line it costs. (None, False) when
|
|
865
673
|
the repo keeps no open plan."""
|
|
866
|
-
|
|
867
|
-
|
|
674
|
+
from tezgah_paths import linked_main
|
|
675
|
+
# a linked worktree's plans are its main checkout's (plan 097)
|
|
676
|
+
paths = sorted(glob.glob(os.path.join(linked_main(root) or root, ".tezgah",
|
|
677
|
+
"plans", "open", "*.md")))
|
|
868
678
|
if not paths:
|
|
869
679
|
return None, False
|
|
870
680
|
branch = git(root, "rev-parse", "--abbrev-ref", "HEAD")
|
|
@@ -876,13 +686,33 @@ def _active_plan(root):
|
|
|
876
686
|
return paths[0], False
|
|
877
687
|
|
|
878
688
|
|
|
689
|
+
def slice_lines(root, session_id=None, others=False):
|
|
690
|
+
"""This session's pending slices and jobs (`tezgah_task.slice_lines`): at
|
|
691
|
+
most SLICE_LINES lines of at most SLICE_CHARS each, newest first, or []
|
|
692
|
+
with no session, under `slices-off`, without the journal, or when it
|
|
693
|
+
raises - a broken journal costs the lines, never the injection. Another
|
|
694
|
+
session's slices are never listed: they are that session's to resume (omp's
|
|
695
|
+
`--resume` keeps the session id). With `others` - the resume block, read at
|
|
696
|
+
session start and after a compaction - one line counts them, so a session
|
|
697
|
+
that came back under a new id still learns they exist."""
|
|
698
|
+
reader = getattr(tezgah_task, "slice_lines", None)
|
|
699
|
+
if not reader or not session_id or off("slices-off"):
|
|
700
|
+
return []
|
|
701
|
+
try:
|
|
702
|
+
lines = reader(root, limit=SLICE_LINES, session=session_id, others=others) or []
|
|
703
|
+
except Exception:
|
|
704
|
+
return []
|
|
705
|
+
return [cut(str(ln), SLICE_CHARS) for ln in lines[:SLICE_LINES + 1]]
|
|
706
|
+
|
|
707
|
+
|
|
879
708
|
def resume_state(root, session_id):
|
|
880
709
|
"""The live turn state as a context block, or "" when every part is empty.
|
|
881
710
|
|
|
882
|
-
The
|
|
711
|
+
The five parts, in order: the active plan's State and Next, the branch's last
|
|
883
712
|
RESUME_LOG commits, the newest `verify_ok`/`verify_fail` the ledger holds,
|
|
884
|
-
|
|
885
|
-
|
|
713
|
+
the files this turn's writes changed, and the journal's stopped or open
|
|
714
|
+
slices (`slice_lines`). Read by session_start and the post-compaction path;
|
|
715
|
+
see the note above for why both."""
|
|
886
716
|
lines = []
|
|
887
717
|
plan, mine = _active_plan(root)
|
|
888
718
|
if plan:
|
|
@@ -909,7 +739,7 @@ def resume_state(root, session_id):
|
|
|
909
739
|
# lessons, pointer - for a session whose ledger holds one bad line. So a
|
|
910
740
|
# damaged ledger costs these two bullets and nothing else.
|
|
911
741
|
try:
|
|
912
|
-
row = last_check(session_id) if session_id else None
|
|
742
|
+
row = last_check(session_id, root) if session_id else None
|
|
913
743
|
files = sorted(changed_files(session_id)) if session_id else []
|
|
914
744
|
except ValueError:
|
|
915
745
|
row, files = None, []
|
|
@@ -919,6 +749,7 @@ def resume_state(root, session_id):
|
|
|
919
749
|
RESUME_CHECK_CHARS)))
|
|
920
750
|
if files:
|
|
921
751
|
lines.append("- changed this turn: %s" % ", ".join(files[:RESUME_FILES]))
|
|
752
|
+
lines += slice_lines(root, session_id, others=True)
|
|
922
753
|
if not lines:
|
|
923
754
|
return ""
|
|
924
755
|
return "## Session so far\n" + "\n".join(lines)
|
|
@@ -1079,11 +910,12 @@ def _lessons_state(root):
|
|
|
1079
910
|
lines, tainted_lessons(root))).encode()).hexdigest()[:8]]
|
|
1080
911
|
|
|
1081
912
|
|
|
1082
|
-
def state_stamp(root):
|
|
1083
|
-
"""The comparable state of the repo this turn is about
|
|
913
|
+
def state_stamp(root, ws=None):
|
|
914
|
+
"""The comparable state of the repo this turn is about: the checkout's HEAD,
|
|
915
|
+
the plans and lessons of its workspace `ws` (the main checkout's)."""
|
|
1084
916
|
return {"head": git(root, "rev-parse", "HEAD") or "nogit",
|
|
1085
|
-
"plans": _plan_ids(root),
|
|
1086
|
-
"lessons": _lessons_state(root)}
|
|
917
|
+
"plans": _plan_ids(ws or root),
|
|
918
|
+
"lessons": _lessons_state(ws or root)}
|
|
1087
919
|
|
|
1088
920
|
|
|
1089
921
|
def read_stamp(session_id):
|
|
@@ -1194,35 +1026,8 @@ def constraint_notice(cwd, session_id):
|
|
|
1194
1026
|
# So the ask is judged per clause: it arms `product` only when no code marker -
|
|
1195
1027
|
# a file name, an identifier, SQL, a build, test or runtime word - shares its
|
|
1196
1028
|
# 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")
|
|
1029
|
+
DATA_ASK = re.compile(r"\b(?:%s)" % alts("data_ask"))
|
|
1030
|
+
CODE_SENSE = re.compile(alts("code_sense"))
|
|
1226
1031
|
|
|
1227
1032
|
|
|
1228
1033
|
def _data_ask(low):
|
|
@@ -1458,11 +1263,22 @@ def session_of(payload):
|
|
|
1458
1263
|
# ADHD rules inline from the first turn (+1042 B), the fan-out pointer (+112 B)
|
|
1459
1264
|
# and the `tezgah-skill` line (+113 B) took the healthy fixture's session_start
|
|
1460
1265
|
# 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.
|
|
1266
|
+
# and sibling checkouts to pay for core rules; 13000 B restores the headroom.
|
|
1267
|
+
# Moved again on 2026-10-11 (plan 080): the integrity paragraph carries the
|
|
1268
|
+
# residual rule (the four blockers and the "Engeller" format) and the reminder
|
|
1269
|
+
# its one-line form, +1473 B of core and +151 B of reminder; at 13000 B the
|
|
1270
|
+
# healthy fixture (13588 B) gave up its lessons, plans and steer blocks (950 B;
|
|
1271
|
+
# 1002 B where a subagents note rides along), and at 6000 B the busiest prompt
|
|
1272
|
+
# turn its active-task line (255 B), so
|
|
1273
|
+
# the session budgets move to 14500 B and the prompt budget to 6500 B.
|
|
1274
|
+
# Moved again on 2026-10-12 (plan 086, S3): the integrity paragraph carries
|
|
1275
|
+
# the Ask policy (+913 B of core), and at 14500 B the healthy fixture gave up
|
|
1276
|
+
# its lessons, plans or knowledge block (235-338 B), so the session budgets
|
|
1277
|
+
# move to 15500 B. A
|
|
1462
1278
|
# budget in bytes, not tokens: this file has no tokenizer and a wrong estimate
|
|
1463
1279
|
# would be worse than a bound.
|
|
1464
|
-
CONTEXT_BUDGET = {"session_start":
|
|
1465
|
-
"subagent_start": 5500, "user_prompt":
|
|
1280
|
+
CONTEXT_BUDGET = {"session_start": 15500, "post_compact": 15500,
|
|
1281
|
+
"subagent_start": 5500, "user_prompt": 6500}
|
|
1466
1282
|
DEFAULT_BUDGET = 13000
|
|
1467
1283
|
# The blocks in the order they are given up when the budget is exceeded, lowest
|
|
1468
1284
|
# value first: text another surface already carries (the plan table lives in the
|
|
@@ -1477,8 +1293,10 @@ DEFAULT_BUDGET = 13000
|
|
|
1477
1293
|
# block, which outlives every status and availability line because it is the only
|
|
1478
1294
|
# one that says what THIS session was doing (a `git log` and a ledger read are
|
|
1479
1295
|
# turns the session would otherwise spend) but still yields to a rule - then the
|
|
1480
|
-
#
|
|
1481
|
-
#
|
|
1296
|
+
# journal's stopped or open slices, which outlive the resume block because after
|
|
1297
|
+
# an omp `/clear` they are the one pointer left to delegated work in flight - then
|
|
1298
|
+
# the scratch-path warning, which is about evidence the turn may already have
|
|
1299
|
+
# claimed - then the active task's phase, which outlives both because a phase is what
|
|
1482
1300
|
# stops a refused write before it happens - then the delta, and the skill pointer
|
|
1483
1301
|
# last. A key absent from this tuple is never dropped: the always-on core, the
|
|
1484
1302
|
# per-turn reminder and an armed paragraph ARE the rules, and a budget that could
|
|
@@ -1487,7 +1305,8 @@ DEFAULT_BUDGET = 13000
|
|
|
1487
1305
|
DROP_ORDER = ("knowledge", "worktrees", "lessons", "skill", "lessons_turn",
|
|
1488
1306
|
"taste", "plans", "subagents", "steer", "orca", "consult", "research",
|
|
1489
1307
|
"research_broken", "graph", "offnote", "orchestrate",
|
|
1490
|
-
"index", "resume", "scratch", "task", "delta",
|
|
1308
|
+
"index", "resume", "slices", "context", "scratch", "task", "delta",
|
|
1309
|
+
"pointer")
|
|
1491
1310
|
|
|
1492
1311
|
|
|
1493
1312
|
def _first_item(text):
|
|
@@ -1609,7 +1428,8 @@ SCRATCH_CHARS = 120
|
|
|
1609
1428
|
# while the tool hooks never run - an unloaded plugin, a matcher the host
|
|
1610
1429
|
# renamed, a hook path that moved. The gate is then off and nothing says so.
|
|
1611
1430
|
# What the prompt hook can observe is the host's own transcript (Claude and
|
|
1612
|
-
# Codex hand `transcript_path` to every hook
|
|
1431
|
+
# Codex hand `transcript_path` to every hook, omp's bridge hands its session
|
|
1432
|
+
# file to the prompt hook) and this session's ledger: gated
|
|
1613
1433
|
# tool calls in the transcript since the session's first turn marker, and not
|
|
1614
1434
|
# one row from the gate over the same span, is a gate that is not running. Only
|
|
1615
1435
|
# a bounded tail of the transcript is read, the ledger is scanned as bytes, and
|
|
@@ -1618,7 +1438,8 @@ GATE_TAIL = 262144
|
|
|
1618
1438
|
GATE_MIN_CALLS = 3
|
|
1619
1439
|
# The tool names whose every call leaves a PreToolUse row (`began`,
|
|
1620
1440
|
# tezgah_gate.decision's write and shell branch) and every refused one a `deny`:
|
|
1621
|
-
# Claude's write and shell tools
|
|
1441
|
+
# Claude's write and shell tools, omp's `bash`/`edit`/`write` and Codex's
|
|
1442
|
+
# `exec_command`/`apply_patch`. A
|
|
1622
1443
|
# tool the gate may pass without a row (Read, Grep, Task, WebFetch, an MCP
|
|
1623
1444
|
# call) is not counted, so its absence from the ledger is never read as a
|
|
1624
1445
|
# disarmed gate.
|
|
@@ -1639,25 +1460,32 @@ def _iso_epoch(value):
|
|
|
1639
1460
|
return None
|
|
1640
1461
|
|
|
1641
1462
|
|
|
1642
|
-
def
|
|
1643
|
-
"""
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
"payload": {"type": "function_call", "name"}}`; anything else counts 0."""
|
|
1463
|
+
def _tail_lines(path):
|
|
1464
|
+
"""The raw lines of a `.jsonl` transcript's last GATE_TAIL bytes, the cut
|
|
1465
|
+
first line dropped. [] for any other path or an unreadable file, so a host
|
|
1466
|
+
that hands no transcript pays nothing."""
|
|
1647
1467
|
if not isinstance(path, str) or not path.endswith(".jsonl"):
|
|
1648
|
-
return
|
|
1468
|
+
return []
|
|
1649
1469
|
try:
|
|
1650
1470
|
with open(path, "rb") as fh:
|
|
1651
1471
|
size = fh.seek(0, 2)
|
|
1652
1472
|
fh.seek(max(0, size - GATE_TAIL))
|
|
1653
1473
|
lines = fh.read().split(b"\n")
|
|
1654
1474
|
except OSError:
|
|
1655
|
-
return
|
|
1656
|
-
if size > GATE_TAIL
|
|
1657
|
-
|
|
1475
|
+
return []
|
|
1476
|
+
return lines[1:] if size > GATE_TAIL else lines
|
|
1477
|
+
|
|
1478
|
+
|
|
1479
|
+
def _transcript_calls(path, since):
|
|
1480
|
+
"""Gated tool calls in the transcript's last GATE_TAIL bytes stamped at or
|
|
1481
|
+
after `since` (epoch seconds). Claude writes `{"timestamp", "message":
|
|
1482
|
+
{"content": [{"type": "tool_use", "name"}]}}`, omp the same with
|
|
1483
|
+
`"toolCall"` blocks, Codex `{"timestamp", "payload": {"type":
|
|
1484
|
+
"function_call", "name"}}`. Anything else counts 0."""
|
|
1658
1485
|
calls = 0
|
|
1659
|
-
for raw in
|
|
1660
|
-
if b"tool_use" not in raw and b"
|
|
1486
|
+
for raw in _tail_lines(path):
|
|
1487
|
+
if (b"tool_use" not in raw and b"toolCall" not in raw
|
|
1488
|
+
and b"function_call" not in raw):
|
|
1661
1489
|
continue
|
|
1662
1490
|
try:
|
|
1663
1491
|
row = json.loads(raw)
|
|
@@ -1670,7 +1498,7 @@ def _transcript_calls(path, since):
|
|
|
1670
1498
|
msg = row.get("message")
|
|
1671
1499
|
if isinstance(msg, dict) and isinstance(msg.get("content"), list):
|
|
1672
1500
|
names = [b.get("name") for b in msg["content"]
|
|
1673
|
-
if isinstance(b, dict) and b.get("type")
|
|
1501
|
+
if isinstance(b, dict) and b.get("type") in ("tool_use", "toolCall")]
|
|
1674
1502
|
pay = row.get("payload")
|
|
1675
1503
|
if isinstance(pay, dict) and pay.get("type") == "function_call":
|
|
1676
1504
|
names.append(pay.get("name"))
|
|
@@ -1678,10 +1506,72 @@ def _transcript_calls(path, since):
|
|
|
1678
1506
|
return calls
|
|
1679
1507
|
|
|
1680
1508
|
|
|
1509
|
+
# The usage keys that make up one step's input side: Claude's names, then omp's.
|
|
1510
|
+
# Codex reports the cached share inside `input_tokens`.
|
|
1511
|
+
INPUT_KEYS = ("input_tokens", "cache_creation_input_tokens",
|
|
1512
|
+
"cache_read_input_tokens", "input", "cacheRead", "cacheWrite")
|
|
1513
|
+
CONTEXT_NUDGE = (
|
|
1514
|
+
"Router context %dk tokens: push heavy reads and check output through a "
|
|
1515
|
+
"slice that returns the exit code and the failing lines. Hand the open "
|
|
1516
|
+
"work to a slice or run `/compact` before the next heavy step.")
|
|
1517
|
+
# The row a compaction leaves: omp's `{"type": "compaction"}`, Claude's
|
|
1518
|
+
# `compact_boundary` system row. A step before it measured the old context.
|
|
1519
|
+
COMPACTED = re.compile(rb'"(?:type"\s*:\s*"compaction|subtype"\s*:\s*"compact_boundary)"')
|
|
1520
|
+
|
|
1521
|
+
|
|
1522
|
+
def context_tokens(path):
|
|
1523
|
+
"""Input-side tokens of the newest main-thread assistant step in the
|
|
1524
|
+
transcript tail, or None. Claude and omp put `usage` on the assistant
|
|
1525
|
+
message (a Claude `isSidechain` row is a subagent's), Codex on a
|
|
1526
|
+
`token_count` event's `last_token_usage`. A compaction row met first means
|
|
1527
|
+
no step since the compaction, so None."""
|
|
1528
|
+
for raw in reversed(_tail_lines(path)):
|
|
1529
|
+
if COMPACTED.search(raw):
|
|
1530
|
+
return None
|
|
1531
|
+
if b"usage" not in raw:
|
|
1532
|
+
continue
|
|
1533
|
+
try:
|
|
1534
|
+
row = json.loads(raw)
|
|
1535
|
+
except ValueError:
|
|
1536
|
+
continue
|
|
1537
|
+
if not isinstance(row, dict) or row.get("isSidechain"):
|
|
1538
|
+
continue
|
|
1539
|
+
msg, pay = row.get("message"), row.get("payload")
|
|
1540
|
+
usage = None
|
|
1541
|
+
if isinstance(msg, dict) and msg.get("role") == "assistant":
|
|
1542
|
+
usage = msg.get("usage")
|
|
1543
|
+
elif isinstance(pay, dict) and pay.get("type") == "token_count":
|
|
1544
|
+
usage = (pay.get("info") or {}).get("last_token_usage")
|
|
1545
|
+
if isinstance(usage, dict):
|
|
1546
|
+
n = sum(v for k, v in usage.items() if k in INPUT_KEYS and isinstance(v, int))
|
|
1547
|
+
if n:
|
|
1548
|
+
return n
|
|
1549
|
+
return None
|
|
1550
|
+
|
|
1551
|
+
|
|
1552
|
+
def context_key(session_id):
|
|
1553
|
+
"""The router-context line's key in the `nudged` mark store."""
|
|
1554
|
+
return "context:" + slug(str(session_id))
|
|
1555
|
+
|
|
1556
|
+
|
|
1557
|
+
def context_nudge(payload, session_id):
|
|
1558
|
+
"""The one line asking the router to delegate heavy reading, or "" below
|
|
1559
|
+
CONTEXT_NUDGE_TOKENS, without a transcript, under `orchestrate-off`, or
|
|
1560
|
+
once `context_for` latched it for this session (cleared by a compaction)."""
|
|
1561
|
+
path = payload.get("transcript_path") if isinstance(payload, dict) else None
|
|
1562
|
+
if not path or off("orchestrate-off") or tezgah_store.marked(
|
|
1563
|
+
"nudged", context_key(session_id)):
|
|
1564
|
+
return ""
|
|
1565
|
+
tokens = context_tokens(path)
|
|
1566
|
+
return CONTEXT_NUDGE % (tokens // 1000) if (
|
|
1567
|
+
tokens or 0) >= CONTEXT_NUDGE_TOKENS else ""
|
|
1568
|
+
|
|
1569
|
+
|
|
1681
1570
|
# The ledger kinds a hook other than the tool hooks writes: the prompt hook's
|
|
1682
1571
|
# turn marker, judge row, lesson rows and disarm row, the Stop hook's claim,
|
|
1683
1572
|
# refusal, after_block, shape and stop_spec rows, the subagent-end hook's subagent_end row,
|
|
1684
|
-
# compaction, the subagent mark
|
|
1573
|
+
# compaction, the subagent mark and the `subagent_start` lineage row (both from
|
|
1574
|
+
# SubagentStart), the omp fork's `forked` row, the guard's crash
|
|
1685
1575
|
# row (any hook), the damage row any reader writes and the session start's
|
|
1686
1576
|
# attest row. Every other kind - deny, nudge, began, snapshot, ... - can only
|
|
1687
1577
|
# come from a tool hook.
|
|
@@ -1691,8 +1581,8 @@ def _transcript_calls(path, since):
|
|
|
1691
1581
|
NOT_TOOL_HOOK = frozenset((b"turn", b"judge", b"claim", b"refusal",
|
|
1692
1582
|
b"after_block", b"subagent_end", b"stop_spec",
|
|
1693
1583
|
b"shape", b"compact", b"orch", b"crash", b"route",
|
|
1694
|
-
b"spawned", b"lesson", b"disarm", b"ledger_damage",
|
|
1695
|
-
b"attest"))
|
|
1584
|
+
b"spawned", b"forked", b"lesson", b"disarm", b"ledger_damage",
|
|
1585
|
+
b"attest", b"subagent_start"))
|
|
1696
1586
|
# The rows PostToolUse writes (`note_tool`): they prove the host ran the call,
|
|
1697
1587
|
# not that the gate saw it - a PostToolUse hook keeps writing them while a
|
|
1698
1588
|
# broken PreToolUse hook lets every call through - so they are no proof either.
|
|
@@ -1828,15 +1718,31 @@ POINTER_NEEDLE = "tezgah-contract"
|
|
|
1828
1718
|
# constraint that the user never meant is a rule the session did not get from
|
|
1829
1719
|
# the user. tests/constraint-fixtures.md holds the real prompts it was measured
|
|
1830
1720
|
# on, its false positives listed.
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
r"(?
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1721
|
+
def _constraint_shapes():
|
|
1722
|
+
"""Every pack's `constraint.dont` and `constraint.ask` shapes in one
|
|
1723
|
+
pattern, each named group suffixed with its pack's code (`obj_en`,
|
|
1724
|
+
`ask_tr`), so `_shape` can tell which language's shape matched."""
|
|
1725
|
+
return re.compile(r"(?i)\b(?:%s)" % "|".join(
|
|
1726
|
+
frag.replace("(?P<obj>", "(?P<obj_%s>" % code).replace(
|
|
1727
|
+
"(?P<ask>", "(?P<ask_%s>" % code)
|
|
1728
|
+
for code, pack in packs().items() for kind in ("dont", "ask")
|
|
1729
|
+
for frag in pack["constraint"][kind]))
|
|
1730
|
+
|
|
1731
|
+
|
|
1732
|
+
CONSTRAINT_SHAPES = _constraint_shapes()
|
|
1733
|
+
|
|
1734
|
+
|
|
1735
|
+
def _shape(m):
|
|
1736
|
+
"""(kind, group, pack) of a CONSTRAINT_SHAPES match: `obj` for a
|
|
1737
|
+
prohibition's object, `ask` for an ask-first clause's verb. Read off the
|
|
1738
|
+
one named group that matched (`groupdict`), so a pack fragment's own
|
|
1739
|
+
groups cannot stand in for it (tests/test_lang_packs.py holds each
|
|
1740
|
+
fragment to its one named group)."""
|
|
1741
|
+
group = next(k for k, v in m.groupdict().items() if v is not None)
|
|
1742
|
+
kind, code = group.split("_", 1)
|
|
1743
|
+
return kind, group, packs()[code]
|
|
1744
|
+
|
|
1745
|
+
|
|
1840
1746
|
# Pasted material is not the user's constraint: a shape that starts inside a
|
|
1841
1747
|
# quoted span, inline code, a fenced block or a `>` quote line is skipped (a
|
|
1842
1748
|
# quoted object after an unquoted shape stays the user's), and a prompt
|
|
@@ -1846,11 +1752,12 @@ CONSTRAINT_SHAPES = re.compile(
|
|
|
1846
1752
|
# log alone matched six clauses of another agent's reasoning. ponytail: a long
|
|
1847
1753
|
# prompt the user did write loses its constraints - the cost of a length rule.
|
|
1848
1754
|
CONSTRAINT_PROMPT_MAX = 12000
|
|
1755
|
+
# Single quotes count only off a word ("main'e" is no quote), with ‘…’ and «…».
|
|
1849
1756
|
QUOTED = re.compile(r"```.*?```|`[^`\n]*`|\"[^\"\n]{0,200}\"|“[^”\n]{0,200}”"
|
|
1757
|
+
r"|(?<!\w)'[^'\n]{1,200}'(?!\w)|‘[^’\n]{0,200}’|«[^»\n]{0,200}»"
|
|
1850
1758
|
r"|^\s*>.*$", re.S | re.M)
|
|
1851
1759
|
SENTENCE_END = re.compile(r"[.!?;:](?=\s|$)|\n")
|
|
1852
|
-
NEEDLE_STOP = frozenset(
|
|
1853
|
-
"before after from into of but ve veya ile".split())
|
|
1760
|
+
NEEDLE_STOP = frozenset(w for p in packs().values() for w in p["needle_stop"])
|
|
1854
1761
|
QUOTE_MARKS = re.compile(r"[`\"“”]")
|
|
1855
1762
|
NEEDLE_WORDS = 3
|
|
1856
1763
|
CONSTRAINT_CHARS = 120
|
|
@@ -1860,8 +1767,8 @@ CONSTRAINTS_MAX = 5
|
|
|
1860
1767
|
def user_constraints(prompt):
|
|
1861
1768
|
"""The constraint clauses a prompt issues, redacted and cut, in order and
|
|
1862
1769
|
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."""
|
|
1770
|
+
sentence's start for an `object_first` pack's prohibition (Turkish), whose
|
|
1771
|
+
object comes first. The stamp keeps the clause, never the prompt."""
|
|
1865
1772
|
text = prompt or ""
|
|
1866
1773
|
if len(text) > CONSTRAINT_PROMPT_MAX:
|
|
1867
1774
|
return []
|
|
@@ -1870,8 +1777,11 @@ def user_constraints(prompt):
|
|
|
1870
1777
|
for m in CONSTRAINT_SHAPES.finditer(text):
|
|
1871
1778
|
if any(a <= m.start() < b for a, b in quoted):
|
|
1872
1779
|
continue
|
|
1780
|
+
kind, _group, pack = _shape(m)
|
|
1873
1781
|
start = (max((e.end() for e in SENTENCE_END.finditer(text, 0, m.start())),
|
|
1874
|
-
default=0)
|
|
1782
|
+
default=0)
|
|
1783
|
+
if kind == "obj" and pack["word_order"] == "object_first"
|
|
1784
|
+
else m.start())
|
|
1875
1785
|
end = SENTENCE_END.search(text, m.end())
|
|
1876
1786
|
clause = " ".join(text[start:end.start() if end else len(text)].split())
|
|
1877
1787
|
clause = cut(redact(clause.strip("-*• ")), CONSTRAINT_CHARS)
|
|
@@ -1879,6 +1789,97 @@ def user_constraints(prompt):
|
|
|
1879
1789
|
out.append(clause)
|
|
1880
1790
|
return out
|
|
1881
1791
|
|
|
1792
|
+
# A grant is the positive half of the same reader (plan 086, S3): "bana sormadan
|
|
1793
|
+
# pushla", "without asking", "onay veriyorum" lets the session take an outward
|
|
1794
|
+
# step without asking again; "artık bana sor", "ask me again" or a later
|
|
1795
|
+
# prohibition that names the step ("bana sormadan push etme", "don't push")
|
|
1796
|
+
# lifts it. Each is one `grant` row in the session's ledger, scoped to the
|
|
1797
|
+
# OUTWARD class the sentence names, `all` when it names none; the Stop rule's
|
|
1798
|
+
# `needless ask` and the ask-tool warning read them back
|
|
1799
|
+
# (`tezgah_integrity.grants_in_force`). Never a class-F step: a grant lowers
|
|
1800
|
+
# asks, it never licenses a force push, a delete or a spend over the cap.
|
|
1801
|
+
LIFT_SHAPES = re.compile(r"(?i)\b(?:%s)" % alts("constraint", "lift"))
|
|
1802
|
+
# A negated outward step ("Don't push.", "şimdilik push yapma") lifts only the
|
|
1803
|
+
# class its own words name, never `all`.
|
|
1804
|
+
LIFT_OUTWARD = re.compile(r"(?i)\b(?:%s)" % alts("constraint", "lift_outward"))
|
|
1805
|
+
# A grant shape in a sentence that forbids, complains or asks is no grant:
|
|
1806
|
+
# "bana sormadan hiçbir şey yapma", "neden bana sormadan pushladın?", "Never
|
|
1807
|
+
# tag without asking me". Each pack's rules read only the sentences its own
|
|
1808
|
+
# grant shape matched, so "theme" is no Turkish -me: `grant_not` anywhere in
|
|
1809
|
+
# the sentence with the grant shape blanked ("don't ask me again" stays a
|
|
1810
|
+
# grant), the pack's question particle, and `grant_not_verb` only on the word
|
|
1811
|
+
# right after the grant shape or the sentence's last word - one of
|
|
1812
|
+
# `grant_not_soft` ("durma", "unutma") only after an outward word. ponytail: a
|
|
1813
|
+
# Turkish noun ending in -ma/-me in those two places also reads as a
|
|
1814
|
+
# prohibition - a missed grant costs one question, a false one an unasked push.
|
|
1815
|
+
|
|
1816
|
+
|
|
1817
|
+
def _any(frags):
|
|
1818
|
+
return re.compile(r"(?i)(?:%s)" % ("|".join(frags) or "(?!)"))
|
|
1819
|
+
|
|
1820
|
+
|
|
1821
|
+
GRANT_PACKS = [(re.compile(r"(?i)\b(?:%s)" % "|".join(p["constraint"]["grant"])),
|
|
1822
|
+
_any(p["constraint"]["grant_not"]
|
|
1823
|
+
+ [r"\b(?:%s)\b" % q for q in p["question_end"]]),
|
|
1824
|
+
_any(p["constraint"]["grant_not_verb"]),
|
|
1825
|
+
_any(p["constraint"]["grant_not_soft"]))
|
|
1826
|
+
for p in packs().values() if p["constraint"]["grant"]]
|
|
1827
|
+
WORD = re.compile(r"[^\W\d_]+")
|
|
1828
|
+
|
|
1829
|
+
|
|
1830
|
+
def _refuses(text, start, at, to, stop, nots, verb, soft):
|
|
1831
|
+
"""True when the sentence `text[start:stop]` takes back the grant shape
|
|
1832
|
+
at `text[at:to]` (GRANT_PACKS' rules of the pack that matched it)."""
|
|
1833
|
+
if text[stop:stop + 1] == "?" or nots.search(text[start:at] + " " + text[to:stop]):
|
|
1834
|
+
return True
|
|
1835
|
+
words = [m for m in WORD.finditer(text, start, stop) if not at <= m.start() < to]
|
|
1836
|
+
near = [m for m in words if m.start() >= to][:1] + words[-1:]
|
|
1837
|
+
return any(verb.fullmatch(m.group(0)) and not (
|
|
1838
|
+
soft.fullmatch(m.group(0)) and not outward_classes(text[start:m.start()]))
|
|
1839
|
+
for m in near)
|
|
1840
|
+
|
|
1841
|
+
|
|
1842
|
+
def user_grants(prompt):
|
|
1843
|
+
"""[(clause, target)] for every grant and lift a prompt issues, in order:
|
|
1844
|
+
`target` is an OUTWARD class or `all`, `lift:` before it for a lift. A
|
|
1845
|
+
sentence is read whole for the class it names, since a Turkish grant often
|
|
1846
|
+
names the step before "sormadan"; a grant its sentence takes back
|
|
1847
|
+
(`_refuses`) is none, a lift shape inside a grant shape ("don't ask me
|
|
1848
|
+
again") is the grant's, and a LIFT_OUTWARD match lifts only the classes it
|
|
1849
|
+
names. Pasted material and an over-long prompt are skipped as
|
|
1850
|
+
`user_constraints` skips them."""
|
|
1851
|
+
text = prompt or ""
|
|
1852
|
+
if len(text) > CONSTRAINT_PROMPT_MAX:
|
|
1853
|
+
return []
|
|
1854
|
+
quoted = [q.span() for q in QUOTED.finditer(text)]
|
|
1855
|
+
found = [(m.start(), m.end(), rules) for shapes, *rules in GRANT_PACKS
|
|
1856
|
+
for m in shapes.finditer(text)]
|
|
1857
|
+
grants = [(a, b) for a, b, _r in found]
|
|
1858
|
+
found += [(m.start(), m.end(), kind) for kind, shapes in
|
|
1859
|
+
(("lift", LIFT_SHAPES), ("outward", LIFT_OUTWARD))
|
|
1860
|
+
for m in shapes.finditer(text)
|
|
1861
|
+
if not any(a <= m.start() < b for a, b in grants)]
|
|
1862
|
+
out = []
|
|
1863
|
+
for at, to, rules in sorted(found, key=lambda f: f[:2]):
|
|
1864
|
+
if any(a <= at < b for a, b in quoted):
|
|
1865
|
+
continue
|
|
1866
|
+
lift = isinstance(rules, str)
|
|
1867
|
+
start = max((e.end() for e in SENTENCE_END.finditer(text, 0, at)), default=0)
|
|
1868
|
+
end = SENTENCE_END.search(text, to)
|
|
1869
|
+
stop = end.start() if end else len(text)
|
|
1870
|
+
if not lift and _refuses(text, start, at, to, stop, *rules):
|
|
1871
|
+
continue
|
|
1872
|
+
sentence = " ".join(text[start:stop].split())
|
|
1873
|
+
clause = cut(redact(sentence.strip("-*• ")), CONSTRAINT_CHARS)
|
|
1874
|
+
classes = (sorted(outward_classes(text[at:to])) if rules == "outward"
|
|
1875
|
+
else sorted(outward_classes(sentence)) or ["all"])
|
|
1876
|
+
for cls in classes:
|
|
1877
|
+
out.append((clause, ("lift:" if lift else "") + cls))
|
|
1878
|
+
for clause in user_constraints(text):
|
|
1879
|
+
out += [(clause, "lift:" + cls) for cls in sorted(outward_classes(clause))]
|
|
1880
|
+
return list(dict.fromkeys(out))
|
|
1881
|
+
|
|
1882
|
+
|
|
1882
1883
|
|
|
1883
1884
|
def constraint_needle(clause):
|
|
1884
1885
|
"""The fragment of a constraint a summary keeps when it kept the constraint:
|
|
@@ -1900,13 +1901,14 @@ def constraint_needle(clause):
|
|
|
1900
1901
|
if len(kept) == NEEDLE_WORDS or bare != word:
|
|
1901
1902
|
break
|
|
1902
1903
|
return kept
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1904
|
+
kind, group, pack = _shape(m)
|
|
1905
|
+
if kind == "ask":
|
|
1906
|
+
needle = m.group(group)
|
|
1907
|
+
elif pack["word_order"] == "object_first":
|
|
1908
|
+
before = phrase(reversed(clause[:m.end(group)].split()))
|
|
1907
1909
|
needle = " ".join(reversed(before)).split("'")[0]
|
|
1908
1910
|
else:
|
|
1909
|
-
needle = m.group
|
|
1911
|
+
needle = " ".join(phrase(clause[m.start(group):].split()))
|
|
1910
1912
|
return needle or clause
|
|
1911
1913
|
|
|
1912
1914
|
|
|
@@ -1980,6 +1982,11 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
1980
1982
|
if not under(cwd):
|
|
1981
1983
|
return None
|
|
1982
1984
|
root = repo_root(cwd)
|
|
1985
|
+
# The repository's one workspace: a linked worktree reads and creates the
|
|
1986
|
+
# main checkout's `.tezgah`, where `git worktree remove` cannot take it and
|
|
1987
|
+
# where bin/tezgah-task already keeps the plans (plan 097).
|
|
1988
|
+
from tezgah_paths import linked_main
|
|
1989
|
+
ws = linked_main(root) or root
|
|
1983
1990
|
ACTIVE_ROOT[0] = root_for(cwd) or ""
|
|
1984
1991
|
# What the compaction kept, recorded before the block is built: the summary
|
|
1985
1992
|
# is the host's own text and this path is the only place it is handed over, so
|
|
@@ -1993,6 +2000,8 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
1993
2000
|
if event == "post_compact" or (event == "session_start" and isinstance(
|
|
1994
2001
|
payload, dict) and payload.get("source") == "compact"):
|
|
1995
2002
|
forget_seen(session_of(payload))
|
|
2003
|
+
# the context line may fire again on the compacted context
|
|
2004
|
+
tezgah_store.drop("nudged", context_key(session_of(payload)))
|
|
1996
2005
|
# The switch latch: every off() below answers for this session, and a
|
|
1997
2006
|
# prompt's turn marker (with its `authorized` row) is written first, so a
|
|
1998
2007
|
# switch the user's prompt names is honored from this very turn.
|
|
@@ -2023,6 +2032,13 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2023
2032
|
# even when the per-turn text is off. Never in DROP_ORDER, so no budget
|
|
2024
2033
|
# gives it up - it says the rules below are not being enforced.
|
|
2025
2034
|
gate = gate_inactive(session_id, payload)
|
|
2035
|
+
# The user's grants and lifts (`user_grants`), before the reminder
|
|
2036
|
+
# switch too: the Stop rule reads them whether or not this turn's text
|
|
2037
|
+
# is shown. A subagent's prompt is its parent's brief, never the
|
|
2038
|
+
# user's grant (omp's bridge sends `subagent`).
|
|
2039
|
+
for clause, target in ([] if isinstance(payload, dict) and payload.get("subagent")
|
|
2040
|
+
else user_grants(prompt)):
|
|
2041
|
+
note(session_id, GRANT_KIND, clause, target=target)
|
|
2026
2042
|
# per-turn nudge: openers decay over long sessions. Kept short because
|
|
2027
2043
|
# it is paid every turn, and on Claude the output style already carries
|
|
2028
2044
|
# the same rules on every response. The conditional rules ride along
|
|
@@ -2061,7 +2077,7 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2061
2077
|
# only here: a conditional paragraph reaches no static file. The
|
|
2062
2078
|
# one-line repeat carries it too: it is a fact, not rule text.
|
|
2063
2079
|
parts.append(("armed", render("\n\n".join(armed).replace(
|
|
2064
|
-
"{OPEN_LINES}", open_lines_note(
|
|
2080
|
+
"{OPEN_LINES}", open_lines_note(ws)))))
|
|
2065
2081
|
else:
|
|
2066
2082
|
audit_classification(set(), 0)
|
|
2067
2083
|
# One cheap judgement in front of the skill choice: the roster reaches the
|
|
@@ -2084,7 +2100,7 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2084
2100
|
# state_delta/constraint_notice), and the glance says whether the graph
|
|
2085
2101
|
# is behind that move - a comparison that used to end in a status glyph
|
|
2086
2102
|
# the model never reads.
|
|
2087
|
-
stamp = state_stamp(root)
|
|
2103
|
+
stamp = state_stamp(root, ws)
|
|
2088
2104
|
delta = state_delta(root, previous, stamp)
|
|
2089
2105
|
if delta:
|
|
2090
2106
|
parts.append(("delta", delta))
|
|
@@ -2097,8 +2113,8 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2097
2113
|
if (previous or {}).get("root") == root else []
|
|
2098
2114
|
relevant, keys = "", []
|
|
2099
2115
|
if prompt and ".no-lessons" not in repo_marks(cwd)[1]:
|
|
2100
|
-
relevant, keys = relevant_lessons(
|
|
2101
|
-
if relevant and workspace_from_repo(
|
|
2116
|
+
relevant, keys = relevant_lessons(ws, prompt, seen)
|
|
2117
|
+
if relevant and workspace_from_repo(ws):
|
|
2102
2118
|
relevant, keys = (("", []) if "provided" in seen else
|
|
2103
2119
|
(repo_provided(".tezgah/lessons.md"), ["provided"]))
|
|
2104
2120
|
if relevant:
|
|
@@ -2118,6 +2134,26 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2118
2134
|
if scratch:
|
|
2119
2135
|
command = str(scratch.get("detail") or "").strip()
|
|
2120
2136
|
parts.append(("scratch", SCRATCH_REMINDER % command[:SCRATCH_CHARS]))
|
|
2137
|
+
# The journal's stopped or open slices and background jobs: an omp
|
|
2138
|
+
# `/clear` fires no session_start, so the resume block never reaches the
|
|
2139
|
+
# next turn and this part is the one that names the work left in flight.
|
|
2140
|
+
pending = slice_lines(root, session_id)
|
|
2141
|
+
if pending:
|
|
2142
|
+
parts.append(("slices", "\n".join([SLICES_HEAD] + pending)))
|
|
2143
|
+
# The router's own context: above the threshold, one line asks it to
|
|
2144
|
+
# push heavy reading through a slice (orchestrator-flow-audit S2 F1).
|
|
2145
|
+
heavy = context_nudge(payload, session_id)
|
|
2146
|
+
if heavy:
|
|
2147
|
+
parts.append(("context", heavy))
|
|
2148
|
+
# a long session outlives a provider's reset: the same due check as
|
|
2149
|
+
# session start (one small JSON read), so a family rejoins omp's chain
|
|
2150
|
+
# without waiting for the next session
|
|
2151
|
+
try:
|
|
2152
|
+
import tezgah_models
|
|
2153
|
+
if tezgah_models.health_due():
|
|
2154
|
+
tezgah_models.start_health()
|
|
2155
|
+
except Exception:
|
|
2156
|
+
pass
|
|
2121
2157
|
stale = index_notice(cwd)
|
|
2122
2158
|
if stale:
|
|
2123
2159
|
parts.append(("index", stale))
|
|
@@ -2125,6 +2161,12 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2125
2161
|
parts.append(("offnote", "(off this session: %s)"
|
|
2126
2162
|
% ", ".join(disabled)))
|
|
2127
2163
|
text = budgeted(event, parts)
|
|
2164
|
+
# latched only when the budget kept it, so a dropped line comes back
|
|
2165
|
+
if heavy and heavy in text:
|
|
2166
|
+
try:
|
|
2167
|
+
tezgah_store.mark("nudged", context_key(session_id))
|
|
2168
|
+
except tezgah_store.ERRORS:
|
|
2169
|
+
pass # fail open: an unwritable store shows the line again
|
|
2128
2170
|
items = [ln for ln in relevant.split("\n") if ln.startswith("- ")]
|
|
2129
2171
|
shown = ([k for k, ln in zip(keys, items) if ln in text] if items
|
|
2130
2172
|
else keys if relevant and relevant in text else [])
|
|
@@ -2189,10 +2231,19 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2189
2231
|
"research to a host subagent and say the tooling is "
|
|
2190
2232
|
"unavailable; do not improvise its protocol."))
|
|
2191
2233
|
if event == "session_start":
|
|
2192
|
-
|
|
2193
|
-
if
|
|
2234
|
+
synced = sync_agents(root)
|
|
2235
|
+
if synced:
|
|
2194
2236
|
parts.append(("subagents",
|
|
2195
|
-
"Subagents (this repo, generated): %s" %
|
|
2237
|
+
"Subagents (this repo, generated): %s" % synced))
|
|
2238
|
+
# the provider health behind omp's subagent chains: re-probed in the
|
|
2239
|
+
# background at most every few hours or once a recorded reset passes, so
|
|
2240
|
+
# a family at its limit leaves the chain and rejoins it after the reset
|
|
2241
|
+
try:
|
|
2242
|
+
import tezgah_models
|
|
2243
|
+
if tezgah_models.health_due():
|
|
2244
|
+
tezgah_models.start_health()
|
|
2245
|
+
except Exception:
|
|
2246
|
+
pass
|
|
2196
2247
|
# orchestrate-off says "do not delegate", so no line may name delegates
|
|
2197
2248
|
steer = (None if off("orchestrate-off")
|
|
2198
2249
|
else steering(root, (payload or {}).get("host")))
|
|
@@ -2202,8 +2253,8 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2202
2253
|
# The workspace every per-project artifact lands in: created, ignored by
|
|
2203
2254
|
# the project and given its private git before anything writes there.
|
|
2204
2255
|
# A root is not a project, and the helper returns None outside a work tree.
|
|
2205
|
-
if
|
|
2206
|
-
ensure_workspace(
|
|
2256
|
+
if ws not in roots():
|
|
2257
|
+
ensure_workspace(ws)
|
|
2207
2258
|
# The live turn state, first in this region: on a compacted or resumed
|
|
2208
2259
|
# session it is the one thing the rest of the block cannot re-derive.
|
|
2209
2260
|
resume = resume_state(root, session_of(payload))
|
|
@@ -2219,9 +2270,9 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2219
2270
|
|
|
2220
2271
|
def provided():
|
|
2221
2272
|
if not from_repo:
|
|
2222
|
-
from_repo.append(workspace_from_repo(
|
|
2273
|
+
from_repo.append(workspace_from_repo(ws))
|
|
2223
2274
|
return from_repo[0]
|
|
2224
|
-
plans = open_plans(
|
|
2275
|
+
plans = open_plans(ws)
|
|
2225
2276
|
if plans:
|
|
2226
2277
|
parts.append(("plans", repo_provided(".tezgah/plans/open/")
|
|
2227
2278
|
if provided() else plans))
|
|
@@ -2236,7 +2287,7 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2236
2287
|
# The project's own rule files, agents and skills (tezgah-migrate's
|
|
2237
2288
|
# index): they stay where the project keeps them, and one line makes a
|
|
2238
2289
|
# session read the rows its task touches instead of never seeing them.
|
|
2239
|
-
if os.path.isfile(os.path.join(
|
|
2290
|
+
if os.path.isfile(os.path.join(ws, ".tezgah", "analysis",
|
|
2240
2291
|
"project-knowledge.md")):
|
|
2241
2292
|
parts.append(("knowledge",
|
|
2242
2293
|
"Project knowledge: this repo keeps its own rule files, "
|
|
@@ -2245,14 +2296,14 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2245
2296
|
"the task touches before starting; a nested AGENTS.md or "
|
|
2246
2297
|
"CLAUDE.md binds the subtree it sits in."))
|
|
2247
2298
|
if ".no-lessons" not in marks:
|
|
2248
|
-
past = lessons(
|
|
2299
|
+
past = lessons(ws)
|
|
2249
2300
|
if past:
|
|
2250
2301
|
parts.append(("lessons", repo_provided(".tezgah/lessons.md")
|
|
2251
2302
|
if provided() else past))
|
|
2252
2303
|
if not provided():
|
|
2253
|
-
recent = lesson_lines(
|
|
2304
|
+
recent = lesson_lines(ws)[-LESSON_LINES:]
|
|
2254
2305
|
injected = list(zip(recent, _lesson_shown(
|
|
2255
|
-
recent, tainted_lessons(
|
|
2306
|
+
recent, tainted_lessons(ws))))
|
|
2256
2307
|
# The opt-in taste learnings (hooks/tezgah_taste_ledger.block): the
|
|
2257
2308
|
# active ones as rules or hints, read from the ledger with no call out;
|
|
2258
2309
|
# off, `enabled` costs the marker stat. The injection is recorded below,
|
|
@@ -2278,7 +2329,7 @@ def context_for(event, cwd, payload=None, with_core=True):
|
|
|
2278
2329
|
# bulk import at most daily (tezgah_store.import_later).
|
|
2279
2330
|
if event == "session_start":
|
|
2280
2331
|
tezgah_store.import_later(cache_dir())
|
|
2281
|
-
broken = tezgah_research.failing(
|
|
2332
|
+
broken = tezgah_research.failing(ws) if not off("research-off") else []
|
|
2282
2333
|
if broken:
|
|
2283
2334
|
line_slug, err = broken[0]
|
|
2284
2335
|
parts.append(("research_broken",
|