@r1z4x/tezgah 1.11.0 → 1.14.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 (109) hide show
  1. package/CHANGELOG.md +507 -1
  2. package/MANIFEST +40 -0
  3. package/NOTICE +152 -0
  4. package/README.de.md +41 -20
  5. package/README.es.md +39 -19
  6. package/README.fr.md +41 -20
  7. package/README.md +18 -10
  8. package/README.tr.md +39 -21
  9. package/bin/codegen +9 -2
  10. package/bin/consult +6 -0
  11. package/bin/tezgah-adhd +3 -0
  12. package/bin/tezgah-agents +3 -0
  13. package/bin/tezgah-capture +3 -0
  14. package/bin/tezgah-design +329 -54
  15. package/bin/tezgah-docs +158 -42
  16. package/bin/tezgah-dsh +1 -1
  17. package/bin/tezgah-gate +7 -2
  18. package/bin/tezgah-import-ai-research +16 -4
  19. package/bin/tezgah-index +3 -0
  20. package/bin/tezgah-lessons +13 -4
  21. package/bin/tezgah-mcp +4 -0
  22. package/bin/tezgah-migrate +3 -0
  23. package/bin/tezgah-ocr-review +2 -1
  24. package/bin/tezgah-optimize +4 -1
  25. package/bin/tezgah-pony +3 -0
  26. package/bin/tezgah-research +51 -10
  27. package/bin/tezgah-rollback +3 -0
  28. package/bin/tezgah-route +1 -1
  29. package/bin/tezgah-setup +1741 -95
  30. package/bin/tezgah-status +11 -1
  31. package/bin/tezgah-task +58 -17
  32. package/bin/tezgah.js +10 -3
  33. package/commands/adhd.md +1 -1
  34. package/docs/README.md +19 -8
  35. package/docs/architecture.md +2 -2
  36. package/docs/citations-baseline.json +11 -17
  37. package/docs/clarity-baseline.json +12 -6
  38. package/docs/contract.md +83 -34
  39. package/docs/environment.md +59 -24
  40. package/docs/evidence.md +128 -25
  41. package/docs/gate.md +77 -23
  42. package/docs/glossary.md +1 -1
  43. package/docs/hosts.md +343 -48
  44. package/docs/layers.md +2 -2
  45. package/docs/models.md +6 -0
  46. package/docs/operations.md +83 -29
  47. package/docs/research.md +37 -11
  48. package/docs/skills.md +31 -8
  49. package/docs/status-line.md +39 -23
  50. package/docs/testing.md +50 -17
  51. package/docs/vision.md +6 -2
  52. package/hooks/hooks.json +1 -1
  53. package/hooks/lang/en.json +16 -2
  54. package/hooks/lang/tr.json +14 -2
  55. package/hooks/projects-posttooluse.py +11 -3
  56. package/hooks/projects-stop.py +3 -2
  57. package/hooks/tezgah_agents.py +27 -3
  58. package/hooks/tezgah_attest.py +79 -10
  59. package/hooks/tezgah_context.py +61 -31
  60. package/hooks/tezgah_gate.py +480 -126
  61. package/hooks/tezgah_integrity.py +577 -133
  62. package/hooks/tezgah_lessons.py +5 -2
  63. package/hooks/tezgah_paths.py +60 -8
  64. package/hooks/tezgah_policy.py +48 -18
  65. package/hooks/tezgah_replay.py +8 -2
  66. package/hooks/tezgah_research.py +167 -24
  67. package/hooks/tezgah_skill_pick.py +1 -1
  68. package/hooks/tezgah_snapshot.py +6 -3
  69. package/hooks/tezgah_stack.py +236 -1
  70. package/hooks/tezgah_stopspec.py +2 -1
  71. package/hooks/tezgah_task.py +2 -1
  72. package/hosts/amp/hook.py +217 -0
  73. package/hosts/amp/tezgah-plugin.ts.in +396 -0
  74. package/hosts/antigravity/hook.py +255 -0
  75. package/hosts/codex/hook.py +3 -2
  76. package/hosts/commandcode/hook.py +239 -0
  77. package/hosts/copilot/hook.py +284 -0
  78. package/hosts/cursor/hook.py +4 -2
  79. package/hosts/droid/hook.py +259 -0
  80. package/hosts/dsh/hooks.json +1 -1
  81. package/hosts/gemini/hook.py +213 -0
  82. package/hosts/goose/hook.py +145 -0
  83. package/hosts/omp/hook.py +5 -2
  84. package/hosts/omp/tezgah-hook.ts.in +157 -42
  85. package/hosts/opencode/plugins/tezgah.js +209 -63
  86. package/hosts/qwen/hook.py +206 -0
  87. package/output-styles/tezgah.md +11 -6
  88. package/package.json +6 -3
  89. package/packaging/build.sh +4 -4
  90. package/packaging/install.sh +11 -0
  91. package/packaging/upgrade.sh +3 -0
  92. package/skills/ai-research/LICENSE +21 -0
  93. package/skills/design-contract/SKILL.md +27 -6
  94. package/skills/design-library/LICENSE +21 -0
  95. package/skills/harness/SKILL.md +2 -0
  96. package/skills/i-have-adhd/SKILL.md +10 -1
  97. package/skills/plan-add/SKILL.md +5 -1
  98. package/skills/plan-add/render_table.py +7 -0
  99. package/skills/pm-frameworks/LICENSE +21 -0
  100. package/skills/research/SKILL.md +22 -9
  101. package/skills/rl-env/LICENSE +201 -0
  102. package/skills/tezgah-contract/SKILL.md +21 -8
  103. package/statusline.py +20 -9
  104. package/third_party/open-code-review/LICENSE +201 -0
  105. package/third_party/open-code-review/README.md +15 -0
  106. package/third_party/open-code-review/prompts/main_task_system.md +26 -0
  107. package/third_party/open-code-review/prompts/review_filter_task_system.md +13 -0
  108. package/workflows/graph-map.js +14 -5
  109. package/workflows/graph-review.js +14 -2
package/bin/tezgah-status CHANGED
@@ -117,7 +117,8 @@ import sys
117
117
  HERE = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
118
118
  sys.path.insert(0, os.path.join(HERE, "hooks"))
