@r1z4x/tezgah 1.8.2 → 1.9.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/docs/gate.md CHANGED
@@ -183,7 +183,7 @@ Trigger: a write tool, or a shell command that writes a file, while an active ta
183
183
  against `hooks/tezgah_gate.py::SHELL_WRITE`); the allowlist refuses a target outside it (`hooks/tezgah_gate.py::TASK_SCOPE_DENY`) — a write tool's path, and a shell command's redirect or `tee` target (`hooks/tezgah_gate.py::write_paths`); and a write whose target IS the record, by a write tool or a shell redirect/`tee`, is refused
184
184
  (`hooks/tezgah_gate.py::TASK_RECORD_DENY`) - the active task's record and any other open plan that carries a phase (`hooks/tezgah_task.py::phased_record`), matched without case and read against the main checkout, so a session in a worktree meets the same rule - because only `bin/tezgah-task` writes it and that CLI makes the checks a retyped frontmatter would skip. Told:
185
185
  the move that lifts it, made by the session itself — `tezgah-task phase implementation` or `tezgah-task allow <glob> ... --why "<reason>"` — since the orchestrator owns the plan lifecycle and the CLI refuses
186
- the move until the plan's Acceptance, spike and checkpoint are in order, and records the reason for a widened scope (`allowed_why:`). The record's body has its own writers, on any open plan, run from the main checkout or from any of its worktrees. `tezgah-task tick NNN <n> [--undo]` ticks Acceptance item n (`bin/tezgah-task::cmd_tick`). `tezgah-task note NNN state|next "<text>"` replaces the newest `## State` or the `## Next` body (`bin/tezgah-task::cmd_note`). `tezgah-task spike NNN "<answer>"` writes `spike_recorded:`, the answer `implementation` waits for (`bin/tezgah-task::cmd_spike`). None of them commits. Standing: only the record changing lifts it.
186
+ the move until the plan's Acceptance, spike and checkpoint are in order, and records the reason for a widened scope (`allowed_why:`). The record's body has its own writers, on any open plan, run from the main checkout or from any of its worktrees. `tezgah-task tick NNN <n> [--undo]` ticks Acceptance item n (`bin/tezgah-task::cmd_tick`). `tezgah-task note NNN state|next "<text>"` replaces the newest `## State` or the `## Next` body (`bin/tezgah-task::cmd_note`). `tezgah-task spike NNN "<answer>"` writes `spike_recorded:`, the answer `implementation` waits for (`bin/tezgah-task::cmd_spike`). `tezgah-task unmet <NNN> <n> "<reason, who accepted it>"` annotates Acceptance item n with `UNMET:`, the form `close NNN done` accepts in place of a ticked box (`bin/tezgah-task::cmd_unmet`). `tezgah-task sweep` makes one idempotent pass over what the last session left behind (`bin/tezgah-task::cmd_sweep`). It lists every pending slice as one line. It removes the landed worktrees and branches, and clears a `phase:` whose branch is gone. It prints the pending slices instead of closing them, because resuming one is the session's move. None of them commits. Standing: only the record changing lifts it.
187
187
  Scratch meets no allowlist: a temp file or a device, such as `pytest > /tmp/check.log` (`hooks/tezgah_gate.py::task_reason`). Build output inside the repo stays held. So does a file in the repo or its worktrees when the checkout lives under the temp dir, as CI's does.
188
188
  The task rule holds repository paths only. A target under no git checkout and no configured root, such as `~/.config/runpod/key`, is no plan's work (`hooks/tezgah_gate.py::_no_repo`). Neither the phase nor the allowlist refuses it, while every other rule, the secret rule among them, still reads that write. The gate reads a relative target in the call's own directory. Any doubt keeps the target held. A line that moves the directory stays held, however it spells the `cd`, `pushd` or `popd`. So does a line that reads `$PWD` back. A line that makes a link stays held: `ln`, `install`, `cp -s`, `cp -l`. So does a target whose parent directory does not exist yet. So does a file with a second hard link. Host agent and instruction files and the shell rc files stay held too, in any letter case (`hooks/tezgah_gate.py::HOME_HELD`). Under `~/.claude` those are `agents/**`, `hooks/**`, `commands/**`, `skills/**` and `CLAUDE.md`. Elsewhere they are `~/.omp/agent/**`, `~/.codex/AGENTS.md`, `~/.codex/prompts/**`, `~/.cursor/**` and `~/.config/opencode/**`. The rc files are `~/.zshrc`, `~/.zshenv`, `~/.zprofile`, `~/.zlogin`, `~/.bashrc`, `~/.bash_profile`, `~/.bash_login` and `~/.profile`. A target in another repository stays refused as outside this one.
189
189
  Three targets are no project file, so neither the phase nor the allowlist refuses them (`hooks/tezgah_gate.py::unnamed_target`). A URI is one: a tool device (`xd://<tool>`), an agent message (`agent://X`) or a conflict resolution (`conflict://2`). A `file://` URI names a checkout file and stays held. A shell target with an unknown variable is another. `write_paths` first expands a variable from an assignment earlier on the line. So the allowlist holds `B=src; git diff > $B/x.patch` as `src/x.patch`. A variable the same line set to the checkout, such as `R=$(git rev-parse --show-toplevel)`, is read as the checkout (`hooks/tezgah_gate.py::known_tail`). The third is tezgah's own workspace under `.tezgah/`, such as research lines, lessons and briefs (`hooks/tezgah_gate.py::workspace_free`). The slice journal stays held. So does every shell write into `.tezgah/plans/open/`, because such a file could make itself the active task. A plan file stays held too when its text, once the write lands, carries a `phase:` line (`hooks/tezgah_gate.py::_text_after`). The gate judges the whole text, so two edits cannot each pass on their own. The record rule still refuses the active record there.
