@r1z4x/tezgah 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +333 -1
- package/MANIFEST +22 -0
- package/README.md +4 -3
- package/bin/tezgah-docs +1 -1
- package/bin/tezgah-research +39 -4
- package/bin/tezgah-setup +1229 -22
- package/bin/tezgah-task +55 -17
- package/docs/citations-baseline.json +1 -1
- package/docs/clarity-baseline.json +9 -3
- package/docs/contract.md +6 -3
- package/docs/environment.md +3 -2
- package/docs/evidence.md +81 -8
- package/docs/gate.md +6 -2
- package/docs/glossary.md +1 -1
- package/docs/hosts.md +297 -15
- package/docs/layers.md +1 -1
- package/docs/models.md +6 -0
- package/docs/operations.md +16 -5
- package/docs/research.md +36 -10
- package/docs/status-line.md +6 -0
- package/docs/testing.md +3 -1
- package/hooks/hooks.json +1 -1
- package/hooks/lang/en.json +11 -0
- package/hooks/lang/tr.json +11 -0
- package/hooks/projects-stop.py +2 -1
- package/hooks/tezgah_agents.py +27 -3
- package/hooks/tezgah_attest.py +79 -10
- package/hooks/tezgah_context.py +21 -8
- package/hooks/tezgah_gate.py +79 -23
- package/hooks/tezgah_integrity.py +212 -38
- package/hooks/tezgah_paths.py +43 -2
- package/hooks/tezgah_policy.py +35 -13
- package/hooks/tezgah_replay.py +8 -2
- package/hooks/tezgah_research.py +167 -24
- package/hooks/tezgah_stack.py +236 -1
- package/hooks/tezgah_task.py +2 -1
- package/hosts/amp/hook.py +217 -0
- package/hosts/amp/tezgah-plugin.ts.in +386 -0
- package/hosts/antigravity/hook.py +255 -0
- package/hosts/codex/hook.py +2 -1
- package/hosts/commandcode/hook.py +239 -0
- package/hosts/copilot/hook.py +284 -0
- package/hosts/cursor/hook.py +2 -1
- package/hosts/droid/hook.py +259 -0
- package/hosts/dsh/hooks.json +1 -1
- package/hosts/gemini/hook.py +213 -0
- package/hosts/goose/hook.py +145 -0
- package/hosts/omp/hook.py +4 -1
- package/hosts/omp/tezgah-hook.ts.in +30 -7
- package/hosts/opencode/plugins/tezgah.js +2 -2
- package/hosts/qwen/hook.py +206 -0
- package/output-styles/tezgah.md +10 -5
- package/package.json +1 -1
- package/skills/harness/SKILL.md +2 -0
- package/skills/i-have-adhd/SKILL.md +10 -1
- package/skills/plan-add/SKILL.md +5 -1
- package/skills/research/SKILL.md +22 -9
- package/skills/tezgah-contract/SKILL.md +19 -6
- package/statusline.py +20 -9
- package/workflows/graph-review.js +14 -2
|
@@ -315,6 +315,22 @@ function emptyRun(content: any): boolean {
|
|
|
315
315
|
return EMPTY_RUN.test(text.slice(-EMPTY_RUN_TAIL));
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
// The text of one assistant message, whatever shape it arrives in: omp hands
|
|
319
|
+
// `session_stop` the message OBJECT its `getLastAssistantMessage()` returns,
|
|
320
|
+
// and the core must judge the prose, never its `str()` (plan 143). A thinking
|
|
321
|
+
// or tool block is not the reply: only `type: "text"` is.
|
|
322
|
+
function textOf(message: any): string | undefined {
|
|
323
|
+
if (typeof message === "string") return message;
|
|
324
|
+
if (typeof message?.content === "string") return message.content;
|
|
325
|
+
if (Array.isArray(message?.content)) {
|
|
326
|
+
// joined and trimmed exactly as the core does (`reply_prose`), so the two
|
|
327
|
+
// never hand the rule a different text for one reply
|
|
328
|
+
return message.content.map((p: any) => (p?.type === "text" && typeof p.text === "string"
|
|
329
|
+
? p.text : "")).join("\n").trim();
|
|
330
|
+
}
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
|
|
318
334
|
// The text of the newest assistant message in an agent_end `messages` list:
|
|
319
335
|
// the subagent's final report, which the hook reads like a Stop reply.
|
|
320
336
|
function lastReply(messages: any): string | undefined {
|
|
@@ -322,11 +338,8 @@ function lastReply(messages: any): string | undefined {
|
|
|
322
338
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
323
339
|
const m = messages[i];
|
|
324
340
|
if (m?.role !== "assistant") continue;
|
|
325
|
-
|
|
326
|
-
if (
|
|
327
|
-
return m.content.map((p: any) => (p?.type === "text" && typeof p.text === "string"
|
|
328
|
-
? p.text : "")).join("\n");
|
|
329
|
-
}
|
|
341
|
+
const text = textOf(m);
|
|
342
|
+
if (text !== undefined) return text;
|
|
330
343
|
}
|
|
331
344
|
return undefined;
|
|
332
345
|
}
|
|
@@ -785,9 +798,19 @@ export default function tezgahOmp(pi: any): void {
|
|
|
785
798
|
});
|
|
786
799
|
|
|
787
800
|
pi.on("session_stop", async (event: any, ctx: any) => {
|
|
801
|
+
// `sessionModes` exists only in an interactive TUI session; its `plan`
|
|
802
|
+
// state is what every other host sends as `permission_mode`, so the core
|
|
803
|
+
// reads one field (tezgah_integrity.read_only_turn). A session without it
|
|
804
|
+
// (print, RPC, a subagent) sends nothing and is judged as `default`.
|
|
805
|
+
let mode: string | undefined;
|
|
806
|
+
try {
|
|
807
|
+
mode = ctx?.sessionModes?.getState?.()?.plan === "active" ? "plan" : undefined;
|
|
808
|
+
} catch { mode = undefined; }
|
|
788
809
|
const res = ask({ event: "stop", cwd: cwdOf(ctx), session_id: sessionOf(ctx),
|
|
789
|
-
last_assistant_message: event?.last_assistant_message
|
|
790
|
-
|
|
810
|
+
last_assistant_message: textOf(event?.last_assistant_message)
|
|
811
|
+
?? lastReply(event?.messages),
|
|
812
|
+
stop_hook_active: event?.stop_hook_active === true,
|
|
813
|
+
permission_mode: mode });
|
|
791
814
|
if (hookError) {
|
|
792
815
|
markBroken(ctx);
|
|
793
816
|
return undefined; // an unanswerable Stop rule must not block the turn
|
|
@@ -190,13 +190,13 @@ const EXPLORE_DENY =
|
|
|
190
190
|
// as _CREDIT is there. The parity test in tests/test_opencode_plugin.py reads
|
|
191
191
|
// this literal and the Python one and fails if the two alternations differ.
|
|
192
192
|
const ATTRIB =
|
|
193
|
-
/co-authored-by\s*:|generated with|made with|(?:generated|written|authored|built|assisted)\s+by\s+(?:(?:an?|the)\s+)?(?:ai|llm|claude|chatgpt|gpt|copilot|codex|cursor|gemini|deepseek|openai|anthropic)\b|noreply@anthropic|\u{1F916}/iu
|
|
193
|
+
/co-authored-by\s*:|generated with|made with|(?:generated|written|authored|built|assisted|drafted|produced|composed)\s+by\s+(?:(?:an?|the)\s+)?(?:ai|llm|claude|chatgpt|gpt|copilot|codex|cursor|gemini|deepseek|openai|anthropic)\b|(?:generated|written|authored|built|assisted|drafted|produced|composed)\s+(?:with|using)\s+(?:(?:an?|the)\s+)?(?:ai|llm|claude|chatgpt|gpt|copilot|codex|gemini|deepseek|openai|anthropic)(?:-[\w.]+)?(?:\s+(?:code|cli))?(?:\s*\([^)\n]*\))?(?=[.!)\]*_`]*(?:[\x22\x27,;]|\s+-\s|\s*(?:\n|$)))|noreply@anthropic|\u{1F916}/iu
|
|
194
194
|
// The same forms anchored to the start of a line, for the text a write/edit
|
|
195
195
|
// tool is about to land. Mirrors hooks/tezgah_gate.py ATTRIB_LINE: a credit owns
|
|
196
196
|
// its line, while prose naming the ban does not, and that anchor is what keeps
|
|
197
197
|
// the rule from denying the documentation that describes it.
|
|
198
198
|
const ATTRIB_LINE =
|
|
199
|
-
/^[\s>#*/<!+-]*(?:co-authored-by\s*:|generated with|made with|(?:generated|written|authored|built|assisted)\s+by\s+(?:(?:an?|the)\s+)?(?:ai|llm|claude|chatgpt|gpt|copilot|codex|cursor|gemini|deepseek|openai|anthropic)\b|noreply@anthropic|\u{1F916})/imu
|
|
199
|
+
/^[\s>#*/<!+-]*(?:co-authored-by\s*:|generated with|made with|(?:generated|written|authored|built|assisted|drafted|produced|composed)\s+by\s+(?:(?:an?|the)\s+)?(?:ai|llm|claude|chatgpt|gpt|copilot|codex|cursor|gemini|deepseek|openai|anthropic)\b|(?:generated|written|authored|built|assisted|drafted|produced|composed)\s+(?:with|using)\s+(?:(?:an?|the)\s+)?(?:ai|llm|claude|chatgpt|gpt|copilot|codex|gemini|deepseek|openai|anthropic)(?:-[\w.]+)?(?:\s+(?:code|cli))?(?:\s*\([^)\n]*\))?(?=[.!)\]*_`]*(?:[\x22\x27,;]|\s+-\s|\s*(?:\n|$)))|noreply@anthropic|\u{1F916})/imu
|
|
200
200
|
const EDIT_TEXT = ["content", "new_string", "newString", "new_str", "file_text",
|
|
201
201
|
"patch", "text"]
|
|
202
202
|
const WRITE_CMD =
|
|
@@ -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) # the switch latch (tezgah_paths.off)
|
|
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()
|
package/output-styles/tezgah.md
CHANGED
|
@@ -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
|
|
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
|
|
116
|
-
|
|
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
|
|
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
package/skills/harness/SKILL.md
CHANGED
|
@@ -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)
|
|
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
|
package/skills/plan-add/SKILL.md
CHANGED
|
@@ -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.
|
|
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:
|
package/skills/research/SKILL.md
CHANGED
|
@@ -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` | `
|
|
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
|
|
663
|
-
criterion, no `flip:`; and a
|
|
664
|
-
numeric criterion. A judged cell with fewer
|
|
665
|
-
refuses). A design or plan (and an analysis on
|
|
666
|
-
`decisions/` warns while the line runs, is refused
|
|
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
|
|
685
|
-
`
|
|
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
|
|
@@ -265,7 +265,10 @@ Routing: `~/.config/tezgah/bin/tezgah-route "<brief>"` names the worker - tezgah
|
|
|
265
265
|
never the host's generic task agent (overrides for stored data, credentials,
|
|
266
266
|
security first; then a Jev judgement; the static phase table with no key). A report missing an output field, or an
|
|
267
267
|
`ESCALATE:`, gets one narrower re-brief of the same role naming that field or judgement; only a second miss is
|
|
268
|
-
restarted on tezgah-frontier with the original brief, never continued; the main thread never switches model (the cache is per model).
|
|
268
|
+
restarted on tezgah-frontier with the original brief, never continued; the main thread never switches model (the cache is per model).
|
|
269
|
+
A subagent's question only the owner can answer (a class-F item, `ESCALATE: owner`) is relayed to
|
|
270
|
+
the user in the approval batch: the orchestrator never answers it for the user
|
|
271
|
+
and never escalates it to a stronger model. A
|
|
269
272
|
code-discovery brief names codegraph's tools (`codegraph callers`, `callees`,
|
|
270
273
|
`impact`, `node`, `files`, `codegraph_explore`) and says "answer from
|
|
271
274
|
the graph, grep only for literal text"; never a grep-only explorer (observed).
|
|
@@ -391,7 +394,11 @@ The reader has to act on the answer, and the friction between "got it" and
|
|
|
391
394
|
line - never also narrate the plan as prose.
|
|
392
395
|
4. End with the approval questions the Ask policy leaves (the executive mode's
|
|
393
396
|
"Ask policy"), or with nothing when none is left. A step you can take
|
|
394
|
-
yourself is taken, never handed over
|
|
397
|
+
yourself is taken, never handed over: "run `pytest -q` and tell me what it
|
|
398
|
+
says" is a step you run. In the host's plan mode nothing can run, and there
|
|
399
|
+
the plan is the deliverable: the approval question is the one question that
|
|
400
|
+
turn asks, every step waits for it, and nothing is reported as done or
|
|
401
|
+
tested, because nothing ran.
|
|
395
402
|
5. Suppress tangents: finish the issue in hand, then raise the second as its own
|
|
396
403
|
question. A question the reader raised mid-work is not a tangent - answer it
|
|
397
404
|
and fold the result in.
|
|
@@ -438,7 +445,8 @@ intuitive". Such a request is NEVER built from a guess. Before any code:
|
|
|
438
445
|
3. Ask at most three blocking questions, the ones that change the outcome and
|
|
439
446
|
that no default may take. Every other question goes in the approval batch
|
|
440
447
|
with a recommended default, and the work proceeds on the defaults in the
|
|
441
|
-
same turn (the executive mode's "Ask policy")
|
|
448
|
+
same turn (the executive mode's "Ask policy"); a class-F item waits for the
|
|
449
|
+
answer.
|
|
442
450
|
4. Verify design, behavior and quality claims against an external source, never
|
|
443
451
|
from memory alone: `~/.config/tezgah/bin/consult --online "<question>"` for a
|
|
444
452
|
fast second opinion, or the OpenResearch CLI (`orx` on PATH, else
|
|
@@ -614,7 +622,12 @@ a third round.
|
|
|
614
622
|
A round spawns its reviewers in ONE message, one per dimension (correctness
|
|
615
623
|
and contract, security, tests and performance) or file group; a diff under 300
|
|
616
624
|
changed lines gets one reviewer. Per-task work gets tests, not a reviewer, and
|
|
617
|
-
`~/.config/tezgah/bin/tezgah-task review` refuses a third round.
|
|
625
|
+
`~/.config/tezgah/bin/tezgah-task review` refuses a third round. No fresh subagent, no review verdict:
|
|
626
|
+
on a host or mode that cannot start a fresh reviewer, say so and ask the owner;
|
|
627
|
+
never record a verdict from your own read. The author contests a confirmed
|
|
628
|
+
finding only by citing evidence the reviewer could read (file:line); knowledge
|
|
629
|
+
outside the files goes to the owner as a question, and the author never lowers
|
|
630
|
+
a severity.
|
|
618
631
|
|
|
619
632
|
**The process is yours: never hand a step back.** The orchestrator runs the
|
|
620
633
|
plan lifecycle itself - `~/.config/tezgah/bin/tezgah-task start`, every `phase` move, `review`,
|
|
@@ -642,13 +655,13 @@ whether the user already decided it (named it this session, or granted it).
|
|
|
642
655
|
| C. Reversible choice the evidence settles | a fix's scope inside the ask, naming, a palette when tokens exist | decide from evidence; real taste goes in the batch with a default, taken this turn |
|
|
643
656
|
| D. Intent fork | two readings of the ask with different deliverables | batch with a default; do the shared part first and proceed on the default |
|
|
644
657
|
| E. Outward on the user's own remote | push, PR, tag, release, publishing the user's own package | act on a grant written in chat ("push without asking", "I approve", "bana sormadan pushla", "onay veriyorum"); else ask once per session, never again after a grant |
|
|
645
|
-
| F. Never on a default | force push or history rewrite on a shared ref; deleting data the session did not make; production deploy, stop or migration; a credential, account or identity change; sending to a third party; a spend over `spend_cap
|
|
658
|
+
| F. Never on a default | force push or history rewrite on a shared ref; deleting data the session did not make; production deploy, stop or migration; a credential, account or identity change; sending to a third party; a spend over `spend_cap`; building a plan this turn wrote from a research, analysis, review or plan ask; 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) | do all other work first, ask, and wait: silence is no consent, and only an answer that names the action and its target clears it |
|
|
646
659
|
| G. ACCESS | a credential or grant no tool supplies | take every route that needs none first, then ask once, naming what it unlocks |
|
|
647
660
|
|
|
648
661
|
A grant is read from the user's prompt and recorded for the session; "artık
|
|
649
662
|
bana sor", "ask me again" or a later "don't push" lifts it. The batch is one
|
|
650
663
|
block in the reply language: one "Approval questions"/"Onay için sorular" heading, one line saying
|
|
651
|
-
the work proceeds on the defaults when no answer comes, then numbered items -
|
|
664
|
+
the work proceeds on the defaults when no answer comes (a class-F item waits for its answer), then numbered items -
|
|
652
665
|
the question in bold, one line of context, "Recommended:"/"Öneri:" the default, and
|
|
653
666
|
the other option. Class-F items come first, marked as waiting; the defaulted
|
|
654
667
|
items follow, with no count cap, because they do not block. An ask tool (omp
|
package/statusline.py
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
python3 statusline.py # Claude Code (or any Claude-format status line)
|
|
5
5
|
python3 statusline.py --cursor # Cursor CLI (same stdin shape, no Orca passthrough)
|
|
6
|
+
python3 statusline.py --qwen # Qwen Code `ui.statusLine` command (same fields read)
|
|
7
|
+
python3 statusline.py --droid # Factory Droid `statusLine` command (`current_dir`)
|
|
8
|
+
python3 statusline.py --copilot # Copilot CLI `statusLine` command (fields undocumented)
|
|
9
|
+
python3 statusline.py --antigravity # Antigravity CLI `statusLine` command (`workspace`)
|
|
6
10
|
|
|
7
11
|
Claude Code's statusLine pipes session JSON on stdin; Cursor's spec is aligned
|
|
8
12
|
with it. This wraps Orca's own status line for Claude, then appends the tezgah
|
|
@@ -35,8 +39,9 @@ except Exception as exc:
|
|
|
35
39
|
|
|
36
40
|
HOME = os.path.expanduser("~")
|
|
37
41
|
ORCA = os.path.join(HOME, ".orca", "agent-hooks", "claude-statusline.sh")
|
|
38
|
-
HOST = "cursor"
|
|
39
|
-
|
|
42
|
+
HOST = next((h for h in ("cursor", "qwen", "droid", "copilot", "antigravity")
|
|
43
|
+
if "--" + h in sys.argv
|
|
44
|
+
or os.environ.get("TEZGAH_STATUS_HOST") == h), "claude")
|
|
40
45
|
|
|
41
46
|
data = sys.stdin.read()
|
|
42
47
|
try:
|
|
@@ -54,8 +59,11 @@ if HOST == "claude" and os.access(ORCA, os.X_OK):
|
|
|
54
59
|
except Exception:
|
|
55
60
|
orca_out = ""
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
# Droid 0.251.0 sends `current_dir` at the top level (its status line snapshot
|
|
63
|
+
# carries session_id, transcript_path, current_dir, workspace, model)
|
|
64
|
+
workspace = payload.get("workspace")
|
|
65
|
+
cwd = ((workspace if isinstance(workspace, dict) else {}).get("current_dir")
|
|
66
|
+
or payload.get("current_dir") or payload.get("cwd") or "")
|
|
59
67
|
try:
|
|
60
68
|
real = os.path.realpath(cwd) if cwd else ""
|
|
61
69
|
except OSError:
|
|
@@ -113,11 +121,14 @@ def claude_used():
|
|
|
113
121
|
return used
|
|
114
122
|
|
|
115
123
|
|
|
116
|
-
|
|
117
|
-
#
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
124
|
+
# Copilot documents its status line input only as "session JSON", so its
|
|
125
|
+
# camelCase spelling is read too
|
|
126
|
+
session_id = payload.get("session_id") or payload.get("sessionId")
|
|
127
|
+
seen = used_kinds(session_id) if HOST != "claude" else claude_used()
|
|
128
|
+
# Cursor's, Qwen's, Droid's, Copilot's and Antigravity's lines read tezgah's store, which sees the tool calls their
|
|
129
|
+
# hook fires on and nothing else; Claude's transcript carries the skill reads too.
|
|
130
|
+
observable = TOOL_USE_MEASURES if HOST != "claude" else None
|
|
131
|
+
segs = health_segments(real or os.getcwd(), session_id,
|
|
121
132
|
used_override=seen, observable=observable)
|
|
122
133
|
# Claude Code and Cursor render ANSI; let a user or a plain terminal opt out.
|
|
123
134
|
color = color_default()
|
|
@@ -114,7 +114,18 @@ const unknown = DIMENSIONS.filter((d, i) => !reviewed[i]).map(d => `review:${d.k
|
|
|
114
114
|
const reviews = reviewed.filter(Boolean)
|
|
115
115
|
|
|
116
116
|
const rank = { high: 0, medium: 1, low: 2 }
|
|
117
|
-
|
|
117
|
+
// two lenses often raise one defect: merge on (file, line, lowercased title), keep the higher severity
|
|
118
|
+
const byKey = new Map()
|
|
119
|
+
let raised = 0
|
|
120
|
+
reviewed.forEach((r, i) => ((r && r.findings) || []).forEach(f => {
|
|
121
|
+
raised++
|
|
122
|
+
const key = `${f.file}\u0000${f.line || ''}\u0000${String(f.title).trim().toLowerCase()}`
|
|
123
|
+
const prev = byKey.get(key)
|
|
124
|
+
const keep = prev && rank[prev.severity] <= rank[f.severity] ? prev : f
|
|
125
|
+
byKey.set(key, { ...keep, dimensions: [...new Set([...((prev && prev.dimensions) || []), DIMENSIONS[i].key])] })
|
|
126
|
+
}))
|
|
127
|
+
const all = [...byKey.values()].sort((a, b) => rank[a.severity] - rank[b.severity])
|
|
128
|
+
const caps = { max_verified: MAX_VERIFIED, raised, deduplicated: raised - all.length }
|
|
118
129
|
if (!all.length) {
|
|
119
130
|
const note = !unknown.length ? 'all four dimensions came back clean'
|
|
120
131
|
: !reviews.length ? 'every dimension failed - nothing was reviewed; unknown, not clean'
|
|
@@ -124,7 +135,7 @@ if (!all.length) {
|
|
|
124
135
|
|
|
125
136
|
const toVerify = all.slice(0, MAX_VERIFIED)
|
|
126
137
|
if (all.length > toVerify.length) {
|
|
127
|
-
log(`${all.length} findings
|
|
138
|
+
log(`${all.length} distinct findings (${caps.deduplicated} duplicates merged); verifying the ${toVerify.length} most severe. NOT verified (reported as unverified): ${all.slice(MAX_VERIFIED).map(f => f.title).join('; ')}`)
|
|
128
139
|
}
|
|
129
140
|
|
|
130
141
|
phase('Verify')
|
|
@@ -167,4 +178,5 @@ return {
|
|
|
167
178
|
unverified: unsettled.map(v => ({ ...v.finding, verdict: 'UNVERIFIED - every refuter failed' }))
|
|
168
179
|
.concat(all.slice(MAX_VERIFIED).map(f => ({ ...f, verdict: 'UNVERIFIED - budget cap' }))),
|
|
169
180
|
unknown,
|
|
181
|
+
caps,
|
|
170
182
|
}
|