119
119
  from tezgah_context import ( # noqa: E402
120
- LEGEND, color_default, health_segments, render_line, skill_fitness)
120
+ LEGEND, TOOL_USE_MEASURES, color_default, health_segments, render_line,
121
+ skill_fitness)
121
122
 
122
123
  # How many rows of the tool histogram the plain report prints. The count is on
123
124
  # the line and `--json` carries every row, so a reader can always see what was
@@ -432,6 +433,15 @@ def main():
432
433
  if "--help" in flags:
433
434
  sys.stdout.write(__doc__.strip() + "\n")
434
435
  return 0
436
+ # the two skill-read marks are measures too; a typo here blanked every mark
437
+ measures = TOOL_USE_MEASURES | {"pony", "adhd"}
438
+ named = {n for f in flags if f.startswith("--observable=")
439
+ for n in f.split("=", 1)[1].split(",") if n}
440
+ if named - measures:
441
+ sys.stderr.write("tezgah-status: unknown measure(s) in --observable: %s "
442
+ "(known: %s)\n" % (", ".join(sorted(named - measures)),
443
+ ", ".join(sorted(measures))))
444
+ return 2
435
445
  if "--legend" in flags:
436
446
  sys.stdout.write(LEGEND)
437
447
  return 0
package/bin/tezgah-task CHANGED
@@ -6,7 +6,7 @@
6
6
  tezgah-task allow GLOB ... | --any-path --why REASON replace the allowlist, recording why
7
7
  tezgah-task stop deactivate (clear phase)
8
8
  tezgah-task status [--json] the active task, then every open plan: progress, what it waits on, what can run at once
9
- tezgah-task review <NNN|path> <reviewer> approve|changes record a fresh review's verdict
9
+ tezgah-task review <NNN|path> <reviewer> approve|changes [--no-slice WHY] record a fresh review's verdict; approve needs the reviewer's slice record, or WHY it has none
10
10
  tezgah-task close <NNN|path> done|discarded [--resolved TEXT] move a plan to .tezgah/plans/done, deactivate it, remove merged worktrees
11
11
  tezgah-task tick <NNN|path> <n> [--undo] tick (or untick) Acceptance item n, counted from 1
12
12
  tezgah-task note <NNN|path> state|next "<text>" replace the body of `## State` (the newest one) or `## Next`
@@ -1082,7 +1082,7 @@ def budget_spent(path, ident):
1082
1082
  % (path, REVIEW_ROUNDS, ident, say()["decision"], ident))
1083
1083
 
1084
1084
 