@@ -240,17 +240,22 @@ Three triggers, one per shape a credential takes on its way into a file (`hooks/
240
240
 
241
241
  Standing, no escape hatch, and outside `verify-off`.
242
242
 
243
- ### Plan — a turn's third product file on `main`
244
-
245
- Trigger: a write tool, or a shell redirect/`tee` (`hooks/tezgah_gate.py::write_paths`), whose target is a product path, while the checkout holding that target is on `main` or `master` and this turn has
246
- already written two other product files into the same checkout (`hooks/tezgah_gate.py::plan_reason`, `hooks/tezgah_gate.py::_branch`, `hooks/tezgah_gate.py::_checkout`, `hooks/tezgah_gate.py::_product`). The branch comes from that checkout, not from the session's cwd. The
247
- count folds the turn's own write rows (`hooks/tezgah_integrity.py::turn_rows`) together with the files the call in hand names, so it needs no state of its own, and it counts DISTINCT
248
- paths - a turn that rewrites one file three times is still one file's work. The product paths are the ones at the repository root that a plan exists to scope - `hooks/`, `tests/`, `bin/`,
249
- `skills/`, `docs/`, `statusline.py` and `MANIFEST` (`PLAN_DIRS`/`PLAN_FILES` `hooks/tezgah_gate.py::PLAN_DIRS`, `hooks/tezgah_gate.py::PLAN_FILES`); a `.tezgah/` path is never one of them, and neither is a path outside any git
250
- repository. Told: the branch it read, how many files the turn has written, and the command that opens a plan - the `plan-add` skill, `/tezgah:plan-add <description>` (`PLAN_DENY`
251
- `hooks/tezgah_gate.py::PLAN_DENY`). Standing: only changing the tree's state changes it - the work moves to the `plan/NNN-slug` branch the skill creates (a one- or two-file fix is free either
252
- way), or the repository carries a `.no-plan-gate` mark (the shape of `.no-graph` and `.no-lessons`). It passes wherever the question cannot be answered: no session or ledger, a directory that is
253
- not a git repository, and a detached HEAD with no branch to name.
243
+ ### Plan — a turn's third product file on a branch no plan binds
244
+
245
+ Trigger: a write tool, or a shell redirect/`tee` (`hooks/tezgah_gate.py::write_paths`), whose target is a product path, while the checkout holding that target is on a branch that binds no open plan
246
+ and this turn has already written two other product files into the same checkout (`hooks/tezgah_gate.py::plan_reason`, `hooks/tezgah_gate.py::_branch`, `hooks/tezgah_gate.py::_checkout`,
247
+ `hooks/tezgah_gate.py::_product`). The branch comes from that checkout, not from the session's cwd. A branch binds a plan when it is `plan/NNN-slug` for an open plan NNN, or when an open plan
248
+ declares it in its `branch:` frontmatter (`hooks/tezgah_gate.py::_unplanned`, `hooks/tezgah_task.py::plan_of_branch`, `hooks/tezgah_task.py::declared_branches`). So `plan/999-nope` binds
249
+ nothing, and Orca's `r1z4x/<slug>` branches bind by declaration. The task rule holds a bound checkout, by its plan's phase and allowlist. `main` and `master` never bind a plan, whatever a plan
250
+ declares. The count folds the turn's own write rows (`hooks/tezgah_integrity.py::turn_rows`) together with the files the call in hand names, so it needs no state of its own, and it counts DISTINCT
251
+ paths - a turn that rewrites one file three times is still one file's work. A product path is any path in the checkout, whatever the project's layout (`apps/`, `packages/`, `.maestro/` count as
252
+ `hooks/` does). Tezgah's and git's own state does not count: `.tezgah/`, `.codegraph/` and `.git/` (`hooks/tezgah_gate.py::PLAN_SKIP`). Neither does a host's generated `agents/` set
253
+ (`hooks/tezgah_gate.py::PLAN_AGENT_HOSTS`). A path outside any git repository never counts. Told: the branch it read, how many files the turn has written, and
254
+ the command that opens a plan - the `plan-add` skill, `/tezgah:plan-add <description>` (`hooks/tezgah_gate.py::PLAN_DENY`). It also names the bind route: `tezgah-task start NNN --phase P --branch
255
+ <branch>` writes the plan's `branch:` (`bin/tezgah-task::cmd_start`). The record rule refuses a hand edit of a phased record. Standing: only changing the tree's state changes it - the
256
+ work moves to the `plan/NNN-slug` branch the skill creates or to a bound branch (a one- or two-file fix is free either way), or the repository carries a `.no-plan-gate` mark (the shape of
257
+ `.no-graph` and `.no-lessons`). It passes while a merge or rebase is in progress (`MERGE_HEAD`, `rebase-merge/` or `rebase-apply/` in the checkout's gitdir, `hooks/tezgah_gate.py::_merging`):
258
+ resolving conflicts is not new work. It passes wherever the question cannot be answered: no session or ledger, a directory that is not a git repository, and a detached HEAD with no branch to name.
254
259
 
255
260
  ### Ordering — a commit while the newest check failed
256
261
 
@@ -391,7 +396,7 @@ A rule is a refusal with a cause: something happened, the check was written, and
391
396
  only as a sentence inside each rule's own paragraph above. It is a table here instead, so it is one place and a machine reads it: `bin/tezgah-docs --citations`
392
397
  opens `docs/gate.md`, reads this table, and refuses a rule name the source can still produce with no row, and a row whose `pin` names a test that does not exist.
393
398
  The names are read off the source, not restated: the gate's rules are the `_deny(session_id, "<rule>", ...)` calls in `hooks/tezgah_gate.py`, and the Stop
394
- rule's triggers are the classes `_stop_block` returns, the four `_shape_block` and the six `_evidence_block` hand back included.
399
+ rule's triggers are the classes `_stop_block` returns, the four `_shape_block` hands back and the six `_evidence_text` words for the temporal spec included.
395
400
 
396
401
  - `incident` — what produced the rule; `designed` where none is recorded. The ratio of incident-derived to designed rules is the maturity signal the research
397
402
  entry names, which is why the distinction is written down rather than smoothed over.
@@ -419,7 +424,7 @@ rule's triggers are the classes `_stop_block` returns, the four `_shape_block` a
419
424
  | `evidence tampered` | a terminated garbage line made `_parse` raise, and the guard then failed the Stop rule open for the turn; corrupting a `verify_fail` row was an allow route. A `verify_ok` appended with `python3 -c` licensed a claim (security-03), so a pass with no `began` row is an orphan (`hooks/tezgah_integrity.py::_pair`) | `hooks/tezgah_integrity.py::_parse` | `tests/test_integrity.py::test_a_damaged_ledger_blocks_a_done_claim_as_evidence_tampered` |
420
425
  | `check failed` | designed | the newest check failing is its own class; no incident recorded | `tests/test_integrity.py::test_failed_check_blocks` |
421
426
  | `partial failure` | designed | a failure the turn never resolved; no incident recorded | `tests/test_integrity.py::test_an_unresolved_failure_blocks_even_after_an_earlier_pass` |
422
- | `no ui_ok` | reading the fold the old way refused honest turns and named a screen check the turn had in fact run | `hooks/tezgah_integrity.py::_evidence_block` | `tests/test_integrity.py::test_a_green_unit_run_does_not_license_a_ui_change` |
427
+ | `no ui_ok` | reading the fold the old way refused honest turns and named a screen check the turn had in fact run | `hooks/tezgah_stopspec.py::FORMULAS` | `tests/test_integrity.py::test_a_green_unit_run_does_not_license_a_ui_change` |
423
428
  | `stale evidence` | measured 2026-09-19: reading a rewrite as a change refused honest turns | `hooks/tezgah_integrity.py::_post_write` | `tests/test_integrity.py::test_a_write_after_the_check_makes_the_check_stale` |
424
429
  | `no verify_ok` | E2: 0 of 10 description-shaped claims refused before the `worked` trigger | `hooks/tezgah_integrity.py::_stop_block` | `tests/test_integrity.py::test_work_with_no_passing_check_is_refused_without_a_claim_word` |
425
430
  | `no external read` | two turns: "npm 0.22.0 is missing" off a stale client, and "make NPM_TOKEN an automation token" when it already was | `hooks/tezgah_integrity.py::_external_read_row` | `tests/test_integrity.py::test_an_external_claim_with_no_read_is_refused` |
package/docs/models.md CHANGED
@@ -215,13 +215,20 @@ moved against the snapshot (`SNAPSHOT`, `hooks/tezgah_models.py::SNAPSHOT`;
215
215
  re-applies omp's overrides. Scores are not re-read - they need a key - so
216
216
  `tezgah-route --check` reports the snapshot's age and exits 1 past 60 days or with
217
217
  a flag (`check`, `hooks/tezgah_models.py::check`); that is the moment to re-read
218
- the leaderboards and edit `SLOTS`. `tezgah-route --mode anthropic|zai|any|auto|off` pins
218
+ the leaderboards and edit `SLOTS`. `tezgah-route --mode anthropic|zai|any|openai|auto|off` pins
219
219
  omp's family instead of following the session default; `off` removes every entry
220
220
  and role the table wrote and writes nothing, which is the way back when the
221
221
  provider those selectors need has no budget - a measured case, not a
222
- hypothetical one. `tezgah-setup --install`
222
+ hypothetical one. The switch reaches every routed agent in one command. It writes
223
+ the table over a selector set by hand and names each one it replaced
224
+ (`apply_omp`, `hooks/tezgah_models.py::apply_omp`). The automatic re-apply at
225
+ session start leaves a hand-set selector alone. `tezgah-setup --install`
223
226
  writes omp's overrides and `--uninstall` removes only the `tezgah-*` entries; the
224
227
  omp status report carries a row for them.
228
+ `openai` runs every routed agent on omp's `openai-codex` provider, a ChatGPT
229
+ login (`OMP_OPENAI`, `hooks/tezgah_models.py::OMP_OPENAI`). Its ids are not the
230
+ Codex host's `openai` column, which omp does not list. The mode is never a
231
+ fallback for the others, and with no `openai-codex` login it writes nothing.
225
232
 
226
233
  ## Source of truth
227
234
 
@@ -701,11 +701,14 @@ background child. That child's SubagentStop closes it as `done`
701
701
  record settles from the newest child transcript in the parent's session dir.
702
702
  omp names a same-name re-dispatch `<name>-2.jsonl`. The last 64 KB of that file
703
703
  decide `stopped` or `done` (`hooks/tezgah_task.py::pending`). A record
704
- nothing moved for six hours closes `stale`, and a background `job` after
705
- omp's 3600 s deadline. Codex reports no child end tezgah can see, so its
706
- records end `stale`. Every prompt lists up to three pending records, newest
707
- first, each with the command that resumes it (`hooks/tezgah_task.py::slice_lines`).
708
- They are the records this session spawned or nobody owns, such as one opened by hand. The resume block at
704
+ nothing moved for six hours closes `stopped` with no failure class. It stays
705
+ owed, because an end this machine cannot see is not a finished slice. A
706
+ background `job` past omp's 3600 s deadline closes `stale`, since the parent
707
+ transcript already holds its result. Codex reports no child end tezgah can see,
708
+ so its records take the `stopped` path too. Every prompt lists up to three
709
+ pending records, newest first, each with the command that resumes it
710
+ (`hooks/tezgah_task.py::slice_lines`). They are the records this session spawned
711
+ or nobody owns, such as one opened by hand. The resume block at
709
712
  session start and after a compaction adds one line that counts the other
710
713
  sessions' records. `tezgah-task slice reconcile` lists them all.
711
714
 
@@ -718,6 +721,7 @@ sessions' records. `tezgah-task slice reconcile` lists them all.
718
721
  | `tezgah-task slice stats [--window DAYS] [--json]` | counts records per status, failure class and host. A `class+host` signature with 3 or more events is an RSI lever candidate, and so is one with 5% of a window that holds 20 or more slices. The line names the lever |
719
722
  | `tezgah-task slice board [--parent P]` | lists the open slices one parent spawned - the session's own, or for a subagent its parent's - newest first: the `Owns:` globs, worktree, branch and last commit of each, bounded to 12 lines (`hooks/tezgah_task.py::board`) |
720
723
  | `tezgah-task slice merge-plan [--base REF]` | forecasts every done slice branch that carries unmerged work against REF (HEAD) and against the others with `git merge-tree`, and leaves out a `merge=union` file such as CHANGELOG.md. A branch touching `MANIFEST` or the citation baseline comes last, the rest fewest conflicts first. A branch merge-tree cannot forecast is printed as `not judged` and the exit is 1 (`bin/tezgah-task::merge_plan`) |
724
+ | `tezgah-task sweep` | one idempotent pass over what the last session left behind: every pending slice as one line with its resume command, the landed worktrees and branches `reconcile --prune` removes, and a `phase:` whose branch is gone while the record carries a `checkpoint` (`bin/tezgah-task::cmd_sweep`). It prints the pending slices rather than closing them, and a second run reports `0 change(s)` |
721
725
 
722
726
  `tezgah-task review NNN <reviewer> approve` refuses while an open, stopped or
723
727
  stale record names that reviewer as its id, agent or spawned name. Under
package/docs/testing.md CHANGED
@@ -17,6 +17,12 @@ ruff check . # lint; config in pypr
17
17
  [`CONTRIBUTING.md`](../CONTRIBUTING.md) repeats the same three before a pull
18
18
  request (`CONTRIBUTING.md:15-20`).
19
19
 
20
+ `bin/tezgah-optimize next` decides which checks a change owes, and when
21
+ (`AGENTS.md`, "Checks, by tier"). It spends a green row for the same change on a
22
+ clean tree. It defers a command whose recorded median is at or above 105 s to the
23
+ merge step. Everything else runs now. `bin/tezgah-optimize push` coalesces the CI
24
+ run: one push per change.
25
+
20
26
  | Check | What it catches that the other two do not |
21
27
  |---|---|
22
28
  | `compileall` | A syntax error in any `.py` under `hooks/` and `hosts/`, plus `statusline.py`, without running a line of it and with no environment set up. The `bin/` CLIs are Python sources with no `.py` suffix, so only running one compiles it (`test_setup.py::SetupBase.setup`). |
@@ -109,11 +115,13 @@ generates the gate half: one `gate-<rule>` mutant per rule `decision` refuses wi
109
115
  It reads HEAD with the AST reader of the `--citations` rule check. Each mutant turns
110
116
  every `return _deny(...)` of its rule into `pass` at its line span.
111
117
  The script generates the Stop half too. Each `return ("<class>", ...)` site of
112
- `_stop_block`, `_shape_block` and `_evidence_block` gets one `stop-<class>` mutant. Hand-written
113
- rows cover the guards around those classes: the pass predicate, the empty-run
114
- read and the "doğrulanmadı" clear. The lost call, the bookkeeping and idle-turn
115
- fallbacks and the refusal row have rows too. The shortcut half is still
116
- hand-written (`MUTANTS`), so a new guard there needs a row.
118
+ `_stop_block`, `_shape_block`, `_evidence_text` and `_residual_block` gets one
119
+ `stop-<class>` mutant. Hand-written rows cover the guards around those classes:
120
+ the pass predicate, the empty-run read and the "doğrulanmadı" clear. The lost
121
+ call, the unseen pass, the bookkeeping and idle-turn fallbacks and the refusal
122
+ row have rows too. So do the two guards of the spec's decision list. The
123
+ shortcut half is still hand-written (`MUTANTS`), so a new guard there needs a
124
+ row.
117
125
  Each mutant first runs alone the test that killed it last time, read from
118
126
  `tests/neuter_killers.json`. When that test passes or is gone, the mutant runs
119
127
  every module in its own HOME and TMPDIR. The module that reads the mutated file
@@ -132,15 +140,11 @@ opencode plugin's ports. That step runs even when the core step failed.
132
140
  (`tests/fuzz_shell.py::main`). So a reader that misreads a bash 5 line fails
133
141
  the leg instead of printing a count. `tests/test_packaging.py` pins the job.
134
142
 
135
- The Stop rule's temporal spec (`hooks/tezgah_stopspec.py`, plan 063) has its own
136
- local script, `tests/stop_spec_eval.py`. `--replay` judges plan 055's replayed
137
- Stop events with both folds and reads GO 2 only on the events whose selector
138
- reached the fold. `--shadow-rows` reads the live `stop_spec` rows the same way.
139
- `--timing` times both folds in-process. `--stop-cost` times the whole
140
- `stop_reason`, shadow on and off, on copies of the real ledgers. `--mutants`, also reached through
141
- `tests/neuter_matrix.py --stop-generated`, runs seeded mutants of the formula
142
- table through the in-process Stop tests of `tests/test_integrity.py`. On any
143
- suite run, `TEZGAH_STOPSPEC_STRICT=1` fails the test that reached a disagreement.
143
+ The Stop rule's temporal spec (`hooks/tezgah_stopspec.py`, plan 063) decides
144
+ classes 5-10. Its formulas have their own local script, `tests/stop_spec_eval.py`.
145
+ `--mutants`, also reached through `tests/neuter_matrix.py --stop-generated`, runs
146
+ seeded mutants of the formula table through the in-process Stop tests of
147
+ `tests/test_integrity.py`.
144
148
 
145
149
  The plan report (`render_table.py --acceptance --strict`) is not in CI: it reads
146
150
  the gitignored `.tezgah/`, so on a runner it reads nothing and cannot fail. Run
@@ -211,6 +211,9 @@
211
211
  "never_default": [
212
212
  "force[- ]?push\\w*|--force\\b|rewrit\\w* (?:the )?history|reset --hard|\\bdelet\\w*|\\bdrop (?:the )?(?:table|database|volume)s?|unpublish\\w*|deprecat\\w*|production|\\bprod\\b|migrat\\w*|credential\\w*|password\\w*|secret\\w*|api key|\\baccount\\w*|e-?mail|send (?:it |this |them )?to\\b"
213
213
  ],
214
+ "plan_build": [
215
+ "\\bplans?\\b[^.?!\\n]{0,40}\\b(?:is ready|ready|written|drafted)\\b|\\b(?:wrote|opened|drafted|added|created) (?:the |a |this )?plans?\\b|\\b(?:build|implement|execute|start on|start) (?:the |this |that )?plans?\\b"
216
+ ],
214
217
  "spend": [
215
218
  "spend\\w*|\\bpaid\\b|billing|\\bcost\\w*|\\bbudget\\w*|\\busd\\b|\\$\\s?\\d"
216
219
  ],
@@ -255,6 +255,9 @@
255
255
  "never_default": [
256
256
  "zorla|geçmiş\\w* (?:yeniden yaz|değiştir)\\w*|\\bsil\\w*|kaldır\\w*|yok et\\w*|üretim\\w*|\\bcanlı\\w*|\\bprod\\w*|göç\\w*|kimlik bilgi\\w*|şifre\\w*|parola\\w*|\\bhesa[bp]\\w*|e-?posta\\w*|gönder\\w*"
257
257
  ],
258
+ "plan_build": [
259
+ "\\bplan\\w*[^.?!\\n]{0,40}(?:hazır|yazıldı|yazdım|oluşturdum|oluşturuldu|açtım|açıldı|eklendi|uygula\\w*|başla\\w*|kur\\w*)|(?:yazdığım|açtığım|hazırladığım|oluşturduğum) plan\\w*"
260
+ ],
258
261
  "spend": [
259
262
  "ücret\\w*|ödeme\\w*|harca\\w*|bütçe\\w*|maliyet\\w*|\\bpara\\b|dolar\\w*|\\$\\s?\\d"
260
263
  ],
@@ -33,9 +33,11 @@ from tezgah_paths import (CACHE, CONTEXT_NUDGE_TOKENS, REPO_MARKS, SWITCHES,
33
33
  ai_research_dir, alts, armed,
34
34
  cache_dir, codegraph_bin, consult_options,
35
35
  ensure_workspace, fallback_cache, have_judge_key, language,
36
+ link_workspace,
36
37
  off, orx_bin, packs, pony_level, project_top, root_for,
37
38
  roots, say, tool,
38
- workspace, workspace_from_repo, worktrees, writable_dir)
39
+ work_tree, workspace, workspace_from_repo, worktrees,
40
+ writable_dir)
39
41
 
40
42
  try: # The task record is the active plan's frontmatter (see tezgah_task), read
41
43
  # once per user prompt for the phase line. The module is newer than some
@@ -537,18 +539,22 @@ def open_plans(root):
537
539
  def sibling_line(root):
538
540
  """One line naming every checkout of this repository, or "" when it has one.
539
541
  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
543
- session-start git budget): `worktrees` reads `.git/worktrees/*/gitdir`, and
544
- the counts are directory listings."""
542
+ named with its open plan and research line counts; a bare repository's git
543
+ directory holds it in the main checkout's place (plan 098), and is named as
544
+ that, not counted as a checkout. A linked checkout whose own `.tezgah` still
545
+ holds data is named with what it holds, because `git worktree remove`
546
+ deletes it with the checkout. Fork-free (the pinned session-start git
547
+ budget): `worktrees` reads `.git/worktrees/*/gitdir`, and the counts are
548
+ directory listings."""
545
549
  checkouts = worktrees(root)
546
550
  here = os.path.realpath(root)
547
551
  if here not in checkouts:
548
552
  checkouts.append(here)
549
- if len(checkouts) < 2:
550
- return ""
551
553
  main = checkouts[0]
554
+ bare = not os.path.lexists(os.path.join(main, ".git"))
555
+ if len(checkouts) - bare < 2:
556
+ return ""
557
+ owner = "the bare repository" if bare else "the main checkout"
552
558
  items = []
553
559
  for checkout in checkouts:
554
560
  ws = workspace(checkout)
@@ -566,19 +572,20 @@ def sibling_line(root):
566
572
  except OSError:
567
573
  held = []
568
574
  lines = tezgah_research.slugs(checkout) if held else []
569
- state = ("uses the main checkout's `.tezgah`" if not held else
575
+ state = ("uses %s's `.tezgah`" % owner if not held else
570
576
  "its own `.tezgah` still holds %s, which `git worktree remove` "
571
577
  "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))
578
+ "from %s, and the rest by hand"
579
+ % (", ".join(held), tool("tezgah-research"), checkout,
580
+ "another worktree" if bare else "the main checkout"))
574
581
  if lines:
575
582
  state += " (lines: %s)" % ", ".join(lines)
576
583
  items.append("%s (%s%s)" % (checkout, "this checkout, " if checkout == here
577
584
  else "", state))
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`."
581
- % (len(checkouts), "; ".join(items), tool("tezgah-research")))
585
+ return ("Worktrees: %d checkouts of this repository share %s's `.tezgah` - "
586
+ "plans, lessons, research and analysis are written there, never into a "
587
+ "worktree's own: %s. Detail: `%s --all`."
588
+ % (len(checkouts) - bare, owner, "; ".join(items), tool("tezgah-research")))
582
589
 
