@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
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env python3
2
+ """Qwen Code lifecycle hook. Reads Qwen's JSON on stdin and answers in Qwen's own
3
+ output shape, onto the shared tezgah core (hooks/).
4
+
5
+ Qwen Code 0.21.13 fires SessionStart, UserPromptSubmit, PreToolUse, PostToolUse,
6
+ PostToolUseFailure, SubagentStart, SubagentStop and Stop here
7
+ (`bin/tezgah-setup::QWEN_HOOK_EVENTS`); anything else is ignored. Inert outside
8
+ the roots.
9
+
10
+ The always-on core is the managed block in `~/.qwen/QWEN.md`, which Qwen loads
11
+ into every session, so SessionStart repeats it only when that block is missing.
12
+ PreToolUse translates Qwen's runtime tool ids into the gate's vocabulary and
13
+ denies with `hookSpecificOutput.permissionDecision` on exit 0 - never exit 2,
14
+ whose stderr channel carries no reason the ledger and the probe can read. Qwen
15
+ fires PostToolUse for a successful call and PostToolUseFailure for a failed one
16
+ (a shell exit Qwen counts as an error, `isShellExitError`), so the event is the
17
+ call's outcome. Stop blocks a done/tested claim with no successful check behind
18
+ it, judged on `last_assistant_message`, the same rule Claude's Stop hook enforces
19
+ (hooks/tezgah_integrity.py). The status line is `tezgah-statusline --qwen`, Qwen's
20
+ `ui.statusLine` command, so no event here carries it.
21
+ """
22
+ import json
23
+ import os
24
+ import sys
25
+
26
+ ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
27
+ sys.path.insert(0, os.path.join(ROOT, "hooks"))
28
+ from tezgah_guard import attest_session, import_failed, safe # noqa: E402
29
+ try:
30
+ from tezgah_context import command_text, context_for, record, shell_kind # noqa: E402
31
+ from tezgah_gate import cross_project_note, decision, graph_advice # noqa: E402
32
+ from tezgah_integrity import ( # noqa: E402
33
+ SUBAGENT_CHANNEL, bind_session, note, note_tool, ran_nothing, report_bytes,
34
+ stop_reason, untrusted_source)
35
+ from tezgah_paths import HOST_DIRS, off, root_for # noqa: E402
36
+ from tezgah_taste import write_note # noqa: E402
37
+ from tezgah_untrusted import marks # noqa: E402
38
+ except Exception as exc:
39
+ import_failed(exc)
40
+
41
+ # Qwen's runtime tool ids (the `ToolNames` table in the 0.21.13 bundle) -> the
42
+ # shared gate's vocabulary. `replace` is the edit tool's id in older builds; the
43
+ # 0.21.13 bundle has none, and an unknown id passes the gate untouched. MCP tools
44
+ # arrive as `mcp__<server>__<tool>`, the shared shape, and pass through.
45
+ # `monitor` runs a shell command too (Qwen's headless deny list pairs it with
46
+ # SHELL), and `notebook_edit` writes an .ipynb (review of plan 138).
47
+ GATE_TOOLS = {
48
+ "run_shell_command": "Bash",
49
+ "monitor": "Bash",
50
+ "write_file": "Write",
51
+ "edit": "Edit",
52
+ "replace": "Edit",
53
+ "notebook_edit": "NotebookEdit",
54
+ "agent": "Task",
55
+ "grep_search": "Grep",
56
+ }
57
+
58
+
59
+ def gate_name(name):
60
+ """The tool name the gate denies under and the ledger row records.
61
+
62
+ `call_id` hashes the name it is handed, so both halves map through here: a
63
+ PostToolUse row under the raw id would never match the PreToolUse call and
64
+ the loop guard would never deny (`hosts/codex/hook.py::gate_name`)."""
65
+ return GATE_TOOLS.get(name, name)
66
+
67
+
68
+ def classify(payload):
69
+ """The used-tool kind for a tool payload, or None."""
70
+ name = payload.get("tool_name", "") or ""
71
+ if name.startswith("mcp__") and "codegraph" in name:
72
+ return "graph"
73
+ if name == "agent":
74
+ return "orch"
75
+ if name == "run_shell_command":
76
+ return shell_kind(command_text(payload.get("tool_input")))
77
+ return None
78
+
79
+
80
+ def subagent_id(payload):
81
+ """The subagent id Qwen adds to a hook input run inside a subagent, else None."""
82
+ aid = payload.get("agent_id")
83
+ return aid if isinstance(aid, str) and aid else None
84
+
85
+
86
+ def rules_carry_core():
87
+ """True when `QWEN.md` in Qwen's home holds tezgah-setup's managed block."""
88
+ try:
89
+ with open(os.path.join(HOST_DIRS["qwen"], "QWEN.md"), encoding="utf-8") as fh:
90
+ return "<!-- tezgah:start" in fh.read()
91
+ except OSError:
92
+ return False
93
+
94
+
95
+ def result_size(result):
96
+ """The result's top-level length, the Claude-family writer's measure; a value
97
+ with no length counts as non-empty and only a missing result is None."""
98
+ if isinstance(result, (str, bytes, bytearray, list, tuple, dict, set, frozenset)):
99
+ return len(result)
100
+ return 1 if result is not None else None
101
+
102
+
103
+ def inp_of(payload):
104
+ inp = payload.get("tool_input")
105
+ return inp if isinstance(inp, dict) else {}
106
+
107
+
108
+ def post_tool(payload, cwd, session_id, failed):
109
+ """The ledger row for one finished call, and the notice for the model."""
110
+ tool = gate_name(payload.get("tool_name", ""))
111
+ inp = inp_of(payload)
112
+ # a `monitor` PostToolUse fires when the watch starts ("Monitor started"),
113
+ # long before its command ends: it records a run, never a pass (review of
114
+ # plan 138, round 2); a failed start still arrives as PostToolUseFailure
115
+ if payload.get("tool_name") == "monitor" and failed is False:
116
+ failed = None
117
+ safe(session_id, record, session_id, classify(payload))
118
+ if not root_for(cwd):
119
+ return None
120
+ result = payload.get("tool_response")
121
+ # read before this call's row lands: `source` is the taint's own mark
122
+ got = safe(session_id, marks, tool, inp, session_id, result)
123
+ source, notice = got or (None, None)
124
+ safe(session_id, note_tool, session_id, tool, inp, failed=failed,
125
+ interrupted=bool(failed and payload.get("is_interrupt") is True),
126
+ source=source, cwd=cwd,
127
+ out_bytes=(report_bytes(result)
128
+ if untrusted_source(tool, inp) == SUBAGENT_CHANNEL
129
+ else result_size(result)),
130
+ empty_run=ran_nothing(result), agent=subagent_id(payload))
131
+ advice = safe(session_id, graph_advice, tool, inp, cwd, "qwen")
132
+ taste = cross = None
133
+ if not failed:
134
+ taste = safe(session_id, write_note, session_id, inp, cwd, tool)
135
+ cross = safe(session_id, cross_project_note, session_id, tool, inp, cwd)
136
+ return "\n".join(p for p in (notice, advice, taste, cross) if p)
137
+
138
+
139
+ def context(event, text):
140
+ return {"hookSpecificOutput": {"hookEventName": event,
141
+ "additionalContext": text}} if text else {}
142
+
143
+
144
+ def main():
145
+ try:
146
+ payload = json.load(sys.stdin)
147
+ except Exception:
148
+ return
149
+ if not isinstance(payload, dict):
150
+ return
151
+ event = payload.get("hook_event_name") or ""
152
+ cwd = payload.get("cwd") or os.getcwd()
153
+ session_id = payload.get("session_id")
154
+ safe(session_id, bind_session, session_id, payload.get("parent")) # the switch latch
155
+ out = {}
156
+ if event == "PreToolUse":
157
+ reason = safe(session_id, decision, gate_name(payload.get("tool_name", "")),
158
+ inp_of(payload), cwd, session_id, agent=subagent_id(payload))
159
+ if reason:
160
+ out = {"hookSpecificOutput": {
161
+ "hookEventName": "PreToolUse",
162
+ "permissionDecision": "deny",
163
+ "permissionDecisionReason": reason,
164
+ }}
165
+ elif event in ("PostToolUse", "PostToolUseFailure"):
166
+ out = context(event, post_tool(payload, cwd, session_id,
167
+ event == "PostToolUseFailure"))
168
+ elif event == "SessionStart":
169
+ safe(session_id, attest_session, "qwen", session_id, cwd)
170
+ out = context(event, safe(session_id, context_for, "session_start", cwd,
171
+ payload, with_core=not rules_carry_core()))
172
+ elif event == "UserPromptSubmit":
173
+ out = context(event, safe(session_id, context_for, "user_prompt", cwd, payload))
174
+ elif event == "SubagentStart":
175
+ safe(session_id, record, session_id, "orch")
176
+ kind = payload.get("agent_type")
177
+ if subagent_id(payload) and isinstance(kind, str) and root_for(cwd):
178
+ safe(session_id, note, session_id, "subagent_start", kind,
179
+ agent=subagent_id(payload))
180
+ # the delegate is a fresh context: Qwen documents additionalContext here
181
+ # as the subagent's initial context
182
+ out = context(event, safe(session_id, context_for, "subagent_start", cwd,
183
+ payload))
184
+ elif event == "SubagentStop":
185
+ # a subagent's end is judged record-only (ADR 011): one `subagent_end`
186
+ # row from its last message, never a block
187
+ safe(session_id, record, session_id, "orch")
188
+ if not off("verify-off") and root_for(cwd):
189
+ safe(session_id, stop_reason, payload.get("last_assistant_message") or "",
190
+ session_id, cwd=cwd, subagent=True)
191
+ elif event == "Stop":
192
+ # Qwen's Stop carries `last_assistant_message` and `stop_hook_active`,
193
+ # and takes {"decision": "block", "reason": ...}. The reply after a block
194
+ # is judged again and refused at most once more (tezgah_integrity).
195
+ if not off("verify-off") and root_for(cwd):
196
+ reason = safe(session_id, stop_reason,
197
+ payload.get("last_assistant_message"), session_id,
198
+ cwd=cwd, after_block=bool(payload.get("stop_hook_active")),
199
+ mode=payload.get("permission_mode"))
200
+ if reason:
201
+ out = {"decision": "block", "reason": reason}
202
+ if out:
203
+ print(json.dumps(out))
204
+
205
+
206
+ main()
@@ -38,7 +38,7 @@ first; write what was cut only when you consciously cut something the user asked
38
38
  for, in at most three lines. In force from the first turn; levels
