@r1z4x/tezgah 1.6.0 → 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.
Files changed (64) hide show
  1. package/CHANGELOG.md +80 -1
  2. package/MANIFEST +5 -0
  3. package/README.md +6 -3
  4. package/bin/tezgah-docs +18 -16
  5. package/bin/tezgah-doctor +152 -0
  6. package/bin/tezgah-research +234 -10
  7. package/bin/tezgah-route +48 -1
  8. package/bin/tezgah-setup +26 -13
  9. package/bin/tezgah-task +589 -17
  10. package/bin/tezgah-taste +48 -10
  11. package/docs/architecture.md +7 -4
  12. package/docs/clarity-baseline.json +2 -2
  13. package/docs/contract.md +108 -24
  14. package/docs/evidence.md +201 -28
  15. package/docs/feature-audit.md +1 -1
  16. package/docs/gate.md +60 -22
  17. package/docs/glossary.md +4 -4
  18. package/docs/hosts.md +8 -8
  19. package/docs/index.json +11 -5
  20. package/docs/judge.md +9 -3
  21. package/docs/models.md +35 -2
  22. package/docs/operations.md +103 -3
  23. package/docs/research.md +346 -13
  24. package/docs/skills.md +1 -1
  25. package/docs/status-line.md +5 -5
  26. package/docs/testing.md +2 -2
  27. package/docs/vision.md +12 -4
  28. package/hooks/hooks.json +2 -2
  29. package/hooks/lang/en.json +323 -0
  30. package/hooks/lang/tr.json +344 -0
  31. package/hooks/projects-posttooluse.py +24 -5
  32. package/hooks/projects-pretooluse.py +9 -4
  33. package/hooks/projects-stop.py +28 -16
  34. package/hooks/tezgah_agents.py +29 -10
  35. package/hooks/tezgah_clarity.py +7 -3
  36. package/hooks/tezgah_context.py +425 -406
  37. package/hooks/tezgah_gate.py +583 -33
  38. package/hooks/tezgah_integrity.py +1480 -197
  39. package/hooks/tezgah_models.py +337 -15
  40. package/hooks/tezgah_paths.py +103 -10
  41. package/hooks/tezgah_policy.py +258 -126
  42. package/hooks/tezgah_rank.py +29 -27
  43. package/hooks/tezgah_replay.py +200 -30
  44. package/hooks/tezgah_research.py +1294 -9
  45. package/hooks/tezgah_stopspec.py +7 -0
  46. package/hooks/tezgah_store.py +74 -27
  47. package/hooks/tezgah_task.py +915 -6
  48. package/hosts/codex/hook.py +9 -10
  49. package/hosts/cursor/hook.py +21 -14
  50. package/hosts/dsh/hooks.json +2 -2
  51. package/hosts/omp/hook.py +44 -23
  52. package/hosts/omp/tezgah-hook.ts.in +93 -7
  53. package/hosts/opencode/plugins/tezgah.js +102 -2
  54. package/output-styles/tezgah.md +76 -42
  55. package/package.json +1 -1
  56. package/skills/i-have-adhd/EVALS.md +22 -2
  57. package/skills/i-have-adhd/SKILL.md +12 -5
  58. package/skills/plan-add/SKILL.md +5 -3
  59. package/skills/plan-sync/SKILL.md +5 -3
  60. package/skills/ponytail/EVALS.md +21 -2
  61. package/skills/ponytail/SKILL.md +4 -4
  62. package/skills/research/SKILL.md +89 -1
  63. package/skills/tezgah-contract/EVALS.md +52 -7
  64. package/skills/tezgah-contract/SKILL.md +151 -56