583
590
 
584
591
  def task_line(task):
@@ -1274,11 +1281,14 @@ def session_of(payload):
1274
1281
  # Moved again on 2026-10-12 (plan 086, S3): the integrity paragraph carries
1275
1282
  # the Ask policy (+913 B of core), and at 14500 B the healthy fixture gave up
1276
1283
  # its lessons, plans or knowledge block (235-338 B), so the session budgets
1277
- # move to 15500 B. A
1284
+ # move to 15500 B. Moved again on 2026-10-13 (plan 105): the fidelity rule says
1285
+ # a research, analysis, review or plan ask ends at the plan (+105 B of core), and
1286
+ # the fixture's subagent brief reached 5500 B exactly, so subagent_start moves to
1287
+ # 5800 B. A
1278
1288
  # budget in bytes, not tokens: this file has no tokenizer and a wrong estimate
1279
1289
  # would be worse than a bound.
1280
1290
  CONTEXT_BUDGET = {"session_start": 15500, "post_compact": 15500,
1281
- "subagent_start": 5500, "user_prompt": 6500}
1291
+ "subagent_start": 5800, "user_prompt": 6500}
1282
1292
  DEFAULT_BUDGET = 13000
1283
1293
  # The blocks in the order they are given up when the budget is exceeded, lowest
