@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/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,141 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.8.2] - 2026-10-13
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **The v1.8.1 release builds.** A packaging test still pinned the serial suite step that 1.8.1 removed from CI. So the release workflow stopped before it built the artifact. `tests/impacted.py` no longer crashes on a deleted test module. Each neuter fall-through module runs in its own HOME, as the control does.
|
|
14
|
+
|
|
15
|
+
## [1.8.1] - 2026-10-13
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **The neuter matrix runs in CI, and in minutes.** Each mutant first runs the one test that killed it last time, kept in `tests/neuter_killers.json`. A full search runs only when that test passes. A cached run took 240 s, against about an hour before. CI runs it on a push that changes a guarded file, and weekly. A session checks a new rule's mutant with `--only <name>`.
|
|
20
|
+
- **The suite is faster.** It starts the largest modules first. Slow tests share their expensive installs per class, and one test now covers each duplicated path. A change under `benchmarks/arm-bench/` runs that lab's module, not all 100.
|
|
21
|
+
- **CI runs the suite once per Python.** The serial `unittest discover` step is gone. The sharded job runs the same modules, and both caught the same failures. A push to `main` now takes about 11 minutes, not 30.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **A checkout under the temp dir is not scratch.** The gate treated any path under the temp dir as scratch. So the task scope, the race rule and the main-checkout rule skipped it, and a plan worktree there let a write outside its scope pass. A file inside a git checkout now meets those rules, and a plain temp file stays scratch.
|
|
26
|
+
- **The test suite runs on CI runners.** A fixture had no git identity for the record commit that `close` now makes. The opencode capture test read only the SQLite ledger, so it missed the rows node 20 writes to the legacy file.
|
|
27
|
+
|
|
28
|
+
## [1.8.0] - 2026-10-13
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- **Typed worker output and a recall hop.** Tier workers and `tezgah-tester` now end their report with an `Output to orchestrator` block. It comes after any other shape the session asks for. A worker lists changed files, the commit sha and the `ESCALATE:` reason. For each check it gives the command, the exit code and the last lines of its output file. The tester gives pass, fail and skip counts and the first failure verbatim. A report that misses a field, or an `ESCALATE:`, gets one narrower re-brief of the same role. Only a second miss goes to `tezgah-frontier`, with the original brief and a fresh start. This ships as text only. An h05 spike on omp+tezgah spawned no subagent, so the arm-bench pilot could not measure it. No default changed.
|
|
33
|
+
- **A route joins the workers it actually started.** `tezgah-route --report` used to drop a route when the session spawned unrouted workers of its type. It also dropped a route whose one task call started several workers. A route now owns the first task call of its type after it. One failed first check in that call fails the route. A task call more than 30 minutes after its route stays unjoined. Two routes before one shared task call stay unjoined, since the report cannot tell whose worker is whose. On this machine 6 of the 10 real omp routes now join, up from 1.
|
|
34
|
+
- **On Claude Code, `tezgah-task` now reads the session id.** Claude's shells now carry `TEZGAH_SESSION`, as omp's do. So the review guard counts only this session's slice records. A bare name in `slice board`, `slice done` or `slice resume` now matches only this session's or unowned records.
|
|
35
|
+
- **A key passed to a script as an environment prefix is no longer refused.** `API_KEY="$(cat key)" python3 tool.py > run.log` now passes, since the key reaches only the program's environment. Only a script runner given a script file qualifies. A trace or verbose option, a startup or trace variable, `/proc/*/environ` or a function defined on the line holds it. A later printer that reads a key assigned earlier on the line is now refused too. The opencode plugin answers the same.
|
|
36
|
+
- **One `.tezgah` per repository, in its main checkout.** A session or CLI in a linked worktree now reads and writes the main checkout's `.tezgah`. That covers the session context, research lines, decisions, lessons, taste and the design contract. Before, `git worktree remove` deleted research lines and lessons written there. A worktree's session also never saw the lessons or open plans. `tezgah-research import` moves a line left in an old worktree workspace.
|
|
37
|
+
- **Any `plan/NNN-*` branch answers to plan NNN.** A plan's slice worktrees on their own `plan/NNN-*` branches now meet that plan's phase and allowlist. When every phased plan is held by a worktree, the main checkout has no active task. It used to take the first held plan's scope.
|
|
38
|
+
- **`tezgah-task close NNN done` refuses an open Acceptance item.** It names each unticked item that carries no `UNMET: <reason>`. `--resolved` does not lift it, and `discarded` still closes freely. Close also records the `landed:` commit once main holds the work, and commits the moved record in the private `.tezgah` repository.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- **Claude Code, Codex and Cursor routes can join their workers.** On Claude and Codex, SubagentStart writes a `subagent_start` row. It names the child's id and type, and it goes into the parent's ledger. Claude's SessionStart exports `TEZGAH_SESSION` through `CLAUDE_ENV_FILE`. On Codex, `tezgah-route` writes under the shell's `CODEX_THREAD_ID`. On Cursor it writes under `CURSOR_CONVERSATION_ID`. Cursor's subagentStart opens the child's ledger with a row that names the parent conversation. The report counts that link only after the child writes a row of its own. `cursor-agent` fires no subagentStart, so its routes stay unjoined, and so do dsh routes.
|
|
43
|
+
- **Two plans at once, one worktree each.** A linked worktree on a plan's own `branch:` now answers to that plan's phase, allowlist and checkpoint. This holds for a session inside the worktree and for a write into it from anywhere else. `tezgah-task start` keeps another plan's phase only when both plans run in their own worktrees on disjoint scopes. Otherwise it clears the others as before. The main checkout reads the first phased plan no worktree holds. A worktree on any other branch reads the main checkout's task. `allow` and `phase --any-path` refuse a scope that a held plan could share. The CLI takes a pending checkpoint from the plan's own worktree, and its refusal names `git -C <worktree>`. `tick`, `note`, `spike`, `review` and `close` run from any worktree. `review` records the plan worktree's HEAD, and the verification steps print `git -C <worktree> diff`.
|
|
44
|
+
- **`tezgah-task spike NNN "<answer>"`.** It writes `spike_recorded:` on an open plan, active or not. `phase implementation` then passes, with no stop, hand edit and restart.
|
|
45
|
+
- **A forced-delegation arm and 20 harder tasks for arm-bench.** `omp+tezgah-delegate` hands every edit to the worker `tezgah-route` names, so a run exercises the role text. One paid arming run verified it. `block.py` takes task globs and `--dry-run`. Tasks x01-x20 cover hidden invariants, sites a grep misses, spec edge clauses and long sweeps. On deepseek-v4-flash they passed 99 of 100 runs, so they stay as a regression corpus.
|
|
46
|
+
- **The Stop rule holds a done claim over an in-progress todo item.** The new `open todo` class names the open items. Finish them, mark them done, or type each one with its blocker. A step a running slice works on passes. A Cursor `merge: true` list is partial, so the class skips it.
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- **A route row never lands in another host's ledger.** A host started from another host's agent shell inherits that host's session id. `tezgah-route` now writes no row when its three session variables name different sessions. Such a route counts as unjoined.
|
|
51
|
+
- **A Codex subagent's repeat limits count only its own attempts**, as on Claude. The root's next try no longer fails for the subagent's attempts.
|
|
52
|
+
- **A shell line is read target by target.** Each redirect and `tee` target meets the phase, allowlist and checkpoint of the plan it lands under. Some lines also meet the task where the session runs. That holds for a line with more write shapes than read targets. It also holds for a positional writer such as `cp` or `sed -i`. So `tee <plan worktree>/a README.md` no longer escapes a reading phase. Two redirects into one plan worktree meet only that plan.
|
|
53
|
+
- **Two edits cannot give a plan a phase.** The gate judged an edit to an open plan by its new string alone. So `xphase: implementation` and then `xphase` -> `phase` both passed. It now judges the plan's text as it will read after the write.
|
|
54
|
+
- **The plan record is the record in any case and from any checkout.** The gate refuses a write to `.TEZGAH/PLANS/open/<plan>.md` as a record write. It refuses a write to any phased record from a session in a linked worktree too.
|
|
55
|
+
- **A slice keeps its own plan.** Its tag comes from its `plan/NNN` branch first. Next comes `plan NNN` in its brief or shared context, then its earlier tag. A re-open with no brief keeps the tag.
|
|
56
|
+
- **`tezgah-task close` run inside a landed worktree keeps that worktree.** The session's own directory is no longer removed from under it.
|
|
57
|
+
- **arm-bench's arming proof reads the evidence store.** `session_rows` and `stop_classes` read the old JSONL ledger, which the hooks no longer write. So every arm recorded 0 and `{}`, armed or not. Both now read the SQLite store, as `counters` does. A fixture copy also leaves out stale `__pycache__` and bytecode.
|
|
58
|
+
- **The task rule holds repository paths only.** Some writes land under no git checkout and no configured root, such as a user's `~/.config/runpod/key`. Those no longer meet the active plan's phase, allowlist or checkpoint. The secret rule and every other rule still read them. A target in another repository is still refused. Any doubt holds the write. That covers a line that moves the directory or reads `$PWD`, and a line that makes a link. It also covers a target whose parent does not exist yet and a file with a second hard link. Host agent and instruction files and shell rc files stay held too, in any letter case.
|
|
59
|
+
- **`tezgah-task` exits cleanly when its reader leaves early.** `tezgah-task stop | head -1` prints no traceback. It finishes its work and keeps its own exit code, buffered or under `PYTHONUNBUFFERED=1`.
|
|
60
|
+
- **Nothing inside `.tezgah` treats it as a project.** The workspace keeps its own `.git`, so git named `.tezgah` itself as the top level there. Session start then set up `.tezgah/.tezgah`, agents and a graph index inside it. The plans table renderer wrote `.tezgah/.tezgah/plans/README.md`. Every resolver now maps a path inside `.tezgah` to the repository around it.
|
|
61
|
+
- **Plan records survive concurrent writers.** `tick`, `note`, field writes and `close` hold a lock on the plans directory and replace the file atomically. A reader saw a torn record in 29 of 40 rounds before. `tick` now finds its item under the lock.
|
|
62
|
+
- **The slice journal closes a landed slice whose worktree `close` removed.** `reconcile` no longer names the main checkout's HEAD as a removed worktree's last commit.
|
|
63
|
+
- **`close` deletes a squash-merged plan branch.** It used `git branch -d`, which refuses a squash merge, and dropped the error. It now deletes with `-D` only after proving the branch landed, and names a branch it keeps.
|
|
64
|
+
- **Two worktrees can commit to the private `.tezgah` repository at once.** In a fixture, a busy `index.lock` failed 11 of 19 close commits. Close still exited 0. Commits there now wait out a short lock, and close exits 1 when its record commit fails.
|
|
65
|
+
|
|
66
|
+
## [1.7.0] - 2026-10-12
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
|
|
70
|
+
- **Exploratory research lines (rules 7).** An ask for new ideas, methods or approaches now gets a campaign: `tezgah-research init --method exploratory`. Its arena holds the problem and a budget, and the line's scope says what it covers. A measurable arena adds an evaluator, its sha256 and its metric. The session commits the arena before the first idea. `idea`, `stage` and `match` append `ideas.jsonl`, `stages.jsonl` and `tournament.jsonl` under the lock. One function judges each row type for the writer and for `check`. An idea moves forward only, and each stage needs the one before it: prior art, then screening, a pilot, a full run and a confirmation. A kill with a reason ends it at any stage, and a duplicate ends in a kill. Prior art needs three indexed nearest works and a verdict, and `known` kills the idea in the same append. `check` refuses ideas or results committed before the arena and an arena changed after it. It refuses every stage row while the evaluator hash differs. It also refuses a confirmation on the promoting run. The same goes for a set another idea looked at first, or one this idea looked at before its promotion. It warns about thin judging and about a report that hides how many ideas it tested. `board`, `dedup` and `yield` are reports. `board` gives a Bradley-Terry prior and each idea's next action, and it is the resume point after a crash. `dedup` names its backend, embed-mrl or BM25. Its thresholds come from 40 fixture pairs and 400 human-labelled MRPC pairs. `yield` counts per operator and model family. Lines opened under rules 1-6 read none of it. The research paragraph armed per turn grows by 149 characters, and the on-demand research rule by 593.
|
|
71
|
+
- **An ask policy.** One class table in the contract now says what is asked at all. Work and process steps, a local merge of the session's branch included, are taken, never asked. A push or a release runs on a grant the user wrote in chat ("bana sormadan pushla") and is otherwise asked once. A force push, a delete, production, a credential or a spend over the new `spend_cap` setting waits for an answer. Every other question goes in one "Onay için sorular" batch with a recommended default, and the work goes on with the defaults. A new Stop class, `needless ask`, refuses once per turn a reply that ends on a question the agent can decide. A KARAR item with no options or default is no blocker any more. An omp `ask` or Claude AskUserQuestion question with no recommended option gets a warning, never a deny, and leaves an `ask` row. Each prompt's grants and lifts leave `grant` rows. `tezgah-gate replay` prints four new counts: process questions, cheap answers, ask-tool questions without a default and outward turns without a grant.
|
|
72
|
+
- **Language packs.** The detection words now live in data files, one per language: `hooks/lang/en.json` and `hooks/lang/tr.json`. They cover claims, admissions, openers, closers, residual labels, blocker tokens, prompt hints, constraint shapes and ranking stopwords. Every reader compiles the union of the packs, so a third language is one file. On a corpus of 45,810 test, doc and transcript strings, the en and tr packs answer as the inlined regexes did.
|
|
73
|
+
- **`language` and `--language`.** The reply-language setting is `language` in `config.json`. An older `reply_lang` is still read and never rewritten, and `--reply-lang` still works. A first install takes the locale (`LANGUAGE`, `LC_ALL`, `LC_MESSAGES`, `LANG`), and a locale with no pack gives English. An install over a config with neither key writes `tr`.
|
|
74
|
+
- **A router context nudge.** The prompt hook reads the router's newest input-side token count from the transcript. This works on omp, Claude and Codex, and omp now hands its session file to the prompt hook for it. At 700 k or more it adds one line, once per session until a compaction. The line says to push heavy reads and check output through a slice, or to run `/compact`. The threshold sits just under the measured p90 of 724 k. `orchestrate-off` silences it. The contract skill now says heavy check output goes through a reader slice. The disarmed-gate check now reads omp's transcript too.
|
|
75
|
+
- **An ownership lease on every delegated slice.** `Owns:` lines in a brief
|
|
76
|
+
name the globs only that slice writes. They sit on the line or as `- glob`
|
|
77
|
+
items under it. The journal stores them as `owns`. The gate refuses a
|
|
78
|
+
sibling's write into them while the owner runs. A brief may declare its
|
|
79
|
+
worktree on a `Worktree:`, `Checkout` or `Cwd:` line. The gate then refuses
|
|
80
|
+
that slice's write into the main checkout, the shape two wave-1 slices hit
|
|
81
|
+
with relative paths. A shell target resolves against the call's own `cwd`
|
|
82
|
+
and its `cd` or `pushd`. A path the brief only names declares nothing, so an
|
|
83
|
+
integrator that lists worktrees to remove still works in the main checkout.
|
|
84
|
+
The journal keeps that named worktree for reconcile, resume and the board.
|
|
85
|
+
The refusal names the owner, its globs, how to reach it - omp `write
|
|
86
|
+
agent://<name>`, Claude `SendMessage`, else the parent - and the
|
|
87
|
+
`tezgah-task slice done <id>` that frees it. `.tezgah/` and scratch stay
|
|
88
|
+
free. A closed owner frees its files. A reader agent holds no lease. omp
|
|
89
|
+
maps a writing session to its
|
|
90
|
+
record through its ledger's `spawned` row. A Claude background child maps
|
|
91
|
+
through the `agentId` its launch result names, and its SubagentStop closes
|
|
92
|
+
the record. Every other host fails open. `tezgah-task slice board` lists one
|
|
93
|
+
parent's open slices with their lease, worktree, branch and last commit.
|
|
94
|
+
`slice merge-plan` puts a branch touching MANIFEST or the citation baseline
|
|
95
|
+
last. It orders the rest by their forecast `git merge-tree` conflicts and
|
|
96
|
+
leaves CHANGELOG.md to its union driver. It names a branch it cannot
|
|
97
|
+
forecast and exits 1. A spawn whose writing slices name one worktree gets a
|
|
98
|
+
warning. The orchestration text now tells every writing brief to name
|
|
99
|
+
`Owns:` globs and declare its worktree. It gives the shared append points to
|
|
100
|
+
the integrator. Siblings message each other only on an interface or
|
|
101
|
+
ownership change. `orchestrate-off` and `slices-off` disarm it.
|
|
102
|
+
|
|
103
|
+
- **`CHANGELOG.md` merges with the union driver.** A new `.gitattributes` keeps both branches' lines, so parallel slices no longer conflict on the `[Unreleased]` head.
|
|
104
|
+
- **The todo list is the orchestrator's step queue.** The orchestration text, the fan-out rule and the orchestrator agent say to dispatch every ready step at once, mark each dispatched step blocked on its slice and never `wait` while a step is ready. Once every open step waits on a running slice, `wait` and ending the turn are both fine. The per-turn checks still run in the turn, and only the heavy tier joins at merge. Every host's todo call now leaves a `todo` ledger row with its open, ready and blocked counts and the ready heads: omp `todo` and `wait` are watched, Claude and dsh `TodoWrite` joined the PostToolUse matcher, opencode records `todowrite`, and Cursor `todo_write` and Codex `update_plan` arrive with no matcher. No pre-tool rule reads a todo call, so only omp's `wait` reaches the gate. A `ready steps` Stop class refuses, once per turn, a turn that did work and ends while the session's newest todo list holds a ready step the reply neither did nor named beside KARAR, YETKİ, GERİ ALINAMAZ or ORTAM. A step an open slice of the session works on is not ready, and a subagent's own list never counts. An omp `wait` right after a spawn, with ready steps open, gets a warning with its result, never a block.
|
|
105
|
+
- **`tezgah-task tick` and `tezgah-task note`.** `tick NNN <n>` ticks Acceptance item n, and `--undo` unticks it. `note NNN state|next "<text>"` replaces the newest State body or the Next body. Both work on any open plan and commit nothing. The gate refuses a direct write to the active plan, so an agent used to report a box it could not tick as a blocker. The refusal now names both commands.
|
|
106
|
+
- **The subagent model chain follows provider health.** A family whose provider answered a limit leaves omp's chain until its reset. The reset is read from a `429 ... reset at` or a `retry-after-ms`. An OpenRouter key with no limit left is down too. `tezgah-route --health` probes the other families, records the downs and re-applies the chain. Session start and each prompt start it in the background when it is due. `tezgah-route --fallback vendor|any|none` sets which families a chain may fall back to. Under `any`, zai comes before OpenRouter for every agent. Before, any credential counted as funded, so subagents fell back onto a key past its limit.
|
|
107
|
+
- **A `residual work` Stop class.** A turn that did work can no longer end on a list of what is left, such as "Açık kalanlar", "Doğrulanmadı:", "Sıradaki adım", "TODO" or "Next steps". The Stop rule refuses it once with "Bu maddeleri şimdi yap veya engel türü ve kanıtla yaz." An item stays when it names KARAR or GERİ ALINAMAZ in capitals. It also stays when it names YETKİ or ORTAM in capitals and quotes a command that failed in the turn; the type or the quote alone does not keep it. A label that closes itself ("Düzeltilen eksik", "Doğrulanmayan kalmadı"), a risk, and "kalan" as an adjective ("Kalan riskler", "Kalan süre") open no list. A turn with no work, a subagent's end and a nested session stay unjudged. Such a refusal counts in `blocked_claims` but not as a false completion. Before the class, 503 of 1876 final replies carried such a label, 467 of them in turns with work.
|
|
108
|
+
- **Trap cases for doable work listed as open.** The EVALS of tezgah-contract, i-have-adhd and ponytail each gained one.
|
|
109
|
+
- **Held-out, repeat and noise-band warnings (rules 6).** A measured line may name `evaluation.held_out` and log each look at it in `held_out_log.jsonl`. It may also declare `evaluation.noise_band`. `check` warns when a decision picks and reports a variant on the same cells. It warns when one candidate looks at the held-out set twice. It warns when an armed experiment runs once per arm. It warns when `capability_tolerance` has no measured band, or a wider one. None of these refuses, `--strict` included.
|
|
110
|
+
- **`tezgah-gate replay --split search|holdout`.** A stable hash of the session id picks the side. Tune a gate threshold on one side and report it on the other. The report names its side.
|
|
111
|
+
- **A `lesson_changed` ledger row.** Rewriting or removing a `.tezgah/lessons.md` line now leaves one row per line. The row holds the old line's key and its redacted text. Taint does not matter, and it never refuses.
|
|
112
|
+
- **`tezgah-doctor --repair-evidence`.** It finds sessions whose legacy ledger file went into the store more than once and counts the extra rows. Each import offset row of a file is one import, so a file imported three times keeps one copy, not two. A row written to the database directly beside the copies stays. `--apply` backs the database up first and deletes the copies in one transaction. It counts the rows against the plan before the commit and rolls back, exiting 1, if one is missing or left. It leaves a session whole for manual review when the session holds a line fewer times than its imports put it in. It names a file that is gone and skips it.
|
|
113
|
+
- **A slice journal for delegated work.** Each spawned subagent slice gets one row in `.tezgah/slices/slices.db`, a SQLite file opened like the taste store. The first open imports an older one-file-per-record journal once and deletes it. A crash, an omp `/clear`, a quota or 429 stop, or a model or host switch no longer loses it. The hooks open a record on every spawn call of omp, Claude, dsh, Cursor, Codex and opencode, and close it with a failure class. Session start lists the pending slices. `tezgah-task slice reconcile --prune` lists them with a resume brief and removes landed worktrees and branches. `slice resume` prints the native resume path or a re-dispatch brief that starts from the last commit. `slice stats` counts failures per class and host and names the lever for a repeating one. `slices-off` turns the journal off.
|
|
114
|
+
- **A SWE-Bench Pro split for arm-bench.** `benchmarks/arm-bench/swe_pro.py` samples public Pro instances and grades them with the official test script. Its `validate` checks that the gold patch passes and the base fails. The committed 3-instance sample validates 3 of 3.
|
|
115
|
+
- **A cross-project write notice.** A write into another repository than the session's cwd gets one line, once per session and target repository. The line says that repository's plans, lessons and slice journal do not apply to this session. It never refuses. A worktree counts as its main checkout. opencode does not show it yet.
|
|
116
|
+
|
|
117
|
+
### Fixed
|
|
118
|
+
|
|
119
|
+
- **A subagent's limit stop moves the routing at once.** A slice that stops on a quota or rate limit now holds its provider family down in the health record and re-applies omp's chain. Before, the next session start's probe moved it. A quota is held until its reset or six hours, a rate limit until its retry-after or ten minutes. The mode's own family still leads every chain. The record is re-read under a lock before each write. A YETKİ or ORTAM blocker that quotes a model provider's quota or rate limit is refused while another family the fallback setting allows is up. The refusal names that family. A GitHub or npm rate limit stays a blocker. The task gate's scope refusal prints the ready `tezgah-task allow` line with the current globs and the new path, and none for a path outside the repository. `RELEASING.md` ends with a local `tezgah-setup --refresh` and version read.
|
|
120
|
+
- **An English install no longer asks for Turkish tokens.** The contract and the Stop refusals name the blocker heading, the admission word and the four blocker tokens in the configured language: `Blockers`, `unverified` and DECISION/ACCESS/IRREVERSIBLE/ENV under `en`. Under `tr` they stay "Engeller", `doğrulanmadı` and KARAR/YETKİ/GERİ ALINAMAZ/ORTAM. The Turkish first lines of the residual, ready-step and stopped-slice refusals now come only with `tr`. The reviewer and orchestrator agents and `tezgah-task`'s review lines take the token from the same pack. The contract, plan-sync and i-have-adhd skills name both spellings. opencode's todo reader takes its tokens from the packs. The Stop rule accepts both spellings.
|
|
121
|
+
- **Spanish, French and Dutch prose no longer passes as Turkish.** The reply-language markers dropped de, da, en, ne, ya, her and mi. Those replies now score 0. No Turkish reply in the 4,003 judged corpus texts changed sides.
|
|
122
|
+
- **Another session no longer takes over a stopped slice.** The journal keyed a slice record by the agent name alone. A second session that spawned an unrelated `Fixer` re-opened the first session's stopped `Fixer`, which then left that session's resume line and its Stop rule. A same-named spawn under another brief now gets its own record, `Fixer-<sha1[:6]>`. The same brief from another session still re-opens the record. `slice done|stop|resume` takes an exact id from any session, so a crashed session's slice stays reachable. A bare spawned name answers only for this session under `TEZGAH_SESSION`. The review guard counts this session's records by id, agent or name, and its refusal prints the exact id. A slice whose brief works in another repository names it as `repo`, and its line says `(in <name>)`.
|
|
123
|
+
- **The race rule knows a session's own subagent and its fork.** A subagent stopped on a quota error. The rule then refused its parent's takeover for ten minutes. A subagent whose slice record closed after its last write of the file is now no race. A live one still is. The refusal now names how each writer relates to this session, such as your subagent or an omp fork. It used to print a bare ledger id. The omp hook records a fork's parent at its start.
|
|
124
|
+
- **The transcript miners read only the current repository.** `tezgah-taste mine` and `tests/replay_context.py` read every project under `~/Projects` by default. Another project's prompts and paths then landed in this repository's research data. Both now read the current repository. `--all-projects` reads every project. `mine` writes a pseudonym (`project-<sha1[:8]>`) for each mined repository, in its `repo` and `cwd` and in every prompt, in any case.
|
|
125
|
+
- **The resume block names this repository's last check.** A session that last ran a check in another project showed that check as this repository's.
|
|
126
|
+
- **The residual-list rule asks every turn it should.** On omp the reply after a Stop refusal sat in a turn of its own. A rewrite with no tool call skipped `residual work`, and a failure quoted from the refused turn did not count. That reply is now judged on the rows of every turn in the re-ask chain. A subagent's end is still never refused, but its `subagent_end` row counts the items no type accounts for. The parent's Stop refuses a reply that types none of its own. Claude and Cursor write that row from their subagent-stop events. omp's bridge now writes it from a task session's `agent_end`, into the ledger of the session that spawned the child, once per child. Codex, opencode and dsh still write none. A turn of known reads, such as `git status`, `tail`, `find`, `jq`, `sed -n`, `gh pr view`, `curl -s`, a read-only `sqlite3` or `ssh host ls`, no longer owes a check. A reader with a writing option or script, such as `find -exec`, `sort -o` or `curl -o`, still does, and so does any other command, a commit or an unknown program included. The labels gained first-person negatives ("Yapamadım", "Test etmedim"), "Yarım kalanlar", "Henüz kontrol edilmeyen", "Unresolved", "Still open" and a residual word alone on a line over a list. "Engeller: yok", an empty "Engeller" heading and a blocker item that reports itself gone ("… yoktu. Artık var.") hold no item. A YETKİ or ORTAM item now needs an error of its type, such as a missing tool or a 403. It also needs a quoted command that failed, or, for an item about a subagent or a provider, a spawn call of the turn that failed at the provider. An assertion, a grep alone that matched nothing, a bare "yok" or a failed edit no longer passes as one. A spent quota now classifies as a `user` failure.
|
|
127
|
+
- **A session lists only its own slices.** Every prompt listed each pending slice of the repository, so another session's stopped slices rode this session's context. The prompt now lists only the slices this session spawned and those opened by hand. The resume block at session start adds one line that counts the other sessions' slices. `tezgah-task slice reconcile` still lists them all. The `.tezgah` repository also ignores the journal now.
|
|
128
|
+
- **A device-id change no longer imports a ledger twice.** The store found a legacy file's offset by name, device and inode. After the volume's device id changed, every file went in again from the start, and most ledgers read every row twice. The store now resumes from the longest import under any device id, so an old id coming back imports nothing again either. It keeps one offset row per earlier import for the repair to count.
|
|
129
|
+
- **The task rule no longer refuses targets that are no project file.** It refused `write xd://<tool>`, `agent://X` and `conflict://2` as paths outside the allowlist. It refused `git diff > $B/x.patch` under the literal name `$B/x.patch`. It refused files in tezgah's own `.tezgah/` workspace. Now a URI, a shell target with an unknown variable and the `.tezgah/` workspace pass. A `file://` URI and a variable set to the checkout stay held. So do the slice journal and a plan file that could make itself active. The gate first expands a variable set earlier on the line. The record rule still refuses the active plan record, and `tezgah-task start`, `review`, `tick` and `note` take only an open plan.
|
|
130
|
+
- **The task allowlist reads linked worktrees.** The gate used to place a file in a linked worktree outside the repo root. It then refused every in-scope write there. The gate now matches such a file against the worktree's own top. A worktree of another repository stays outside.
|
|
131
|
+
- **The task allowlist expands `~`.** It read a `~/...` path as relative to the working directory. It then refused an in-scope file spelled that way. With a `**` allowlist it also let a write to the home directory through as a repo path. It now expands `~` first.
|
|
132
|
+
- **The plan and slow role fallbacks reach omp.** `apply_omp` wrote them under `retry.fallbackChains.fallbackChains.plan`, a key omp never reads. They now go under `plan` and `slow`, and `apply_omp` deletes the misplaced keys.
|
|
133
|
+
|
|
134
|
+
### Changed
|
|
135
|
+
|
|
136
|
+
- **The Stop rule judges the reply after a block.** The rule used to only record that reply, so a new fault in it went unjudged. Now the rule refuses it once more for a new class, one it has not refused in that turn. An example is a blocker list with no evidence after a `no verify_ok` refusal. A turn takes at most two refusals and never two of one class. The core counts them from the turn's ledger rows for Claude, Codex, Cursor and omp. On omp the block comes back as a new `turn` row, and a refusal row's `reask` key ties that row to its turn. Before, omp left that reply unjudged and unrecorded. A host's own flag decides only when the session has no turn row.
|
|
137
|
+
- **"doğrulanmadı" no longer ends a turn by itself.** The admission used to clear every Stop refusal, so writing it cost less than verifying. It now clears only a class whose repair is a check: check failed, partial failure, stale evidence and no verify_ok. It needs a check that ran and failed after the turn's newest change. It clears no ui_ok only when that failed check is the screen or design check itself, it never clears no external read or evidence tampered, and the tamper check now runs first.
|
|
138
|
+
- **Residuals are work, not a report section.** The Integrity paragraph carries this rule. Its label reads "Integrity: evidence, or a named blocker." It asks for every open item to be done now. An item that cannot be done stays as one of the four blockers, with its evidence, under one "Engeller" heading of at most three lines. The per-turn reminder carries the short form. Ponytail notes only a requested part the session cut. A reply ends on the one step only the reader can take, or on nothing. The decision-quality line no longer asks to name a skipped check: it asks to run every check the change reaches. The session fixes a minor review finding, and fix-later needs a user decision (KARAR).
|
|
139
|
+
- **The session budgets move with the core.** The residual rule added 1473 B to the core and 151 B to the reminder. `session_start` and `post_compact` go from 13000 to 14500 B, and `user_prompt` from 6000 to 6500 B.
|
|
140
|
+
- **Heavy checks run once, at merge.** In a turn the session runs compileall, ruff and the impacted tests of what changed. The full suite, neuter, both docs audits, `render_table.py --acceptance --strict` and the e2e scripts run once at the merge step and in CI. A replay of 522 turns keeps about 0.6 of the in-turn checks with no lost failure. The Stop refusals for stale evidence and a missing check now name `python3 tests/impacted.py --run <paths>`. `tezgah-task` lists the deferred checks as the merge step's one run, and CI runs `tezgah-docs --clarity` beside every citations step.
|
|
141
|
+
- **The session resumes a stopped slice instead of absorbing it.** The orchestration text and the orchestrator agent say so. A slice commits `wip:` after each step. The session resumes or re-dispatches a stopped slice, and `tezgah-task slice reconcile --prune` runs before the turn that merged slices ends.
|
|
142
|
+
- **Judge figures name their provider.** Each judge figure in `docs/judge.md` and `docs/models.md` now names its provider and model. Where the record does not say, the page says so. A figure does not carry over to another default.
|
|
143
|
+
|
|
9
144
|
## [1.6.0] - 2026-10-10
|
|
10
145
|
|
|
11
146
|
### Added
|
|
@@ -1026,7 +1161,11 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
1026
1161
|
the copy could not find `hooks/` and died on import. It now links into the
|
|
1027
1162
|
installed tree, and also installs the command as `tezgah`.
|
|
1028
1163
|
|
|
1029
|
-
[Unreleased]: https://github.com/r1z4x/tezgah/compare/v1.
|
|
1164
|
+
[Unreleased]: https://github.com/r1z4x/tezgah/compare/v1.8.2...HEAD
|
|
1165
|
+
[1.8.2]: https://github.com/r1z4x/tezgah/releases/tag/v1.8.2
|
|
1166
|
+
[1.8.1]: https://github.com/r1z4x/tezgah/releases/tag/v1.8.1
|
|
1167
|
+
[1.8.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.8.0
|
|
1168
|
+
[1.7.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.7.0
|
|
1030
1169
|
[1.6.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.6.0
|
|
1031
1170
|
[1.5.1]: https://github.com/r1z4x/tezgah/releases/tag/v1.5.1
|
|
1032
1171
|
[1.5.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.5.0
|
package/MANIFEST
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
.gitattributes
|
|
1
2
|
.github/ISSUE_TEMPLATE/bug_report.yml
|
|
2
3
|
.github/ISSUE_TEMPLATE/config.yml
|
|
3
4
|
.github/ISSUE_TEMPLATE/feature_request.yml
|
|
@@ -111,6 +112,8 @@ docs/status-line.md
|
|
|
111
112
|
docs/testing.md
|
|
112
113
|
docs/vision.md
|
|
113
114
|
hooks/hooks.json
|
|
115
|
+
hooks/lang/en.json
|
|
116
|
+
hooks/lang/tr.json
|
|
114
117
|
hooks/projects-auto-init.py
|
|
115
118
|
hooks/projects-posttooluse.py
|
|
116
119
|
hooks/projects-pretooluse.py
|
|
@@ -664,6 +667,8 @@ tests/e2e_tezgah_mcp.py
|
|
|
664
667
|
tests/fuzz_shell.py
|
|
665
668
|
tests/gate_vectors.json
|
|
666
669
|
tests/impacted.py
|
|
670
|
+
tests/install_cache.py
|
|
671
|
+
tests/neuter_killers.json
|
|
667
672
|
tests/neuter_matrix.py
|
|
668
673
|
tests/replay_context.py
|
|
669
674
|
tests/routing-fixtures.md
|
|
@@ -719,6 +724,7 @@ tests/test_integrity.py
|
|
|
719
724
|
tests/test_interpreter.py
|
|
720
725
|
tests/test_judge.py
|
|
721
726
|
tests/test_lang.py
|
|
727
|
+
tests/test_lang_packs.py
|
|
722
728
|
tests/test_lesson_lineage.py
|
|
723
729
|
tests/test_lessons.py
|
|
724
730
|
tests/test_lessons_compile.py
|
|
@@ -752,6 +758,7 @@ tests/test_shapes.py
|
|
|
752
758
|
tests/test_skill_pick.py
|
|
753
759
|
tests/test_skill_search.py
|
|
754
760
|
tests/test_skills.py
|
|
761
|
+
tests/test_slices.py
|
|
755
762
|
tests/test_snapshot.py
|
|
756
763
|
tests/test_stack.py
|
|
757
764
|
tests/test_statusline.py
|
package/README.md
CHANGED
|
@@ -120,9 +120,12 @@ it set out to arm is not armed.
|
|
|
120
120
|
turns it off.
|
|
121
121
|
- **orx.** The install fetches the OpenResearch CLI that research tasks route
|
|
122
122
|
through; `TEZGAH_NO_DEPS=1` skips it.
|
|
123
|
-
- **Reply language.**
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
- **Reply language.** A first install asks for replies in your locale's
|
|
124
|
+
language (`LANG` and friends) when tezgah ships a pack for it (`tr`, `en`),
|
|
125
|
+
else English; an existing install keeps its language. `--language en` asks
|
|
126
|
+
for English and `any` for your own language: `curl -fsSL … | sh -s --
|
|
127
|
+
--reply-lang en` (the installer's older spelling of `--language`). Only
|
|
128
|
+
Turkish replies are checked by the Stop rule.
|
|
126
129
|
|
|
127
130
|
Rather let your coding assistant do it? Paste this into omp, Claude Code,
|
|
128
131
|
Codex, Cursor or opencode:
|
package/bin/tezgah-decisions
CHANGED
|
@@ -46,7 +46,6 @@ Exit codes: 0 every record is whole (or there are none), 1 a record was refused
|
|
|
46
46
|
import json
|
|
47
47
|
import os
|
|
48
48
|
import re
|
|
49
|
-
import subprocess
|
|
50
49
|
import sys
|
|
51
50
|
|
|
52
51
|
HERE = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
|
@@ -67,19 +66,12 @@ RECORD = re.compile(r"^(\d{3})-[^/]+\.md$")
|
|
|
67
66
|
|
|
68
67
|
|
|
69
68
|
def repo_root(path):
|
|
70
|
-
"""The repository this run is about:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
out = subprocess.run(["git", "-C", path, "rev-parse", "--show-toplevel"],
|
|
77
|
-
capture_output=True, text=True)
|
|
78
|
-
if out.returncode == 0 and out.stdout.strip():
|
|
79
|
-
return out.stdout.strip()
|
|
80
|
-
except OSError:
|
|
81
|
-
pass
|
|
82
|
-
return os.path.realpath(path)
|
|
69
|
+
"""The repository this run is about: its main checkout, the one whose
|
|
70
|
+
`.tezgah` holds the records from a linked worktree or from inside `.tezgah`
|
|
71
|
+
too (`tezgah_task.main_repo`, the root bin/tezgah-task keeps plans under),
|
|
72
|
+
else the directory the CLI was called in - a decision recorded under a tree
|
|
73
|
+
git does not know is still a decision."""
|
|
74
|
+
return tt.main_repo(path) or os.path.realpath(path)
|
|
83
75
|
|
|
84
76
|
|
|
85
77
|
def decision_record(text):
|
package/bin/tezgah-design
CHANGED
|
@@ -30,6 +30,10 @@ import os
|
|
|
30
30
|
import re
|
|
31
31
|
import sys
|
|
32
32
|
|
|
33
|
+
HERE = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
|
34
|
+
sys.path.insert(0, os.path.join(HERE, "hooks"))
|
|
35
|
+
from tezgah_paths import linked_main, project_top # noqa: E402
|
|
36
|
+
|
|
33
37
|
# The state set every interactive control and every data view owes: seven per
|
|
34
38
|
# control, eight per data view, thirteen distinct states - `loading` and `error`
|
|
35
39
|
# are the two the halves share, and the shipped lists name them twice rather than
|
|
@@ -818,10 +822,14 @@ def _print_json(payload):
|
|
|
818
822
|
|
|
819
823
|
|
|
820
824
|
def derive_cli(opts):
|
|
821
|
-
root = os.path.abspath(opts.get("repo") or "")
|
|
825
|
+
root = project_top(os.path.abspath(opts.get("repo") or ""))
|
|
822
826
|
if not os.path.isdir(root):
|
|
823
827
|
raise Usage("--repo must name a directory")
|
|
824
|
-
|
|
828
|
+
# the tokens are this checkout's; the contract goes to the repository's one
|
|
829
|
+
# workspace, the main checkout's `.tezgah` (plan 097), which a worktree's
|
|
830
|
+
# removal does not take with it
|
|
831
|
+
ws = linked_main(root) or root
|
|
832
|
+
out = opts.get("out") or os.path.join(ws, ".tezgah", "design-contract.md")
|
|
825
833
|
if os.path.exists(out):
|
|
826
834
|
# A contract that exists is the repository's own decision: it carries
|
|
827
835
|
# the component inventory someone filled in, so overwriting it would
|
|
@@ -829,7 +837,7 @@ def derive_cli(opts):
|
|
|
829
837
|
# deliberate `rm` first.
|
|
830
838
|
raise Usage("the contract already exists at %s: a contract is edited, "
|
|
831
839
|
"not overwritten - remove it first or pass --out elsewhere"
|
|
832
|
-
% os.path.
|
|
840
|
+
% os.path.abspath(out))
|
|
833
841
|
tokens, source, sources = derive(root)
|
|
834
842
|
if tokens is None:
|
|
835
843
|
if opts.get("json"):
|
|
@@ -850,7 +858,7 @@ def derive_cli(opts):
|
|
|
850
858
|
fh.write(text)
|
|
851
859
|
except OSError as exc:
|
|
852
860
|
raise Usage("cannot write %s: %s" % (out, exc))
|
|
853
|
-
shown = os.path.relpath(out,
|
|
861
|
+
shown = os.path.relpath(out, ws)
|
|
854
862
|
if opts.get("json"):
|
|
855
863
|
_print_json({"ok": True, "out": shown, "source": source,
|
|
856
864
|
"colors": len(data["tokens"]["colors"]),
|
|
@@ -858,9 +866,11 @@ def derive_cli(opts):
|
|
|
858
866
|
"type_scale": data["tokens"]["type_scale"],
|
|
859
867
|
"components": len(data["components"])})
|
|
860
868
|
else:
|
|
869
|
+
# absolute: from a worktree the contract is the main checkout's, which a
|
|
870
|
+
# `.tezgah/...` read from the worktree's cwd does not name (review N1)
|
|
861
871
|
sys.stdout.write(
|
|
862
872
|
"wrote %s (source: %s, %d colour roles, %s, %d type steps)\n"
|
|
863
|
-
% (
|
|
873
|
+
% (os.path.abspath(out), source, len(data["tokens"]["colors"]),
|
|
864
874
|
"%spx unit" % data["tokens"]["space_unit"]
|
|
865
875
|
if data["tokens"]["space_unit"] else "no spacing unit found",
|
|
866
876
|
len(data["tokens"]["type_scale"])))
|
|
@@ -876,6 +886,12 @@ def check_cli(opts):
|
|
|
876
886
|
measured = opts.get("measured")
|
|
877
887
|
if not contract or not measured:
|
|
878
888
|
raise Usage("check wants --contract FILE and --measured FILE|-")
|
|
889
|
+
if (not os.path.isabs(contract) and not os.path.exists(contract)
|
|
890
|
+
and contract.replace(os.sep, "/").startswith(".tezgah/")):
|
|
891
|
+
# a worktree's `.tezgah/...` is the main checkout's workspace (plan 097,
|
|
892
|
+
# review N1): the path the Stop refusal and the reviewer prompt name
|
|
893
|
+
cwd = os.getcwd()
|
|
894
|
+
contract = os.path.join(linked_main(cwd) or cwd, contract)
|
|
879
895
|
data, reason = read_contract(contract)
|
|
880
896
|
if data is None:
|
|
881
897
|
raise Usage(reason)
|
package/bin/tezgah-docs
CHANGED
|
@@ -106,7 +106,7 @@ RUN = re.compile(r"((?:`[A-Za-z_][A-Za-z0-9_]*(?:\(\))?`\s*[/,]?\s*){1,5})$")
|
|
|
106
106
|
CITE = re.compile(r"`([\w.-]+(?:/[\w.-]+)+|[\w-]+\.\w+):(\d+)(?:-(\d+))?`"
|
|
107
107
|
r"|`(:(\d+)(?:-(\d+))?)`")
|
|
108
108
|
# The symbol written after the citation instead of before it, the way a table
|
|
109
|
-
# cell reads: `hooks/tezgah_policy.py:
|
|
109
|
+
# cell reads: `hooks/tezgah_policy.py:784-1100` (`CORE`).
|
|
110
110
|
AFTER = re.compile(r"\s*\(`([A-Za-z_][A-Za-z0-9_]*)(?:\(\))?`\)")
|
|
111
111
|
# Any `path:line`, backticked or not: what CITE does not match of these (a
|
|
112
112
|
# bare token, a comma list `x.py:12,40`) is counted as unjudged, so a citation
|
|
@@ -405,12 +405,14 @@ def ratchet_drift(unjudged, baseline):
|
|
|
405
405
|
# audit reads a symbol: the gate's rules are the `_deny(session_id, "<rule>",
|
|
406
406
|
# ...)` calls in hooks/tezgah_gate.py, and the Stop rule's triggers are the
|
|
407
407
|
# classes `_stop_block` returns - including the four `_shape_block` and the six
|
|
408
|
-
# `_evidence_block` hand back,
|
|
408
|
+
# `_evidence_block` hand back, and the one `_residual_block` adds at every exit
|
|
409
|
+
# that would end the turn, which is why those three functions are read too.
|
|
409
410
|
|
|
410
411
|
GATE = os.path.join(HERE, "hooks", "tezgah_gate.py")
|
|
411
412
|
STOP = os.path.join(HERE, "hooks", "tezgah_integrity.py")
|
|
412
413
|
LEDGER = os.path.join(HERE, "docs", "gate.md")
|
|
413
414
|
LEDGER_COLUMNS = ("rule", "incident", "evidence", "pin")
|
|
415
|
+
STOP_FUNCS = ("_stop_block", "_shape_block", "_evidence_block", "_residual_block")
|
|
414
416
|
|
|
415
417
|
|
|
416
418
|
def source(path):
|
|
@@ -450,13 +452,12 @@ def gate_rules(text=None):
|
|
|
450
452
|
|
|
451
453
|
|
|
452
454
|
def stop_triggers():
|
|
453
|
-
"""The classes `_stop_block` can return, the shape and
|
|
454
|
-
included."""
|
|
455
|
+
"""The classes `_stop_block` can return, the shape, evidence and residual
|
|
456
|
+
halves included."""
|
|
455
457
|
tree = ast.parse(source(STOP))
|
|
456
458
|
out = set()
|
|
457
459
|
for node in ast.walk(tree):
|
|
458
|
-
if isinstance(node, ast.FunctionDef) and node.name in
|
|
459
|
-
"_stop_block", "_shape_block", "_evidence_block"):
|
|
460
|
+
if isinstance(node, ast.FunctionDef) and node.name in STOP_FUNCS:
|
|
460
461
|
out |= returned_classes(node)
|
|
461
462
|
return out
|
|
462
463
|
|
|
@@ -535,12 +536,13 @@ CONTRACT_SKILL = os.path.join(HERE, "skills", "tezgah-contract", "SKILL.md")
|
|
|
535
536
|
SETUP = os.path.join(HERE, "bin", "tezgah-setup")
|
|
536
537
|
NUMBERS = ("zero", "one", "two", "three", "four", "five", "six", "seven",
|
|
537
538
|
"eight", "nine", "ten")
|
|
538
|
-
# ADR 007 (2026-10-05) leaves these unclassified:
|
|
539
|
-
# kill-switch paragraph does not list
|
|
540
|
-
#
|
|
541
|
-
#
|
|
539
|
+
# ADR 007 (2026-10-05) leaves these unclassified: three `off()` names the CORE
|
|
540
|
+
# kill-switch paragraph does not list (`slices-off` joined on 2026-10-11 with
|
|
541
|
+
# the slice journal) and two opt-in markers read with `armed()`. Until the owner
|
|
542
|
+
# decides whether they are rule switches the check pins CORE against the code
|
|
543
|
+
# and carries these as the one explicit exception.
|
|
542
544
|
UNCLASSIFIED_SWITCHES = frozenset((
|
|
543
|
-
"agents-off", "update-check-off", "taste-on", "skill-suggest-on"))
|
|
545
|
+
"agents-off", "update-check-off", "slices-off", "taste-on", "skill-suggest-on"))
|
|
544
546
|
|
|
545
547
|
|
|
546
548
|
def rule_sites(text=None):
|
|
@@ -835,8 +837,8 @@ def _guarded_returns(func, funcs, pick):
|
|
|
835
837
|
names, with what its enclosing `if` tests guard it by. A conjunct of a test
|
|
836
838
|
that reads a switch (`_switch_words`, or a call to a function in `funcs`
|
|
837
839
|
that reads one: `_adhd_armed`) is a switch guard; a conjunct that names no
|
|
838
|
-
argument or local of `func` is a condition on the environment
|
|
839
|
-
|
|
840
|
+
argument or local of `func` is a condition on the environment
|
|
841
|
+
(`_markers(language())`); a conjunct reading the call's own data is the trigger itself and
|
|
840
842
|
is not a guard. The switch a whole hook checks first (`pretooluse-off`, the
|
|
841
843
|
Stop hook's `verify-off`) is outside these functions and is not counted."""
|
|
842
844
|
own = {a.arg for a in func.args.args + func.args.kwonlyargs} | {
|
|
@@ -874,11 +876,11 @@ def _guarded_returns(func, funcs, pick):
|
|
|
874
876
|
|
|
875
877
|
|
|
876
878
|
def stop_sites():
|
|
877
|
-
"""[(class, switches, conditions)] for every class `
|
|
878
|
-
|
|
879
|
+
"""[(class, switches, conditions)] for every class the `STOP_FUNCS`
|
|
880
|
+
return (`_guarded_returns`)."""
|
|
879
881
|
tree = ast.parse(source(STOP), filename=STOP)
|
|
880
882
|
funcs = {n.name: n for n in tree.body if isinstance(n, ast.FunctionDef)}
|
|
881
|
-
return [site for name in
|
|
883
|
+
return [site for name in STOP_FUNCS
|
|
882
884
|
for site in _guarded_returns(funcs[name], funcs,
|
|
883
885
|
lambda r: sorted(returned_classes(r)))]
|
|
884
886
|
|
package/bin/tezgah-doctor
CHANGED
|
@@ -42,8 +42,13 @@ this tree.
|
|
|
42
42
|
tezgah-doctor --prune-sessions 30 delete sessions idle > 30 days, then vacuum
|
|
43
43
|
tezgah-doctor --import-evidence import the legacy JSONL evidence ledgers
|
|
44
44
|
into the evidence database now
|
|
45
|
+
tezgah-doctor --repair-evidence [--apply]
|
|
46
|
+
the rows a legacy ledger imported again
|
|
47
|
+
under another device id holds; --apply
|
|
48
|
+
backs the database up and deletes them
|
|
45
49
|
"""
|
|
46
50
|
import argparse
|
|
51
|
+
import collections
|
|
47
52
|
import importlib.machinery
|
|
48
53
|
import importlib.util
|
|
49
54
|
import json
|
|
@@ -343,6 +348,143 @@ def import_evidence():
|
|
|
343
348
|
for base in dict.fromkeys((CACHE, fallback_cache()))}
|
|
344
349
|
|
|
345
350
|
|
|
351
|
+
def evidence_plan(conn, cache):
|
|
352
|
+
"""The copies every legacy ledger the remount bug imported more than once
|
|
353
|
+
put in the cache database `conn`: ({session: [row n to delete]},
|
|
354
|
+
{(name, ino): (current dev, longest import's bytes)} to collapse to,
|
|
355
|
+
{(session, ino): why that file is left alone}, {session: {line text: rows it
|
|
356
|
+
holds after the repair}}).
|
|
357
|
+
|
|
358
|
+
Every `imported` row of one (name, ino) is one import stream: a store from
|
|
359
|
+
before the fix started over at offset 0 under a renumbered device id, and
|
|
360
|
+
the store now swaps only the row it resumed from and keeps a file cut
|
|
361
|
+
below what went in as one stream (`tezgah_store._record`).
|
|
362
|
+
A stream put the file's first `bytes` in, so a line text went in once per
|
|
363
|
+
occurrence in each stream's prefix. The longest stream's count stays; every
|
|
364
|
+
other stream's count is a copy, and that many of the newest rows go. A row
|
|
365
|
+
written to the database directly beside them stays. A session holding a
|
|
366
|
+
line fewer times than its streams put it in is not what they explain - a
|
|
367
|
+
lost row, or a row that only continued another import, as the unreleased
|
|
368
|
+
plan-078 store wrote - so it is left whole for review; only a direct row
|
|
369
|
+
equal to every line such a row covers would hide it.
|
|
370
|
+
|
|
371
|
+
Only the file still at its name with that inode, and at least as long as
|
|
372
|
+
its longest stream, is read: another file there is listed and skipped,
|
|
373
|
+
because counting its lines would delete the wrong rows."""
|
|
374
|
+
import tezgah_store
|
|
375
|
+
streams = collections.defaultdict(list)
|
|
376
|
+
for name, ino, size in conn.execute(
|
|
377
|
+
"SELECT name, ino, bytes FROM imported WHERE (name, ino) IN "
|
|
378
|
+
"(SELECT name, ino FROM imported GROUP BY name, ino HAVING count(*) > 1) "
|
|
379
|
+
"ORDER BY name"):
|
|
380
|
+
streams[(name, ino)].append(size)
|
|
381
|
+
doomed, collapse, review, bounds = {}, {}, {}, {}
|
|
382
|
+
for (name, ino), sizes in streams.items():
|
|
383
|
+
path = os.path.join(cache, "evidence", name)
|
|
384
|
+
session = tezgah_store._ledger(path)[1]
|
|
385
|
+
try:
|
|
386
|
+
st = os.stat(path)
|
|
387
|
+
with open(path, "rb") as fh:
|
|
388
|
+
raw = fh.read(max(sizes))
|
|
389
|
+
except OSError:
|
|
390
|
+
st = None
|
|
391
|
+
if st is None or st.st_ino != ino or st.st_size < max(sizes):
|
|
392
|
+
review[(session, ino)] = ("skipped: its file is gone, replaced or shorter "
|
|
393
|
+
"than its imports")
|
|
394
|
+
continue
|
|
395
|
+
counts = []
|
|
396
|
+
for size in sizes:
|
|
397
|
+
prefix = raw[:size]
|
|
398
|
+
lines = prefix[:prefix.rfind(b"\n") + 1].split(b"\n")
|
|
399
|
+
counts.append(collections.Counter(filter(None, map(tezgah_store._line_text, lines))))
|
|
400
|
+
longest = counts[sizes.index(max(sizes))]
|
|
401
|
+
put = sum(counts, collections.Counter())
|
|
402
|
+
rows = conn.execute("SELECT n, row FROM evidence WHERE session = ? ORDER BY n DESC",
|
|
403
|
+
(session,)).fetchall()
|
|
404
|
+
copies = collections.defaultdict(list)
|
|
405
|
+
for n, text in rows:
|
|
406
|
+
copies[text].append(n)
|
|
407
|
+
short = sum(1 for text, many in put.items() if len(copies[text]) < many)
|
|
408
|
+
if short:
|
|
409
|
+
review[(session, ino)] = ("manual review: %d line(s) held fewer times than its "
|
|
410
|
+
"imports put them in" % short)
|
|
411
|
+
continue
|
|
412
|
+
extra = put - longest
|
|
413
|
+
doomed[session] = [n for text, many in extra.items() for n in copies[text][:many]]
|
|
414
|
+
bounds[session] = {text: len(copies[text]) - extra[text] for text in longest}
|
|
415
|
+
collapse[(name, ino)] = (st.st_dev, max(sizes))
|
|
416
|
+
return doomed, collapse, review, bounds
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def print_plan(plan):
|
|
420
|
+
doomed, _collapse, review, _bounds = plan
|
|
421
|
+
for session, ns in doomed.items():
|
|
422
|
+
if ns:
|
|
423
|
+
print(" %s %d row(s)" % (session, len(ns)))
|
|
424
|
+
for (session, _ino), why in review.items():
|
|
425
|
+
print(" %s %s" % (session, why))
|
|
426
|
+
total = sum(map(len, doomed.values()))
|
|
427
|
+
print(" total %d row(s) in %d session(s)" % (total, sum(1 for ns in doomed.values() if ns)))
|
|
428
|
+
return total
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def repair_evidence(apply):
|
|
432
|
+
"""`--repair-evidence` over the cache database and its sandbox fallback's:
|
|
433
|
+
the dry run prints `evidence_plan`; with `apply` the database is backed up
|
|
434
|
+
through the SQLite backup API first, the rows go and `imported` collapses to
|
|
435
|
+
one row per file in one transaction, whose rows are counted against the
|
|
436
|
+
plan before it commits: a count off the plan rolls it back. The exit code."""
|
|
437
|
+
import tezgah_store
|
|
438
|
+
code = 0
|
|
439
|
+
for cache in dict.fromkeys((CACHE, fallback_cache())):
|
|
440
|
+
db = os.path.join(cache, tezgah_store.CACHE_DB)
|
|
441
|
+
if not os.path.exists(db):
|
|
442
|
+
continue
|
|
443
|
+
print("%s: evidence repair, %s" % (db, "apply" if apply else "dry run"))
|
|
444
|
+
conn = sqlite3.connect(db, isolation_level=None)
|
|
445
|
+
try:
|
|
446
|
+
conn.execute("PRAGMA busy_timeout = %d" % tezgah_store.BUSY_MS)
|
|
447
|
+
if not apply:
|
|
448
|
+
print_plan(evidence_plan(conn, cache))
|
|
449
|
+
continue
|
|
450
|
+
backup = "%s.bak-%s" % (db, time.strftime("%Y%m%d-%H%M%S"))
|
|
451
|
+
os.close(os.open(backup, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600))
|
|
452
|
+
dst = sqlite3.connect(backup)
|
|
453
|
+
try:
|
|
454
|
+
conn.backup(dst)
|
|
455
|
+
finally:
|
|
456
|
+
dst.close()
|
|
457
|
+
print(" backup %s" % backup)
|
|
458
|
+
with tezgah_store.transaction(conn):
|
|
459
|
+
doomed, collapse, _review, bounds = plan = evidence_plan(conn, cache)
|
|
460
|
+
print_plan(plan)
|
|
461
|
+
conn.executemany("DELETE FROM evidence WHERE n = ?",
|
|
462
|
+
[(n,) for ns in doomed.values() for n in ns])
|
|
463
|
+
for (name, ino), (dev, size) in collapse.items():
|
|
464
|
+
conn.execute("DELETE FROM imported WHERE name = ? AND ino = ?", (name, ino))
|
|
465
|
+
conn.execute("INSERT INTO imported (name, dev, ino, bytes) "
|
|
466
|
+
"VALUES (?, ?, ?, ?)", (name, dev, ino, size))
|
|
467
|
+
# the rows against the plan before the commit: a row the plan
|
|
468
|
+
# kept that is gone (missing), or a copy it deleted still there
|
|
469
|
+
# (left), means the deletion went wrong, and it rolls back
|
|
470
|
+
missing = left = 0
|
|
471
|
+
for session, want in bounds.items():
|
|
472
|
+
held = collections.Counter(text for (text,) in conn.execute(
|
|
473
|
+
"SELECT row FROM evidence WHERE session = ?", (session,)))
|
|
474
|
+
for text, many in want.items():
|
|
475
|
+
missing += max(0, many - held[text])
|
|
476
|
+
left += max(0, held[text] - many)
|
|
477
|
+
print(" re-check: %d row(s) missing, %d row(s) left" % (missing, left))
|
|
478
|
+
if missing or left:
|
|
479
|
+
raise sqlite3.DatabaseError("the re-check is off the plan; rolled back")
|
|
480
|
+
except (OSError, sqlite3.Error) as exc:
|
|
481
|
+
print(" failed: %s" % exc, file=sys.stderr)
|
|
482
|
+
code = 1
|
|
483
|
+
finally:
|
|
484
|
+
conn.close()
|
|
485
|
+
return code
|
|
486
|
+
|
|
487
|
+
|
|
346
488
|
|
|
347
489
|
def load_probe():
|
|
348
490
|
"""The bench spike, imported by path so its two coverage rules stay the only
|
|
@@ -465,14 +607,24 @@ def main():
|
|
|
465
607
|
help="import the legacy JSONL evidence ledgers into the "
|
|
466
608
|
"evidence database now, instead of at the next "
|
|
467
609
|
"session start's daily run")
|
|
610
|
+
ap.add_argument("--repair-evidence", action="store_true",
|
|
611
|
+
help="count the evidence rows a legacy ledger imported "
|
|
612
|
+
"again under another device id (dry run)")
|
|
613
|
+
ap.add_argument("--apply", action="store_true",
|
|
614
|
+
help="with --repair-evidence: back the database up, then "
|
|
615
|
+
"delete those rows")
|
|
468
616
|
args = ap.parse_args()
|
|
469
617
|
if args.retention_days < 1:
|
|
470
618
|
ap.error("--retention-days wants a positive day count")
|
|
619
|
+
if args.apply and not args.repair_evidence:
|
|
620
|
+
ap.error("--apply goes with --repair-evidence")
|
|
471
621
|
|
|
472
622
|
if args.coverage:
|
|
473
623
|
repo = os.path.realpath(os.path.expanduser(args.repo[0]) if args.repo
|
|
474
624
|
else os.getcwd())
|
|
475
625
|
return coverage_report(repo, args.dump)
|
|
626
|
+
if args.repair_evidence:
|
|
627
|
+
return repair_evidence(args.apply)
|
|
476
628
|
if args.import_evidence:
|
|
477
629
|
import tezgah_store
|
|
478
630
|
try:
|