@r1z4x/tezgah 1.7.0 → 1.8.3
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 +74 -1
- package/MANIFEST +2 -0
- package/bin/tezgah-decisions +6 -14
- package/bin/tezgah-design +21 -5
- package/bin/tezgah-lessons +4 -1
- package/bin/tezgah-research +15 -7
- package/bin/tezgah-route +22 -7
- package/bin/tezgah-task +320 -75
- package/bin/tezgah-taste +8 -5
- package/docs/architecture.md +24 -7
- package/docs/environment.md +3 -1
- package/docs/evidence.md +9 -1
- package/docs/gate.md +24 -7
- package/docs/models.md +40 -12
- package/docs/operations.md +31 -1
- package/docs/research.md +3 -2
- package/docs/testing.md +18 -11
- package/hooks/projects-auto-init.py +29 -1
- package/hooks/tezgah_agents.py +24 -7
- package/hooks/tezgah_context.py +71 -35
- package/hooks/tezgah_gate.py +430 -64
- package/hooks/tezgah_integrity.py +108 -41
- package/hooks/tezgah_models.py +90 -30
- package/hooks/tezgah_paths.py +72 -6
- package/hooks/tezgah_policy.py +3 -3
- package/hooks/tezgah_research.py +9 -5
- package/hooks/tezgah_task.py +319 -67
- package/hooks/tezgah_taste.py +8 -4
- package/hosts/codex/hook.py +23 -3
- package/hosts/cursor/hook.py +11 -0
- package/hosts/opencode/plugins/tezgah.js +171 -4
- package/package.json +1 -1
- package/skills/plan-add/SKILL.md +23 -9
- package/skills/plan-add/render_table.py +8 -3
- package/skills/tezgah-contract/SKILL.md +3 -3
package/bin/tezgah-taste
CHANGED
|
@@ -500,7 +500,8 @@ DIFF_MAX = 1500
|
|
|
500
500
|
def repo_root(path):
|
|
501
501
|
out = subprocess.run(["git", "-C", path, "rev-parse", "--show-toplevel"],
|
|
502
502
|
capture_output=True, text=True)
|
|
503
|
-
|
|
503
|
+
top = out.stdout.strip() if out.returncode == 0 else ""
|
|
504
|
+
return tezgah_paths.project_top(top) if top else None
|
|
504
505
|
|
|
505
506
|
|
|
506
507
|
def sig_id(*parts):
|
|
@@ -712,10 +713,12 @@ def write_line(learning):
|
|
|
712
713
|
|
|
713
714
|
|
|
714
715
|
def store_or_exit(args):
|
|
715
|
-
"""The repository whose taste store a command opens
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
716
|
+
"""The repository whose taste store a command opens: the main checkout's,
|
|
717
|
+
from a linked worktree too (plan 097). Refused when its `.tezgah` came with
|
|
718
|
+
the clone: those files are the project's data, and opening the store would
|
|
719
|
+
import, rename or create files in them."""
|
|
720
|
+
top = repo_root(os.path.abspath(args.repo or os.getcwd()))
|
|
721
|
+
repo = top and (tezgah_paths.linked_main(top) or top)
|
|
719
722
|
if not repo:
|
|
720
723
|
raise SystemExit(misuse("not inside a git repository"))
|
|
721
724
|
if tezgah_paths.workspace_from_repo(repo):
|
package/docs/architecture.md
CHANGED
|
@@ -168,13 +168,30 @@ The cache dir is resolved once per process and falls back to a temp dir on a
|
|
|
168
168
|
sandboxed host, so one session's state never splits across two files
|
|
169
169
|
(`hooks/tezgah_paths.py::cache_dir`). Per-repo state — `.tezgah/plans/open/`,
|
|
170
170
|
`.tezgah/lessons.md`, `.tezgah/research/` — lives in the repository, and the
|
|
171
|
-
checkout is the source from which the plugin copy is made.
|
|
172
|
-
|
|
173
|
-
and
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
(`
|
|
171
|
+
checkout is the source from which the plugin copy is made. A repository keeps
|
|
172
|
+
one workspace, in its main checkout. A session or CLI in a linked `git worktree`
|
|
173
|
+
reads and writes the main checkout's `.tezgah`: the session context
|
|
174
|
+
(`hooks/tezgah_context.py::context_for`), `tezgah-task`
|
|
175
|
+
(`bin/tezgah-task::main_root`), the slice journal
|
|
176
|
+
(`hooks/tezgah_task.py::slice_dir`), `tezgah-research`
|
|
177
|
+
(`bin/tezgah-research::repo_root`), `tezgah-decisions`, the taste CLI and taste
|
|
178
|
+
capture (`hooks/tezgah_taste.py::enabled`). A
|
|
179
|
+
worktree's own `.tezgah` went with `git worktree remove`, and so did everything
|
|
180
|
+
a session wrote there. `tezgah-research check` still resolves a line's citations
|
|
181
|
+
against the checkout it runs in, where the branch's code is. The session-start
|
|
182
|
+
line names every checkout through `worktrees`, which lists
|
|
183
|
+
`<main>/.git/worktrees/*/gitdir` with no git fork and skips an entry whose
|
|
184
|
+
checkout is gone, as git's own `prunable` does (`hooks/tezgah_paths.py::worktrees`).
|
|
185
|
+
It also names a worktree whose own `.tezgah` still holds data from before, with
|
|
186
|
+
the `tezgah-research import` that moves its lines (`hooks/tezgah_context.py::sibling_line`).
|
|
187
|
+
Session start in a linked worktree makes its `.tezgah` a link to the main one
|
|
188
|
+
(`hooks/tezgah_paths.py::link_workspace`). A relative `.tezgah/...` write lands
|
|
189
|
+
there, and `/.tezgah` goes to the repository's `info/exclude`.
|
|
190
|
+
A second clone is another repository, with a workspace of its own.
|
|
191
|
+
A path inside `.tezgah` belongs to the repository around it, never to `.tezgah`.
|
|
192
|
+
The workspace keeps its own `.git`, so git's top-level answers `.tezgah` there.
|
|
193
|
+
Every resolver passes that answer through `hooks/tezgah_paths.py::project_top`.
|
|
194
|
+
So tezgah never creates a `.tezgah`, `.cursor`, `.opencode` or graph index inside it.
|
|
178
195
|
|
|
179
196
|
## Invariants
|
|
180
197
|
|
package/docs/environment.md
CHANGED
|
@@ -112,7 +112,9 @@ the kill switches are files, not variables, and live in
|
|
|
112
112
|
|
|
113
113
|
| Variable | Read by | Default | Effect |
|
|
114
114
|
|---|---|---|---|
|
|
115
|
-
| `TEZGAH_SESSION` | `bin/tezgah-status::main`, `bin/tezgah-docs`, `bin/tezgah-route`, `bin/tezgah-triage`, `hosts/omp/tezgah-hook.ts.in` | unset; the omp hook exports it to its shells | The session id `tezgah-docs`, `tezgah-route` and `tezgah-triage` write their ledger rows under, and the id `tezgah-status` lights its "used" marks from when none is passed. |
|
|
115
|
+
| `TEZGAH_SESSION` | `bin/tezgah-status::main`, `bin/tezgah-docs`, `bin/tezgah-route`, `bin/tezgah-triage`, `bin/tezgah-task`, `bin/tezgah-doctor`, `hooks/tezgah_guard.py`, `hosts/omp/tezgah-hook.ts.in` | unset; the omp hook exports it to its shells, and on Claude `hooks/projects-auto-init.py::export_session` appends it to `CLAUDE_ENV_FILE` | The session id `tezgah-docs`, `tezgah-route` and `tezgah-triage` write their ledger rows under, and the id `tezgah-status` lights its "used" marks from when none is passed. `tezgah-task` limits its review guard, slice board and slice lookups to this session. `tezgah-doctor --clean` keeps this session's state. The hook guard writes its crash row under it when the payload names no session. |
|
|
116
|
+
| `CODEX_THREAD_ID`, `CURSOR_CONVERSATION_ID` | `bin/tezgah-route` | set by Codex and Cursor in their agent shells | The session `tezgah-route` writes its rows under when `TEZGAH_SESSION` is unset. When two of the three name different sessions, an inherited id is present and no row is written. |
|
|
117
|
+
| `CLAUDE_ENV_FILE` | `hooks/projects-auto-init.py::main` | set by Claude Code for SessionStart hooks | The file Claude sources before each Bash call; `export_session` appends `TEZGAH_SESSION` to it. |
|
|
116
118
|
| `TEZGAH_CORE_IN_FILE` | `hooks/projects-auto-init.py::core_is_in_a_file`, `hooks/hooks.json` | unset | `1` (set by the Claude plugin's hook rows) drops the core rules from the injection when the host's static file really carries the tezgah block; a file without it falls back to the hook. |
|
|
117
119
|
| `TEZGAH_NESTED` | `hooks/tezgah_integrity.py::_shape_block`, `bin/consult` | unset | Set by consult on the agent CLIs it runs: that session's Stop rule does not fire. |
|
|
118
120
|
| `TEZGAH_CALL_OUTCOME` | `hooks/projects-posttooluse.py::main`, `hosts/dsh/hooks.json` | unset | `none` (dsh's Task rows) records the call's outcome as unknown instead of as a pass. |
|
package/docs/evidence.md
CHANGED
|
@@ -117,7 +117,7 @@ a write tool is `edit`, a shell call is `verify` when its command matches the ch
|
|
|
117
117
|
| `turn` | `hooks/tezgah_integrity.py::note_turn`, from the prompt path | `hooks/tezgah_integrity.py::_turn_start`, scoping every turn rule; `hooks/tezgah_integrity.py::_claim_key` |
|
|
118
118
|
| `run`, `edit`, `verify`, `verify_ok`, `verify_fail`, `interrupted` | `hooks/tezgah_integrity.py::note_tool`, the call's own name in `tool`, on opencode the plugin writes these same kinds and the same field (`hosts/opencode/plugins/tezgah.js:1215-1293`) | the Stop rule's `worked` set `hooks/tezgah_integrity.py::WORK_KINDS`; `counters.steps` `hooks/tezgah_integrity.py::_counts`; `last_verify`/`partial_state`; the `--trend` tool histogram |
|
|
119
119
|
| `external`, `unknown` | `hooks/tezgah_integrity.py::note_tool` | the taint notice, via `source`; the `--trend` tool histogram; nothing counts them as work |
|
|
120
|
-
| `todo` | `hooks/tezgah_integrity.py::note_todo`, from `note_tool` for a todo call: omp `todo` (the bridge sends the list from the result's `details`), Claude and dsh `TodoWrite` (their PostToolUse matcher), opencode `todowrite` (the plugin's own `recordTodo`), Cursor `todo_write` and Codex `update_plan`, which arrive with no matcher. `open`, `ready` and `blocked` count the list,
|
|
120
|
+
| `todo` | `hooks/tezgah_integrity.py::note_todo`, from `note_tool` for a todo call: omp `todo` (the bridge sends the list from the result's `details`), Claude and dsh `TodoWrite` (their PostToolUse matcher), opencode `todowrite` (the plugin's own `recordTodo`), Cursor `todo_write` and Codex `update_plan`, which arrive with no matcher. `open`, `ready` and `blocked` count the list, `detail` holds the ready items' heads, one per line, `taken` holds the in-progress items' heads, and `partial` marks a Cursor `merge: true` list | the `ready steps` and `open todo` Stop classes (`hooks/tezgah_integrity.py::ready_left`, `hooks/tezgah_integrity.py::open_left`) and the idle-wait warning (`hooks/tezgah_gate.py::ready_advice`) |
|
|
121
121
|
| `wait` | `hooks/tezgah_integrity.py::note_tool`, for omp's `wait` | `hooks/tezgah_gate.py::ready_advice`, which counts it as idle; nothing counts it as work |
|
|
122
122
|
| `ask` | `hooks/tezgah_gate.py::ask_advice`, for an omp `ask` or Claude AskUserQuestion call: `detail` holds the questions, cut; `questions` counts them and `defaults` the ones with a recommended option | nothing refuses on it; the row is the record of what the session asked |
|
|
123
123
|
| `grant` | `hooks/tezgah_context.py::context_for`, from each prompt's `user_grants` (`hooks/tezgah_context.py::user_grants`): `detail` holds the user's clause, `target` the outward class it covers (`push`, `release`, `publish`, `pr` or `all`), or `lift:<class>` when the prompt took it back | `hooks/tezgah_integrity.py::grants_in_force`, read by the `needless ask` Stop class and the ask-tool warning |
|
|
@@ -499,6 +499,14 @@ The four shape classes share their switches with the text they enforce: `adhd-of
|
|
|
499
499
|
(`hooks/tezgah_gate.py::ready_advice`). The warning needs a spawn with nothing after it but
|
|
500
500
|
waits and todo updates, and a ready item no running slice names. It rides back with the wait's
|
|
501
501
|
result.
|
|
502
|
+
**open todo**, beside it (plan 097): the reply claims done while the same `todo` row still holds
|
|
503
|
+
an item in progress. The count is `open - blocked - ready`, and `taken` names the heads
|
|
504
|
+
(`hooks/tezgah_integrity.py::open_left`). The claim is the one `asserted_claims` reads, so a
|
|
505
|
+
question or a negation is no claim. An in-progress item passes like a ready one: one reply line
|
|
506
|
+
names it beside a blocker token. A blocked item does not count. A Cursor `merge: true` row
|
|
507
|
+
holds only the changed items, so the class skips a row marked `partial`. `_residual_block`
|
|
508
|
+
asks it right after `ready steps`, on the same parsed row (`hooks/tezgah_integrity.py::_turn_todo`),
|
|
509
|
+
under the same scope and switches. It counts as a false completion, since the reply claims done.
|
|
502
510
|
13. **needless ask** — the reply ends on a question the agent can decide (plan 086, S3). The
|
|
503
511
|
last line asks: it ends on "?" or a pack's question particle
|
|
504
512
|
(`hooks/tezgah_integrity.py::QUESTION_END`). Its paragraph names no recommended default, and
|
package/docs/gate.md
CHANGED
|
@@ -155,7 +155,7 @@ Off: `lang-off`, the rule's own switch (`hooks/tezgah_gate.py::decision`), and `
|
|
|
155
155
|
Trigger: another session recorded a write of this call's file inside `RACE_WINDOW_MIN = 10` minutes (`hooks/tezgah_gate.py::RACE_WINDOW_MIN`). The call's files are
|
|
156
156
|
its path field, its `apply_patch` headers, or a shell redirect or `tee` target (`hooks/tezgah_gate.py::write_paths`, `hooks/tezgah_gate.py::race_reason`).
|
|
157
157
|
The reader counts an `edit` row and a shell `run` row that carries a `target` (`hooks/tezgah_integrity.py::writers_elsewhere`).
|
|
158
|
-
Scratch is skipped: a temp file or a device (`hooks/tezgah_integrity.py::scratch_target`). A `run` row that writes only scratch carries no `target`. The rule also skips the harness's own channels (`hooks/tezgah_gate.py::URI_CHANNEL`), such as `agent://`, `xd://` and `conflict://`. It skips a shell target with an unknown variable too.
|
|
158
|
+
Scratch is skipped: a temp file or a device (`hooks/tezgah_integrity.py::scratch_target`). A file a git checkout holds is never scratch, even when the checkout lives under the temp dir (`hooks/tezgah_gate.py::_scratch`). A `run` row that writes only scratch carries no `target`. The rule also skips the harness's own channels (`hooks/tezgah_gate.py::URI_CHANNEL`), such as `agent://`, `xd://` and `conflict://`. It skips a shell target with an unknown variable too.
|
|
159
159
|
It skips this session's own subagent once its slice record has closed after the subagent's newest write of the file (`hooks/tezgah_gate.py::ended_child`). The writer ledger's `spawned` row names the child, and the record is read after `pending` closes an ended omp child. A child with no record, an open one, or a write newer than the close stays refused.
|
|
160
160
|
Told: re-read the file and re-apply the change to what is on disk now (`RACE_DENY`
|
|
161
161
|
`hooks/tezgah_gate.py::RACE_DENY`). The text names each writer by its relation to this session (`hooks/tezgah_gate.py::RACE_WHO`, `hooks/tezgah_integrity.py::_relation`).
|
|
@@ -170,7 +170,7 @@ Trigger: a write into a file an open sibling slice owns, or into the main checko
|
|
|
170
170
|
A brief's `Owns:` line holds the globs, split on commas and spaces, or lists them as `- glob` items under it. The slice record stores them as `owns` (`hooks/tezgah_task.py::owned_globs`, `hooks/tezgah_task.py::open_slice`). The owner is the open record whose globs match (`hooks/tezgah_task.py::owner_of`). A reader, an agent named for a read-only role such as a reviewer, holds no lease.
|
|
171
171
|
The main-checkout refusal reads the worktree a `Worktree:`, `Checkout` or `Cwd:` line declares, the slice's own task first (`hooks/tezgah_task.py::_declared_in`). A path the brief only names declares nothing, and a declared main checkout means the slice works there. The journal still keeps the named worktree for reconcile, resume and the board.
|
|
172
172
|
A shell target resolves against the call's own `cwd` and every `cd` or `pushd` before it (`hooks/tezgah_gate.py::_shell_dir`). A redirect to `/dev/*` or a descriptor writes no file.
|
|
173
|
-
A slice may write a path its own `Owns:` names. The `.tezgah` directory and scratch stay free, and a closed owner frees its files.
|
|
173
|
+
A slice may write a path its own `Owns:` names. The `.tezgah` directory and scratch stay free, and a closed owner frees its files. A checkout under the temp dir is not scratch here either (`hooks/tezgah_gate.py::_scratch`).
|
|
174
174
|
On omp, the ledger's `spawned` row maps the writing session to its record (`hooks/tezgah_integrity.py::spawn_of`, `hooks/tezgah_task.py::slice_of`). A Claude background child maps through the `agent_id` its calls carry, which the spawn's result named (`hooks/projects-posttooluse.py`). Its SubagentStop closes the record (`hooks/tezgah_task.py::close_child`). Everything else fails open. A Claude foreground agent, dsh, Cursor and opencode open the record only once the child has ended, and Codex writes no `spawned` row.
|
|
175
175
|
Told: the owner, its globs, how to reach it - omp `write agent://<name>`, Claude `SendMessage`, else the parent - and the `tezgah-task slice done <id>` that frees it (`hooks/tezgah_gate.py::LEASE_DENY`, `hooks/tezgah_gate.py::MAIN_CHECKOUT_DENY`). Standing while the owner's record runs.
|
|
176
176
|
Off: `orchestrate-off`, whose fan-out rule it backs, and `slices-off`, which keeps no record to read (`hooks/tezgah_gate.py::decision`).
|
|
@@ -181,12 +181,14 @@ A spawn whose writing slices name one worktree gets a warning, never a refusal,
|
|
|
181
181
|
Trigger: a write tool, or a shell command that writes a file, while an active task is set — a plan file under `.tezgah/plans/open` whose frontmatter carries a valid
|
|
182
182
|
`phase` (`tezgah_task.active`). The phase refuses a write in a reading phase (`hooks/tezgah_gate.py::TASK_PHASE_DENY`) and a shell command that writes one (`hooks/tezgah_gate.py::TASK_SHELL_DENY`,
|
|
183
183
|
against `hooks/tezgah_gate.py::SHELL_WRITE`); the allowlist refuses a target outside it (`hooks/tezgah_gate.py::TASK_SCOPE_DENY`) — a write tool's path, and a shell command's redirect or `tee` target (`hooks/tezgah_gate.py::write_paths`); and a write whose target IS the record, by a write tool or a shell redirect/`tee`, is refused
|
|
184
|
-
(`hooks/tezgah_gate.py::TASK_RECORD_DENY`), because only `bin/tezgah-task` writes it and that CLI makes the checks a retyped frontmatter would skip. Told:
|
|
184
|
+
(`hooks/tezgah_gate.py::TASK_RECORD_DENY`) - the active task's record and any other open plan that carries a phase (`hooks/tezgah_task.py::phased_record`), matched without case and read against the main checkout, so a session in a worktree meets the same rule - because only `bin/tezgah-task` writes it and that CLI makes the checks a retyped frontmatter would skip. Told:
|
|
185
185
|
the move that lifts it, made by the session itself — `tezgah-task phase implementation` or `tezgah-task allow <glob> ... --why "<reason>"` — since the orchestrator owns the plan lifecycle and the CLI refuses
|
|
186
|
-
the move until the plan's Acceptance, spike and checkpoint are in order, and records the reason for a widened scope (`allowed_why:`). The record's body has its own writers, on any open plan. `tezgah-task tick NNN <n> [--undo]` ticks Acceptance item n (`bin/tezgah-task::cmd_tick`). `tezgah-task note NNN state|next "<text>"` replaces the newest `## State` or the `## Next` body (`bin/tezgah-task::cmd_note`).
|
|
187
|
-
Scratch meets no allowlist: a temp file or a device, such as `pytest > /tmp/check.log` (`hooks/tezgah_gate.py::task_reason`). Build output inside the repo stays held.
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
the move until the plan's Acceptance, spike and checkpoint are in order, and records the reason for a widened scope (`allowed_why:`). The record's body has its own writers, on any open plan, run from the main checkout or from any of its worktrees. `tezgah-task tick NNN <n> [--undo]` ticks Acceptance item n (`bin/tezgah-task::cmd_tick`). `tezgah-task note NNN state|next "<text>"` replaces the newest `## State` or the `## Next` body (`bin/tezgah-task::cmd_note`). `tezgah-task spike NNN "<answer>"` writes `spike_recorded:`, the answer `implementation` waits for (`bin/tezgah-task::cmd_spike`). None of them commits. Standing: only the record changing lifts it.
|
|
187
|
+
Scratch meets no allowlist: a temp file or a device, such as `pytest > /tmp/check.log` (`hooks/tezgah_gate.py::task_reason`). Build output inside the repo stays held. So does a file in the repo or its worktrees when the checkout lives under the temp dir, as CI's does.
|
|
188
|
+
The task rule holds repository paths only. A target under no git checkout and no configured root, such as `~/.config/runpod/key`, is no plan's work (`hooks/tezgah_gate.py::_no_repo`). Neither the phase nor the allowlist refuses it, while every other rule, the secret rule among them, still reads that write. The gate reads a relative target in the call's own directory. Any doubt keeps the target held. A line that moves the directory stays held, however it spells the `cd`, `pushd` or `popd`. So does a line that reads `$PWD` back. A line that makes a link stays held: `ln`, `install`, `cp -s`, `cp -l`. So does a target whose parent directory does not exist yet. So does a file with a second hard link. Host agent and instruction files and the shell rc files stay held too, in any letter case (`hooks/tezgah_gate.py::HOME_HELD`). Under `~/.claude` those are `agents/**`, `hooks/**`, `commands/**`, `skills/**` and `CLAUDE.md`. Elsewhere they are `~/.omp/agent/**`, `~/.codex/AGENTS.md`, `~/.codex/prompts/**`, `~/.cursor/**` and `~/.config/opencode/**`. The rc files are `~/.zshrc`, `~/.zshenv`, `~/.zprofile`, `~/.zlogin`, `~/.bashrc`, `~/.bash_profile`, `~/.bash_login` and `~/.profile`. A target in another repository stays refused as outside this one.
|
|
189
|
+
Three targets are no project file, so neither the phase nor the allowlist refuses them (`hooks/tezgah_gate.py::unnamed_target`). A URI is one: a tool device (`xd://<tool>`), an agent message (`agent://X`) or a conflict resolution (`conflict://2`). A `file://` URI names a checkout file and stays held. A shell target with an unknown variable is another. `write_paths` first expands a variable from an assignment earlier on the line. So the allowlist holds `B=src; git diff > $B/x.patch` as `src/x.patch`. A variable the same line set to the checkout, such as `R=$(git rev-parse --show-toplevel)`, is read as the checkout (`hooks/tezgah_gate.py::known_tail`). The third is tezgah's own workspace under `.tezgah/`, such as research lines, lessons and briefs (`hooks/tezgah_gate.py::workspace_free`). The slice journal stays held. So does every shell write into `.tezgah/plans/open/`, because such a file could make itself the active task. A plan file stays held too when its text, once the write lands, carries a `phase:` line (`hooks/tezgah_gate.py::_text_after`). The gate judges the whole text, so two edits cannot each pass on their own. The record rule still refuses the active record there.
|
|
190
|
+
The gate matches a file in a linked worktree of the same repository relative to that worktree's top (`hooks/tezgah_task.py::relative`). Each writing slice gets its own worktree, so `hooks/a.py` there is in scope when `hooks/a.py` is. The phase, allowlist and checkpoint rules read a sibling. The record rule reads the main checkout, and the workspace rule keeps the cwd's own root. The worktree's admin directory must point back at it, and a worktree of another repository stays outside.
|
|
191
|
+
Two plans can run at once, one per worktree. A linked worktree holds plan NNN when its branch is `plan/NNN-...` (`hooks/tezgah_task.py::branch_task`). That covers the plan's declared `branch:` and every slice branch the plan fans out to. Of several, the declared branch's worktree is the one the checkpoint and review read (`hooks/tezgah_task.py::plan_holders`). It then answers to plan NNN's phase, allowlist and checkpoint while plan NNN carries a phase. A session whose cwd is that worktree reads plan NNN as the active task. A write from anywhere else into that worktree meets plan NNN too (`hooks/tezgah_task.py::worktree_task`). A shell line meets the plan of every redirect and `tee` target it names (`hooks/tezgah_gate.py::_owners`). It meets the cwd's task as well when it writes where no target was read. That is a positional writer such as `sed -i`, a `tee` with more than one file, or more write shapes than targets. The checkpoint's sha is read against that worktree's HEAD and tree, and the refusal names `git -C <worktree>`. Every other checkout reads the main checkout's plans. Its task is the first phased plan no worktree holds. When every phased plan is held, it has no scope of its own, and a write outside the plan worktrees meets no plan (`hooks/tezgah_task.py::active`). The record rule still refuses a direct write to each held record. `tezgah-task start` keeps another plan's phase in one case only (`bin/tezgah-task::cmd_start`). Both plans then run in their own worktrees, and their allowlists share no prefix. Otherwise it clears that phase. `allow`, and `phase --any-path`, refuse a scope that shares a prefix with such a held plan (`bin/tezgah-task::held_overlap`). Off: `task-off`
|
|
190
192
|
(`hooks/tezgah_gate.py::decision`). It fails open wherever the question cannot be answered: no active task, no readable record.
|
|
191
193
|
|
|
192
194
|
The fourth refusal under the same rule is the checkpoint. Trigger: a write, or a shell command that writes a file (`hooks/tezgah_gate.py::SHELL_WRITE`), while the task is in
|
|
@@ -208,6 +210,20 @@ Three triggers, one per shape a credential takes on its way into a file (`hooks/
|
|
|
208
210
|
- **Shell text.** One simple command (split on `&&`, `||`, `;`, newline — `hooks/tezgah_gate.py::SEGMENT`) carrying both a token (`hooks/tezgah_gate.py::SECRET_TOKEN` — an
|
|
209
211
|
`Authorization: Bearer` header, or a `name=value` assignment for `api_key`/`token`/`secret`/`password`/...) and a sink (`hooks/tezgah_gate.py::SECRET_SINK` — `>`/`>>`,
|
|
210
212
|
`| tee`, a curl `--trace`, or `git add`). Told: record the name, length or a fingerprint instead, and pass the value through the tool's environment (`hooks/tezgah_gate.py::SECRET_DENY`).
|
|
213
|
+
A token only in the leading `NAME=value` words of a program reaches that program's environment, not the file.
|
|
214
|
+
So `API_KEY="$(cat key)" python3 tool.py > run.log` passes (`hooks/tezgah_gate.py::_env_prefix_only`).
|
|
215
|
+
An allowlist is the ceiling of this case (`hooks/tezgah_gate.py::_secret_runner`). Only a script runner named by itself passes: `python`, `node`, `bun`, `deno run`, `bash`, `sh` or `zsh`.
|
|
216
|
+
It needs a script file ending in `.py`, `.js`, `.mjs`, `.ts` or `.sh`.
|
|
217
|
+
No option before the file may run inline text, stdin, a preload, interactive mode or a trace.
|
|
218
|
+
Those are `-c`, `-e`, `-p`, `-`, `-r`, `-i`, `-x`, `-v` and `-o` (`hooks/tezgah_gate.py::SECRET_INLINE`).
|
|
219
|
+
Every other program holds, because it may print its environment: `env`, `echo`, `xargs env`, `node -pe`, a bare `prog`, a program named by its path.
|
|
220
|
+
It also holds when the sink is a `git add` or a curl trace (`hooks/tezgah_gate.py::SECRET_OUTPUT`).
|
|
221
|
+
A line that reads `/proc/*/environ`, defines a function or runs `set -x` holds too.
|
|
222
|
+
So does a line that sets an interpreter's startup or trace variable (`hooks/tezgah_gate.py::SECRET_STARTUP`: `BASH_ENV`, `NODE_OPTIONS`, `PYTHONSTARTUP`, `LD_PRELOAD`, ...).
|
|
223
|
+
The rule cannot read the script itself: an allowlisted script that prints its own environment still writes the key.
|
|
224
|
+
A name an earlier command on the line set to a token stays carried. A printer that reads it into a file still denies, as in `API_KEY=x; echo $API_KEY > f` (`hooks/tezgah_gate.py::_printer_owns_sink`).
|
|
225
|
+
That check reads the pipeline stage that owns the sink, so `echo "$TOKEN" | docker login --password-stdin > log` passes. A `> /dev/null` is not a file there.
|
|
226
|
+
The rule reads token shapes, not file names: `echo "$KEY" > f` and `cat key > f` carry no token and pass.
|
|
211
227
|
- **A credential file staged.** A `git add` that names a credential file (`hooks/tezgah_snapshot.py::SECRET_FILE`: `.env`, `.env.local`, a key, `id_rsa`, `credentials.json`).
|
|
212
228
|
The command is read word by word (`hooks/tezgah_gate.py::_secret_add`). `.env.example`, `.env.sample`, `.env.template`, `.env.dist` and `.env.defaults` carry names without values and pass (`hooks/tezgah_gate.py::SECRET_TEMPLATES`).
|
|
213
229
|
The rule does not read `git add -A` or `git add .` (`hooks/tezgah_gate.py::SECRET_ADD_DENY`).
|
|
@@ -409,6 +425,7 @@ rule's triggers are the classes `_stop_block` returns, the four `_shape_block` a
|
|
|
409
425
|
| `no external read` | two turns: "npm 0.22.0 is missing" off a stale client, and "make NPM_TOKEN an automation token" when it already was | `hooks/tezgah_integrity.py::_external_read_row` | `tests/test_integrity.py::test_an_external_claim_with_no_read_is_refused` |
|
|
410
426
|
| `residual work` | the owner's report (2026-10-11): turns end on "Açık kalanlar / Doğrulanmadı / Sıradaki adım" instead of the work; measured 503 of 1876 final replies with such a label at a line start, 467 in turns that called a non-read tool | `hooks/tezgah_integrity.py::_residual_block` | `tests/test_integrity.py::test_a_residual_list_is_refused_once_and_the_reply_after_it_recorded` |
|
|
411
427
|
| `ready steps` | measured 2026-10-11 (research line `orchestrator-flow-audit`): 13.1% of 451 omp user turns were a bare "devam", and 192 of 281 spawns were followed by `wait` with no other call between, at 82 of them with four or more todo items open | `hooks/tezgah_integrity.py::ready_left` | `tests/test_integrity.py::test_a_ready_todo_step_left_is_refused_once` |
|
|
428
|
+
| `open todo` | the owner's report (2026-10-12, plan 097 item 2): `ready_left` counted only `pending` items, so a done claim over an `in_progress` todo item ended the turn | `hooks/tezgah_integrity.py::open_left` | `tests/test_integrity.py::test_a_done_claim_over_an_in_progress_todo_is_refused_once` |
|
|
412
429
|
| `needless ask` | research line `orchestrator-flow-audit` S3: 33 of 48 final replies that ended on a question in the owner's omp transcripts were process questions ("Başlayayım mı?"), and 9 of 47 next prompts were a bare "devam" or "evet" | `hooks/tezgah_integrity.py::needless_ask` | `tests/test_integrity.py::test_a_process_question_at_the_end_is_refused_once` |
|
|
413
430
|
| `stopped slice` | measured 2026-10-11 (research line `subagent-resume-orchestration`): 118 of 1413 omp subagent runs ended stopped, and in the newest 20 the parent absorbed the work inline 16 times instead of resuming it | `hooks/tezgah_integrity.py::_stopped_slice` | `tests/test_integrity.py::test_a_stopped_slice_of_this_session_refuses_the_turn_once` |
|
|
414
431
|
| `placating opener` | designed | output rule 10 read at the Stop event; no incident recorded | `tests/test_integrity.py::test_sycophantic_opener_blocks` |
|
package/docs/models.md
CHANGED
|
@@ -25,8 +25,11 @@ generated in every tezgah root because routing needs no capability (`ROLES`,
|
|
|
25
25
|
`tezgah-researcher` (`hooks/tezgah_agents.py::SPECIALISTS`). A specialist's
|
|
26
26
|
description names its slot from `AGENT_SLOT`. The tier a host shows and the
|
|
27
27
|
model line it runs therefore cannot disagree, and `tezgah-agents --list` prints
|
|
28
|
-
both. A cheaper worker that meets work above its tier
|
|
29
|
-
|
|
28
|
+
both. A cheaper worker that meets work above its tier answers `ESCALATE: <why>`,
|
|
29
|
+
and a report missing an `Output to orchestrator` field
|
|
30
|
+
(`hooks/tezgah_agents.py::WORKER_OUTPUT`) counts the same: the router sends one
|
|
31
|
+
narrower re-brief of the same role naming that
|
|
32
|
+
field or judgement, and only a second miss restarts the task on `tezgah-frontier`
|
|
30
33
|
with the original brief rather than handing the failed trajectory up: continuing a
|
|
31
34
|
cheap trajectory on a frontier model was the most expensive option measured
|
|
32
35
|
(arXiv 2608.24358, Table 1: 1.61 cost units against 0.72 for frontier-only).
|
|
@@ -156,16 +159,41 @@ an entry the user owns, and `mode` when omp holds none and the session model run
|
|
|
156
159
|
otherwise. `via` keeps the word `jev` even when the chat fallback answered,
|
|
157
160
|
because the report groups history by it. `judge` is where the two differ.
|
|
158
161
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
162
|
+
On omp and opencode a routed worker runs in its own session and its own ledger. That
|
|
163
|
+
ledger opens with a `spawned` row that names the parent session. On opencode the
|
|
164
|
+
child's own title carries its type. On omp the parent's task call records which type
|
|
165
|
+
each named child runs. On Claude Code and Codex the worker runs under its parent's
|
|
166
|
+
session id, and its rows carry its own `agent` id. There the SubagentStart hook writes
|
|
167
|
+
a `subagent_start` row into the parent's ledger with that id and the child's type.
|
|
168
|
+
Both hosts' payloads carry `agent_id` and `agent_type`. Claude's SessionStart hook
|
|
169
|
+
exports `TEZGAH_SESSION` through `CLAUDE_ENV_FILE`. A Codex shell carries
|
|
170
|
+
`CODEX_THREAD_ID`, the root thread's id. So the `route` row lands in the same ledger.
|
|
171
|
+
A Cursor subagent runs under its own conversation id. Each Cursor agent shell carries
|
|
172
|
+
its conversation's id as `CURSOR_CONVERSATION_ID`, so the route row lands in the
|
|
173
|
+
parent's ledger. Cursor's
|
|
174
|
+
subagentStart hook opens the child's ledger under `subagent_id` with a `spawned` row
|
|
175
|
+
that names `parent_conversation_id`. Cursor's docs do not say that the child's own
|
|
176
|
+
calls carry `subagent_id`, so that link counts only once a row of the child lands.
|
|
177
|
+
A host started from another host's agent shell inherits that host's id. When two of
|
|
178
|
+
these ids differ, no variable tells which host runs the call, so `tezgah-route`
|
|
179
|
+
writes no row.
|
|
180
|
+
|
|
181
|
+
`tezgah-route --report` joins by type when any child has a type. A route owns the
|
|
182
|
+
first fan-out of its type after it and before the next route of that type. A fan-out
|
|
183
|
+
is the children of one task call, each started within 2 seconds of the one before
|
|
184
|
+
(`FAN_OUT_GAP`). One failed first check in it fails the route, else a pass passes it
|
|
185
|
+
(`OUTCOME_ORDER`). Later spawns of that type in the window had no route, so the
|
|
186
|
+
report leaves them out. A route gets nothing when the route of its type before it
|
|
187
|
+
had no worker yet: both routes' workers may sit in one fan-out. It also gets nothing
|
|
188
|
+
when an untyped child started inside its span. It gets nothing when its fan-out
|
|
189
|
+
started more than 30 minutes after it (`ROUTE_GAP`). With no typed child, order is the
|
|
190
|
+
fallback. It joins only when one child sits between each route and the next, with
|
|
191
|
+
no two spawns in one second. The report then reads each child's first check: `pass`,
|
|
192
|
+
`fail`, `ran` (no outcome seen) or `none`. A route it cannot prove counts as
|
|
193
|
+
`unjoined`: a tie, an unrouted spawn, or a host with no child row. Two cases write
|
|
194
|
+
none. `cursor-agent` 2026.10.01 fires no subagentStart, and its child's calls name no
|
|
195
|
+
parent. The dsh bridge runs Claude's hook, and no dsh SubagentStart payload on
|
|
196
|
+
record shows `agent_id`. The parent's own next check never counts for the worker.
|
|
169
197
|
One line per `via` and tier gives the first-attempt pass rate. The `jev-below-static`
|
|
170
198
|
line counts Jev routes under the static tier and how many of them failed. That is the
|
|
171
199
|
measurement that decides whether Jev routing becomes the default or the static table
|
package/docs/operations.md
CHANGED
|
@@ -616,6 +616,36 @@ it costs nothing there. On held-out Turkish phrasings of the docs index,
|
|
|
616
616
|
meaning alone found the page in the top three for 0.91 with `embed-mrl` and 0.78
|
|
617
617
|
with `embed-m2v`. The fusion found it for 0.75 and 0.74, BM25 for 0.68.
|
|
618
618
|
|
|
619
|
+
## Plan records: the lock, `landed`, the close commit
|
|
620
|
+
|
|
621
|
+
Every `tezgah-task` writer of a plan record holds an exclusive lock on the
|
|
622
|
+
`<main checkout>/.tezgah/plans` directory across its read, change and write
|
|
623
|
+
(`hooks/tezgah_task.py::plans_lock`). The lock leaves no file behind for the
|
|
624
|
+
private repository to commit. `tick` finds its item's line under the lock
|
|
625
|
+
(`hooks/tezgah_task.py::set_box`). A writer writes a temp file beside the record
|
|
626
|
+
and renames it over the record, so a reader never sees a torn file
|
|
627
|
+
(`hooks/tezgah_task.py::_write`). Windows has no `fcntl`, so writers run unlocked
|
|
628
|
+
there, and the rename alone keeps reads whole. `close NNN done` records
|
|
629
|
+
`landed:`, the main checkout's HEAD, once the reviewed commit has landed there
|
|
630
|
+
(`bin/tezgah-task::landed_head`). Before the merge it prints a warning and
|
|
631
|
+
records nothing. A squash merge and a branch deletion can orphan
|
|
632
|
+
`reviewed_sha:`, but not that commit. When `.tezgah` is a git repository of its
|
|
633
|
+
own, `close` commits the move as `plan: close NNN <slug>`. The commit holds the
|
|
634
|
+
record's old and new paths and nothing else the owner staged
|
|
635
|
+
(`bin/tezgah-task::commit_close`). Every worktree commits into that one
|
|
636
|
+
repository, so another git can hold its `index.lock`. Each private-repository
|
|
637
|
+
git call waits about 1.5 s for the lock (`hooks/tezgah_paths.py::ws_git`). A
|
|
638
|
+
lock still held after that fails the close: exit 1, with the command that
|
|
639
|
+
commits the moved record.
|
|
640
|
+
|
|
641
|
+
`close` then removes each linked worktree whose branch landed and that holds
|
|
642
|
+
nothing else (`bin/tezgah-task::prune_worktrees`). It deletes that branch with
|
|
643
|
+
`git branch -D` (`bin/tezgah-task::drop_branch`). `git branch -d` refuses a
|
|
644
|
+
squash- or rebase-merged branch, and `_landed` has already shown that its work
|
|
645
|
+
is in a landing base (`bin/tezgah-task::_landed`). A branch that has not landed
|
|
646
|
+
stays with its worktree, and the `worktree kept:` line names it as not merged.
|
|
647
|
+
A delete git still refuses prints `branch kept:` with git's reason.
|
|
648
|
+
|
|
619
649
|
## Slice journal: `tezgah-task slice`
|
|
620
650
|
|
|
621
651
|
A delegated slice can die with its parent. A crash, an omp `/clear`, a quota or
|
|
@@ -683,7 +713,7 @@ sessions' records. `tezgah-task slice reconcile` lists them all.
|
|
|
683
713
|
|---|---|
|
|
684
714
|
| `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
715
|
| `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 |
|
|
716
|
+
| `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, a squash-merged one too (`git branch -D`), 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
717
|
| `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
718
|
| `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
719
|
| `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`) |
|
package/docs/research.md
CHANGED
|
@@ -451,8 +451,9 @@ writes, for a line whose every experiment's order still checks. The ack goes to
|
|
|
451
451
|
|
|
452
452
|
### Moving a line between checkouts
|
|
453
453
|
|
|
454
|
-
|
|
455
|
-
|
|
454
|
+
A repository keeps one `.tezgah`, in its main checkout, and every worktree writes
|
|
455
|
+
there. `import` moves a line out of a worktree `.tezgah` written before plan 097,
|
|
456
|
+
or out of another clone.
|
|
456
457
|
A **copy** of a line, committed in another checkout, lands protocol and results
|
|
457
458
|
in one commit. The copy destroys the proof: `check` refuses it as "one commit
|
|
458
459
|
added both". `tezgah-research import <checkout> [<slug>]` moves the line with
|
package/docs/testing.md
CHANGED
|
@@ -74,17 +74,16 @@ No matrix cancels its other legs when one fails (`fail-fast: false`).
|
|
|
74
74
|
- `test` (`ci.yml:100`) on Python 3.10 and 3.14, the floor and the ceiling.
|
|
75
75
|
It byte-compiles, lints with ruff from `requirements-dev.txt` (pip cached),
|
|
76
76
|
audits the docs citations and clarity and runs `TEZGAH_E2E_STRICT=1 python3
|
|
77
|
-
tests/e2e_plan_flow.py`.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
- `test-sharded` (`ci.yml:145`) on Python 3.10 and 3.14 with node 22 (the
|
|
77
|
+
tests/e2e_plan_flow.py`. A release passes the `release` input and runs one
|
|
78
|
+
3.10 leg.
|
|
79
|
+
- `test-sharded` (`ci.yml:142`) on Python 3.10 and 3.14 with node 22 (the
|
|
81
80
|
first line that loads the omp `.ts` hook without a flag). On `main` and a
|
|
82
81
|
release it runs `python3 tests/impacted.py --all`. On a pull request it runs
|
|
83
82
|
`--ref <base>`: the modules the change maps to, and the whole suite when the
|
|
84
|
-
map does not know a path. It
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
- `apps-e2e` (`ci.yml:
|
|
83
|
+
map does not know a path. It replaced the serial `unittest discover` step on
|
|
84
|
+
2026-10-13: both caught the same failures, and the serial step took about 30
|
|
85
|
+
minutes.
|
|
86
|
+
- `apps-e2e` (`ci.yml:175`) on Python 3.12 and node 20: one step,
|
|
88
87
|
`TEZGAH_E2E_STRICT=1 python3 tests/e2e_analyze_wiring.py`. It completes the
|
|
89
88
|
app-MCP handshake only - no browser, no device - so it stays deterministic;
|
|
90
89
|
strict mode makes a server that cannot start a failure rather than a skip, so
|
|
@@ -99,9 +98,10 @@ No matrix cancels its other legs when one fails (`fail-fast: false`).
|
|
|
99
98
|
opencode live probe and `--uninstall` from the installed tree. Any residue
|
|
100
99
|
under the prefix fails it.
|
|
101
100
|
|
|
102
|
-
`.github/workflows/neuter.yml` runs two jobs weekly
|
|
103
|
-
`
|
|
104
|
-
|
|
101
|
+
`.github/workflows/neuter.yml` runs two jobs weekly, on demand, and on a push to
|
|
102
|
+
`main` that changes a guard, the matrix or a test module it runs. The matrix is
|
|
103
|
+
CI's, not the local loop's. The first job, `neuter`, asks the question the suite
|
|
104
|
+
cannot ask of itself - would a test fail if a guard went?
|
|
105
105
|
`tests/neuter_matrix.py` clones HEAD once per anti-shortcut guard, reverts that
|
|
106
106
|
guard, and runs the gate's test modules; a mutant that still passes is a guard no
|
|
107
107
|
test notices, and an unmutated control that fails voids the run. The script
|
|
@@ -114,6 +114,13 @@ rows cover the guards around those classes: the pass predicate, the empty-run
|
|
|
114
114
|
read and the "doğrulanmadı" clear. The lost call, the bookkeeping and idle-turn
|
|
115
115
|
fallbacks and the refusal row have rows too. The shortcut half is still
|
|
116
116
|
hand-written (`MUTANTS`), so a new guard there needs a row.
|
|
117
|
+
Each mutant first runs alone the test that killed it last time, read from
|
|
118
|
+
`tests/neuter_killers.json`. When that test passes or is gone, the mutant runs
|
|
119
|
+
every module in its own HOME and TMPDIR. The module that reads the mutated file
|
|
120
|
+
goes first. The first failing test becomes its new entry. The
|
|
121
|
+
control runs every module, then every cached killer alone, so a killer that
|
|
122
|
+
fails without a mutant voids the run. A session that adds a deny rule checks its
|
|
123
|
+
one mutant with `python3 tests/neuter_matrix.py --only <name>`.
|
|
117
124
|
|
|
118
125
|
The second, `fuzz-shell`, runs the bash-oracle fuzzer (`tests/fuzz_shell.py`)
|
|
119
126
|
under the ubuntu runner's bash 5.x. Locally, macOS ships bash 3.2. A first step
|
|
@@ -14,7 +14,7 @@ sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
|
|
|
14
14
|
from tezgah_guard import attest_session, import_failed, safe # noqa: E402
|
|
15
15
|
try:
|
|
16
16
|
from tezgah_context import context_for, record # noqa: E402
|
|
17
|
-
from tezgah_paths import HOST_DIRS # noqa: E402
|
|
17
|
+
from tezgah_paths import HOST_DIRS, root_for # noqa: E402
|
|
18
18
|
except Exception as exc:
|
|
19
19
|
import_failed(exc)
|
|
20
20
|
|
|
@@ -62,6 +62,20 @@ def core_is_in_a_file():
|
|
|
62
62
|
return False
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
def export_session(env_file, session_id):
|
|
66
|
+
"""Append `export TEZGAH_SESSION=<id>` to Claude's CLAUDE_ENV_FILE, appended
|
|
67
|
+
because other hooks write the same file."""
|
|
68
|
+
import shlex
|
|
69
|
+
with open(env_file, "a", encoding="utf-8") as fh:
|
|
70
|
+
fh.write("export TEZGAH_SESSION=%s\n" % shlex.quote(session_id))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def note_child(session_id, agent_id, agent_type):
|
|
74
|
+
"""The child's lineage row in its parent's ledger (`tezgah-route --report`)."""
|
|
75
|
+
from tezgah_integrity import note
|
|
76
|
+
note(session_id, "subagent_start", agent_type, agent=agent_id)
|
|
77
|
+
|
|
78
|
+
|
|
65
79
|
def main():
|
|
66
80
|
try:
|
|
67
81
|
payload = json.load(sys.stdin)
|
|
@@ -74,6 +88,14 @@ def main():
|
|
|
74
88
|
# tezgah wires, and dsh (which runs it too) has no transcript to derive
|
|
75
89
|
# the kind from, so the store is the only channel its line has.
|
|
76
90
|
safe(payload.get("session_id"), record, payload.get("session_id"), "orch")
|
|
91
|
+
# The child's lineage row: a Claude subagent runs under its parent's
|
|
92
|
+
# session id, its calls carrying `agent_id`, so this row in the parent's
|
|
93
|
+
# ledger is what types it for `tezgah-route --report` (hooks reference,
|
|
94
|
+
# SubagentStart input: agent_id, agent_type).
|
|
95
|
+
aid, kind = payload.get("agent_id"), payload.get("agent_type")
|
|
96
|
+
if isinstance(aid, str) and aid and isinstance(kind, str) and root_for(cwd):
|
|
97
|
+
safe(payload.get("session_id"), note_child, payload.get("session_id"),
|
|
98
|
+
aid, kind)
|
|
77
99
|
if event == "SessionStart":
|
|
78
100
|
# Claude and dsh run this one script; Claude's manifest declares
|
|
79
101
|
# TEZGAH_CORE_IN_FILE on SessionStart and dsh's does not, so the flag
|
|
@@ -82,6 +104,12 @@ def main():
|
|
|
82
104
|
sid = payload.get("session_id")
|
|
83
105
|
host = "claude" if os.environ.get("TEZGAH_CORE_IN_FILE") == "1" else "dsh"
|
|
84
106
|
safe(sid, attest_session, host, sid, cwd)
|
|
107
|
+
# The session id the route, judge and status CLIs write under: Claude
|
|
108
|
+
# sources this file before every Bash call (CLAUDE_ENV_FILE, SessionStart
|
|
109
|
+
# only), as omp exports TEZGAH_SESSION to its shells
|
|
110
|
+
env_file = os.environ.get("CLAUDE_ENV_FILE")
|
|
111
|
+
if env_file and isinstance(sid, str) and sid:
|
|
112
|
+
safe(sid, export_session, env_file, sid)
|
|
85
113
|
text = safe(payload.get("session_id"), context_for,
|
|
86
114
|
EVENTS.get(event, "session_start"), cwd, payload,
|
|
87
115
|
# the brief is a subagent's own text, never the file's; every
|
package/hooks/tezgah_agents.py
CHANGED
|
@@ -252,17 +252,29 @@ def _worker_body(tier):
|
|
|
252
252
|
scope = ("If the work needs a judgement above this tier - a design choice,\n"
|
|
253
253
|
"a stored-data or ordering invariant, security-sensitive code, or\n"
|
|
254
254
|
"a failure whose cause you cannot find - stop editing and answer\n"
|
|
255
|
-
"`ESCALATE: <why
|
|
255
|
+
"`ESCALATE: <why>`.")
|
|
256
256
|
return ("You are tezgah-%s, a worker on the %s model tier. Do exactly the\n"
|
|
257
257
|
"brief: stay inside the files it names, run the checks it names, and\n"
|
|
258
258
|
"report what changed and what each check printed, verbatim. Report\n"
|
|
259
259
|
"progress, but never redefine the work. Never widen or narrow the brief's\n"
|
|
260
260
|
"scope, budget or question. If the brief cannot be met as written, say\n"
|
|
261
261
|
"so and stop rather than answering a different question.\n%s\n"
|
|
262
|
-
"Never spawn subagents
|
|
262
|
+
"Never spawn subagents.\n%s" % (tier, tier, scope, WORKER_OUTPUT))
|
|
263
263
|
return body
|
|
264
264
|
|
|
265
265
|
|
|
266
|
+
# The report a consumer reads: the orchestrator re-briefs a report missing one
|
|
267
|
+
# of these fields (_orch_body), so only a role with a reader gets them. One
|
|
268
|
+
# text on every host, since a worker body ignores its host.
|
|
269
|
+
WORKER_OUTPUT = ("End the report, after any other shape asked for, with `Output to\n"
|
|
270
|
+
"orchestrator`, every field, `none` when empty:\n"
|
|
271
|
+
"- changed: the files you changed\n"
|
|
272
|
+
"- commit: the sha\n"
|
|
273
|
+
"- checks: each command, its exit code and the last lines of its\n"
|
|
274
|
+
" output file, verbatim\n"
|
|
275
|
+
"- escalate: the `ESCALATE:` reason (always `none` on tezgah-frontier)")
|
|
276
|
+
|
|
277
|
+
|
|
266
278
|
# The specialists beside the tier workers: name -> (what it does, the skills it
|
|
267
279
|
# loads first, its tools, the role text). The tier is not here: it is
|
|
268
280
|
# tezgah_models.AGENT_SLOT, the row every host's model line is rendered from, so
|
|
@@ -282,9 +294,11 @@ SPECIALISTS = {
|
|
|
282
294
|
"import the changed modules. Keep each check last on its line with its\n"
|
|
283
295
|
"output in a file, then read the file in a separate call: never pipe a\n"
|
|
284
296
|
"check into tail or grep, never `|| true`, never `--no-verify`, never a\n"
|
|
285
|
-
"new skip. Set the shell timeout above 300 s for a long run.
|
|
286
|
-
"
|
|
287
|
-
"
|
|
297
|
+
"new skip. Set the shell timeout above 300 s for a long run. Never edit\n"
|
|
298
|
+
"source or test files; a fix belongs to the caller.\n"
|
|
299
|
+
"End the report, after any other shape asked for, with `Output to\n"
|
|
300
|
+
"orchestrator`, per check: its command, the pass/fail/skip counts and\n"
|
|
301
|
+
"the first failure verbatim (`none` when it passed)."),
|
|
288
302
|
"tezgah-docs": (
|
|
289
303
|
"Writes and repairs docs pages and their `path:line` / `path::symbol` "
|
|
290
304
|
"citations after a code change, under the repo's own docs checks.",
|
|
@@ -449,8 +463,11 @@ def _orch_body(names):
|
|
|
449
463
|
"codegraph tools; a second opinion (`consult`) stays with the main\n"
|
|
450
464
|
"thread. Other work goes to the tier worker\n"
|
|
451
465
|
"`tezgah-route \"<brief>\"` names (tezgah-cheap, tezgah-standard,\n"
|
|
452
|
-
"tezgah-frontier)
|
|
453
|
-
"
|
|
466
|
+
"tezgah-frontier). A report missing an `Output to orchestrator` field,\n"
|
|
467
|
+
"or an ESCALATE, gets one narrower re-brief of the same role naming the\n"
|
|
468
|
+
"missing field or the escalated judgement; only a second miss is\n"
|
|
469
|
+
"restarted on tezgah-frontier with the original brief, never continued.\n"
|
|
470
|
+
"Never delegate a task a specialist is not\n"
|
|
454
471
|
"listed for, and never let a subagent spawn its own subagents. Verify each\n"
|
|
455
472
|
"returned claim against the code before acting." % (listed, say()["blocker_tokens"]))
|
|
456
473
|
|