1284
1294
  # value first: text another surface already carries (the plan table lives in the
@@ -1569,7 +1579,8 @@ def context_nudge(payload, session_id):
1569
1579
 
1570
1580
  # The ledger kinds a hook other than the tool hooks writes: the prompt hook's
1571
1581
  # turn marker, judge row, lesson rows and disarm row, the Stop hook's claim,
1572
- # refusal, after_block, shape and stop_spec rows, the subagent-end hook's subagent_end row,
1582
+ # refusal, after_block and shape rows and the stop_spec rows of plan 063's shadow
1583
+ # (old ledgers only), the subagent-end hook's subagent_end row,
1573
1584
  # compaction, the subagent mark and the `subagent_start` lineage row (both from
1574
1585
  # SubagentStart), the omp fork's `forked` row, the guard's crash
1575
1586
  # row (any hook), the damage row any reader writes and the session start's
@@ -2254,7 +2265,10 @@ def context_for(event, cwd, payload=None, with_core=True):
2254
2265
  # the project and given its private git before anything writes there.
2255
2266
  # A root is not a project, and the helper returns None outside a work tree.
2256
2267
  if ws not in roots():
2257
- ensure_workspace(ws)
2268
+ # never link to a `.tezgah` ensure_workspace refused: one the
2269
+ # project tracks or a link the clone placed (review of plan 100)
2270
+ if ensure_workspace(ws) and ws != root:
2271
+ link_workspace(root, ws)
2258
2272
  # The live turn state, first in this region: on a compacted or resumed