39
39
  lite|full|ultra are set with `~/.config/tezgah/bin/tezgah-pony` and ride the per-turn reminder when
40
40
  not `full`; read the full `ponytail` skill from the router for the level table
41
- and examples. Off: "stop ponytail".
41
+ and examples. Off: "stop ponytail", `ponytail-auto.off` or `.no-ponytail`.
42
42
 
43
43
  **Output shape: ADHD-friendly.** The answer or the next action is on the first
44
44
  line - a command, path, snippet or the decision itself, never context or the
@@ -47,7 +47,10 @@ bounded action per step, and while it is in flight its position is restated in
47
47
  one line - the todo list is that source, never re-narrate the plan. End with the
48
48
  approval questions the Ask policy (below) leaves, or with nothing when none is
49
49
  left; a step you can take yourself is taken, never handed
50
- over. Finish the issue in hand before raising a second one; an error states
50
+ over, and in the host's plan mode, where nothing can run, the plan is the
51
+ deliverable: its approval is the question that turn asks and nothing is
52
+ reported as done or tested.
53
+ Finish the issue in hand before raising a second one; an error states
51
54
  location, cause and fix with no drama, naming a cause only when the evidence
52
55
  identifies it (else what is known and what would confirm it); after a change say
53
56
  what now works. A list shows at most five items, ranked, the rest kept in