@@ -224,7 +224,7 @@ After a change to the contract text (`hooks/tezgah_policy.py`,
224
224
  - `--refresh` does that without a reinstall, for every armed host's static
225
225
  block and not only opencode's (`refresh_contract`). opencode has no
226
226
  session-start hook, so its plugin runs it once per session
227
- (`hosts/opencode/plugins/tezgah.js:2666-2670`). Until 2026-10-02 it
227
+ (`hosts/opencode/plugins/tezgah.js:2689-2693`). Until 2026-10-02 it
228
228
  re-rendered opencode's files alone and reset the one shared hash, so a rule
229
229
  edit left the other static files stale while `--report` went green (audit
230
230
  M-5).
@@ -371,6 +371,7 @@ does not hold.
371
371
  | `tezgah-doctor` | nothing: sizes, session and event counts, whether opencode is running, the two context-hygiene settings, the `.codegraph` bytes and index presence per repository, and the three host state dirs (`bin/tezgah-doctor::collect`, `bin/tezgah-doctor::report`) |
372
372
  | `tezgah-doctor --clean` | vacuums opencode's database, and only when opencode is not running (`bin/tezgah-doctor::vacuum_db`, `bin/tezgah-doctor::main`); deletes every evidence ledger whose newest row is older than `--retention-days` (default 30), rows, import offsets and old JSONL file together (`hooks/tezgah_store.py::sweep_evidence`), and the turn stamps and used-tool marks last written before then (`hooks/tezgah_store.py::sweep_cache`); and deletes the files not modified for that long - `classify.log`, `context-drops.log`, `debug.log` and the `turns/` and `sessions/` files a running older release still writes - in the cache and its sandbox fallback, keeping the current session's (`$TEZGAH_SESSION`) ledger, stamp and marks whatever their age (`sweep_state`) |
373
373
  | `tezgah-doctor --import-evidence` | imports every old `evidence/*.jsonl` file into the cache's `tezgah.db` now, in the cache and its sandbox fallback (`bin/tezgah-doctor::import_evidence`); session start runs the same import detached at most once a day (`hooks/tezgah_store.py::import_later`) |
374
+ | `tezgah-doctor --repair-evidence [--apply]` | counts, per session, the evidence rows an old `evidence/*.jsonl` file put in again when a volume remount gave it another device id, in the cache and its sandbox fallback. Every `imported` row of one file is one import stream, which put the file's first `bytes` in. For a row text, the longest stream's count stays and every other stream's count is a copy: that many of the newest rows go, so a row written to the database directly beside them stays. A session holding a file line fewer times than its streams put it in is left whole and printed for manual review. A file gone from its name, holding another inode or shorter than its longest stream is printed and skipped, not guessed (`bin/tezgah-doctor::evidence_plan`). It writes nothing without `--apply`, which backs `tezgah.db` up to `tezgah.db.bak-<YYYYmmdd-HHMMSS>` through SQLite's backup API, deletes those rows and leaves one import offset per file in one transaction. Before that transaction commits it counts the rows against the plan, and it rolls back and exits 1 when a row it kept is gone or a copy it deleted is still there (`bin/tezgah-doctor::repair_evidence`) |
374
375
  | `tezgah-doctor --coverage` | every tracked file the codegraph index does not hold, in two classes — a supported extension the index is missing, and a shebang-only script with no `.py` twin — with the file counts it read, so an empty result cannot read as "everything is covered" |
375
376
  | `tezgah-doctor --stack` | per host: the context files in load order, the skill dirs and which copy wins, the hook rows per event with their owner, the MCP servers, and every conflict; exits 1 on one ([hosts](hosts.md#precedence-what-each-host-loads-and-in-which-order), `hooks/tezgah_stack.py::report`) |
376
377
  | `tezgah-doctor --prune-sessions DAYS` | deletes sessions idle longer than DAYS through `opencode session delete`, then vacuums; skipped when opencode is running or its CLI is missing (`bin/tezgah-doctor::prune_sessions`, `bin/tezgah-doctor::main`) |
@@ -412,7 +413,7 @@ ledger row. A rollback therefore never reads it as a pre-state (`capture_after`,
412
413
  `hooks/tezgah_snapshot.py::capture_after`). A file outside the repository gets no
413
414
  after blob.
414
415
  opencode reaches the same call through a process (`noteTaste`,
415
- `hosts/opencode/plugins/tezgah.js:2413-2423`).
416
+ `hosts/opencode/plugins/tezgah.js:2436-2446`).
416
417
 
417
418
  ```sh
418
419
  touch ~/.config/tezgah/taste-on # arm; rm it to disarm
@@ -425,7 +426,8 @@ bin/tezgah-taste export # accepted learnings into AGENTS.m
425
426
  bin/tezgah-taste label; bin/tezgah-taste label ID preference # the calibration sample
426
427
  bin/tezgah-taste calibrate # the bound a rule needs
427
428
  bin/tezgah-taste gate # the pooled before/after test
428
- bin/tezgah-taste mine --host omp --stats # sessions, writes, prompts after a writing turn
429
+ bin/tezgah-taste mine --host omp --stats # this repository's sessions, writes, prompts after a writing turn
430
+ bin/tezgah-taste mine --all-projects # every session under ~/Projects, each repo pseudonymised
429
431
  bin/tezgah-taste measure --samples S --labels L [--typed] # judge precision/recall
430
432
  ```
431
433
 
@@ -614,6 +616,102 @@ it costs nothing there. On held-out Turkish phrasings of the docs index,
614
616
  meaning alone found the page in the top three for 0.91 with `embed-mrl` and 0.78
615
617
  with `embed-m2v`. The fusion found it for 0.75 and 0.74, BM25 for 0.68.
616
618
 
619
+ ## Slice journal: `tezgah-task slice`
620
+
621
+ A delegated slice can die with its parent. A crash, an omp `/clear`, a quota or
622
+ 429 answer, or a model or host switch ends the parent session. The child's
623
+ worktree and commits stay, but no one remembers them. The slice journal is that
624
+ memory, and it is host-neutral.
625
+
626
+ Each spawned slice gets one row in `<repo root>/.tezgah/slices/slices.db`, a SQLite
627
+ file opened like the taste store (`hooks/tezgah_task.py::SLICE_SCHEMA`).
628
+ An older release wrote one JSON file per record there. The first open imports each one and deletes it.
629
+ `.gitignore` excludes `.tezgah/`, a private directory, so the journal never reaches a commit.
630
+ The id is the host's own agent name: the omp task `name` or the Codex
631
+ `task_name`. A Claude `Agent`/`Task` input may carry a `name` too. Otherwise the
632
+ id is the first 12 hex digits of the brief's sha1. A record holds the host, the agent
633
+ type, the first 4000 characters of the brief, the parent session and the cwd.
634
+ It also holds the linked worktree the brief names, its branch, and the root's
635
+ HEAD at open. Its `status` is `open`, `done`, `stopped`, `resumed` or `stale`.
636
+ Its `failure` is one of `quota`, `rate_limit`, `aborted`, `timeout`, `cut_off`
637
+ or `error`. `attempts` counts each re-open of the same id
638
+ (`hooks/tezgah_task.py::open_slice`, `hooks/tezgah_task.py::close_slice`).
639
+ Another session's record of the same name is not re-opened under another brief.
640
+ The spawn gets this session's own record of that name, else `<name>-<sha1[:6]>` of its brief.
641
+ `name` keeps the spawned name, and the same brief from another session still re-opens the record
642
+ (`hooks/tezgah_task.py::_own_id`). The record stays in cwd's journal, the one its
643
+ owner's Stop rule, resume block and review guard read. When every absolute path its
644
+ brief names lies in one other repository, `repo` names that repository, and the
645
+ slice line says `(in <name>)` (`hooks/tezgah_task.py::_brief_repo`). A repository's
646
+ own `.tezgah` counts as that repository (`hooks/tezgah_task.py::main_repo`).
647
+
648
+ The slice's own task text (without a batch's shared `context`) decides its
649
+ worktree and its lease. The worktree is the one the task names last, else the
650
+ brief (`hooks/tezgah_task.py::_worktree_in`). `declared` holds the checkout a
651
+ `Worktree:`, `Checkout` or `Cwd:` line declares, which only the main-checkout
652
+ refusal reads (`hooks/tezgah_task.py::_declared_in`). Its `Owns:` lines name
653
+ repository-relative globs, and only that slice may write those paths while its
654
+ record runs (`hooks/tezgah_task.py::owned_globs`, `hooks/tezgah_task.py::owner_of`). A
655
+ Claude background child's record also keeps the launch's `agentId` as `child`,
656
+ which its own calls carry (`hooks/tezgah_task.py::slice_of`). The
657
+ gate holds siblings to the lease (`docs/gate.md`, the Lease rule), and the
658
+ spawn-time warning names two writing slices that share one worktree
659
+ (`hooks/tezgah_task.py::worktree_clash`).
660
+
661
+ The hooks write it. A spawn call - omp `task`, Claude, dsh and Cursor
662
+ `Agent`/`Task`, Codex `spawn_agent`, opencode `task` - opens one record per
663
+ slice (`hooks/tezgah_task.py::spawned`, `hooks/tezgah_task.py::note_spawn`).
664
+ Claude, dsh, Cursor and opencode answer the call once the child ended. So the
665
+ record closes at once (`hooks/tezgah_task.py::classify`). A failed call becomes
666
+ `stopped` with its failure class, and a good one `done`. The omp `task` and Codex
667
+ `spawn_agent` calls answer while the child runs, so the record stays `open`. So
668
+ does a Claude launch sent with `run_in_background` or one whose result names a
669
+ background child. That child's SubagentStop closes it as `done`
670
+ (`hooks/tezgah_task.py::close_child`). An omp
671
+ record settles from the newest child transcript in the parent's session dir.
672
+ omp names a same-name re-dispatch `<name>-2.jsonl`. The last 64 KB of that file
673
+ decide `stopped` or `done` (`hooks/tezgah_task.py::pending`). A record
674
+ nothing moved for six hours closes `stale`, and a background `job` after
675
+ omp's 3600 s deadline. Codex reports no child end tezgah can see, so its
676
+ records end `stale`. Every prompt lists up to three pending records, newest
677
+ first, each with the command that resumes it (`hooks/tezgah_task.py::slice_lines`).
678
+ They are the records this session spawned or nobody owns, such as one opened by hand. The resume block at
679
+ session start and after a compaction adds one line that counts the other
680
+ sessions' records. `tezgah-task slice reconcile` lists them all.
681
+
682
+ | Command | What it does |
683
+ |---|---|
684
+ | `tezgah-task slice open --id ID [--kind agent\|job] [--host H] [--agent A] [--brief TEXT\|--brief-file F] [--parent P] [--cwd PATH]` | records a slice by hand, for a host whose spawn the hooks do not see |
685
+ | `tezgah-task slice done ID` / `slice stop ID [--class C]` | closes a record as done, or as stopped with a failure class. An exact record id answers from any session. A spawned name answers too, and under `TEZGAH_SESSION` only for that session's records and unowned ones (`hooks/tezgah_task.py::find_slice`) |
686
+ | `tezgah-task slice reconcile [--prune]` | lists every pending record with a resume brief. `--prune` also removes landed worktrees and marks those records done. It deletes a landed local branch only when no worktree holds it, it tracks no upstream and its reflog shows a commit of its own; it never deletes main, master or the current branch |
687
+ | `tezgah-task slice resume ID` | prints the native path first: omp `write agent://<id>` while the parent lives, Claude `SendMessage` when the id is an agent name. Otherwise it prints a re-dispatch brief: the original brief plus the last commit, the uncommitted files and "do not redo committed steps". A quota or rate-limit stop is sent to another provider. It sets the status to `resumed` |
688
+ | `tezgah-task slice stats [--window DAYS] [--json]` | counts records per status, failure class and host. A `class+host` signature with 3 or more events is an RSI lever candidate, and so is one with 5% of a window that holds 20 or more slices. The line names the lever |
689
+ | `tezgah-task slice board [--parent P]` | lists the open slices one parent spawned - the session's own, or for a subagent its parent's - newest first: the `Owns:` globs, worktree, branch and last commit of each, bounded to 12 lines (`hooks/tezgah_task.py::board`) |
690
+ | `tezgah-task slice merge-plan [--base REF]` | forecasts every done slice branch that carries unmerged work against REF (HEAD) and against the others with `git merge-tree`, and leaves out a `merge=union` file such as CHANGELOG.md. A branch touching `MANIFEST` or the citation baseline comes last, the rest fewest conflicts first. A branch merge-tree cannot forecast is printed as `not judged` and the exit is 1 (`bin/tezgah-task::merge_plan`) |
691
+
692
+ `tezgah-task review NNN <reviewer> approve` refuses while an open, stopped or
693
+ stale record names that reviewer as its id, agent or spawned name. Under
694
+ `TEZGAH_SESSION` only that session's records and unowned ones count. The refusal
695
+ prints the exact id to resume or close. A record keeps the plan
696
+ that was active when it opened, and only that plan's records count. A record
697
+ with no plan counts when it changed after the plan's checkpoint commit. A
698
+ `resumed` record hands the guard to its re-dispatch's own record
699
+ (`bin/tezgah-task::unfinished_review`).
700
+ The lever map is a closed set. `rate_limit` and `quota` point at the
701
+ `hooks/tezgah_models.py` fallback chain. `cut_off` and `timeout` point at a
702
+ per-slice deadline, `aborted` at the `wip:` commit cadence, a duplicate job at
703
+ journalling background jobs. Plain `error` has no lever: read the transcript.
704
+
705
+ The orchestration text carries the session's rule
706
+ (`hooks/tezgah_policy.py::ORCHESTRATE`, `hooks/tezgah_agents.py::_orch_body`). A
707
+ slice commits `wip: <step>` after each step. The session resumes or re-dispatches
708
+ a stopped slice and never absorbs it inline. `slice board` shows the sibling
709
+ leases and `slice merge-plan` orders the landed branches. `slice reconcile
710
+ --prune` runs before the turn that merged slices ends.
711
+ The `~/.config/tezgah/slices-off` switch
712
+ stops the hooks from writing records (`hooks/tezgah_paths.py::UNCLASSIFIED_SWITCHES`).
713
+ A cwd outside a git repository writes nothing either.
714
+
617
715
  ## Troubleshooting
618
716
 
619
717
  | Symptom | First check | Likely cause |
@@ -760,6 +858,8 @@ page exits 1 with the message it always printed (`available()`,
760
858
  its pins, the fetch and conversion, the reader and `fuse`; the `embed-mrl` and
761
859
  `embed-m2v` registry rows that `--enable`, `--disable` and `--features` read.
762
860
  `tests/test_embed.py` builds tiny models in the test and pins the rest.
861
+ - `hooks/tezgah_task.py`, `bin/tezgah-task` — the slice journal: the record, the
862
+ spawn parser, reconcile, resume, prune and stats.
763
863
  - `hooks/tezgah_paths.py`, `hooks/tezgah_context.py` — the paths and `off()`; the
764
864
  marks and the kill-switch filtering.
765
865
  - `RELEASING.md`, `README.md`, `AGENTS.md`, `.gitignore`, `tests/test_setup.py` —
package/docs/research.md CHANGED
@@ -65,7 +65,9 @@ nothing recorded the ask and `close --limit` accepted any string.
65
65
  `state.json` `rules` is the line's format version. `init` writes `RULES`, the
66
66
  newest set (`hooks/tezgah_research.py::RULES`). Each set keeps its own gate. Rules 2
67
67
  are the standards rules. Rules 3 add the ask contract. Rules 4 add the protocol
68
- sections below. Rules 5 add the scope lock. A bump never weakens an older set.
68
+ sections below. Rules 5 add the scope lock. Rules 6 add the selection warnings
69
+ ("Held-out set, repeats and noise band" below). Rules 7 add the `exploratory`
70
+ method ("Rules 7: exploratory lines" below). A bump never weakens an older set.
69
71
 
70
72
  A `rules` above `RULES` means a later tezgah wrote the line. `check` then reports
71
73
  one error that names both numbers and reads nothing else. The writers refuse the
@@ -121,7 +123,8 @@ ideation moves had no fence. Rules 5 lock a line to its question:
121
123
 
122
124
  - `init --in "<covered>" --out "<left alone>"` (repeatable) write `state.json`
123
125
  `scope`. The in-scope items are `S1`, `S2`, ... in order. `--method
124
- qualitative|measured` writes `method`. `scope_problems`
126
+ qualitative|measured|exploratory` writes `method` (`exploratory` from rules 7,
127
+ see "Rules 7: exploratory lines"). `scope_problems`
125
128
  (`hooks/tezgah_research.py::scope_problems`) names an empty `scope.in`, an
126
129
  empty `scope.out` and a missing method. `check` refuses them past `bootstrap`
127
130
  and warns at it.
@@ -149,11 +152,14 @@ gets the question, its scope and its method in its brief. The research skill's
149
152
 
150
153
  | Path | Holds |
151
154
  |---|---|
152
- | `state.json` | the question, the phase (`bootstrap`/`inner`/`outer`/`concluded`), the direction, `method` and `scope` (rules 5, see "Scope lock"), the locked evaluation and its optional second gate (`capability_tolerance`, `counter_metric`), the session events (`PHASES`, `hooks/tezgah_research.py::PHASES`), `rules` (the rule set `init` opened it under, `RULES`), `deliverable` (`kind`, `path`, `ask`, `min_variants`), `supersedes` for a new version of an older line, and `closed` once `close` records a deliberate limit. The phase is the author's declaration; `derived_phase` (`hooks/tezgah_research.py::derived_phase`) reads the *other* authority beside it from the line's own artifacts, and the checker names the two disagreeing |
155
+ | `state.json` | the question, the phase (`bootstrap`/`inner`/`outer`/`concluded`), the direction, `method` and `scope` (rules 5, see "Scope lock"), the locked evaluation and its optional second gate (`capability_tolerance`, `counter_metric`) with its `noise_band`, the optional `held_out` set (rules 6), the `arena` of an exploratory line (`problem`, `budget`, and `evaluator`, `evaluator_sha256`, `metric` for a measurable one - rules 7, see "The arena lock"), the session events (`PHASES`, `hooks/tezgah_research.py::PHASES`), `rules` (the rule set `init` opened it under, `RULES`), `deliverable` (`kind`, `path`, `ask`, `min_variants`), `supersedes` for a new version of an older line, and `closed` once `close` records a deliberate limit. The phase is the author's declaration; `derived_phase` (`hooks/tezgah_research.py::derived_phase`) reads the *other* authority beside it from the line's own artifacts, and the checker names the two disagreeing |
153
156
  | `log.md` | the decision log, newest last: one line per decision, experiment, dead end or pivot, with the evidence that drove it |
154
157
  | `findings.md` | the four sections every line answers, named by `FINDINGS_SECTIONS` (`hooks/tezgah_research.py::FINDINGS_SECTIONS`) |
155
158
  | `claims.jsonl` | one JSON object per row: `statement`, `falsification`, `proof`, `provenance`, `status`, `kind`, `trace` (the in-scope item, rules 5), `scope` (what the claim's numbers were measured on, which may not be wider than the rows it rests on), and `supersedes` when the row replaces an earlier claim |
156
159
  | `predictions.jsonl` | one JSON object per row: `commit` (a 40-character sha the row is bound to), `claim` (an id the line holds, or empty when the prediction stands alone), `metric`, `value_before`, `value_after`, `falsifier`, `components` (the keys of the components the change touches, read from `hooks/tezgah_components.py`), and `granted_by` when a human granted what the frozen paths below forbid |
160
+ | `ideas.jsonl` | an exploratory line's ideas, one JSON object per row, appended by `idea` (rules 7, see "Three files") |
161
+ | `stages.jsonl` | an exploratory line's stage moves, one row per move, appended by `stage`; an idea's stage is its newest row |
162
+ | `tournament.jsonl` | an exploratory line's judged pairwise matches, appended by `match` |
157
163
  | `experiments/<hypothesis>/protocol.md` | what the change is, what it predicts, what result would falsify it, why - committed before the run |
158
164
  | `experiments/<hypothesis>/results.jsonl` | the rows the run produced, one JSON object per non-blank line, each carrying a non-empty `source` and, once declared, a `scope` of `real`, `fixture` or `derived`, and - when that scope is `fixture` - a `fixture` description of what was generated |
159
165
  | `experiments/<hypothesis>/analysis.md` | what the rows mean, and which claim they move |
@@ -168,7 +174,9 @@ gets the question, its scope and its method in its brief. The research skill's
168
174
  file that exists (`init`, `hooks/tezgah_research.py::init`); `predictions.jsonl` is not
169
175
  one of them, because a file that exists without a row is a line that looks like
170
176
  it predicted something - it appears with the first `predict` the line records,
171
- and `check` reads an absent one as no rows rather than as a missing file. A slug
177
+ and `check` reads an absent one as no rows rather than as a missing file. The
178
+ three campaign files work the same way. Each appears with its first `idea`,
179
+ `stage` or `match`. A slug
172
180
  is lowercase letters, digits and dashes (`valid_slug`,
173
181
  `hooks/tezgah_research.py::valid_slug`), because it is the directory name `check`, `status` and
174
182
  `claim` look for.
@@ -244,6 +252,13 @@ through its seal.
244
252
  | `tezgah-research seal <slug> [--history-lost] --ack "<owner decision>"` | seals a line under `done/` that was concluded before seals existed: plainly, as `conclude` would, when every experiment's order still checks; with `--history-lost`, with that verdict for every experiment whose order the lost history left undecidable. Refuses a line with a real order violation, a plain seal on any order finding, and a `history-lost` seal with nothing lost (`retro_seal`, `hooks/tezgah_research.py::retro_seal`) | 0, 1 refused, 2 misuse |
245
253
  | `tezgah-research import <checkout> [<slug>] [--allow-open "<reason>"]` | brings a line - or every line this checkout lacks - from another checkout's `.tezgah` repository with its history; an open line arrives beside open ones only with `--allow-open` (`import_line`, `hooks/tezgah_research.py::import_line`) | 0, 1 refused, 2 misuse |
246
254
  | `tezgah-research next --topic <t> [--flow f] [--json]` | prints every step of a product flow with its status from the topic's files under `.tezgah/analysis/<topic>/`, then the first missing or due step's skill and command; `--flow` picks `metric-dashboard` (the default) or `feature`, and `--json` prints the same steps as data (`next_steps`, `hooks/tezgah_product.py::next_steps`) | 0, 2 unknown flow or no `--topic` |
255
+ | `tezgah-research init <slug> --ask "<the user's words>" --method exploratory --in "<covered>" --out "<left alone>" --arena "<problem>" --budget "<budget>" [--evaluator <path> --metric "<metric>"]` | opens a rules-7 exploratory line and writes the arena into `state.json` (`arena_record`, `hooks/tezgah_research.py::arena_record`). `--ask`, `--in` and `--out` are required as on any rules-5 line. Without `--arena` or `--budget` it is misuse; `--evaluator` and `--metric` go together; the evaluator has to be a file in the repository; the four flags on any other method are misuse. Its next step says to commit the arena before the first idea | 0, 1 refused over open lines, 2 misuse |
256
+ | `tezgah-research idea <slug>` | reads one idea row from stdin and appends it to `ideas.jsonl` under the lock, or refuses it with one reason per problem (`append_idea`, `hooks/tezgah_research.py::append_idea`) | 0 written, 1 refused, 2 misuse |
257
+ | `tezgah-research stage <slug> <idea> <stage> [--evidence "<ref or JSON>"] [--reason "<why>"]` | appends one stage move to `stages.jsonl` under the lock, or refuses it. `--reason` is the kill's reason, and a `known` prior art needs it for the kill appended with it (`append_stage`, `hooks/tezgah_research.py::append_stage`) | 0 written, 1 refused, 2 misuse |
258
+ | `tezgah-research match <slug>` | reads one judged match from stdin and appends it to `tournament.jsonl` under the lock, or refuses it (`append_match`, `hooks/tezgah_research.py::append_match`) | 0 written, 1 refused, 2 misuse |
259
+ | `tezgah-research board <slug> [--json]` | the campaign as its files show it: arena, rows, torn tails, each idea's stage, score and next action, the ideas tested and the next step; writes nothing (`board_text`, `hooks/tezgah_research.py::board_text`) | 0, 2 misuse |
260
+ | `tezgah-research dedup <slug> [--threshold <(0, 1]>] [--json]` | clusters the `proposed` ideas by statement and prints the `stage ... duplicate` command for each member; names the backend and the threshold's source; writes nothing (`dedup_text`, `hooks/tezgah_research.py::dedup_text`). A `--threshold` outside `(0, 1]` is misuse | 0, 2 misuse |
261
+ | `tezgah-research yield [--json] [--all-lines]` | per operator and per generator family, how many ideas reached each stage, the kill reasons, the board-score-to-pilot correlation and the next campaign's operator weights; writes nothing (`yield_text`, `hooks/tezgah_research.py::yield_text`) | 0, 2 misuse |
247
262
 
248
263
  Exit code 2 is always misuse, so a caller can tell it from a line that fails the
249
264
  checks (`misuse`, `bin/tezgah-research::misuse`). `check` asks nothing at all - no
@@ -487,18 +502,288 @@ in `hooks/tezgah_research.py`, a module the frozen set forbids a prediction's
487
502
  commit to touch without a human `granted_by` (`FROZEN_PATHS`), so the gate that
488
503
  decides which candidate survives is not machinery the candidate can rewrite.
489
504
 
490
- What the pair cannot catch: **a tolerance its own author sets wide enough**. The
491
- band is a sentence, not a measurement, and `check` can see that it is written and
492
- readable - never that 0.02 is tight. A line that writes "capability metric within
493
- 5.0 of the baseline" has adopted the form of the two-gate rule and none of its
494
- force, and the review is the only place that is decided.
505
+ What the pair cannot catch alone: **a tolerance its own author sets wide
506
+ enough**. A line that writes "capability metric within 5.0 of the baseline" has
507
+ adopted the form of the two-gate rule and none of its force. From rules 6 the band
508
+ is read against a measurement. `evaluation.noise_band` is
509
+ `{"experiment": "<the experiment dir that reran the unchanged baseline>",
510
+ "spread": <the spread that rerun measured>}`. `check` warns in three cases. The
511
+ line declares `capability_tolerance` and no `noise_band`. `noise_band.experiment`
512
+ names no experiment of the line. The tolerance sentence holds a number wider than
513
+ `spread`, so the tolerance passes what noise alone would. A digit inside a name
514
+ such as `p95` is not that number (`_check_selection`,
515
+ `hooks/tezgah_research.py::_check_selection`). Whether the rerun was a fair one
516
+ stays the review's call.
517
+
518
+ ## Held-out set, repeats and noise band (rules 6)
519
+
520
+ The measured line `rsi-borrow-screen` found three gaps. Of 27 decisions, 25 select
521
+ a variant and report it on the same cells. Of 23 armed experiments, 13 run once per
522
+ arm. Three lines declare a tolerance, and none of them measured it. Rules 6 add one
523
+ warning for each (`_check_selection`, `hooks/tezgah_research.py::_check_selection`).
524
+ Each stays a warning under `--strict` too, never a refusal (ADR 019):
525
+
526
+ - **Held-out set.** A measured line may name its held-out set in
527
+ `evaluation.held_out` (a non-empty string). It may log each look at that set in
528
+ `held_out_log.jsonl` at the line root, one row
529
+ `{"set", "candidate", "date", "result"}` per look. `check` warns when a
530
+ `decisions/<id>/` holds comparison cells and the line names no `held_out`: the
531
+ variant was selected and reported on the same cells. It also warns when one
532
+ candidate looked at one set more than once: the held-out set became development
533
+ data for it.
534
+ - **Repeats.** An experiment is armed when its `results.jsonl` rows carry `arm` or
535
+ `variant` with two or more values. `check` warns when no row has `repeat`,
536
+ `repeats` or `k` above 1 and its `protocol.md` states no `k:`/`repeats:` above 1.
537
+ An armless experiment is not read.
538
+ - **Noise band.** `evaluation.noise_band`, read in "Two gates" above.
539
+
540
+ A line opened under rules 5 or earlier is not read for any of the three.
541
+
542
+ ## Rules 7: exploratory lines
543
+
544
+ A `qualitative` or `measured` line answers a question. Some asks want new ideas,
545
+ methods or approaches instead of an answer. Rules 7 add the method `exploratory`
546
+ for those asks (`hooks/tezgah_research.py::EXPLORE_RULES`,
547
+ `hooks/tezgah_research.py::METHODS`). The line runs one campaign. Many ideas
548
+ enter it, a funnel kills most of them, and a run under a frozen evaluator judges
549
+ the few that remain. One campaign is one line, so "One open line at a time" still
550
+ holds. `scope_problems` (`hooks/tezgah_research.py::scope_problems`) accepts
551
+ `exploratory` only on a rules-7 line. On an older line it names `qualitative` and
552
+ `measured` as the two methods the line can take.
553
+
554
+ ### The arena lock
555
+
556
+ `init <slug> --ask "<the user's words>" --method exploratory --in "<covered>"
557
+ --out "<left alone>" --arena "<problem>" --budget "<budget>"` opens
558
+ the line. The arena is the problem in the user's words. The budget says how many
559
+ ideas, pilots and full runs, and how much wall time, the campaign may spend. A
560
+ measurable arena adds `--evaluator <path> --metric "<metric>"`. The two flags go
561
+ together, and the evaluator has to be a file in the repository. A missing arena
562
+ or budget is misuse (exit 2).
563
+
564
+ `arena_record` (`hooks/tezgah_research.py::arena_record`) builds `state.json`
565
+ `arena`. It holds the problem and the budget. A measurable arena also holds the
566
+ evaluator path, the sha256 of the evaluator's bytes and the metric. `init` also
567
+ writes `deliverable: {"kind": "finding"}`. The campaign delivers its staged ideas
568
+ as findings, never as compared variants (`hooks/tezgah_research.py::init`).
569
+
570
+ The lock has three halves:
571
+
572
+ - `arena_problems` (`hooks/tezgah_research.py::arena_problems`) names an absent
573
+ arena, an empty problem or budget, and an evaluator with no sha256 or no metric.
574
+ `check` reads it through `scope_problems`. It refuses these past `bootstrap` and
575
+ warns at it.
576
+ - `_check_arena_order` (`hooks/tezgah_research.py::_check_arena_order`) finds the
577
+ lock commit. That commit is the first one along HEAD whose `state.json` arena
578
+ names a problem and a budget. `ideas.jsonl` and every experiment's
579
+ `results.jsonl` have to enter the history after it. An arena that differs from
580
+ the locked one is a refusal too. A new arena means a new line.
581
+ - `evaluator_problem` (`hooks/tezgah_research.py::evaluator_problem`) hashes the
582
+ evaluator again. While the hash differs from the locked one, the writer and
583
+ `check` refuse every stage row. The candidate a campaign judges must not move
584
+ its judge.
585
+ - `append_stage` (`hooks/tezgah_research.py::append_stage`) also compares the
586
+ working `state.json` arena with the arena at the lock commit
587
+ (`hooks/tezgah_research.py::_arena_lock`). While they differ, it refuses every
588
+ stage row. So a rewritten `evaluator_sha256` does not reopen the campaign.
589
+
590
+ `init` prints the next step: commit the arena with `tezgah-research commit` before
591
+ the first idea. The arena replaces the one locked metric of a measured line.
592
+ `_check_evaluation` (`hooks/tezgah_research.py::_check_evaluation`) and
593
+ `_check_evaluation_order` (`hooks/tezgah_research.py::_check_evaluation_order`)
594
+ skip an exploratory line for that reason.
595
+
596
+ ### Three files
597
+
598
+ The campaign lives in three append-only files at the line root. One function
599
+ judges each row type, and the writer and `check` both call it. No file stores a
600
+ status. An idea's stage is its newest `stages.jsonl` row
601
+ (`hooks/tezgah_research.py::campaign_stages`).
602
+
603
+ | File | One row | Writer | Judge |
604
+ |---|---|---|---|
605
+ | `ideas.jsonl` (`IDEAS_FILE`) | `id` (`I<n>`, unused so far), `statement` (two sentences at most), `mechanism`, `trace` (an in-scope item), `operator` (one of `OPERATORS`), `generator` (`{family, model}`), `kill` (the result that would kill it), `date`, and `parents` for a `mutation` (one or more) or a `crossover` (two or more) | `idea <slug>` | `idea_problems` |
606
+ | `stages.jsonl` (`STAGES_FILE`) | `idea`, `stage`, `evidence`, `reason` for a kill, `date` | `stage <slug> <idea> <stage>` | `stage_problems` |
607
+ | `tournament.jsonl` (`TOURNAMENT_FILE`) | `a`, `b`, `judge` (`{family, model}`), `order` (`ab` or `ba`), `winner` (`a`, `b` or `tie`), `rationale` | `match <slug>` | `match_problems` |
608
+
609
+ The field list is `IDEA_FIELDS` (`hooks/tezgah_research.py::IDEA_FIELDS`). The
610
+ moves are `OPERATORS` (`hooks/tezgah_research.py::OPERATORS`). `user` marks an
611
+ idea the person asked for. `PARENT_OPERATORS`
612
+ (`hooks/tezgah_research.py::PARENT_OPERATORS`) sets how many parents a derived
613
+ idea names. An idea outside the arena's scope gets one line under Open questions
614
+ in `findings.md`, never a row.
615
+
616
+ Every append goes through `_append_campaign`
617
+ (`hooks/tezgah_research.py::_append_campaign`). It takes the file lock and reads
618
+ the rows from the locked descriptor. It judges the new row against them, cuts any
619
+ torn tail (`truncate_to_committed`) and writes. A refused append leaves no file
620
+ behind. `check` refuses a committed row that a later version changed or dropped.
621
+
622
+ ### The stage funnel
623
+
624
+ `STAGES` (`hooks/tezgah_research.py::STAGES`) gives the order: `proposed`,
625
+ `duplicate`, `prior-art`, `screened`, `piloted`, `promoted`, `confirmed`, and
626
+ `killed`. `stage_problems` (`hooks/tezgah_research.py::stage_problems`) holds
627
+ the moves to it:
628
+
629
+ - The idea row is the `proposed` stage. A stage row that says `proposed` is a
630
+ refusal.
631
+ - A stage moves forward in that order only, or to `killed`. A kill needs
632
+ `--reason`.
633
+ - A stage also needs its prerequisite somewhere in the idea's history
634
+ (`hooks/tezgah_research.py::PREREQUISITES`). `screened` needs a `prior-art`
635
+ row. `piloted` needs `screened`, `promoted` needs `piloted`, and `confirmed`
636
+ needs `promoted`.
637
+ - `killed` is final. A revived idea is a new idea that names the old one as its
638
+ parent.
639
+ - After `duplicate`, the only allowed row is `killed`. The same holds after a
640
+ `known` prior art.
641
+
642
+ Each stage asks for its own evidence. `--evidence` takes a reference or a JSON
643
+ object.
644
+
645
+ | Stage | Evidence | What the judge asks |
646
+ |---|---|---|
647
+ | `duplicate` | `{"of": "<id>", "similarity": <0..1>}` | `of` names another idea of the line |
648
+ | `prior-art` | `{"verdict": "novel\|incremental\|known", "nearest": [...], "delta": "<one sentence>"}` | `nearest` names at least `NEAREST_MIN` (3) notes. `literature/INDEX.jsonl` indexes each one, and not all of them are `agent-report`. `delta` is one sentence (`_prior_art_problems`) |
649
+ | `screened` | none | `tournament.jsonl` holds at least one match that names the idea |
650
+ | `piloted`, `promoted` | `<h>` or `experiments/<h>` | `protocol.md` names the idea id and repeats its `kill` text. `results.jsonl` holds a row. When the check asks git, `protocol.md` is in a commit. If `results.jsonl` is committed too, `protocol.md` entered the history before it (`_run_problems`) |
651
+ | `confirmed` | `{"experiment": "<h>", "set": "<set>"}` | the arena has an evaluator. The experiment differs from every `promoted` row's experiment. The run passes the same `_run_problems` checks. The first `held_out_log.jsonl` row for that set names this idea. Each look of this idea at the set carries a `YYYY-MM-DD` date, and none is dated before the idea's promotion: an earlier look was part of the selection. A look logged after the confirmation changes nothing (`_confirm_problems`) |
652
+ | `killed` | `--reason "<why>"` | a reason |
653
+
654
+ A `known` prior art kills the idea in the same write. `append_stage`
655
+ (`hooks/tezgah_research.py::append_stage`) needs `--reason` for it. It appends
656
+ the prior-art row and its `killed` row under one lock, so no file state holds a
657
+ known idea alive. `check` refuses a `known` prior art with no kill after it.
658
+
659
+ ### The tournament is a prior
660
+
661
+ A match is one judge's verdict on a pair. `bradley_terry`
662
+ (`hooks/tezgah_research.py::bradley_terry`) fits a strength per idea from the
663
+ matches. `board` ranks the ideas by it. The score moves no stage. `screened`
664
+ asks only that a match exists. An LLM ranking is a prior, and a run under the
665
+ frozen evaluator is the judge.
666
+
667
+ `_tournament_warnings` (`hooks/tezgah_research.py::_tournament_warnings`) warns
668
+ about thin judging. A pair judged only by the family that generated one of its
669
+ ideas warns, unless both generators' families judged it. A pair judged in one
670
+ presentation order warns. An idea with fewer than two judges warns.
671
+
672
+ ### Reports: board, dedup, yield
673
+
674
+ The three reports write nothing and exit 0. None of them is a gate.
675
+
676
+ - `board <slug>` (`hooks/tezgah_research.py::board`) shows the arena and whether
677
+ the evaluator still hashes as locked. It shows each file's row count, each
678
+ idea's rank, stage, score, record, operator, generator family and next action,
679
+ and the campaign's next step. An idea whose prior art is `known` gets the
680
+ `stage ... killed --reason` command as its next action. That resumes a campaign
681
+ whose kill row was torn off.
682
+ - `dedup <slug>` (`hooks/tezgah_research.py::dedup`) clusters the `proposed`
683
+ ideas by statement, at one similarity threshold. The lowest id of a cluster
684
+ represents it. For every other member it prints the `stage ... duplicate`
685
+ command to run. `similarity_matrix` (`hooks/tezgah_research.py::similarity_matrix`)
686
+ uses the static embedding when the feature is on and pinned. Otherwise it reads
687
+ BM25 scores (`hooks/tezgah_rank.py::scores`) as a similarity. Every output names
688
+ the backend and where its threshold came from.
689
+ - `yield` (`hooks/tezgah_research.py::yield_report`) counts, per operator and per
690
+ generator family, how many ideas reached each stage. It lists the kill reasons.
691
+ It gives Spearman's rho between the board score and the pilot outcome
692
+ (`hooks/tezgah_research.py::spearman`). It proposes the next campaign's operator
693
+ weights (`hooks/tezgah_research.py::operator_weights`). `EXPLORE_SHARE` (0.2)
694
+ of each draw stays uniform, so a move with no payoff yet still gets tried. It
695
+ reads the open exploratory lines, or every one with `--all-lines`.
696
+
697
+ ### Resume from the files
698
+
699
+ Nothing outside the three files holds campaign state. A fresh session runs
700
+ `board` and goes on from its `next:` line. A writer that dies mid-append can
701
+ leave a fragment past the last newline. `_campaign_rows`
702
+ (`hooks/tezgah_research.py::_campaign_rows`) reads up to the committed boundary
703
+ and reports the fragment's size. `board` prints it on a `resume:` line. The next
704
+ append drops the fragment, so the fix is to run the append that crashed again.
705
+
706
+ ### How many ideas were tested
707
+
708
+ Fifty ideas tested at the 5% level yield about 2.5 winners by chance. A reader
709
+ cannot discount what nobody counted. `_check_tested`
710
+ (`hooks/tezgah_research.py::_check_tested`) warns when `to_human/report.md` has
711
+ no line that names `piloted` with the number of ideas that reached it. The same
712
+ holds for `promoted`. A number followed by a full stop counts, as in "reached
713
+ piloted: 2.". `board` prints both numbers on its `tested:` line.
714
+
715
+ ### The dedup threshold (two label sets)
716
+
717
+ Si et al. called two seed ideas duplicates at cosine 0.8 on MiniLM. That number
718
+ does not carry to another similarity. Two label sets measured it here.
719
+
720
+ - The fixture: 40 idea-statement pairs, 20 of them duplicates. The author of the
721
+ code labelled them. The acceptance runner records the sweeps as fixture-scoped
722
+ experiments: `dedup-calibration` (BM25) and `dedup-calibration-embed`
723
+ (embed-mrl). Copies stay under `.tezgah/scratch/calibration/`.
724
+ - MRPC: 400 pairs of the Microsoft Research Paraphrase Corpus test set, 200
725
+ paraphrases and 200 not, sampled with seed 87. Human annotators labelled them.
726
+ The pairs are news sentences, and the negatives share many words. At BM25 0.30
727
+ the sample kept every paraphrase and 95% of the negatives. The script and its
728
+ output are `.tezgah/scratch/calib2/run.py` and `out.txt`.
729
+
730
+ | Backend | F1 at 0.8 (fixture / MRPC) | Default | F1 at the default (fixture / MRPC) |
731
+ |---|---|---|---|
732
+ | embed-mrl | 0.62 / 0.64 | 0.65 | 0.90 / 0.705 |
733
+ | BM25 | 0.09 / 0.41 | 0.40 | 0.70 / 0.69 |
734
+
735
+ The default is the threshold, in steps of 0.05, whose worse F1 over the two sets
736
+ is highest. Neither the author's labels nor the news domain picks it alone. On
737
+ MRPC the best F1 beats the always-duplicate baseline (0.667) by about 0.04. The
738
+ defaults are a coarse filter for a session to review, not a judge.
739
+
740
+ `DEDUP_THRESHOLDS` (`hooks/tezgah_research.py::DEDUP_THRESHOLDS`) holds the two
741
+ values. Any other backend keeps `DEDUP_THRESHOLD`
742
+ (`hooks/tezgah_research.py::DEDUP_THRESHOLD`), Si et al.'s 0.8, and the output
743
+ calls it uncalibrated. `--threshold` overrides all of them. Its range is
744
+ `(0, 1]`.
745
+
746
+ ### The evidence behind the method
747
+
748
+ - Si et al., arXiv 2409.04109: about 5% of 4,000 generated seed ideas were not
749
+ duplicates (cosine 0.8). The best LLM judge (a Claude-3.5 pairwise ranker)
750
+ agreed with the human ranking 53.3% of the time. Split-half reviewer panels
751
+ agreed 56.1%.
752
+ - The ideation-execution gap, arXiv 2506.20803: after execution, AI ideas lost
753
+ 1.98 points overall and human ideas lost 0.63.
754
+ - Gupta and Pruthi, arXiv 2502.16487: 24% of 50 AI research documents
755
+ paraphrased or substantially borrowed earlier work. Plagiarism detectors missed
756
+ them.
757
+ - HindSight, arXiv 2603.15164: novelty as an LLM judges it correlates negatively
758
+ with realized impact (rho = -0.29).
759
+ - Darwin Godel Machine, arXiv 2505.22954: an archive of stepping stones beats
760
+ hill-climbing. The loop also removed its own detection markers and faked test
761
+ logs.
762
+ - ShinkaEvolve, arXiv 2509.19349: parent sampling by fitness and novelty,
763
+ novelty rejection by embedding, and UCB1 over models.
764
+ - POPPER, arXiv 2502.09858: falsification experiments on measurable implications,
765
+ with Type-I error control.
766
+
767
+ The consequences:
768
+
769
+ - An LLM ranking is a prior, never a verdict.
770
+ - A run under the frozen evaluator is the judge.
771
+ - Each idea gets its own prior-art check. No idea reaches `screened` without a
772
+ `prior-art` row (`hooks/tezgah_research.py::PREREQUISITES`).
773
+ - `dedup` and the operator weights keep the ideas diverse.
774
+ - The report always states how many ideas the campaign tested.
775
+
776
+ Rules 1-6, `FROZEN_PATHS` and ADR 009 stay as they were. Predictions and
777
+ components take nothing new. `research-off` covers this method too.
495
778
 
496
779
  ## What `check` refuses, and what it only warns about
497
780
 
498
781
  A **refusal** (exit 1) is a guarantee the checker can decide. A **warning** is a
499
782
  guarantee it cannot: the artifact is not wrong, it is unverifiable. `--strict`
500
- turns that whole class into a refusal, which is what a release gate wants and
501
- what a session mid-flight must not have, so the default stays `warn`.
783
+ turns the warnings the listed rules write through `_soft` into a refusal, which is
784
+ what a release gate wants and what a session mid-flight must not have, so the
785
+ default stays `warn`. A warning written outside `_soft` stays one. The section
786
+ below names three groups of them.
502
787
 
503
788
  Refused:
504
789
 
@@ -577,6 +862,40 @@ Refused:
577
862
  nothing, or a finding whose `quote` does not occur verbatim in its `target`
578
863
  (`_check_review`, `hooks/tezgah_research.py::_check_review`).
579
864
 
865
+ On an exploratory line (rules 7), `check` also refuses each of these
866
+ (`_check_exploratory`, `hooks/tezgah_research.py::_check_exploratory`, called
867
+ from `_check_standards`):
868
+
869
+ - an arena that `arena_problems` faults, past `bootstrap`. The faults are no
870
+ arena object, no problem, no budget, and an evaluator with no sha256 or no
871
+ metric (`hooks/tezgah_research.py::arena_problems`).
872
+ - an arena that differs from the one the lock commit holds. The same goes for an
873
+ `ideas.jsonl` or `results.jsonl` that entered the history before that commit
874
+ (`hooks/tezgah_research.py::_check_arena_order`).
875
+ - an idea row that `idea_problems` faults
876
+ (`hooks/tezgah_research.py::idea_problems`). That means a missing field, a
877
+ `generator` that is not `{family, model}`, or an id that is not `I<n>` or
878
+ repeats an earlier one. It also means an operator outside `OPERATORS`, a parent
879
+ no earlier idea carries, or too few parents for a `mutation` or `crossover`. The
880
+ last cases are a statement of more than two sentences and a date that is not
881
+ `YYYY-MM-DD`. A trace to no in-scope item and a missing arena fail it too.
882
+ - a stage row that `stage_problems` faults
883
+ (`hooks/tezgah_research.py::stage_problems`). The funnel above lists the
884
+ moves and the evidence each stage asks for. Every stage row fails while the
885
+ evaluator's hash differs from the locked one
886
+ (`hooks/tezgah_research.py::evaluator_problem`).
887
+ - a stage row that skips its `PREREQUISITES` stage, and any row but `killed`
888
+ after `duplicate` or after a `known` prior art.
889
+ - a `known` prior art with no `killed` row after it.
890
+ - a match row that `match_problems` faults
891
+ (`hooks/tezgah_research.py::match_problems`). That means a side that names no
892
+ idea, an idea matched against itself, or a `judge` that is not
893
+ `{family, model}`. It also means an `order` outside `MATCH_ORDERS`, a `winner`
894
+ outside `MATCH_WINNERS`, or no `rationale`.
895
+ - a campaign file line that does not parse or is not an object
896
+ (`hooks/tezgah_research.py::_campaign_rows`).
897
+ - a committed campaign row that a later version changed or dropped.
898
+
580
899
  Warned, and refused under `--strict` - each is the honest state of an artifact
581
900
  this layer cannot decide on, not a defect:
582
901
 
@@ -674,7 +993,18 @@ this layer cannot decide on, not a defect:
674
993
  judgement, so no migration can write one for the lines that concluded before
675
994
  the rule existed.
676
995
 
677
- One warn is **not** promoted by `--strict`, and it is the only one: a `phase`
996
+ On an exploratory line, two more warnings take this class. One is an arena
997
+ fault at `bootstrap`. The other is a campaign file git cannot order against the
998
+ arena lock (`_check_arena_order`).
999
+
1000
+ Three groups of warnings are **not** promoted by `--strict`. The rules-6
1001
+ three (held-out set, repeats, noise band) stay warnings by ADR 019. The rules-7
1002
+ four stay warnings for the same reason: a tournament is a prior and a count is a
1003
+ disclosure. They are a pair that only a generator family of one of its ideas
1004
+ judged, unless both generators' families judged it. They are a pair judged in one
1005
+ order, and an idea with fewer than two judges (`_tournament_warnings`). The fourth
1006
+ is a `report.md` that does not state how many ideas reached `piloted` and
1007
+ `promoted` (`_check_tested`). The last is a `phase`
678
1008
  declared in `state.json` behind the phase the line's own artifacts show
679
1009
  (`_check_derived_phase`, `hooks/tezgah_research.py::_check_derived_phase`). The field is the author's
680
1010
  declared intent and `_check_report` and `_check_review` read it, so a derivation
@@ -872,12 +1202,15 @@ armed. It removes the rule, not just the mark - the diff is in `core_split`,
872
1202
  frozen set they read (`FROZEN_PATHS`), the protocol order and its tracked pair,
873
1203
  the protocol's two questions, the concluded report's limits, the `supersedes`
874
1204
  relation, `component_report` with the state it reads, `migrate`, `source_run`,
875
- `init` and `summary`.
1205
+ `init` and `summary`, and the rules-7 campaign: the arena lock, the three row
1206
+ judges and their writers, `board`, `dedup` and `yield_report`.
876
1207
  - `hooks/tezgah_components.py` - the manifest: the one definition of an editable
877
1208
  component, the keys a prediction row names and the rules a row's outcome is
878
1209
  attributed to.
879
1210
  - `bin/tezgah-research` - the CLI, its subcommands and its exit codes.
880
1211
  - `hooks/tezgah_product.py` - the product-flow steps and the `next` status reader.
1212
+ - `hooks/tezgah_rank.py` - `scores`, the BM25 reading `dedup` falls back to when
1213
+ the static embedding is off.
881
1214
  - `hooks/tezgah_policy.py` - the `RESEARCH` paragraph a session is armed with.
882
1215
  - `hooks/tezgah_context.py` - the prompt hint, the missing-orx note, the broken
883
1216
  line note and the `research` mark.
package/docs/skills.md CHANGED
@@ -32,7 +32,7 @@ skill is simply not read.
32
32
 
33
33
  opencode's plugin does inject prompt-time text - the builder's per-turn block,
34
34
  the conditional rules included
35
- (`hosts/opencode/plugins/tezgah.js:2629-2658`) - but that text names at most one
35
+ (`hosts/opencode/plugins/tezgah.js:2652-2681`) - but that text names at most one
36
36
  skill (the hint), never the roster, so the installer writes one:
37
37
  `~/.config/tezgah/opencode-skills.md` always-on and
38
38
  `opencode-skills.full.md` on demand (`bin/tezgah-setup::SKILL_ROUTER`, `bin/tezgah-setup::SKILL_ROUTER_FULL`), the first