1085
- def unfinished_review(root, reviewer, plan="", since=0):
1085
+ def unfinished_review(root, reviewer, plan="", since=0, no_slice="", ran_since=0):
1086
1086
  """The refusal of an `approve` while a slice journal record whose id or agent
1087
1087
  is `reviewer` is open, stopped or stale: a review slice that died on a
1088
1088
  provider limit had its approve recorded anyway, and a NameError it would have
@@ -1097,7 +1097,11 @@ def unfinished_review(root, reviewer, plan="", since=0):
1097
1097
  `tt.open_slice`) counts too. Under TEZGAH_SESSION only this session's
1098
1098
  records and unowned ones count, by id, agent or name alike: another
1099
1099
  session's review is not this approve's. The refusal names the exact id,
1100
- which `slice resume` and `slice done` take from any session."""
1100
+ which `slice resume` and `slice done` take from any session. No finished
1101
+ record changed at or after `ran_since` refuses too: a requested reviewer with
1102
+ no status did not run, and a plan-readiness review before the checkpoint, an
1103
+ earlier round's review or a `resumed` record with no re-run is not this
1104
+ review - unless `no_slice` says why the reviewer ran outside the journal."""
1101
1105
  from tezgah_paths import off
1102
1106
  if off("slices-off"):
1103
1107
  return []
@@ -1113,21 +1117,42 @@ def unfinished_review(root, reviewer, plan="", since=0):
1113
1117
  return rec["plan"] == plan
1114
1118
  return (_stamp(rec.get("updated")) or 0) >= since
1115
1119
 
1116
- return ["review slice %s is %s%s: let it finish, or resume it with `tezgah-task "
1117
- "slice resume %s` and wait for that run; if the review was finished "
1118
- "another way, record it with `tezgah-task slice done %s` - an approve "
1119
- "from a review that did not finish reads nothing"
1120
- % (rec["id"], rec.get("status"),
1121
- (" (%s)" % rec["failure"]) if rec.get("failure") else "",
1122
- rec["id"], rec["id"])
1123
- for rec in tt.slices(root)
1124
- if rec.get("status") in ("open", "stopped", "stale")
1125
- and named(rec) and ours(rec)]
1120
+ found = [rec for rec in tt.slices(root) if named(rec) and ours(rec)]
1121
+ refusals = ["review slice %s is %s%s: let it finish, or resume it with `tezgah-task "
1122
+ "slice resume %s` and wait for that run; if the review was finished "
1123
+ "another way, record it with `tezgah-task slice done %s` - an approve "
1124
+ "from a review that did not finish reads nothing"
1125
+ % (rec["id"], rec.get("status"),
1126
+ (" (%s)" % rec["failure"]) if rec.get("failure") else "",
1127
+ rec["id"], rec["id"])
1128
+ for rec in found if rec.get("status") in ("open", "stopped", "stale")]
1129
+ ran = [rec for rec in found if rec.get("status") == "done"
1130
+ and (_stamp(rec.get("updated")) or 0) >= ran_since]
1131
+ if not ran and not refusals and not no_slice:
1132
+ # a requested reviewer with no finished record since the work is a
1133
+ # review that did not run
1134
+ refusals.append(
1135
+ "no finished review slice names %s for this plan since its work began: "
1136
+ "a reviewer with no record did not run, and a readiness review, an "
1137
+ "earlier round or a resumed record is not this review. Spawn the fresh "
1138
+ "reviewer and approve once its slice is done; a host that keeps no "
1139
+ "slice journal records why with "
1140
+ "`--no-slice \"<why: the reviewer ran outside the slice journal>\"`"
1141
+ % reviewer)
1142
+ return refusals
1126
1143
 
1127
1144
 
1128
1145
  def cmd_review(args):
1146
+ no_slice = ""
1147
+ if "--no-slice" in args:
1148
+ i = args.index("--no-slice")
1149
+ no_slice = " ".join((args[i + 1:i + 2] or [""])[0].split())
1150
+ args = args[:i] + args[i + 2:]
1151
+ if not no_slice:
1152
+ return misuse("--no-slice takes why the reviewer ran outside the slice journal")
1129
1153
  if len(args) != 3 or args[2] not in VERDICTS:
1130
- return misuse("review takes <NNN|path> <reviewer> %s" % "|".join(VERDICTS))
1154
+ return misuse("review takes <NNN|path> <reviewer> %s [--no-slice WHY]"
1155
+ % "|".join(VERDICTS))
1131
1156
  path, problem = open_plan(args[0])
1132
1157
  if problem:
1133
1158
  return problems([problem])
@@ -1144,9 +1169,21 @@ def cmd_review(args):
1144
1169
  mark = (fields.get("checkpoint") or "").split()
1145
1170
  since = (git_out(root, "log", "-1", "--format=%ct", mark[-1])
1146
1171
  if mark and mark[-1] != tt.CHECKPOINT_PENDING else None)
1172
+ since = int(since) if (since or "").isdigit() else 0
1173
+ # this round's review finished after the first commit of the work it
1174
+ # reads: past the checkpoint in round one (a plan-readiness review ran
1175
+ # before it), past the commit round one read in round two (round one's
1176
+ # reviewer finished before it); with no such commit, after the base itself
1147
1177
  ident = fields.get("id") or os.path.basename(path).split("-")[0]
1148
- bad = unfinished_review(root, args[1], ident,
1149
- int(since) if (since or "").isdigit() else 0)
1178
+ base = fields.get("reviewed_sha") or (
1179
+ mark[-1] if mark and mark[-1] != tt.CHECKPOINT_PENDING else "")
1180
+ first = ""
1181
+ if base:
1182
+ after = (git_out(tt.plan_holders(root).get(ident) or root, "log",
1183
+ "--reverse", "--format=%ct", base + "..HEAD") or "").split()
1184
+ first = after[0] if after else git_out(root, "log", "-1", "--format=%ct", base)
1185
+ ran_since = max(since, int(first) if (first or "").isdigit() else 0)
1186
+ bad = unfinished_review(root, args[1], ident, since, no_slice, ran_since)
1150
1187
  if bad:
1151
1188
  return problems(bad)
1152
1189
  # a plan held by its own worktree is built on that branch: its HEAD is the
@@ -1154,7 +1191,8 @@ def cmd_review(args):
1154
1191
  ident = fields.get("id") or os.path.basename(path).split("-")[0]
1155
1192
  where = tt.plan_holders(root).get(ident) or root
1156
1193
  sha = (git_out(where, "rev-parse", "HEAD") or "")[:12]
1157
- record = {"review": "%s %s %s" % (args[1], args[2], today()),
1194
+ record = {"review": "%s %s %s%s" % (args[1], args[2], today(),
1195
+ " no-slice: " + no_slice if no_slice else ""),
1158
1196
  "review_round": str(int(prior) + 1 if prior.isdigit() else 1),
1159
1197
  "updated": today()}
1160
1198
  if sha:
@@ -2010,6 +2048,9 @@ def main(argv):
2010
2048
  print(__doc__, file=sys.stderr)
2011
2049
  return 2
2012
2050
  cmd, rest = argv[1], argv[2:]
2051
+ if cmd in ("-h", "--help"):
2052
+ print(__doc__.strip())
2053
+ return 0
2013
2054
  if writes_a_record(cmd, rest):
2014
2055
  mark = owner_mark(repo_root(os.getcwd()))
2015
2056
  if mark:
package/bin/tezgah.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // bin/tezgah.js - npm's bin entry: finds a Python interpreter and execs
3
3
  // bin/tezgah-setup with every argument passed through. npm resolves this
4
4
  // symlink to the global prefix, so process.argv[1] is the shim, not the target.
5
- const { spawn } = require("child_process");
5
+ const { spawn, spawnSync } = require("child_process");
6
6
  const { existsSync } = require("fs");
7
7
  const os = require("os");
8
8
  const path = require("path");
@@ -18,9 +18,12 @@ const candidates = process.env.TEZGAH_PYTHON
18
18
  ? [process.env.TEZGAH_PYTHON]
19
19
  : ["python3", "python3.12", "python3.11", "python3.10", "python"];
20
20
 
21
+ // Probed exactly as it is spawned below - no shell - so a value the probe
22
+ // accepts is one spawn can start (`TEZGAH_PYTHON="python3 -S"` passed a shell
23
+ // probe, then died in spawn with a stack trace).
21
24
  const py = candidates.find(c => {
22
- try { require("child_process").execSync(c + " -c ''", { stdio: "ignore" }); return true; }
23
- catch (_) { return false; }
25
+ const probe = spawnSync(c, ["-c", ""], { stdio: "ignore" });
26
+ return !probe.error && probe.status === 0;
24
27
  });
25
28
 
26
29
  if (!py) {
@@ -29,6 +32,10 @@ if (!py) {
29
32
  }
30
33
 
31
34
  const child = spawn(py, [setup, ...process.argv.slice(2)], { stdio: "inherit" });
35
+ child.on("error", err => {
36
+ process.stderr.write("tezgah: cannot start " + py + ": " + err.message + "\n");
37
+ process.exit(1);
38
+ });
32
39
  // A child killed by a signal reports code null, and process.exit(null) is exit
33
40
  // 0: an installer killed mid-run read as a success (audit L-10, ENV-06). The
34
41
  // shell convention 128 + signal number carries the failure out instead.
package/commands/adhd.md CHANGED
@@ -12,7 +12,7 @@ adhd="$HOME/.config/tezgah/bin/tezgah-adhd"
12
12
 
13
13
  - No argument: run it bare, which prints `on` or `off`.
14
14
  - `off`: disarms the rule everywhere on this machine.
15
- - `on`: arms it again (`off` is the default).
15
+ - `on`: arms it again (`on` is the default).
16
16
  - Anything else: the CLI exits 2 with the usage on stderr - show that to the
17
17
  user and stop.
18
18
 
package/docs/README.md CHANGED
@@ -7,7 +7,8 @@ tool to someone deciding whether to install it; `AGENTS.md` says how to check a
7
7
  change; these pages explain how the thing actually works, so a session does not
8
8
  have to re-derive it by reading 55 Python files. Every page is written for one
9
9
  reader with one question in mind, and every non-obvious claim in it carries a
10
- citation you can open: `path::symbol` into Python code, `path:line` elsewhere.
10
+ citation you can open: `path::symbol` into Python, JavaScript and TypeScript
11
+ code, `path:line` elsewhere.
11
12
 
12
13
  ## Start here
13
14
 
@@ -55,26 +56,36 @@ citation you can open: `path::symbol` into Python code, `path:line` elsewhere.
55
56
  belong on a page: delete it rather than soften it.
56
57
  - Code in a Python file is cited by symbol: `path::name` for a top-level def,
57
58
  class or assignment, `path::Class.method` or `path::outer.inner` for a nested
58
- one (`hooks/tezgah_gate.py::decision`). It carries no line number, so an edit
59
+ one (`hooks/tezgah_gate.py::decision`). A page cites a JavaScript or
60
+ TypeScript file's top-level `const`, `let`, `function` or `class` the same way
61
+ (`hosts/opencode/plugins/tezgah.js::noteBegan`). It carries no line number, so an edit
59
62
  that moves code above or inside the symbol moves no citation. The sentence says
60
63
  which part of the symbol it means. Everything else stays `path:line`: a
61
- JavaScript, TypeScript, JSON, YAML or Markdown target, module-level code outside
62
- every def and class, and a range across several symbols. Never name a symbol
64
+ JSON, YAML, shell or Markdown target, code outside every top-level symbol, and a
65
+ range across several symbols. Never name a symbol
63
66
  that does not contain what the sentence cites.
64
67
  - `bin/tezgah-docs --citations` checks both forms. It reads every page under `docs/`
65
68
  and the comments and docstrings of the Python code. A string literal, such as a
66
69
  fixture or a captured output, is data and is not read. A symbol citation is
67
- resolved against the AST of the file it names. It fails when that file defines
70
+ resolved against the AST of the Python file it names, or the top-level
71
+ declarations of a JavaScript or TypeScript one. It fails when that file defines
68
72
  no such name. A name without a dot may also name the one method of that name
69
73
  in a class. That is how a test pin names its test. A def local to a function
70
74
  never stands in for it. A `path:line` with a symbol named
71
75
  before it, or right after it in parentheses, must point inside that symbol's
72
- body. A bare file name means the one code file of that name. The suite also
76
+ body. A bare file name means the one code file of that name, else the file of
77
+ that name at the repository root. A path under the private `.tezgah/` is never
78
+ judged, because a CI checkout has no copy of it. A comma list reads every range in it. A bare
79
+ range, `(8-9)`, `(14,34-38)` or an unquoted :8-9, is read against the path cited
80
+ before it in the same sentence or table row. One that starts at 0 or runs
81
+ backwards is a value, not lines. The audit flags a cited range past the end of
82
+ its file, and one over blank lines only, with or without a symbol beside it. In a shell script, a name beside the range must be on its
83
+ lines. The suite also
73
84
  checks that a cited file exists and that a cited line is inside it
74
85
  (`tests/test_docs.py`). Whether a line still *shows the thing the sentence names*
75
86
  is a judgement, not a regex. So a `path:line` with no symbol beside it is left
76
- unjudged rather than guessed at. So is a `path:N` the pattern cannot read
77
- (unquoted, or a comma list), but it is counted.
87
+ unjudged rather than guessed at. So is an unquoted `path:N` the pattern cannot
88
+ read, but it is counted.
78
89
  On this tree, 2026-10-06: 996 judged (995 of them symbol citations), 577 not
79
90
  judgeable, 85 of those unreadable.
80
91
  The unjudged count is a ratchet. `docs/citations-baseline.json` holds each file's
@@ -222,8 +222,8 @@ So tezgah never creates a `.tezgah`, `.cursor`, `.opencode` or graph index insid
222
222
  line armed (`hooks/tezgah_guard.py::safe`, `hooks/projects-pretooluse.py::main`).
223
223
  A host that cannot call the core in process gets the same rule in its own
224
224
  language: opencode's plugin runs every awaited core CLI through one `collect`
225
- helper with a 10 s deadline (`SPAWN_DEADLINE_MS`,
226
- `hosts/opencode/plugins/tezgah.js:108-178`). Both fail open, because a core
225
+ helper with a 10 s deadline (`hosts/opencode/plugins/tezgah.js::SPAWN_DEADLINE_MS`,
226
+ `hosts/opencode/plugins/tezgah.js::collect`). Both fail open, because a core
227
227
  that has crashed has refused nothing.
228
228
  - **The root boundary.** Every hook is inert outside a configured root:
229
229
  a linked `git worktree` counts as inside when its main checkout is, read from
@@ -6,43 +6,37 @@
6
6
  "unjudged": 1
7
7
  },
8
8
  "docs/architecture.md": {
9
- "unjudged": 25
9
+ "unjudged": 23
10
10
  },
11
11
  "docs/contract.md": {
12
- "unjudged": 5
12
+ "unjudged": 3
13
13
  },
14
14
  "docs/environment.md": {
15
- "unjudged": 8
15
+ "unjudged": 4
16
16
  },
17
17
  "docs/evidence.md": {
18
- "unjudged": 18
19
- },
20
- "docs/feature-audit.md": {
21
- "unjudged": 2
18
+ "unjudged": 14
22
19
  },
23
20
  "docs/gate.md": {
24
- "unjudged": 29
21
+ "unjudged": 10
25
22
  },
26
23
  "docs/glossary.md": {
27
- "unjudged": 12
24
+ "unjudged": 9
28
25
  },
29
26
  "docs/hosts.md": {
30
- "unjudged": 62
27
+ "unjudged": 15
31
28
  },
32
29
  "docs/layers.md": {
33
- "unjudged": 14
30
+ "unjudged": 7
34
31
  },
35
32
  "docs/operations.md": {
36
- "unjudged": 21
33
+ "unjudged": 4
37
34
  },
38
35
  "docs/skills.md": {
39
- "unjudged": 9
40
- },
41
- "docs/status-line.md": {
42
- "unjudged": 9
36
+ "unjudged": 6
43
37
  },
44
38
  "docs/testing.md": {
45
- "unjudged": 41
39
+ "unjudged": 21
46
40
  },
47
41
  "hooks/tezgah_gate.py": {
48
42
  "unjudged": 1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "CHANGELOG.md": {
3
- "long_sentences": 0,
3
+ "long_sentences": 1,
4
4
  "passive": 2,
5
5
  "replace_words": 0,
6
6
  "semicolons": 0
@@ -25,12 +25,12 @@
25
25
  },
26
26
  "docs/environment.md": {
27
27
  "long_sentences": 3,
28
- "passive": 2,
28
+ "passive": 1,
29
29
  "replace_words": 0,
30
- "semicolons": 3
30
+ "semicolons": 2
31
31
  },
32
32
  "docs/evidence.md": {
33
- "long_sentences": 108,
33
+ "long_sentences": 112,
34
34
  "passive": 55,
35
35
  "replace_words": 1,
36
36
  "semicolons": 39
@@ -54,7 +54,7 @@
54
54
  "semicolons": 14
55
55
  },
56
56
  "docs/hosts.md": {
57
- "long_sentences": 36,
57
+ "long_sentences": 38,
58
58
  "passive": 19,
59
59
  "replace_words": 0,
60
60
  "semicolons": 18
@@ -83,6 +83,12 @@
83
83
  "replace_words": 3,
84
84
  "semicolons": 42
85
85
  },
86
+ "docs/orca.md": {
87
+ "long_sentences": 0,
88
+ "passive": 0,
89
+ "replace_words": 0,
90
+ "semicolons": 0
91
+ },
86
92
  "docs/research.md": {
87
93
  "long_sentences": 105,
88
94
  "passive": 59,
@@ -91,7 +97,7 @@
91
97
  },
92
98
  "docs/skills.md": {
93
99
  "long_sentences": 35,
94
- "passive": 18,
100
+ "passive": 17,
95
101
  "replace_words": 0,
96
102
  "semicolons": 16
97
103
  },
package/docs/contract.md CHANGED
@@ -88,7 +88,7 @@ to an adhd ask still opened on a diagnosis. Measured on this
88
88
  machine: 6-15 ms warm, and 1.36 s for the one cold rebuild over 7 502 sections.
89
89
  The line costs 187-201 bytes on omp, 264-287 on other hosts and 450-483 on
90
90
  Claude, plus 231 for the `i-have-adhd` clause. It is not a judgement, so
91
- `judge-off` leaves it on, and `reminder-off` drops it with the per-turn text.
91
+ `judge-off` leaves it on, and `reminder-off` leaves it on too: that switch drops only the reminder line.
92
92
  The always-on core also
93
93
  sends every session to the same search by hand. Run `tezgah-skill <words>`
94
94
  before working a topic a skill covers, then read only the returned range.
@@ -208,27 +208,31 @@ default, measured on two research lines with a written flip
208
208
  one also gets an OCR review spec: OCR's file selection, its rule groups and a
209
209
  refuter pass. The spec is input to the review, never a verdict
210
210
  ([operations](operations.md#opt-in-ocr-review-spec-ocr),
211
- `bin/tezgah-task::print_ocr_spec`).
211
+ `bin/tezgah-task::print_ocr_spec`). A verdict comes only from a fresh
212
+ reviewer. A host that cannot start one says so and asks the owner. The author
213
+ contests a confirmed finding only with a file:line the reviewer could read. It
214
+ never lowers a severity (`hooks/tezgah_policy.py::EXEC`).
212
215
 
213
216
  ## Composition order of the always-on core
214
217
 
215
218
  Paragraphs are concatenated in the order they appear in `CORE` and identified by
216
219
  the bold label each starts with (`CORE_RULES`, `hooks/tezgah_context.py::CORE_RULES`).
217
- That order, with the line each label sits on in `hooks/tezgah_policy.py`:
218
- `**{REPLY_LANG}, BLUF.**` :802, `**Ponytail (minimal code).**` :811, `**Output shape:
219
- ADHD-friendly.**` :828, `**Deliver the whole ask; never the shortcut.**` :852,
220
- `**Integrity: evidence, or a named blocker.**` :869, `**Loop discipline.**` :915,
221
- `**Spec before building.**` :925 *(conditional)*, `**Lessons ledger: stop
222
- repeating mistakes.**` :939, `**Code discovery: graph first.**` :947
223
- *(conditional)*, `**Consult before irreversible.**` :956 *(conditional)*,
224
- `**Research: route it to OpenResearch.**` :974 *(conditional)*, `**Product
225
- analysis: five axes, one evidence class per finding.**` :1003 *(conditional)*,
226
- `**Design: UI work reads the design skills.**` :1039 *(conditional)*,
227
- `**No AI attribution, ever, on any host.**` :1048, `**Identifiers and messages
228
- stay English.**` :1058, `**Session scope: the user's repo, not tezgah.**` :1064,
229
- ``**Workspace: `.tezgah/` only.**`` :1075, `**Parallel by default: fan out
230
- independent items.**` :1082 *(conditional)*,
231
- `**Kill switches:**` :1112. The first label renders as `**Turkish, BLUF.**` under
220
+ That order, read from `hooks/tezgah_policy.py::CORE` (the labels are the
221
+ anchors, so an edit above one moves nothing here):
222
+ `**{REPLY_LANG}, BLUF.**`, `**Ponytail (minimal code).**`, `**Output shape:
223
+ ADHD-friendly.**`, `**Deliver the whole ask; never the shortcut.**`,
224
+ `**Integrity: evidence, or a named blocker.**`, `**Loop discipline.**`,
225
+ `**Spec before building.**` *(conditional)*, `**Lessons ledger: stop
226
+ repeating mistakes.**`, `**Code discovery: graph first.**`
227
+ *(conditional)*, `**Consult before irreversible.**` *(conditional)*,
228
+ `**Research: route it to OpenResearch.**` *(conditional)*, `**Product
229
+ analysis: five axes, one evidence class per finding.**` *(conditional)*,
230
+ `**Design: UI work reads the design skills.**` *(conditional)*,
231
+ `**No AI attribution, ever, on any host.**`, `**Identifiers and messages
232
+ stay English.**`, `**Session scope: the user's repo, not tezgah.**`,
233
+ ``**Workspace: `.tezgah/` only.**``, `**Parallel by default: fan out
234
+ independent items.**` *(conditional)*,
235
+ `**Kill switches:**`. The first label renders as `**Turkish, BLUF.**` under
232
236
  `language` `tr`.
233
237
 
234
238
  `always_on_core()` (`hooks/tezgah_context.py::always_on_core`) drops the seven
@@ -256,7 +260,7 @@ applied, and it also returns the names of the switches that fired.
256
260
  - **Ponytail (minimal code).** Take the laziest rung that holds (YAGNI → reuse → stdlib → platform → installed dependency → one line), never simplify away validation, error handling or security. A non-default level rides the reminder (`hooks/tezgah_context.py::_pony_level_line`).
257
261
  - **Output shape: ADHD-friendly.** The action or answer is the first line, multi-step work is a numbered list whose position is restated, an estimate is in concrete units. Two upstream rules were rewritten rather than imported verbatim because they collided with rules already in force: the state restatement points at the todo list instead of duplicating it, and a time estimate can no longer be read as a measurement.
258
262
  - **Deliver the whole ask; never the shortcut.** The request is a floor: no cheaper stand-in, no silent scope cut, no token gesture reported as done.
259
- - **Integrity: evidence, or a named blocker.** A done/tested claim holds only if the check ran in this session and its output was seen; "unverified" (tr: "doğrulanmadı") follows a check that ran and failed; leftover work is done, or stays as one of four blockers (DECISION, ACCESS, IRREVERSIBLE, ENV; tr: KARAR, YETKİ, GERİ ALINAMAZ, ORTAM) with its evidence. The mechanical half is enforced by [gate.md](gate.md), the residual half by the Stop rule's `residual work` class ([evidence.md](evidence.md)). Its **Ask policy** says what is asked at all. Work and process are never asked. An outward step runs on a grant. A never-default step waits for an answer. Every other question goes in one batch with a recommended default. The Stop rule's `needless ask` class refuses a reply that ends on a question the agent can decide, and the gate warns on an ask-tool question with no default ([gate.md](gate.md#ask-an-ask-tool-question-with-no-recommended-option)).
263
+ - **Integrity: evidence, or a named blocker.** A done/tested claim holds only if the check ran in this session and its output was seen; "unverified" (tr: "doğrulanmadı") follows a check that ran and failed; leftover work is done, or stays as one of four blockers (DECISION, ACCESS, IRREVERSIBLE, ENV; tr: KARAR, YETKİ, GERİ ALINAMAZ, ORTAM) with its evidence. The mechanical half is enforced by [gate.md](gate.md), the residual half by the Stop rule's `residual work` class ([evidence.md](evidence.md)). Its **Ask policy** says what is asked at all. Work and process are never asked. An outward step runs on a grant. A never-default step waits for an answer. Plan 136 added three to that list after a session ran another session's research line under a hidden dir and wrote a `chosen:` variant the owner never picked: choosing a variant or option a research, analysis or review recommends; a way around a tezgah refusal (another folder, a hidden dir, a hatch the owner did not grant); deciding about another session's work (its line, plan or branch). A taste or intent fork still goes in the batch with a default taken this turn; these do not. A subagent's question of that kind is relayed to the user, never answered by the orchestrator or escalated to a stronger model. Every other question goes in one batch with a recommended default. The Stop rule's `needless ask` class refuses a reply that ends on a question the agent can decide, and passes one that asks the owner to pick a variant or to decide one of those steps (`hooks/tezgah_integrity.py::OWNER_CHOICE`); the gate warns on an ask-tool question with no default ([gate.md](gate.md#ask-an-ask-tool-question-with-no-recommended-option)).
260
264
  - **Loop discipline.** Never repeat an identical failing command; three attempts is the ceiling, then report what is still unknown.
261
265
  - **Lessons ledger: stop repeating mistakes.** `.tezgah/lessons.md` lines are standing constraints; the recent ones are injected at session start, and up to three older ones a prompt shares words with ride that turn, each once per session when the host sends a session id (a compaction resets that, because the compacted context no longer holds them; without an id every matching turn carries them). Write a line rule first, `<rule> - <incident>` (`LESSON_SEPARATOR`, `hooks/tezgah_context.py::LESSON_SEPARATOR`). The session block counts the shown lines that do not open with their rule. A line ending `|| enforced_by: <rule|test>` leaves both blocks while that gate rule or test is on (`hooks/tezgah_lessons.py::lines`). That one reader serves the context, the gate and the CLI. It cuts that clause, a `|| check: ...` clause and an `@<sha> <path>` stamp off each line (`hooks/tezgah_lessons.py::parse`). None of them changes the key, the per-turn digest, the cut or the ranking. The gate records a line written after untrusted text in the same turn as a `lesson_tainted` row ([evidence.md](evidence.md)). That line still rides both blocks, behind a per-line data label (`hooks/tezgah_context.py::LESSON_TAINTED`). ADR 010 makes it a row, never a refusal. `bin/tezgah-lessons` proposes rewrites, merges and retirements, and never writes the file.
262
266
  - **No AI attribution, ever, on any host.** Nothing persisted or published may name a model, vendor or "AI" as author, co-author, generator or helper; one already in pushed history is reported.
@@ -416,9 +420,12 @@ with no transcript pays nothing.
416
420
  ## How a rule is disarmed
417
421
 
418
422
  A kill switch removes the rule's text, not just a status mark
419
- (`hooks/tezgah_context.py::core_split`). `off()` checks `~/.config/tezgah` and the
420
- legacy `~/.claude` (`hooks/tezgah_paths.py::OFF_DIRS`, `hooks/tezgah_paths.py::off`); the drop itself
421
- happens in `core_split()`.
423
+ (`hooks/tezgah_context.py::core_split`). `off()` checks tezgah's config dir and
424
+ the legacy `~/.claude` (`hooks/tezgah_paths.py::OFF_DIRS`, `hooks/tezgah_paths.py::off`).
425
+ The config dir is `~/.config/tezgah`, or `$XDG_CONFIG_HOME/tezgah` when
426
+ `XDG_CONFIG_HOME` is set (`hooks/tezgah_paths.py::CONFIG_DIR`). The core and the
427
+ per-turn reminder name the same dir (`hooks/tezgah_policy.py::SWITCH_DIR`). The
428
+ drop itself happens in `core_split()`.
422
429
 
423
430
  The session latches a switch (`hooks/tezgah_paths.py::latch`). A hook that knows
424
431
  its session binds the session's ledger first
@@ -435,8 +442,8 @@ three cases:
435
442
  (`hooks/tezgah_integrity.py::_in_flight`). A host hands its prompt hook a
436
443
  prompt between calls. An unanswered call older than ten minutes no longer
437
444
  counts, since a host writes no answer for a call the user interrupted.
438
- - it is the stand-down `tezgah-setup --uninstall --full` writes into
439
- `pretooluse-off` (`hooks/tezgah_paths.py::STAND_DOWN`).
445
+ - it is the stand-down a full `tezgah-setup --uninstall` (one covering every
446
+ armed host) writes into `pretooluse-off` (`hooks/tezgah_paths.py::STAND_DOWN`).
440
447
  - it appeared within the first row's second, because the row's `ts` holds whole
441
448
  seconds.
442
449
 
@@ -445,6 +452,18 @@ a hook with no session id bind nothing and read the file alone, as before. The
445
452
  opencode plugin answers the same way (`off(name, sessionID)`). A prompt match is
446
453
  a keyword match, not intent: "don't touch `verify-off`" names the switch too.
447
454
 
455
+ A subagent session is no next session. When the payload names the session that
456
+ spawned it (`parent`), the hook binds the parent's ledger too
457
+ (`hooks/tezgah_integrity.py::bind_session`). The subagent then latches on the
458
+ earlier of its own and its parent's first row. A switch the parent made
459
+ mid-session does not count in a subagent it spawns afterwards. A switch the
460
+ user's prompt to the parent named counts there too. The subagent's own prompt
461
+ is its parent's brief and authorizes nothing. The omp and Amp bridges send
462
+ `parent` on a subagent's events. The opencode plugin keeps the parent opencode
463
+ names on the child's `session.created` (`hosts/opencode/plugins/tezgah.js::PARENTS`)
464
+ and hands it to the core it asks (`bin/tezgah-gate`). The parent always comes
465
+ from the host, never from a ledger row an agent could write.
466
+
448
467
  | Switch file | Rule it removes | Where the drop is implemented |
449
468
  |---|---|---|
450
469
  | `exec-mode.off` | `**{REPLY_LANG}, BLUF.**` | `hooks/tezgah_context.py::switches` |
@@ -454,27 +473,43 @@ a keyword match, not intent: "don't touch `verify-off`" names the switch too.
454
473
  | `design-off` | `**Design: UI work reads the design skills.**` | `hooks/tezgah_context.py::switches` |
455
474
  | `verify-off` | `**Integrity: evidence…**`, and the per-turn evidence-scope line with it | `hooks/tezgah_context.py::switches`, `hooks/tezgah_context.py::context_for` |
456
475
  | `consult-off` | `**Consult before irreversible.**` | `hooks/tezgah_context.py::switches` |
457
- | `research-off` | `**Research: route it to OpenResearch.**` | `hooks/tezgah_context.py::switches` |
476
+ | `research-off` | `**Research: route it to OpenResearch.**` and `**Product analysis: five axes, one evidence class per finding.**` | `hooks/tezgah_context.py::switches` |
458
477
  | `orchestrate-off` | `**Parallel by default: fan out independent items.**`, and the orchestration section of the on-demand skill | `hooks/tezgah_context.py::switches` drops the paragraph, `hooks/tezgah_context.py::context_for` injects "Orchestration is off" and drops the specialists line, and the skill-ignore note is `hooks/tezgah_context.py::context_for` |
459
- | `reminder-off` | the per-turn reminder | `hooks/tezgah_context.py::context_for` returns `None` |
478
+ | `reminder-off` | the per-turn reminder line alone; the armed paragraphs, the gate notice, the off-note and the other per-turn parts stay | `hooks/tezgah_context.py::context_for` leaves the `reminder` part out |
460
479
  | `judge-off` | the judgement seam: the snapshot triage, the docs page fallback and the skill hint | `hooks/tezgah_judge.py::available` — `available()` is asked before any call, so an armed switch makes no request at all |
461
480
  | `triage-off` | the snapshot triage alone (`bin/tezgah-triage`), leaving the docs fallback and the skill hint armed | `bin/tezgah-triage::off_reason` — `off_reason()` answers this switch before the seam's, so the analyze-app loop reads the tree instead of paying for a judgement |
462
481
  | `docs-judge-off` | the docs page fallback alone (`bin/tezgah-docs`), leaving the triage and the skill hint armed | `bin/tezgah-docs::off` — `off()` answers this switch first, so a query the index cannot place is ranked by shared words instead (`ranked`), and exits 1 only when no page shares one |
463
482
  | `lang-off` | `**Identifiers and messages stay English.**` | `hooks/tezgah_context.py::switches` drops the paragraph; the gate's own check reads the same switch (`hooks/tezgah_gate.py::decision`) |
483
+ | `workspace-off` | ``**Workspace: `.tezgah/` only.**``, and the gate's refusal of a root `plans/`, `research/` or `analysis/` write | `hooks/tezgah_context.py::switches` drops the paragraph, and the gate reads the same switch (`hooks/tezgah_gate.py::DENY_RULES`, [gate.md](gate.md#workspace-tezgah-state-outside-tezgah)) |
484
+ | `task-off` | the gate's task rule: the active plan's phase, its path allowlist, the plan record and a shell write | `hooks/tezgah_gate.py::decision` ([gate.md](gate.md#task-the-active-plans-phase-and-path-allowlist)) |
464
485
  | `pretooluse-off` | the gate's denials, not a rule | [gate.md](gate.md) |
465
486
  | `.no-ponytail` | `**Ponytail (minimal code).**` | `hooks/tezgah_context.py::switches` |
466
487
  | `.no-adhd` | `**Output shape: ADHD-friendly.**` | `hooks/tezgah_context.py::switches` |
467
488
  | `.no-graph` | `**Code discovery: graph first.**` | `hooks/tezgah_context.py::switches` |
468
489
  | `.no-lessons` | `**Lessons ledger: stop repeating mistakes.**` | `hooks/tezgah_context.py::switches`, and neither the session's lessons block (`hooks/tezgah_context.py::lessons`) nor the per-turn one (`relevant_lessons`) is injected |
490
+ | `.no-plan-gate` | the gate's plan rule in that repository: a turn's third product file on a branch no plan binds | `hooks/tezgah_gate.py::plan_reason` ([gate.md](gate.md#plan-a-turns-third-product-file-on-a-branch-no-plan-binds)) |
491
+ | `.no-taste` | the opt-in taste capture in that repository | `hooks/tezgah_taste.py::MARK` ([operations](operations.md#opt-in-taste-capture-learn-apply)) |
469
492
 
470
- The last four are per-repo [marks](glossary.md#per-repo-mark), read by
493
+ The last six are per-repo [marks](glossary.md#per-repo-mark), read by
471
494
  `repo_marks()`, walking up to the enclosing [root](glossary.md#root)
472
495
  (`hooks/tezgah_context.py::repo_marks`). Every switch that fired is named back to the
473
- session at start (`hooks/tezgah_context.py::context_for`) and per turn (`hooks/tezgah_context.py::context_for`), with the instruction to
496
+ session at start (`hooks/tezgah_context.py::context_for`), with the instruction to
474
497
  ignore the matching section in `tezgah-contract` — that skill is loaded
475
- separately and would otherwise re-arm the rule. `tezgah-adhd off` writes the same
498
+ separately and would otherwise re-arm the rule — and in Claude's static `tezgah`
499
+ output style, which no switch re-renders. The per-turn note names the switches
500
+ again and the output style alone. Both notes are the budget's `offnote` block.
501
+ A turn over its byte budget drops it like the other droppable blocks
502
+ ([the budget](#when-the-injected-text-grows-too-large)), so "per turn" holds
503
+ while the turn fits. `tezgah-adhd off` writes the same
476
504
  channel as the file (`bin/tezgah-adhd::SWITCH`); the ponytail *level* is not a switch.
477
505
 
506
+ `off()` and `armed()` also read five feature switches, not rule switches.
507
+ ADR 007 leaves them unclassified (`hooks/tezgah_paths.py::SWITCHES`): `agents-off`, `update-check-off`, `slices-off`, `taste-on`, `skill-suggest-on`.
508
+
509
+ One environment variable disarms a refusal too: `TEZGAH_REPO_RULE=0` skips a
510
+ repository's own `.tezgah/gate.py` (`hooks/tezgah_gate.py::REPO_RULE_VALVE`;
511
+ [gate.md](gate.md#a-repositorys-own-rule), [environment](environment.md)).
512
+
478
513
  One more file in a repository's own tree is not a rule switch either. A
479
514
  `tezgah.owner` at the repository's top says who owns the workspace, and its two
480
515
  settings turn seams off rather than dropping a paragraph. `tasks: off` stops the
@@ -542,8 +577,8 @@ brief carries the same caller-list floor (`hooks/tezgah_agents.py`), and
542
577
 
543
578
  ## When the injected text grows too large
544
579
 
545
- Each event has a byte budget: `session_start` and `post_compact` 16000,
546
- `user_prompt` 7000, `subagent_start` 5800, anything else 13000
580
+ Each event has a byte budget: `session_start` and `post_compact` 16200,
581
+ `user_prompt` 7000, `subagent_start` 6100, anything else 13000
547
582
  (`hooks/tezgah_context.py::CONTEXT_BUDGET`). The rationale and the measured
548
583
  sizes sit beside it. In an empty fixture repo, `session_start` with the core
549
584
  builds 9842 B before the ponytail and ADHD rules went inline and 10884 B after
@@ -551,8 +586,9 @@ builds 9842 B before the ponytail and ADHD rules went inline and 10884 B after
551
586
  113 B. The healthy fixture with a lesson and a plan then built 12059 B. At the
552
587
  old 12000 B budget it lost its lessons and sibling checkouts to pay for core
553
588
  rules, so the budget moved to 13000 B. The residual rule (plan 080) added
554
- 1473 B to the core and 151 B to the reminder, so the always-on band is
555
- 11972 B (`tests/test_setup.py`). At 13000 B the healthy fixture gave up its
589
+ 1473 B to the core and 151 B to the reminder, so the always-on band reached
590
+ 11972 B. `tests/test_setup.py::VersionPrefixIsNotContract` pins today's band.
591
+ At 13000 B the healthy fixture gave up its
556
592
  lessons, plans and steer blocks. At 6000 B the busiest prompt turn gave up its
557
593
  active-task line. The session budgets moved to 14500 B, the prompt budget to 6500 B.
558
594
  The Ask policy (plan 086, S3) added 913 B to the core. At 14500 B the healthy
@@ -560,7 +596,10 @@ fixture then gave up a lessons, plans or knowledge block. The session budgets mo
560
596
  The subagent brief stays the same size.
561
597
  A research ask arms the Research and Product paragraphs: 6239 B on Claude
562
598
  (S4Remeasure p08). At 6500 B the skill hint naming the research skill was the
563
- block given up. The prompt budget moved to 7000 B (2026-10-22).
599
+ block given up. The prompt budget moved to 7000 B (2026-10-22). A longer
600
+ subagent pointer (plan 136) moved `subagent_start` to 6100 B. The output-shape
601
+ paragraph's plan-mode clause (plan 142, +168 B) put the worktree fixture's
602
+ `session_start` over 16000 B again. The session budgets moved to 16200 B.
564
603
  `SkillRulesInForceAtSessionStart` (`tests/test_context.py`) pins the rules
565
604
  themselves, not the size. A
566
605
  `user_prompt` that arms all seven conditional rules
@@ -575,7 +614,17 @@ table first, then the Orca line, the tooling-availability lines and the live gra
575
614
  glance, then the index line and the resume state (it outlives those because it
576
615
  is the only one that says what this session was doing, and it still yields to a
577
616
  rule), then the evidence-scope warning, the skill hint, the task phase, the
578
- delta, and the skill pointer last (`hooks/tezgah_context.py::DROP_ORDER`).
617
+ delta, and the skill pointer last (`hooks/tezgah_context.py::DROP_ORDER`). The
618
+ whole tuple, in this order:
619
+
620
+ ```
621
+ knowledge worktrees lessons lessons_turn taste plans subagents steer orca
622
+ consult research research_broken graph offnote orchestrate index resume
623
+ slices context scratch skill task delta pointer
624
+ ```
625
+
626
+ `offnote` is the note naming the kill switches that fired, so a turn over budget
627
+ can lose it before the index line.
579
628
  The skill hint moved from near the front to after `lessons_turn` and `index`.
580
629
  It names the skill this ask needs, and no other block can work that out.
581
630
  It was the first block given up in 32 logged live drops (plan 126). Any key absent from that tuple is