@@ -112,15 +115,17 @@ lines: item - blocker - evidence - what unblocks it. No blockers: no heading.
112
115
  Work and process - the order, "shall I go on", a check, a plan, a consult
113
116
  member, a spend within `spend_cap`, a local merge of the session's branch,
114
117
  removing its worktree - are taken and stated in one line, never asked; a taste
115
- or intent fork goes in the batch with a default taken this turn, but building a
116
- plan this turn wrote waits for the answer. An outward step
118
+ or intent fork goes in the batch with a default taken this turn, but the
119
+ never-default items below wait for the answer. An outward step
117
120
  on the user's own remote (push, PR, release, their package) runs on a grant
118
121
  ("push without asking", "I approve", "bana sormadan pushla", "onay veriyorum"), else is asked once. Never on a
119
122
  default - ask and wait for an answer naming action and target: a force push,
120
123
  deleting what the session did not make, production, a credential, a third
121
- party, a spend over the cap. The batch: one "Approval questions"/"Onay için sorular" heading, waiting
124
+ party, a spend over the cap, building a plan this turn wrote, picking a
125
+ recommended variant, a way around a tezgah refusal, another session's work. The batch: one "Approval questions"/"Onay için sorular" heading, waiting
122
126
  items first, each numbered item with "Recommended:"/"Öneri:" its default; an ask tool
