@r1z4x/tezgah 1.5.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -1
- package/MANIFEST +11 -0
- package/README.md +6 -3
- package/bin/consult +13 -10
- package/bin/tezgah-docs +18 -16
- package/bin/tezgah-doctor +152 -0
- package/bin/tezgah-research +286 -12
- package/bin/tezgah-route +48 -1
- package/bin/tezgah-setup +27 -14
- package/bin/tezgah-task +771 -46
- package/bin/tezgah-taste +48 -10
- package/docs/architecture.md +7 -4
- package/docs/citations-baseline.json +1 -1
- package/docs/clarity-baseline.json +5 -5
- package/docs/contract.md +112 -25
- package/docs/evidence.md +201 -28
- package/docs/feature-audit.md +25 -4
- package/docs/gate.md +70 -33
- package/docs/glossary.md +4 -4
- package/docs/hosts.md +8 -8
- package/docs/index.json +30 -10
- package/docs/judge.md +9 -3
- package/docs/models.md +35 -2
- package/docs/operations.md +103 -3
- package/docs/research.md +392 -13
- package/docs/skills.md +24 -6
- package/docs/status-line.md +5 -5
- package/docs/testing.md +2 -2
- package/docs/vision.md +12 -4
- package/hooks/hooks.json +2 -2
- package/hooks/lang/en.json +323 -0
- package/hooks/lang/tr.json +344 -0
- package/hooks/projects-posttooluse.py +24 -5
- package/hooks/projects-pretooluse.py +9 -4
- package/hooks/projects-stop.py +28 -16
- package/hooks/tezgah_agents.py +29 -9
- package/hooks/tezgah_clarity.py +7 -3
- package/hooks/tezgah_components.py +2 -1
- package/hooks/tezgah_context.py +460 -390
- package/hooks/tezgah_gate.py +640 -104
- package/hooks/tezgah_integrity.py +1480 -197
- package/hooks/tezgah_models.py +337 -15
- package/hooks/tezgah_paths.py +103 -10
- package/hooks/tezgah_policy.py +286 -130
- package/hooks/tezgah_product.py +107 -0
- package/hooks/tezgah_rank.py +29 -27
- package/hooks/tezgah_replay.py +200 -30
- package/hooks/tezgah_research.py +1297 -12
- package/hooks/tezgah_skill_pick.py +58 -17
- package/hooks/tezgah_stopspec.py +7 -0
- package/hooks/tezgah_store.py +74 -27
- package/hooks/tezgah_task.py +917 -8
- package/hosts/codex/hook.py +9 -10
- package/hosts/cursor/hook.py +21 -14
- package/hosts/dsh/hooks.json +2 -2
- package/hosts/omp/hook.py +44 -23
- package/hosts/omp/tezgah-hook.ts.in +93 -7
- package/hosts/opencode/plugins/tezgah.js +119 -39
- package/output-styles/tezgah.md +76 -42
- package/package.json +1 -1
- package/skills/analyze-app/SKILL.md +57 -0
- package/skills/design-contract/EVALS.md +24 -1
- package/skills/design-contract/SKILL.md +17 -1
- package/skills/design-contract/data-views.md +86 -0
- package/skills/feature-audit/SKILL.md +32 -3
- package/skills/i-have-adhd/EVALS.md +22 -2
- package/skills/i-have-adhd/SKILL.md +12 -5
- package/skills/plan-add/EVALS.md +8 -8
- package/skills/plan-add/SKILL.md +22 -21
- package/skills/plan-sync/EVALS.md +2 -2
- package/skills/plan-sync/SKILL.md +14 -8
- package/skills/pm-frameworks/SKILL.md +6 -0
- package/skills/ponytail/EVALS.md +21 -2
- package/skills/ponytail/SKILL.md +4 -4
- package/skills/product-analysis/EVALS.md +23 -1
- package/skills/product-analysis/SKILL.md +50 -11
- package/skills/product-flow/EVALS.md +81 -0
- package/skills/product-flow/SKILL.md +91 -0
- package/skills/research/SKILL.md +99 -2
- package/skills/tezgah-contract/EVALS.md +52 -7
- package/skills/tezgah-contract/SKILL.md +204 -85
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,129 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.7.0] - 2026-10-12
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
- **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.
|
|
16
|
+
- **`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`.
|
|
17
|
+
- **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.
|
|
18
|
+
- **An ownership lease on every delegated slice.** `Owns:` lines in a brief
|
|
19
|
+
name the globs only that slice writes. They sit on the line or as `- glob`
|
|
20
|
+
items under it. The journal stores them as `owns`. The gate refuses a
|
|
21
|
+
sibling's write into them while the owner runs. A brief may declare its
|
|
22
|
+
worktree on a `Worktree:`, `Checkout` or `Cwd:` line. The gate then refuses
|
|
23
|
+
that slice's write into the main checkout, the shape two wave-1 slices hit
|
|
24
|
+
with relative paths. A shell target resolves against the call's own `cwd`
|
|
25
|
+
and its `cd` or `pushd`. A path the brief only names declares nothing, so an
|
|
26
|
+
integrator that lists worktrees to remove still works in the main checkout.
|
|
27
|
+
The journal keeps that named worktree for reconcile, resume and the board.
|
|
28
|
+
The refusal names the owner, its globs, how to reach it - omp `write
|
|
29
|
+
agent://<name>`, Claude `SendMessage`, else the parent - and the
|
|
30
|
+
`tezgah-task slice done <id>` that frees it. `.tezgah/` and scratch stay
|
|
31
|
+
free. A closed owner frees its files. A reader agent holds no lease. omp
|
|
32
|
+
maps a writing session to its
|
|
33
|
+
record through its ledger's `spawned` row. A Claude background child maps
|
|
34
|
+
through the `agentId` its launch result names, and its SubagentStop closes
|
|
35
|
+
the record. Every other host fails open. `tezgah-task slice board` lists one
|
|
36
|
+
parent's open slices with their lease, worktree, branch and last commit.
|
|
37
|
+
`slice merge-plan` puts a branch touching MANIFEST or the citation baseline
|
|
38
|
+
last. It orders the rest by their forecast `git merge-tree` conflicts and
|
|
39
|
+
leaves CHANGELOG.md to its union driver. It names a branch it cannot
|
|
40
|
+
forecast and exits 1. A spawn whose writing slices name one worktree gets a
|
|
41
|
+
warning. The orchestration text now tells every writing brief to name
|
|
42
|
+
`Owns:` globs and declare its worktree. It gives the shared append points to
|
|
43
|
+
the integrator. Siblings message each other only on an interface or
|
|
44
|
+
ownership change. `orchestrate-off` and `slices-off` disarm it.
|
|
45
|
+
|
|
46
|
+
- **`CHANGELOG.md` merges with the union driver.** A new `.gitattributes` keeps both branches' lines, so parallel slices no longer conflict on the `[Unreleased]` head.
|
|
47
|
+
- **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.
|
|
48
|
+
- **`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.
|
|
49
|
+
- **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.
|
|
50
|
+
- **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.
|
|
51
|
+
- **Trap cases for doable work listed as open.** The EVALS of tezgah-contract, i-have-adhd and ponytail each gained one.
|
|
52
|
+
- **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.
|
|
53
|
+
- **`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.
|
|
54
|
+
- **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.
|
|
55
|
+
- **`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.
|
|
56
|
+
- **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.
|
|
57
|
+
- **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.
|
|
58
|
+
- **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.
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- **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.
|
|
63
|
+
- **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.
|
|
64
|
+
- **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.
|
|
65
|
+
- **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>)`.
|
|
66
|
+
- **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.
|
|
67
|
+
- **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.
|
|
68
|
+
- **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.
|
|
69
|
+
- **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.
|
|
70
|
+
- **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.
|
|
71
|
+
- **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.
|
|
72
|
+
- **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.
|
|
73
|
+
- **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.
|
|
74
|
+
- **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.
|
|
75
|
+
- **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.
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
|
|
79
|
+
- **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.
|
|
80
|
+
- **"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.
|
|
81
|
+
- **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).
|
|
82
|
+
- **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.
|
|
83
|
+
- **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.
|
|
84
|
+
- **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.
|
|
85
|
+
- **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.
|
|
86
|
+
|
|
87
|
+
## [1.6.0] - 2026-10-10
|
|
88
|
+
|
|
89
|
+
### Added
|
|
90
|
+
|
|
91
|
+
- **A data, metric or dashboard ask reaches the product rule.** "Bu veriye uygun bir metric dashboard tasarla" armed no rule before. A dashboard, chart or metric-choice ask now arms `product`, judged per clause. A code marker in the same clause, such as a metrics endpoint, keeps it out.
|
|
92
|
+
- **product-analysis chooses metrics.** It names a North Star and its input metrics. Each metric gets one record: goal, signal, definition, window, baseline and more.
|
|
93
|
+
- **Dashboard rules in design-contract.** The new `data-views.md` picks the chart for the question. It also sets the context, the window and the widget states.
|
|
94
|
+
- **feature-audit forward mode.** A feature not built yet gets its five matrices in `spec.md`. The audit after the build compares against it.
|
|
95
|
+
- **analyze-app writes the design measurement.** Nothing produced `measurement.json` before. The skill now gives its shape and the snippet that reads it.
|
|
96
|
+
- **The local skill search reaches design-library's entries.** The search now reads the entries a skill's `INDEX.md` lists, one row each.
|
|
97
|
+
- **The product-flow skill and `tezgah-research next`.** A new product capability now runs in a fixed order, from research to re-measure. Each step leaves a file under `.tezgah/analysis/<topic>/`: a `## <step>` note in `flow.md`, or `spec.md` for the forward spec. `tezgah-research next --topic <t>` prints each step's status from those files, and a re-measure is due once the decision's `observe after:` date has passed. It then names the first missing or due step's skill and command. The skill lists the steps, and `FLOWS` in `hooks/tezgah_product.py` holds their done rules.
|
|
98
|
+
|
|
99
|
+
### Removed
|
|
100
|
+
|
|
101
|
+
- **The product ledger is cut before release (ADR 025).** Plan 075's measured run did not show the ledger beating the playbook alone. Gone: writing and reading `.tezgah/product/ledger.jsonl`, `tezgah-research record`, `due` and `commit --product`, the ledger part of `check`, `product:` citations and the session-start note on due decisions. An existing `ledger.jsonl` is left in place, and nothing reads it.
|
|
102
|
+
|
|
103
|
+
### Changed
|
|
104
|
+
|
|
105
|
+
- **The session runs the plan lifecycle itself.** The gate refused a session's
|
|
106
|
+
own `tezgah-task start|phase|allow|stop`. It told the session to ask the user,
|
|
107
|
+
so every plan ended with a command handed back. That refusal is gone. A phase
|
|
108
|
+
or scope refusal now names the move (`tezgah-task phase implementation`,
|
|
109
|
+
`tezgah-task allow`). The CLI still checks Acceptance, the spike, the
|
|
110
|
+
checkpoint and the review on each move. The gate still refuses a write to the
|
|
111
|
+
plan file itself. `tezgah-task allow` now needs `--why`, and `status` prints
|
|
112
|
+
the reason next to the widened scope.
|
|
113
|
+
- **`close` cleans up.** `tezgah-task close` on the active task clears its
|
|
114
|
+
phase instead of refusing. It then removes each linked worktree whose branch
|
|
115
|
+
has landed and which holds nothing else, through Orca when Orca tracks it. A
|
|
116
|
+
squash merge counts as landed. A branch whose commits cancel out does not. It
|
|
117
|
+
prints every worktree it keeps, with the reason. Uncommitted files, unknown
|
|
118
|
+
ignored files and `.tezgah` data keep a worktree. So do an unmerged branch, a
|
|
119
|
+
branch nobody committed on, an agent at work and an Orca that does not answer.
|
|
120
|
+
- **No process questions.** The contract says the orchestrator picks a process
|
|
121
|
+
parameter the user did not set, such as a spend ceiling, and states it.
|
|
122
|
+
`consult` on exit 4/5 records the first member of another model family
|
|
123
|
+
itself instead of asking the user to choose. After two review rounds the
|
|
124
|
+
session fixes what is left and closes with `close NNN done --resolved`. A
|
|
125
|
+
missing credential sends it to a route that needs none first. A silent gate
|
|
126
|
+
has it run `tezgah-setup --report` itself.
|
|
127
|
+
|
|
128
|
+
### Fixed
|
|
129
|
+
|
|
130
|
+
- **product-analysis no longer says `check` demands three compared solutions.** An analysis answers once. A choice between solutions goes under `decisions/`.
|
|
131
|
+
|
|
9
132
|
## [1.5.1] - 2026-10-09
|
|
10
133
|
|
|
11
134
|
### Fixed
|
|
@@ -981,7 +1104,9 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
981
1104
|
the copy could not find `hooks/` and died on import. It now links into the
|
|
982
1105
|
installed tree, and also installs the command as `tezgah`.
|
|
983
1106
|
|
|
984
|
-
[Unreleased]: https://github.com/r1z4x/tezgah/compare/v1.
|
|
1107
|
+
[Unreleased]: https://github.com/r1z4x/tezgah/compare/v1.7.0...HEAD
|
|
1108
|
+
[1.7.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.7.0
|
|
1109
|
+
[1.6.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.6.0
|
|
985
1110
|
[1.5.1]: https://github.com/r1z4x/tezgah/releases/tag/v1.5.1
|
|
986
1111
|
[1.5.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.5.0
|
|
987
1112
|
[1.4.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.4.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
|
|
@@ -133,6 +136,7 @@ hooks/tezgah_models.py
|
|
|
133
136
|
hooks/tezgah_orca.py
|
|
134
137
|
hooks/tezgah_paths.py
|
|
135
138
|
hooks/tezgah_policy.py
|
|
139
|
+
hooks/tezgah_product.py
|
|
136
140
|
hooks/tezgah_rank.py
|
|
137
141
|
hooks/tezgah_replay.py
|
|
138
142
|
hooks/tezgah_research.py
|
|
@@ -545,6 +549,7 @@ skills/analyze-app/EVALS.md
|
|
|
545
549
|
skills/analyze-app/SKILL.md
|
|
546
550
|
skills/design-contract/EVALS.md
|
|
547
551
|
skills/design-contract/SKILL.md
|
|
552
|
+
skills/design-contract/data-views.md
|
|
548
553
|
skills/design-library/EVALS.md
|
|
549
554
|
skills/design-library/INDEX.md
|
|
550
555
|
skills/design-library/SKILL.md
|
|
@@ -612,6 +617,8 @@ skills/ponytail/EVALS.md
|
|
|
612
617
|
skills/ponytail/SKILL.md
|
|
613
618
|
skills/product-analysis/EVALS.md
|
|
614
619
|
skills/product-analysis/SKILL.md
|
|
620
|
+
skills/product-flow/EVALS.md
|
|
621
|
+
skills/product-flow/SKILL.md
|
|
615
622
|
skills/research/EVALS.md
|
|
616
623
|
skills/research/SKILL.md
|
|
617
624
|
skills/rl-env/SKILL.md
|
|
@@ -715,6 +722,7 @@ tests/test_integrity.py
|
|
|
715
722
|
tests/test_interpreter.py
|
|
716
723
|
tests/test_judge.py
|
|
717
724
|
tests/test_lang.py
|
|
725
|
+
tests/test_lang_packs.py
|
|
718
726
|
tests/test_lesson_lineage.py
|
|
719
727
|
tests/test_lessons.py
|
|
720
728
|
tests/test_lessons_compile.py
|
|
@@ -733,6 +741,8 @@ tests/test_packaging_scripts.py
|
|
|
733
741
|
tests/test_paths.py
|
|
734
742
|
tests/test_paths_worktree.py
|
|
735
743
|
tests/test_platform.py
|
|
744
|
+
tests/test_product_flow_e2e.py
|
|
745
|
+
tests/test_product_next.py
|
|
736
746
|
tests/test_providers.py
|
|
737
747
|
tests/test_rank.py
|
|
738
748
|
tests/test_replay.py
|
|
@@ -746,6 +756,7 @@ tests/test_shapes.py
|
|
|
746
756
|
tests/test_skill_pick.py
|
|
747
757
|
tests/test_skill_search.py
|
|
748
758
|
tests/test_skills.py
|
|
759
|
+
tests/test_slices.py
|
|
749
760
|
tests/test_snapshot.py
|
|
750
761
|
tests/test_stack.py
|
|
751
762
|
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/consult
CHANGED
|
@@ -56,7 +56,8 @@ suffix); 2 no key for the chosen provider, or no member available at all; 3 the
|
|
|
56
56
|
--provider/--models run got no answer; 4 no recorded choice - one "offer:" line
|
|
57
57
|
per available member, nothing was asked; 5 every recorded member (and the
|
|
58
58
|
fallback) failed - one "reoffer:" line per member still available that did not
|
|
59
|
-
fail. On 4 or 5,
|
|
59
|
+
fail. On 4 or 5, the agent records the first listed member of another model
|
|
60
|
+
family with --use itself and asks again; the user is not asked to choose.
|
|
60
61
|
"""
|
|
61
62
|
import concurrent.futures
|
|
62
63
|
import json
|
|
@@ -504,11 +505,11 @@ def retry_hint(classes):
|
|
|
504
505
|
return "the referee answered outside its five headings; re-run, or judge the panel yourself"
|
|
505
506
|
if c == "credit":
|
|
506
507
|
return ("the provider account has no credit (HTTP 402) - the key is fine; "
|
|
507
|
-
"top it up, or
|
|
508
|
+
"top it up, or record another member with --use")
|
|
508
509
|
if c == "key":
|
|
509
|
-
return "set the provider's key, or
|
|
510
|
+
return "set the provider's key, or record another member with --use"
|
|
510
511
|
if c == "missing":
|
|
511
|
-
return "the agent CLI is not installed;
|
|
512
|
+
return "the agent CLI is not installed; record another member with --use"
|
|
512
513
|
if c.startswith("cli-exit"):
|
|
513
514
|
return ("the agent CLI failed (not logged in, or a model name it does not "
|
|
514
515
|
"know); run it once by hand, or record another member with --use")
|
|
@@ -645,8 +646,9 @@ def main():
|
|
|
645
646
|
print("consult: no recorded choice, so nothing was asked.")
|
|
646
647
|
for opt in found:
|
|
647
648
|
print(offer_line("offer", opt))
|
|
648
|
-
print("next:
|
|
649
|
-
"panel)
|
|
649
|
+
print("next: record the first of these from a model family other than "
|
|
650
|
+
"yours (two make a panel) with: consult --use <member>[,<member>], "
|
|
651
|
+
"then ask again - do not ask the user to choose")
|
|
650
652
|
sys.stdout.flush()
|
|
651
653
|
os._exit(4)
|
|
652
654
|
|
|
@@ -775,14 +777,15 @@ def main():
|
|
|
775
777
|
print("retry: %s" % hint)
|
|
776
778
|
code = 0 if answers else 3
|
|
777
779
|
if not answers and not explicit:
|
|
778
|
-
#
|
|
779
|
-
#
|
|
780
|
+
# consult never swaps the recorded choice on its own: it names what is
|
|
781
|
+
# left, and the agent records the next member and asks again.
|
|
780
782
|
lost = {c[1] for c, _cls in failures} | skip_opts
|
|
781
783
|
left = [o for o in options() if o not in lost]
|
|
782
784
|
for opt in left:
|
|
783
785
|
print(offer_line("reoffer", opt))
|
|
784
|
-
print("next:
|
|
785
|
-
"consult --use <member>[,<member>]
|
|
786
|
+
print("next: record the first of these from another model family with: "
|
|
787
|
+
"consult --use <member>[,<member>], then ask again - do not ask the "
|
|
788
|
+
"user to choose" if left else
|
|
786
789
|
"next: nothing else can answer here; tell the user the second "
|
|
787
790
|
"opinion was skipped (install an agent CLI or add a provider key)")
|
|
788
791
|
code = 5
|
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:
|