2259
2273
  # session it is the one thing the rest of the block cannot re-derive.
2260
2274
  resume = resume_state(root, session_of(payload))
@@ -2329,7 +2343,9 @@ def context_for(event, cwd, payload=None, with_core=True):
2329
2343
  # bulk import at most daily (tezgah_store.import_later).
2330
2344
  if event == "session_start":
2331
2345
  tezgah_store.import_later(cache_dir())
2332
- broken = tezgah_research.failing(ws) if not off("research-off") else []
2346
+ # the line's cited files are the session checkout's in a bare repository
2347
+ broken = (tezgah_research.failing(ws, code=work_tree(ws, root))
2348
+ if not off("research-off") else [])
2333
2349
  if broken:
2334
2350
  line_slug, err = broken[0]
2335
2351
  parts.append(("research_broken",
@@ -1549,7 +1549,8 @@ TASK_SCOPE_OUTSIDE = (
1549
1549
  TASK_RECORD_DENY = (
1550
1550
  "Task gate: %s is the active task's own record; only `tezgah-task` writes it, checking the plan's "
1551
1551
  "Acceptance, spike and review on the way. The phase and allowlist: `tezgah-task phase|allow`; an "
1552
- "Acceptance box: `tezgah-task tick NNN <n>`; State/Next: `tezgah-task note NNN state|next \"...\"`.")
1552
+ "Acceptance box: `tezgah-task tick NNN <n>`; an item the owner accepted unmet: "
1553
+ "`tezgah-task unmet NNN <n> \"<reason>\"`; State/Next: `tezgah-task note NNN state|next \"...\"`.")
1553
1554
  # The shapes that make a shell command a write, for the phase rule below. The
1554
1555
  # tools are not the only way to change a file, and E7b measured the other one:
1555
1556
  # with `edit` and `write` refused twelve and six times, the armed arm wrote the
@@ -2222,7 +2223,7 @@ def shell_write_body(command, cwd=None):
2222
2223
  # reported done unless the output was seen - given a mechanical half at the one
2223
2224
  # moment a claim is written into the repository's history.
2224
2225
  #
2225
- # The reader is the Stop rule's own fold over the tail (`_last_verify` on the
2226
+ # The reader is the newest-check fold the Stop rule's `check failed` class asks (`_last_verify` on the
2226
2227
  # last ORDER_TAIL rows), and the rule is structurally incapable of firing in
2227
2228
  # either direction that would punish honest work: no check at all folds to None
2228
2229
  # and a passing newest check folds to "ok", so a commit in a session that ran no
@@ -2412,15 +2413,20 @@ def ready_advice(tool, session_id, cwd=None):
2412
2413
  return READY_WAIT % (len(heads), "; ".join(cut(h, 48) for h in heads[:3]))
2413
2414
 
2414
2415
 
2415
- # --- plan required: work that spans files on main has no plan ----------------
2416
+ # --- plan required: work that spans files on an unplanned branch -------------
2416
2417
  # What this closes: a turn that wrote four product files - a hook, its tests, its
2417
- # docs and the changelog - straight onto `main`, with the plan opened afterwards.
2418
- # The task rule constrains a write only through a record the user made, so with
2419
- # no active task every product write passes and "work for a plan happens on its
2420
- # branch" is advice rather than a mechanism. Here the checkout is the record: on
2421
- # `main`/`master` the third distinct product file a single turn writes is
2422
- # refused, while a one- or two-file fix stays free - a plan is what makes work
2423
- # that spans files reviewable and closeable.
2418
+ # docs and the changelog - straight onto `main`, with the plan opened afterwards;
2419
+ # and Orca worktrees on `r1z4x/<name>` that built five branches of work with no
2420
+ # plan at all (plan 105). The task rule constrains a write only through a record
2421
+ # the user made, so with no active task every product write passes and "work for
2422
+ # a plan happens on its branch" is advice rather than a mechanism. Here the
2423
+ # checkout is the record: on a branch that binds no open plan the third distinct
2424
+ # product file a single turn writes is refused, while a one- or two-file fix
2425
+ # stays free - a plan is what makes work that spans files reviewable and
2426
+ # closeable. A branch binds a plan when it is `plan/NNN-*` or an open plan
2427
+ # declares it in `branch:` (`tezgah_task.plan_of_branch`); such a checkout is
2428
+ # the task rule's to hold. `main`/`master` never bind one, whatever a plan
2429
+ # declares: the trunk is where unplanned work lands.
2424
2430
  #
2425
2431
  # The count is a fold over the turn's own write rows (`turn_rows`), so it needs
2426
2432
  # no state of its own, and the call in hand contributes the files it names before
@@ -2428,32 +2434,41 @@ def ready_advice(tool, session_id, cwd=None):
2428
2434
  # itself.
2429
2435
  #
2430
2436
  # Fail-open wherever the question cannot be answered: no session, no ledger, a
2431
- # checkout that is not a git repository, a detached HEAD (no branch to name), and
2432
- # a repo carrying the `.no-plan-gate` mark all pass - the direction every rule
2433
- # here takes, because a refusal has to rest on something a reader can check.
2437
+ # checkout that is not a git repository, a detached HEAD (no branch to name), a
2438
+ # missing task module (no binding to read), and a repo carrying the
2439
+ # `.no-plan-gate` mark all pass - the direction every rule here takes, because a
2440
+ # refusal has to rest on something a reader can check. A merge or rebase in
2441
+ # progress passes too: resolving its conflicts is not new work that spans files,
2442
+ # and neither way out the refusal names fits it.
2434
2443
  #
2435
2444
  # ponytail: a PRIOR write recorded through the shell is a `run` row whose detail
2436
2445
  # is the command, so it is not counted; only the call in hand contributes a shell
2437
2446
  # target (`write_paths` reads the redirect). The turn this rule answers is the
2438
2447
  # write-tool turn, and reading a shell command's target off a stored command line
2439
2448
  # would be a second reader of the same table, not a new fact.
2440
- PLAN_BRANCHES = ("main", "master")
2449
+ PLAN_TRUNKS = ("main", "master")
2441
2450
  # The product paths the count reads, relative to the checkout holding the write:
2442
- # the code and docs a plan exists to scope. `.tezgah/` - and every other
2443
- # directory - is not one of them, and neither is a path outside any git checkout
2444
- # or a checkout outside every configured root (a scratch fixture repo).
2445
- PLAN_DIRS = ("hooks", "tests", "bin", "skills", "docs")
2446
- PLAN_FILES = ("statusline.py", "MANIFEST")
2451
+ # every path of the project, whatever its layout (ServisTek's unplanned merges
2452
+ # wrote `apps/`, `packages/` and `.maestro/`, review of plan 105), except what
2453
+ # tezgah or git keeps there - the workspace, the graph index, git's own
2454
+ # directory and the generated agent sets. Neither is a path outside any git
2455
+ # checkout or a checkout outside every configured root (a scratch fixture repo).
2456
+ PLAN_SKIP = (".tezgah", ".codegraph", ".git")
2457
+ PLAN_AGENT_HOSTS = (".claude", ".codex", ".cursor", ".opencode") # their agents/
2447
2458
  # The third file: two writes in a turn are a fix, three are work that spans files.
2448
2459
  PLAN_WRITES = 3
2460
+ # git's marks of an operation in progress, in the checkout's own gitdir
2461
+ PLAN_MERGING = ("MERGE_HEAD", "rebase-merge", "rebase-apply")
2449
2462
  PLAN_DENY = (
2450
- "This turn has written %d distinct product files while the checkout is on "
2451
- "`%s`. Work that spans files on `main`/`master` has no plan to review or "
2452
- "close, so the third product file in one turn is refused: open a plan with "
2453
- "the `plan-add` skill (`/tezgah:plan-add <description>`) and do the work on "
2454
- "the `plan/NNN-slug` branch it creates. A one- or two-file fix stays free. "
2455
- "Only `hooks/`, `tests/`, `bin/`, `skills/`, `docs/`, `statusline.py` and "
2456
- "`MANIFEST` are counted; a `.tezgah/` path never is.")
2463
+ "This turn has written %(count)d distinct product files while the checkout is "
2464
+ "on `%(branch)s`, a branch that binds no open plan. Work that spans files there "
2465
+ "has no plan to review or close, so the third product file in one turn is "
2466
+ "refused: open a plan with the `plan-add` skill (`/tezgah:plan-add "
2467
+ "<description>`) and do the work on the `plan/NNN-slug` branch it creates, or "
2468
+ "bind this branch to the open plan with `tezgah-task start NNN --phase P "
2469
+ "--branch %(branch)s` (it writes the plan's `branch:`). A one- or two-file fix "
2470
+ "stays free. Every path in the checkout counts except `.tezgah/`, "
2471
+ "`.codegraph/`, `.git/` and the generated `<host>/agents/` sets.")
2457
2472
 
2458
2473
 
2459
2474
  def _gitdir(root):
@@ -2566,7 +2581,8 @@ def _checkout(path, cwd):
2566
2581
  session on `main` writing into a linked worktree on a plan branch is plan
2567
2582
  work, and one on a plan branch writing into a `main` checkout is not. A
2568
2583
  relative path is resolved against the call's own `cwd`."""
2569
- text = str(path or "").strip()
2584
+ # `~/x` is a home path, not one under the cwd (plan 106)
2585
+ text = os.path.expanduser(str(path or "").strip())
2570
2586
  if not text or not cwd and not os.path.isabs(text):
2571
2587
  return None
2572
2588
  full = os.path.realpath(os.path.join(cwd or "", text))
@@ -2582,15 +2598,38 @@ def _checkout(path, cwd):
2582
2598
 
2583
2599
 
2584
2600
  def _product(rel):
2585
- """True when a checkout-relative path is one a plan exists to scope."""
2601
+ """True when a checkout-relative path is project work a plan exists to
2602
+ scope: anything but `PLAN_SKIP` and a host's generated `agents/` set."""
2586
2603
  parts = rel.split(os.sep)
2587
- return parts[0] in PLAN_DIRS or len(parts) == 1 and parts[0] in PLAN_FILES
2604
+ return parts[0] not in PLAN_SKIP and not (
2605
+ parts[0] in PLAN_AGENT_HOSTS and len(parts) > 2 and parts[1] == "agents")
2606
+
2607
+
2608
+ def _merging(top):
2609
+ """True while a merge or rebase is in progress in the checkout at `top`:
2610
+ git's mark of it stands in the checkout's own gitdir."""
2611
+ gitdir = _gitdir(top)
2612
+ return bool(gitdir) and any(os.path.exists(os.path.join(gitdir, mark))
2613
+ for mark in PLAN_MERGING)
2614
+
2615
+
2616
+ def _unplanned(branch, top):
2617
+ """True when `branch`, checked out at `top`, binds no open plan of its
2618
+ repository: a trunk always, else neither `plan/NNN-*` nor a branch an open
2619
+ plan declares. An empty branch or a missing task module cannot be read, and
2620
+ reads as planned (fail-open)."""
2621
+ if branch in PLAN_TRUNKS:
2622
+ return True
2623
+ if not branch or tezgah_task is None:
2624
+ return False
2625
+ return tezgah_task.plan_of_branch(branch, linked_main(top) or top) is None
2588
2626
 
2589
2627
 
2590
2628
  def plan_reason(inp, cwd, session_id):
2591
2629
  """A deny reason when this write would be a turn's third distinct product
2592
- file in a checkout on `main`/`master`, else None. The section above carries
2593
- the trigger, the counted paths and the fail-open direction."""
2630
+ file in a checkout on a branch that binds no open plan, else None. The
2631
+ section above carries the trigger, the counted paths and the fail-open
2632
+ direction."""
2594
2633
  if not session_id or turn_rows is None:
2595
2634
  return None
2596
2635
  targets = {}
@@ -2603,8 +2642,8 @@ def plan_reason(inp, cwd, session_id):
2603
2642
  rows = None
2604
2643
  for top, written in targets.items():
2605
2644
  branch = _branch(top)
2606
- if branch not in PLAN_BRANCHES \
2607
- or os.path.exists(os.path.join(top, ".no-plan-gate")):
2645
+ if os.path.exists(os.path.join(top, ".no-plan-gate")) \
2646
+ or not _unplanned(branch, top) or _merging(top):
2608
2647
  continue
2609
2648
  if rows is None:
2610
2649
  # a row's `target` is already absolute against the cwd it was
@@ -2614,7 +2653,7 @@ def plan_reason(inp, cwd, session_id):
2614
2653
  written |= {rel for hit in rows if hit and hit[0] == top
2615
2654
  for rel in (hit[1],) if _product(rel)}
2616
2655
  if len(written) >= PLAN_WRITES:
2617
- return PLAN_DENY % (len(written), branch)
2656
+ return PLAN_DENY % {"count": len(written), "branch": branch}
2618
2657
  return None
2619
2658
 
2620
2659
 
@@ -3577,8 +3616,8 @@ def decision(tool, inp, cwd, session_id=None, record=True, agent=None):
3577
3616
  body = inp if t in WRITE_TOOLS else shell_body
3578
3617
  if body and secret_edit(body, cwd):
3579
3618
  return _deny(session_id, "secret", SECRET_EDIT_DENY, tool, inp, base)
3580
- # Plan required: the turn's third product file while the checkout is on
3581
- # main/master (see plan_reason). Below the task, workspace and secret rules,
3619
+ # Plan required: the turn's third product file while the checkout is on a
3620
+ # branch no open plan binds (see plan_reason). Below the task, workspace and secret rules,
3582
3621
  # because each names a more specific fault in the same write, and above the
3583
3622
  # repeat guards, because it is the call that is refused and not a repeat of
3584
3623
  # one. Standing: only changing the tree's state changes it.