123
- sets a recommended option.
127
+ sets a recommended option. No fresh subagent, no review verdict: say so and
128
+ ask. Contest a finding only with a file:line; never lower a severity.
124
129
  **Say what a number was measured on.** A figure produced by a fixture - a temp
125
130
  HOME, a generated repository, a stand-in, a hand-written sample - is evidence
126
131
  about the code path, never a property of the running system, and it is reported
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@r1z4x/tezgah",
3
- "version": "1.11.0",
4
- "description": "One shared working contract for every AI coding assistant you run: Claude Code, Codex, Cursor, opencode, dsh and oh-my-pi (omp).",
3
+ "version": "1.14.0",
4
+ "description": "One shared working contract for every AI coding assistant you run, across fourteen hosts: omp, Claude Code, Codex, Cursor, opencode, dsh, Qwen Code, Factory Droid, Command Code, GitHub Copilot CLI, Gemini CLI, Antigravity CLI, Goose and Amp.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -23,7 +23,10 @@
23
23
  "MANIFEST",
24
24
  "CHANGELOG.md",
25
25
  "LICENSE",
26
- "packaging/"
26
+ "NOTICE",
27
+ "third_party/",
28
+ "packaging/",
29
+ "!**/__pycache__"
27
30
  ],
28
31
  "engines": {
29
32
  "node": ">=18"
@@ -79,12 +79,12 @@ def version():
79
79
 
80
80
 
81
81
  def changelog_version():
82
- """The newest release heading: the second source `plugin_version()` reads."""
82
+ """The newest `## [x.y.z]` heading, as `RELEASE` in hooks/tezgah_context.py."""
83
83
  try:
84
84
  with open(os.path.join(ROOT, "CHANGELOG.md")) as fh:
85
- for line in fh:
86
- if line.startswith("## ["):
87
- return line[4:].split("]")[0].strip()
85
+ for v in (ln[4:].split("]")[0] for ln in fh if ln.startswith("## [")):
86
+ if v.count(".") == 2 and all(p.isdigit() for p in v.split(".")):
87
+ return v
88
88
  except OSError:
89
89
  pass
90
90
  return ""
@@ -26,6 +26,17 @@ while [ $# -gt 0 ]; do case $1 in
26
26
  *) echo "tezgah: unknown option $1" >&2; exit 2 ;;
27
27
  esac; done
28
28
 
29
+ # Python 3.10+ is the floor (README; CI tests nothing older): refused here, by
30
+ # the version found, before anything is fetched.
31
+ py=$(command -v python3 2>/dev/null) || { echo "tezgah: needs Python 3.10 or newer, and no python3 is on PATH" >&2; exit 1; }
32
+ pyver=$("$py" --version 2>&1 | sed -n 's/^Python \([0-9][0-9]*\.[0-9][0-9.]*\).*/\1/p')
33
+ pymajor=0; pyminor=0
34
+ if [ -n "$pyver" ]; then pymajor=${pyver%%.*}; pyminor=${pyver#*.}; pyminor=${pyminor%%.*}; fi
35
+ if [ "$pymajor" -lt 3 ] || { [ "$pymajor" -eq 3 ] && [ "$pyminor" -lt 10 ]; }; then
36
+ echo "tezgah: needs Python 3.10 or newer; $py is ${pyver:-of unknown version}" >&2
37
+ exit 1
38
+ fi
39
+
29
40
  # The default upgrade.sh resolves: the installer is reached through the tree it picks.
30
41
  PREFIX=${PREFIX_ARG:-${TEZGAH_PREFIX:-${XDG_DATA_HOME:-$HOME/.local/share}/tezgah}}
31
42
  finish() { [ -z "$work" ] || rm -rf "$work"; }
@@ -41,6 +41,9 @@ die() {
41
41
  exit 1
42
42
  }
43
43
 
44
+ # First: a missing curl would otherwise read as a fetch failure blaming a token.
45
+ for t in curl tar; do command -v "$t" >/dev/null 2>&1 || die "$t is required (README: Install)"; done
46
+
44
47
  # A private repository answers 404 to `releases/download/...` from a session without
45
48
  # a token (measured 2026-10-17, with the token as URL userinfo and as a header
46
49
  # alike), while the REST asset endpoint returns the blob for `Accept:
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Claude AI Research Skills Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -33,8 +33,8 @@ file, so it costs nothing until it is used.
33
33
  the machine half and the only part `check` reads, and the header is what a person
34
34
  opens. Field names and prose stay English.
35
35
 
36
- - `source` - where the floor came from: `css-custom-properties`, `tailwind-config`
37
- or `theme-json` when it was read out of the repository, `derived` when it could
36
+ - `source` - where the floor came from: `css-custom-properties`, `tailwind-config`,
37
+ `theme-json` or `theme-module` when it was read out of the repository, `derived` when it could
38
38
  not be and someone chose it. An empty `source` is refused: a floor nobody can
39
39
  trace is not a floor.
40
40
  - `tokens.colors` - the colour roles, as role name to hex. A measured colour that
@@ -160,18 +160,25 @@ Read the repository's own token source. In this order, and nothing else:
160
160
  role, named by the property (the `color-`/`c-` prefix and a trailing `-color`
161
161
  are dropped); one named `space`, `spacing`, `unit`, `rhythm` and the like
162
162
  states the unit; `font-size`/`text-size`/`fs` properties are the type scale.
163
- 2. **A Tailwind config** - `tailwind.config.{js,cjs,mjs,ts,json}`, its `colors`,
164
- `spacing` and `fontSize` blocks. `unit`/`base` in `spacing` states the unit;
165
- otherwise the greatest common divisor of the scale the config declares is it.
163
+ 2. **A Tailwind config** - every `tailwind.config.{js,cjs,mjs,ts,json}` in the tree (a monorepo
164
+ has one per app), its `colors`, `spacing` and `fontSize` blocks. A nested colour scale is
165
+ flattened to the class name Tailwind generates (`brand: {500: ...}` is `brand-500`, `DEFAULT`
166
+ is `brand`); a name two configs give different values keeps both (`ink@apps/b/...`). `unit`/
167
+ `base` in `spacing` states a config's unit, otherwise the greatest common divisor of its
168
+ scale does, and the floor's unit is the one every config's unit is a multiple of.
166
169
  3. **A tokens/theme JSON** - `theme.json`, `tokens.json` or `design-tokens.json`,
167
170
  its `colors`/`palette`, `spacing` and `fontSize` keys, found at any depth.
171
+ 4. **A JS/TS theme module** - `theme`, `tokens`, `colors`, `colours` or `palette` `.ts/.js/.mjs`,
172
+ the floor a React Native or Expo app keeps in code: every top-level `const` object's colour
173
+ literals (`palette-ink`, `light-text`), the `spacing` object's numbers (unitless is px/dp), and
174
+ every `fontSize:` number.
168
175
 
169
176
  ```sh
170
177
  ~/.config/tezgah/bin/tezgah-design derive --repo . # writes .tezgah/design-contract.md
171
178
  ~/.config/tezgah/bin/tezgah-design derive --repo . --json # the same, machine-readable
172
179
  ```
173
180
 
174
- `derive` writes nothing and exits 2 when the repository has none of the three, and it never
181
+ `derive` writes nothing and exits 2 when the repository has none of the four, and it never
175
182
  overwrites a contract that already exists - that file carries the component inventory someone
176
183
  filled in, so a re-derive over changed tokens is a hand edit or a deliberate removal first. It
177
184
  does not invent a palette, a type scale or a unit either: a repository with no design system gets
@@ -185,6 +192,20 @@ the artifact says so in its header: the `component-inventory` rule is inert unti
185
192
  `{name, kind}` per component. A measurement names a component; only a person knows which ones the
186
193
  repository owns, so list them in the turn that writes a new one.
187
194
 
195
+ `derive --suggest` is the starting list, never the inventory: it prints one candidate per
196
+ component file (a `.tsx/.jsx/.vue/.svelte/.astro/.swift/.kt/.dart` file under a
197
+ `components`/`widgets`/`ui` directory; tests, stories, mocks, a `.kt`/`.swift` file under `theme/` and Swift `+`
198
+ extensions aside; `Button.ios.tsx` is `Button`), minus the names the contract already lists. `kind`
199
+ is filled only where the name's last word says it, matched whole (`IconButton` is `interactive`,
200
+ `StatCard` is `data`, `FeedbackForm` is `null`), and `null` elsewhere. It writes nothing, exits 2
201
+ when there is no candidate, and exits 2 naming the contract when it cannot read it. Copy the rows
202
+ the repository owns into `components`, check every guessed kind and set every `null` one:
203
+
204
+ ```sh
205
+ ~/.config/tezgah/bin/tezgah-design derive --suggest --repo . # table + pasteable rows
206
+ ~/.config/tezgah/bin/tezgah-design derive --suggest --repo . --json # {ok, candidates: [{name, kind, path}]}
207
+ ```
208
+
188
209
  ## The check
189
210
 
190
211
  ```sh
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MC Dean
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -66,6 +66,8 @@ is reported as such, and an empty list beside it is not evidence of absence.
66
66
 
67
67
  `graph-review` splits findings into `confirmed` (no refuter refuted it), `refuted` (do not report as bugs),
68
68
  and `unverified` (hit the cap, or every refuter failed - label them as such). Never promote a refuted or unverified finding.
69
+ Before the cap, findings with the same file, line and title merge into one. The merge keeps the higher severity
70
+ and lists every raising lens in `dimensions`; `caps.deduplicated` counts the merges.
69
71
 
70
72
  `graph-review` is one review round, not a loop: inside a plan it is round one or round two of the
71
73
  plan's single review (two rounds at most, `~/.config/tezgah/bin/tezgah-task review` refuses a third), never one run per
@@ -102,7 +102,10 @@ If something is left that only the reader can do - a decision, a credential or
102
102
  grant, consent for a step that cannot be undone - name that ONE step, small
103
103
  enough to start now. Several such decisions go in one numbered approval batch,
104
104
  each with a recommended default, and the work proceeds on the defaults (the
105
- tezgah contract's Ask policy). A step you can take yourself is not a next step:
105
+ tezgah contract's Ask policy) - except a class-F item (choosing a variant or
106
+ option a research, analysis or review recommends, a way around a tezgah
107
+ refusal, deciding about another session's work), which waits for the answer.
108
+ A step you can take yourself is not a next step:
106
109
  take it, and never end on asking whether to. When nothing is left, end on what
107
110
  now works.
108
111
 
@@ -110,6 +113,12 @@ Bad: "Hope that helps. Let me know if you want to dig deeper."
110
113
  Bad: "Next: run `npm test` and paste the first failing line." (you can run it)
111
114
  Good: "Blockers: the push to `main` - IRREVERSIBLE - `git push origin main` waits - your yes." (tr: "Engeller", GERİ ALINAMAZ)
112
115
 
116
+ In the host's plan mode nothing can run. There the plan is the deliverable:
117
+ the approval is the one step that is the reader's, every other step waits for
118
+ it, and nothing is reported as done or tested, because nothing ran. The Stop
119
+ rule reads the mode from the host and holds both ends of that
120
+ (`hooks/tezgah_integrity.py::read_only_turn`).
121
+
113
122
  ### 5. Suppress tangents
114
123
 
115
124
  A second issue waits until the first is finished, then arrives as its own
@@ -97,7 +97,11 @@ Input: `$ARGUMENTS` (free text describing the work). Run these steps in order.
97
97
  is refused while the tree is dirty, because the phase starts by recording
98
98
  `checkpoint:` - the commit the work returns to - and uncommitted work has none
99
99
  to record. Make the checkpoint commit it prints, then run the phase move
100
- again and the sha is written. Move the phase yourself as the work reaches it
100
+ again and the sha is written. Before `implementation`, spawn one
101
+ tezgah-reviewer in plan-readiness mode over the plan file. Fix each major
102
+ finding in the plan, or ask the owner when only the owner can decide, then
103
+ move the phase. A plan of one small task may skip this review; say so in
104
+ one line. Move the phase yourself as the work reaches it
101
105
  (`~/.config/tezgah/bin/tezgah-task phase implementation`, then `verification`); when a write is
102
106
  refused for its path and the plan's work genuinely reaches that file, widen
103
107
  the scope with `~/.config/tezgah/bin/tezgah-task allow <glob> ... --why "<reason>"`, which records the reason. Never retype the record:
@@ -7,6 +7,13 @@ Usage: python3 render_table.py [--pr-info 'NNN=text' ...] (extra text appended
7
7
  when an item still under .tezgah/plans/open names no command either)
8
8
  Creates .tezgah/plans/README.md, or appends the marker block to it, when either is missing."""
9
9
  import glob, os, re, sys
10
+ if sys.argv[1:2] in (["-h"], ["--help"]):
11
+ print(__doc__)
12
+ sys.exit(0)
13
+ _bad = [a for a in sys.argv[1:] if a.startswith("-") and a not in ("--pr-info", "--acceptance", "--strict")]
14
+ if _bad:
15
+ print("render_table: unknown flag %s\n%s" % (_bad[0], __doc__.split("\n")[1]), file=sys.stderr)
16
+ sys.exit(2)
10
17
  # The one reader of a plan file lives in the module that owns the record, so the
11
18
  # table and the acceptance report cannot disagree about what a plan says. Three
12
19
  # dirnames up from skills/plan-add/render_table.py is the tree that holds hooks/,
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pawel Huryn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -653,18 +653,27 @@ opened under):
653
653
  | `criteria.json` | `question`, `baseline_variant` (V0, the status quo or current design) and `criteria[]`, each with `id` (`K1`), `name`, `kind` (`measured` or `judged`), `direction` (`max`, `min` or `pass`), optional `threshold` and `guardrail` |
654
654
  | `variants.jsonl` | one row per variant: `id`, `title`, `artifact` (a path in the line or the repository, `git:<branch>` or `orx:<expId>`), `status` (`produced` or `dropped` with `drop_reason`), and `line` when it is an older line's deliverable |
655
655
  | `comparison.jsonl` | one row per cell: `variant`, `criterion`, `value` with its `source`, or `not_checked` with the reason; `rater` on a judged cell. Append with `~/.config/tezgah/bin/tezgah-research compare <slug> <id>` (one JSON object on stdin), which refuses what `check` refuses |
656
- | `decision.md` | `chosen: V2`, one line per rejected id naming the criterion id that ruled it out, and `flip:` - the result that would reverse the choice |
656
+ | `decision.md` | `recommended: V2` (the line's pick), one line per rejected id naming the criterion id that ruled it out, and `flip:` - the result that would reverse the choice; `chosen:` and `owner:` only once the owner picks |
657
657
 
658
658
  `check` refuses: fewer produced variants than the deliverable takes; two produced
659
659
  files with the same content (a renamed copy is not an alternative); `criteria.json`
660
660
  not committed before the first `comparison.jsonl` commit; a comparison row changed
661
661
  after it was committed; a missing cell once `decision.md` exists or the line is
662
- delivering; a `chosen` that is not a produced variant, a rejected variant with no
663
- criterion, no `flip:`; and a chosen variant another one beats or ties on every
664
- numeric criterion. A judged cell with fewer than two raters warns (`--strict`
665
- refuses). A design or plan (and an analysis on a line before rules 5) with no
666
- `decisions/` warns while the line runs, is refused once it delivers, and keeps
667
- the line open.
662
+ delivering; a pick (`chosen:`, else `recommended:`) that is not a produced
663
+ variant, a rejected variant with no criterion, no `flip:`; and a picked variant
664
+ another one beats or ties on every numeric criterion. A judged cell with fewer
665
+ than two raters warns (`--strict` refuses). A design or plan (and an analysis on
666
+ a line before rules 5) with no `decisions/` warns while the line runs, is refused
667
+ once it delivers, and keeps the line open.
668
+
669
+ The line recommends; the owner chooses. Choosing a variant or option a research,
670
+ analysis or review recommends is the owner's call, so a rules-9 line writes
671
+ `recommended:` and never `chosen:` itself - a `chosen:` with no `owner:` line
672
+ fails `check`. Report the recommendation and wait for the answer; building it
673
+ waits too. When the owner answers, record their words:
674
+ `~/.config/tezgah/bin/tezgah-research choose <slug> <decision> <variant> --ack
675
+ "<the owner's words>"` writes `chosen:` and `owner:`, on an open or a done line.
676
+ `conclude` names each recommendation still waiting for the owner.
668
677
 
669
678
  When the variants are code or config, they are sibling orx nodes under one head
670
679
  node (orx cardinal rule 4: fan within a round, then descend onto the winner), and
@@ -681,8 +690,12 @@ into `state.json` `closed` and `log.md`, seals its experiments' file hashes
681
690
  way, and an edit after the seal fails `check`. The no-slug `check` reads a done
682
691
  line by its seal alone and counts it on one line; its other errors show in
683
692
  `check <slug>` and `check --all-lines`. `init --allow-open` is refused
684
- while an open line has `check` errors. A line moves to another checkout with
685
- `import`, never as a copy: a copy loses the order proof.
693
+ while an open line has `check` errors, other than a criterion whose verdict is
694
+ still `pending` (a line in flight). A line another session is running is not this
695
+ session's to fix or close: when a refusal names one, stop and ask the owner -
696
+ never a way around a tezgah refusal (another folder, a hidden dir). A hidden dir
697
+ under `research/` is not a line: `check` and `status` fail it. A line moves to
698
+ another checkout with `import`, never as a copy: a copy loses the order proof.
686
699
 
687
700
  Finally, a `## Patterns` bullet names what it generalises from - a claim id, a
688
701
  `literature/` note or a run - because a pattern is the one finding a reader carries