@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
package/hooks/tezgah_gate.py
CHANGED
|
@@ -114,7 +114,7 @@ from tezgah_integrity import (BASH_TOOLS, BEGAN_KIND, ENV_WORD, GIT_VALUE_OPTS,
|
|
|
114
114
|
write_texts)
|
|
115
115
|
from tezgah_integrity import (SUBAGENT_TOOLS, TODO_TOOLS, file_url_path, newest_todo,
|
|
116
116
|
ready_heads)
|
|
117
|
-
from tezgah_paths import (CACHE, CONFIG_DIR, HOST_DIRS, OFF_DIRS, PLUGIN_ROOT,
|
|
117
|
+
from tezgah_paths import (AMP_PLUGIN, CACHE, CONFIG_DIR, GOOSE_PLUGIN, HOST_DIRS, OFF_DIRS, PLUGIN_ROOT,
|
|
118
118
|
REPO_MARKS, SWITCHES, _toplevel, cache_dir, fallback_cache,
|
|
119
119
|
linked_main, off, root_for, roots)
|
|
120
120
|
|
|
@@ -193,11 +193,11 @@ PATTERN_SEARCH_TOOLS = ("grep", "search", "ast_grep")
|
|
|
193
193
|
BASH_SEARCH = re.compile(r"(?:^|[|;&(]\s*|\s)(?:grep|rg)\s+((?:-\S+\s+)*)(\S+)")
|
|
194
194
|
# the credit forms, not a bare tool name: "OpenAI" or "Claude Code" in a
|
|
195
195
|
# sentence (naming a tool to use it) is fine, "Co-Authored-By: ..." or a
|
|
196
|
-
# generator named as the author (signing it) is the thing to stop.
|
|
197
|
-
#
|
|
198
|
-
# report generated by the build script, or
|
|
199
|
-
# while AI / an LLM / GPT / Claude / ... as that object is a signature, so
|
|
200
|
-
# verb is tied to the name it credits. `_CREDIT` is shared with the anchored
|
|
196
|
+
# generator named as the author (signing it) is the thing to stop. A verb with
|
|
197
|
+
# "by"/"with"/"using" credits only when its object names the machine or a model:
|
|
198
|
+
# a report generated by the build script, or a note drafted with the owner, is
|
|
199
|
+
# prose, while AI / an LLM / GPT / Claude / ... as that object is a signature, so
|
|
200
|
+
# each verb is tied to the name it credits. `_CREDIT` is shared with the anchored
|
|
201
201
|
# twin below so the two forms cannot drift apart.
|
|
202
202
|
#
|
|
203
203
|
# The cost of the model-object branch is deliberate and it falls on a message
|
|
@@ -206,12 +206,26 @@ BASH_SEARCH = re.compile(r"(?:^|[|;&(]\s*|\s)(?:grep|rg)\s+((?:-\S+\s+)*)(\S+)")
|
|
|
206
206
|
# path pays nothing for it - `ATTRIB_LINE` anchors, so documentation that merely
|
|
207
207
|
# names a form mid-sentence lands - and splitting the two sets apart was tried
|
|
208
208
|
# and reverted, because the anchored half then missed `-m "Built by GPT"`.
|
|
209
|
+
# A `with`/`using` credit is a signature only when the name ends its clause
|
|
210
|
+
# ("Drafted with Claude", "Written using Claude Code.", "Generated using GPT-4,
|
|
211
|
+
# reviewed by me", "Written with Claude (Opus)"): "built with cursor support" or
|
|
212
|
+
# "client built using the OpenAI SDK" is prose, so that form takes a name that
|
|
213
|
+
# a quote, a comma, a dash or the line's end closes, and never the word `cursor`
|
|
214
|
+
# (review of plan 137). `by` keeps every name: "built by Cursor" is a credit.
|
|
215
|
+
# ponytail: "Drafted with Cursor" passes, and "built using OpenAI, Anthropic and
|
|
216
|
+
# others" is denied once; the deny names the reason, so one rephrase clears it.
|
|
209
217
|
_CREDIT = (
|
|
210
218
|
r"co-authored-by\s*:|generated with|made with|"
|
|
211
|
-
r"(?:generated|written|authored|built|assisted)
|
|
212
|
-
r"(?:(?:an?|the)\s+)?"
|
|
219
|
+
r"(?:generated|written|authored|built|assisted|drafted|produced|composed)"
|
|
220
|
+
r"\s+by\s+(?:(?:an?|the)\s+)?"
|
|
213
221
|
r"(?:ai|llm|claude|chatgpt|gpt|copilot|codex|cursor|gemini|deepseek|"
|
|
214
|
-
r"openai|anthropic)\b|
|
|
222
|
+
r"openai|anthropic)\b|"
|
|
223
|
+
r"(?:generated|written|authored|built|assisted|drafted|produced|composed)"
|
|
224
|
+
r"\s+(?:with|using)\s+(?:(?:an?|the)\s+)?"
|
|
225
|
+
r"(?:ai|llm|claude|chatgpt|gpt|copilot|codex|gemini|deepseek|openai|anthropic)"
|
|
226
|
+
r"(?:-[\w.]+)?(?:\s+(?:code|cli))?(?:\s*\([^)\n]*\))?"
|
|
227
|
+
r"(?=[.!)\]*_`]*(?:[\x22\x27,;]|\s+-\s|\s*(?:\n|$)))|"
|
|
228
|
+
r"noreply@anthropic|\U0001F916")
|
|
215
229
|
ATTRIB = re.compile(_CREDIT, re.I)
|
|
216
230
|
# The same forms anchored to the start of a line, for the text a write/edit tool
|
|
217
231
|
# is about to land. A credit is a signature: it owns its line, bare or behind a
|
|
@@ -2014,6 +2028,37 @@ def workspace_reason(inp, cwd, base):
|
|
|
2014
2028
|
return None
|
|
2015
2029
|
|
|
2016
2030
|
|
|
2031
|
+
# What this closes (plan 136): `tezgah-research init` refused a line while
|
|
2032
|
+
# another session's line was in flight, and the session ran its whole line under
|
|
2033
|
+
# `.tezgah/research/.staging/<slug>/` - a dot dir `slugs()` skips, so `status`
|
|
2034
|
+
# and `check` never saw it. A dot dir directly under research/ is refused; the
|
|
2035
|
+
# line layouts (`open/`, `done/`) and dot files beside them (`.gitkeep`) are not.
|
|
2036
|
+
# Read against the main checkout (a linked worktree's session writes the main
|
|
2037
|
+
# checkout's `.tezgah`) and against the session's own root.
|
|
2038
|
+
RESEARCH_HIDDEN_DENY = (
|
|
2039
|
+
"Workspace gate: %s is under a hidden dir. A research line lives under "
|
|
2040
|
+
".tezgah/research/open or done; a hidden dir there is invisible to status and "
|
|
2041
|
+
"check. When `~/.config/tezgah/bin/tezgah-research` refuses a line, that "
|
|
2042
|
+
"refusal goes to the owner - never a way around it.")
|
|
2043
|
+
|
|
2044
|
+
|
|
2045
|
+
def research_hidden_reason(inp, cwd, base):
|
|
2046
|
+
"""A deny reason when this write lands under `.tezgah/research/.<name>/` or
|
|
2047
|
+
`.tezgah/research/open|done/.<name>/` (what `stray_dirs` reports), else None."""
|
|
2048
|
+
if tezgah_task is None:
|
|
2049
|
+
return None
|
|
2050
|
+
for path in write_paths(inp):
|
|
2051
|
+
for rel in (tezgah_task.main_relative(path, cwd, base),
|
|
2052
|
+
tezgah_task.relative(path, cwd, base)):
|
|
2053
|
+
parts = (rel or "").casefold().split("/")
|
|
2054
|
+
if len(parts) > 3 and parts[:2] == [".tezgah", "research"] \
|
|
2055
|
+
and (parts[2].startswith(".") or (
|
|
2056
|
+
len(parts) > 4 and parts[2] in ("open", "done")
|
|
2057
|
+
and parts[3].startswith("."))):
|
|
2058
|
+
return RESEARCH_HIDDEN_DENY % path
|
|
2059
|
+
return None
|
|
2060
|
+
|
|
2061
|
+
|
|
2017
2062
|
# --- lesson lineage: a ledger write in a turn that read untrusted text -------
|
|
2018
2063
|
# What this records (security-08): any session can append a standing constraint
|
|
2019
2064
|
# to `.tezgah/lessons.md`, and a line written in a turn that read a web page, an
|
|
@@ -2755,8 +2800,9 @@ CONTROL_CLIS = {"tezgah-gate": ("decide",), "tezgah-capture": "",
|
|
|
2755
2800
|
# under `hosts/<host>/` (HOOK_HOSTS); its launchers carry their own names.
|
|
2756
2801
|
HOOK_ENTRIES = frozenset(("projects-auto-init", "projects-pretooluse",
|
|
2757
2802
|
"projects-posttooluse", "projects-stop",
|
|
2758
|
-
"tezgah-codex-hook", "tezgah-cursor-hook"
|
|
2759
|
-
|
|
2803
|
+
"tezgah-codex-hook", "tezgah-cursor-hook", "tezgah-qwen-hook",
|
|
2804
|
+
"tezgah-droid-hook", "tezgah-commandcode-hook", "tezgah-copilot-hook", "tezgah-gemini-hook", "tezgah-antigravity-hook", "tezgah-goose-hook"))
|
|
2805
|
+
HOOK_HOSTS = frozenset(("codex", "cursor", "omp", "qwen", "droid", "commandcode", "copilot", "gemini", "antigravity", "goose", "amp"))
|
|
2760
2806
|
# Programs that change a path named in their arguments. Every positional is a
|
|
2761
2807
|
# target, except for the copiers, whose last one is (`cp switch /tmp` reads it).
|
|
2762
2808
|
# `sed`/`perl` count only with an in-place flag. `sqlite3` rewrites the cache
|
|
@@ -2797,9 +2843,9 @@ CONTROL_CACHE = ("evidence", "tezgah.db", "tezgah.db-wal", "tezgah.db-shm", "ses
|
|
|
2797
2843
|
"switches", "gate-inactive", "harness-drift", "import-crash",
|
|
2798
2844
|
"workspace-index.json")
|
|
2799
2845
|
# Top-level keys tezgah writes into Claude's settings (bin/tezgah-setup
|
|
2800
|
-
# wire_claude_statusline, wire_claude_attribution)
|
|
2801
|
-
#
|
|
2802
|
-
|
|
2846
|
+
# wire_claude_statusline, wire_claude_attribution) and the ones that turn hooks off: Claude's
|
|
2847
|
+
# `disableAllHooks`, Gemini's `hooksConfig` (Goose's `disabledPlugins` is owned per tezgah entry).
|
|
2848
|
+
OWNED_KEYS = ("statusLine", "attribution", "disableAllHooks", "hooksConfig")
|
|
2803
2849
|
PATCH_DELETE = re.compile(r"(?m)^\*\*\* Delete File: (\S.*?)\s*$")
|
|
2804
2850
|
|
|
2805
2851
|
|
|
@@ -2884,6 +2930,11 @@ def _registration(real):
|
|
|
2884
2930
|
shared = (os.path.join(HOST_DIRS["codex"], "hooks.json"),
|
|
2885
2931
|
os.path.join(HOST_DIRS["codex"], "config.toml"),
|
|
2886
2932
|
os.path.join(HOST_DIRS["cursor"], "hooks.json"),
|
|
2933
|
+
os.path.join(HOST_DIRS["qwen"], "settings.json"),
|
|
2934
|
+
# Droid reads hooks.json, else the `hooks` key of its settings.json
|
|
2935
|
+
os.path.join(HOST_DIRS["droid"], "hooks.json"),
|
|
2936
|
+
os.path.join(HOST_DIRS["droid"], "settings.json"),
|
|
2937
|
+
os.path.join(HOST_DIRS["commandcode"], "settings.json"), os.path.join(HOST_DIRS["copilot"], "settings.json"), os.path.join(HOST_DIRS["gemini"], "settings.json"), os.path.join(HOST_DIRS["antigravity"], "settings.json"), os.path.join(os.path.dirname(HOST_DIRS["antigravity"]), "config", "hooks.json"), os.path.join(HOST_DIRS["goose"], "settings.json"),
|
|
2887
2938
|
os.path.join(claude, "plugins", "installed_plugins.json"),
|
|
2888
2939
|
os.path.join(claude, "plugins", "known_marketplaces.json"))
|
|
2889
2940
|
if (name in ("settings.json", "settings.local.json")
|
|
@@ -2894,8 +2945,8 @@ def _registration(real):
|
|
|
2894
2945
|
return "shared"
|
|
2895
2946
|
owned = [os.path.join(HOST_DIRS["opencode"], d, "tezgah.js")
|
|
2896
2947
|
for d in ("plugins", "plugin")]
|
|
2897
|
-
owned
|
|
2898
|
-
|
|
2948
|
+
owned += [os.path.join(HOST_DIRS["omp"], "agent", "hooks", "pre", "tezgah-hook.ts"),
|
|
2949
|
+
os.path.join(HOST_DIRS["copilot"], "hooks", "tezgah.json")] + [os.path.join(HOST_DIRS["gemini"], "extensions", "tezgah", *p) for p in (("gemini-extension.json",), ("hooks", "hooks.json"))] + [os.path.join(GOOSE_PLUGIN, *p) for p in (("plugin.json",), ("hooks", "hooks.json"))] + [AMP_PLUGIN]
|
|
2899
2950
|
cache = os.path.realpath(os.path.join(claude, "plugins", "cache"))
|
|
2900
2951
|
if real in {_real(p, None) for p in owned} or (
|
|
2901
2952
|
_under(real, cache) and "tezgah" in real[len(cache):].lower()):
|
|
@@ -2988,14 +3039,14 @@ def _project(node):
|
|
|
2988
3039
|
|
|
2989
3040
|
def _owned(node, path=()):
|
|
2990
3041
|
"""The tezgah-owned entries of a parsed JSON document, position-free: the
|
|
2991
|
-
|
|
3042
|
+
OWNED_KEYS top-level keys, any key naming tezgah, and each list item that
|
|
2992
3043
|
names tezgah, projected (`_project`)."""
|
|
2993
3044
|
def dump(value):
|
|
2994
3045
|
return json.dumps(value, sort_keys=True)
|
|
2995
3046
|
if isinstance(node, dict):
|
|
2996
3047
|
out = []
|
|
2997
3048
|
for key, value in node.items():
|
|
2998
|
-
if (not path and key in
|
|
3049
|
+
if (not path and key in OWNED_KEYS) or "tezgah" in key.lower():
|
|
2999
3050
|
out.append((path + (key,), dump(value)))
|
|
3000
3051
|
else:
|
|
3001
3052
|
out += _owned(value, path + (key,))
|
|
@@ -3584,8 +3635,11 @@ def decision(tool, inp, cwd, session_id=None, record=True, agent=None):
|
|
|
3584
3635
|
check=1 if t in BASH_TOOLS and verify_command(command, cwd) else None)
|
|
3585
3636
|
# Claude's Task names the agent `subagent_type`; omp's task names it `agent`,
|
|
3586
3637
|
# on the call (flat shape) or on each `tasks[]` item (batch shape).
|
|
3587
|
-
|
|
3588
|
-
|
|
3638
|
+
# A shell tool's `args` can be a list (Command Code splits argv there): only
|
|
3639
|
+
# a dict carries a subagent type, and a list must not crash the decision,
|
|
3640
|
+
# which the hook's guard would turn into a silent pass (plan 139)
|
|
3641
|
+
args = inp.get("args") if isinstance(inp.get("args"), dict) else {}
|
|
3642
|
+
subs = [inp.get("subagent_type"), args.get("subagent_type"), inp.get("agent")]
|
|
3589
3643
|
subs += [item.get("agent") for item in inp.get("tasks") or ()
|
|
3590
3644
|
if isinstance(item, dict)]
|
|
3591
3645
|
# The body a shell call writes, read once: the three write-tool-only rules
|
|
@@ -3705,9 +3759,10 @@ def decision(tool, inp, cwd, session_id=None, record=True, agent=None):
|
|
|
3705
3759
|
if reason:
|
|
3706
3760
|
return _deny(session_id, "task", reason, tool, inp, base)
|
|
3707
3761
|
# Workspace: tezgah state belongs under .tezgah/, never a root plans/,
|
|
3708
|
-
# research/ or analysis/ the project does not own (see workspace_reason)
|
|
3762
|
+
# research/ or analysis/ the project does not own (see workspace_reason),
|
|
3763
|
+
# nor a hidden dir under .tezgah/research/ (see research_hidden_reason).
|
|
3709
3764
|
if not off("workspace-off") and t in WRITE_TOOLS + BASH_TOOLS:
|
|
3710
|
-
reason = workspace_reason(inp, cwd, base)
|
|
3765
|
+
reason = workspace_reason(inp, cwd, base) or research_hidden_reason(inp, cwd, base)
|
|
3711
3766
|
if reason:
|
|
3712
3767
|
return _deny(session_id, "workspace", reason, tool, inp, base)
|
|
3713
3768
|
# MCP: a server's effect tool lands text the way a write tool or a `gh`
|
|
@@ -3896,7 +3951,8 @@ ASK_NO_DEFAULT = (
|
|
|
3896
3951
|
"default (omp `ask`: `recommended`; AskUserQuestion: a \"({RECOMMENDED})\" "
|
|
3897
3952
|
"option), and the work proceeds on it unless the step waits on the user's "
|
|
3898
3953
|
"answer by nature (a force push, a delete of what the session did not "
|
|
3899
|
-
"make, production, a credential, a spend over the cap
|
|
3954
|
+
"make, production, a credential, a spend over the cap, a recommended "
|
|
3955
|
+
"variant, a way around a tezgah refusal, another session's work). ")
|
|
3900
3956
|
ASK_GRANTED = (
|
|
3901
3957
|
"tezgah: the user already granted %s this session; that step is taken, "
|
|
3902
3958
|
"not asked again.")
|
|
@@ -387,13 +387,22 @@ SHAPE_BLOCKS = frozenset(("placating opener", "forbidden closer", "list cap",
|
|
|
387
387
|
# agent could decide is no false completion. Counted in `blocked_claims` under
|
|
388
388
|
# their own class, kept out of `false_completion`.
|
|
389
389
|
LIST_BLOCKS = frozenset(("residual work", "ready steps", "stopped slice",
|
|
390
|
-
"needless ask"))
|
|
390
|
+
"needless ask", "handed step"))
|
|
391
|
+
# The host permission modes in which the session can neither write nor run: a
|
|
392
|
+
# plan-mode turn reads and proposes, so the step it names is the user's to
|
|
393
|
+
# approve, not one it could have taken (`read_only_turn`). Claude, Command
|
|
394
|
+
# Code and Qwen spell it `plan`, Droid `spec`; omp's extension sends `plan`
|
|
395
|
+
# from `sessionModes.getState()`. A host that sends no mode reads as `default`,
|
|
396
|
+
# which is what every rule did before the field.
|
|
397
|
+
READ_ONLY_MODES = frozenset(("plan", "spec"))
|
|
391
398
|
|
|
392
399
|
WRITE_TOOLS = ("edit", "write", "multiedit", "notebookedit", "apply_patch",
|
|
393
400
|
"str_replace_editor", "create_file", "str_replace", "edit_file",
|
|
394
401
|
"write_file", "search_replace")
|
|
402
|
+
# `monitor` is Claude's and Qwen's background watch: its `command` is a shell
|
|
403
|
+
# command (review of plan 138), so every shell rule reads it.
|
|
395
404
|
BASH_TOOLS = ("bash", "shell", "command", "exec_command", "run_command",
|
|
396
|
-
"powershell", "pwsh")
|
|
405
|
+
"powershell", "pwsh", "monitor")
|
|
397
406
|
# The read/search tools: known calls that do no step of work and that no rule
|
|
398
407
|
# reads a row for, spelled as the hosts send them (Claude/Cursor's capitalized
|
|
399
408
|
# set, omp's lowercase one, codex's `grep`). They are not `unknown` - the ledger
|
|
@@ -517,7 +526,7 @@ LEDGER_FIELDS = frozenset(("id", "exit", "out_bytes", "fail_class", "workspace",
|
|
|
517
526
|
"check", "key", "block", "repo", "empty_run", "cause",
|
|
518
527
|
"host", "switches", "harness", "authorized", "inflight",
|
|
519
528
|
"reask", "open", "ready", "blocked", "heads", "residual",
|
|
520
|
-
"questions", "defaults", "taken", "partial"))
|
|
529
|
+
"questions", "defaults", "taken", "partial", "mode"))
|
|
521
530
|
|
|
522
531
|
# The row contract's own version, stamped by the writer beside `kind` and `ts` so
|
|
523
532
|
# it is not a caller field. It exists because a row is read back to decide a
|
|
@@ -3220,7 +3229,7 @@ def classify(tool, inp, cwd=None):
|
|
|
3220
3229
|
# host can put in front of the model; the taint notice that flags a later effect
|
|
3221
3230
|
# in such a turn is the untrusted module's own, not the gate's.
|
|
3222
3231
|
WEB_TOOLS = ("web_search", "websearch", "web_fetch", "webfetch", "fetch",
|
|
3223
|
-
"browser", "browse")
|
|
3232
|
+
"browser", "browse", "read_web_page")
|
|
3224
3233
|
MCP_TOOL = re.compile(r"^mcp__", re.I)
|
|
3225
3234
|
# The verb classes of an MCP tool, read off its name: what the server is asked to
|
|
3226
3235
|
# do picks which of the gate's existing content rules read the payload
|
|
@@ -4058,26 +4067,67 @@ NEGATION_BEFORE = re.compile(r"(?:%s)(?:\s+\S+){0,2}\s+\Z" % alts("negation_befo
|
|
|
4058
4067
|
QUESTION_PARTICLE = re.compile(r"\s*(?:%s)\b" % alts("question_particle"), re.I)
|
|
4059
4068
|
CLAUSE_END = re.compile(r"[.!?\n,;:\u2014\u2013()]|\b(?:%s)\b" % alts("clause_join"),
|
|
4060
4069
|
re.I)
|
|
4070
|
+
# The first-person claim that a command or a check was run ("test ettim", "i
|
|
4071
|
+
# ran the suite"): the one claim a read-only turn cannot have earned, read by
|
|
4072
|
+
# the `plan claim` class. Narrower than DONE/VERIFIED on purpose - a plan
|
|
4073
|
+
# states its acceptance in those words (plan 142, review round 2).
|
|
4074
|
+
RAN_IT = re.compile(alts("ask", "ran_it"), re.I)
|
|
4061
4075
|
|
|
4062
4076
|
|
|
4063
|
-
def
|
|
4064
|
-
"""
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
rows. ponytail: a window of words, not a parser."""
|
|
4077
|
+
def _asserted(pattern, text):
|
|
4078
|
+
"""The first match of `pattern` in `text` that is neither inside a question
|
|
4079
|
+
nor under a negation of its own clause, else None. ponytail: a window of
|
|
4080
|
+
words, not a parser."""
|
|
4068
4081
|
t = str(text or "")
|
|
4082
|
+
for m in pattern.finditer(t):
|
|
4083
|
+
before = CLAUSE_END.split(t[max(0, m.start() - 80):m.start()])[-1]
|
|
4084
|
+
end = CLAUSE_END.search(t, m.end())
|
|
4085
|
+
asked = (QUESTION_PARTICLE.match(t, m.end())
|
|
4086
|
+
or (end and end.group(0) == "?"))
|
|
4087
|
+
if not (NEGATION_BEFORE.search(before) or asked):
|
|
4088
|
+
return m
|
|
4089
|
+
return None
|
|
4069
4090
|
|
|
4070
|
-
def said(pattern):
|
|
4071
|
-
for m in pattern.finditer(t):
|
|
4072
|
-
before = CLAUSE_END.split(t[max(0, m.start() - 80):m.start()])[-1]
|
|
4073
|
-
end = CLAUSE_END.search(t, m.end())
|
|
4074
|
-
asked = (QUESTION_PARTICLE.match(t, m.end())
|
|
4075
|
-
or (end and end.group(0) == "?"))
|
|
4076
|
-
if not (NEGATION_BEFORE.search(before) or asked):
|
|
4077
|
-
return True
|
|
4078
|
-
return False
|
|
4079
4091
|
|
|
4080
|
-
|
|
4092
|
+
def asserted_claims(text):
|
|
4093
|
+
"""DONE/VERIFIED, with a claim word inside a question or under a negation of
|
|
4094
|
+
its own clause not counted (`_asserted`). Read by `_stop_block` on the
|
|
4095
|
+
no-work path and by `stop_reason` for the claim row; a turn that did work is
|
|
4096
|
+
still judged on its rows."""
|
|
4097
|
+
return bool(_asserted(DONE, text)), bool(_asserted(VERIFIED, text))
|
|
4098
|
+
|
|
4099
|
+
|
|
4100
|
+
def reply_prose(value):
|
|
4101
|
+
"""The reply's prose, whatever the host handed the Stop rule (plan 143).
|
|
4102
|
+
|
|
4103
|
+
omp's `session_stop` carries `last_assistant_message` as the message
|
|
4104
|
+
OBJECT its `getLastAssistantMessage()` returns, not as text, and the bridge
|
|
4105
|
+
passed it through: the rule then judged `str(dict)` - one line, no list
|
|
4106
|
+
items, the prose diluted by the object's own fields. Measured on the
|
|
4107
|
+
owner's ledger, 1643 of 1656 omp `shape` rows of 45 days carried
|
|
4108
|
+
`lines: 1`, against 209 of 231 multi-line on every other host, so every
|
|
4109
|
+
line-shaped class (the list cap, the residual sections, the question the
|
|
4110
|
+
reply ends on, the handed step) read a text that had no lines.
|
|
4111
|
+
|
|
4112
|
+
A string passes through. A message object yields its `text` blocks, joined
|
|
4113
|
+
the way the model wrote them; a thinking or tool block is not the reply.
|
|
4114
|
+
Anything else is no reply at all, which is what the rule already does with
|
|
4115
|
+
a missing one: it never becomes a judgement on a repr.
|
|
4116
|
+
"""
|
|
4117
|
+
if isinstance(value, str):
|
|
4118
|
+
return value
|
|
4119
|
+
if isinstance(value, dict):
|
|
4120
|
+
blocks = value.get("content")
|
|
4121
|
+
if isinstance(blocks, str):
|
|
4122
|
+
return blocks
|
|
4123
|
+
if isinstance(blocks, list):
|
|
4124
|
+
return "\n".join(
|
|
4125
|
+
block["text"] for block in blocks
|
|
4126
|
+
if isinstance(block, dict) and block.get("type") == "text"
|
|
4127
|
+
and isinstance(block.get("text"), str)).strip()
|
|
4128
|
+
if isinstance(value.get("text"), str):
|
|
4129
|
+
return value["text"]
|
|
4130
|
+
return ""
|
|
4081
4131
|
|
|
4082
4132
|
|
|
4083
4133
|
def passing_check(entry):
|
|
@@ -4840,10 +4890,14 @@ def _record_only(refused, turns, after_block, cls):
|
|
|
4840
4890
|
|
|
4841
4891
|
|
|
4842
4892
|
def stop_reason(text, session_id, cwd=None, after_block=0,
|
|
4843
|
-
subagent=False, agent=None, parent=None):
|
|
4893
|
+
subagent=False, agent=None, parent=None, mode=None):
|
|
4844
4894
|
"""Why this turn must not end yet, or None. Used by the Stop hooks (Claude,
|
|
4845
4895
|
Codex, omp and Cursor, which share the payload fields and the block envelope).
|
|
4846
4896
|
`cwd` is the session's directory, read for the repo's `.no-adhd` mark.
|
|
4897
|
+
`mode` is the host's permission mode for this turn (Claude, Command Code
|
|
4898
|
+
and Qwen send `permission_mode`; omp's extension sends its plan state as
|
|
4899
|
+
one): a read-only one (`read_only_turn`) exempts the approval question the
|
|
4900
|
+
mode asks for and refuses the done/tested claim it cannot have earned.
|
|
4847
4901
|
|
|
4848
4902
|
The verdict is read off the current turn's rows (`turn_rows`) and not the
|
|
4849
4903
|
whole ledger: the file grows with the session, the handler runs once per
|
|
@@ -4889,15 +4943,19 @@ def stop_reason(text, session_id, cwd=None, after_block=0,
|
|
|
4889
4943
|
the child keeps a ledger of its own (omp's task sessions); the child's own
|
|
4890
4944
|
rows still judge it. Both return None. A reply counts as
|
|
4891
4945
|
claiming only with `asserted_claims`: "Testler geçti mi?" leaves no
|
|
4892
|
-
`claim ok` row.
|
|
4946
|
+
`claim ok` row. `text` is read through `reply_prose`, so a host that hands
|
|
4947
|
+
over a message object rather than its text is judged on the prose (plan
|
|
4948
|
+
143)."""
|
|
4949
|
+
text = reply_prose(text)
|
|
4893
4950
|
rows, turns = turn_rows(session_id, turns=True)
|
|
4894
4951
|
chain, rows = (_turn_refusals(session_id, rows) if turns
|
|
4895
4952
|
else (list(filter(_blocked, rows)), rows))
|
|
4896
4953
|
refused = [str(entry.get("detail"))[len("blocked: "):] for entry in chain]
|
|
4897
4954
|
if after_block and not subagent and not refused:
|
|
4898
4955
|
return None
|
|
4956
|
+
read_only = read_only_turn(mode)
|
|
4899
4957
|
cls, reason = _stop_block(text, session_id, rows=rows, cwd=cwd,
|
|
4900
|
-
shape=not subagent)
|
|
4958
|
+
shape=not subagent, read_only=read_only)
|
|
4901
4959
|
record_only = subagent or bool(
|
|
4902
4960
|
(refused if turns else after_block)
|
|
4903
4961
|
and (not reason or _record_only(refused, turns, after_block, cls)))
|
|
@@ -4905,7 +4963,11 @@ def stop_reason(text, session_id, cwd=None, after_block=0,
|
|
|
4905
4963
|
# Keyed like the claim row, so Cursor's re-run of the handler on a follow-up
|
|
4906
4964
|
# does not count the same reply twice; above the early return, so a reply
|
|
4907
4965
|
# with no claim vocabulary still leaves its row.
|
|
4966
|
+
# `mode` rides every row of a read-only turn: the verdict depended on it,
|
|
4967
|
+
# and a replay of the ledger reads it back (`tezgah_replay._replay_one`).
|
|
4908
4968
|
shape = reply_shape(text)
|
|
4969
|
+
if read_only:
|
|
4970
|
+
shape = dict(shape, mode=str(mode).strip().lower())
|
|
4909
4971
|
if not record_only and not any(entry.get("kind") == "shape"
|
|
4910
4972
|
and entry.get("id") == key for entry in rows):
|
|
4911
4973
|
note(session_id, "shape", ",".join(shape_flags(text)) or "ok", id=key,
|
|
@@ -5356,7 +5418,8 @@ def _settled(rows):
|
|
|
5356
5418
|
return not after or _bookkeeping_turn(after)
|
|
5357
5419
|
|
|
5358
5420
|
|
|
5359
|
-
def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None
|
|
5421
|
+
def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None,
|
|
5422
|
+
read_only=False):
|
|
5360
5423
|
"""stop_reason's decision as (reason class, block text), without the ledger
|
|
5361
5424
|
side effect. The class names the branch that refused the turn; the text is
|
|
5362
5425
|
what the host shows the model.
|
|
@@ -5413,11 +5476,18 @@ def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None):
|
|
|
5413
5476
|
`fold` is the evidence fold the selector below calls. None, the default,
|
|
5414
5477
|
is the temporal spec's (`tezgah_stopspec.fold`: the class from its formulas,
|
|
5415
5478
|
the text from `_evidence_text`), and that outer call fills the text's
|
|
5416
|
-
placeholders once (`_said`).
|
|
5479
|
+
placeholders once (`_said`).
|
|
5480
|
+
|
|
5481
|
+
`read_only` is the host's word that this turn ran in a plan mode
|
|
5482
|
+
(`read_only_turn`), where no command and no write was possible. Two classes
|
|
5483
|
+
read it: `plan claim` below, which refuses a done/tested claim such a turn
|
|
5484
|
+
cannot have behind it, and the `needless ask` exemption in
|
|
5485
|
+
`_residual_block`, since the approval question is what the mode asks for."""
|
|
5417
5486
|
if fold is None:
|
|
5418
5487
|
# imported here: tezgah_stopspec imports this module at its top
|
|
5419
5488
|
import tezgah_stopspec
|
|
5420
|
-
verdict = _stop_block(text, session_id, rows, cwd, shape,
|
|
5489
|
+
verdict = _stop_block(text, session_id, rows, cwd, shape,
|
|
5490
|
+
tezgah_stopspec.fold, read_only)
|
|
5421
5491
|
return (verdict[0], verdict[1] and _said(verdict[1]))
|
|
5422
5492
|
t = str(text or "")
|
|
5423
5493
|
shaped = _shape_block(t, cwd) if shape else (None, None)
|
|
@@ -5431,6 +5501,19 @@ def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None):
|
|
|
5431
5501
|
return ("evidence tampered", TAMPERED)
|
|
5432
5502
|
if (done or verified) and any(r.get(ORPHAN) for r in rows):
|
|
5433
5503
|
return ("evidence tampered", ORPHANED)
|
|
5504
|
+
# A read-only turn (the host's plan mode) ran no check, so a reply that says
|
|
5505
|
+
# it ran one says what cannot have happened - unless a check of its own
|
|
5506
|
+
# passed, which is evidence like any other. The trigger is the first-person
|
|
5507
|
+
# run claim (RAN_IT: "test ettim", "i ran the suite"), never the done or
|
|
5508
|
+
# verified vocabulary: a plan states its acceptance in those words ("Done
|
|
5509
|
+
# when all tests pass", "her adım `pytest -k apps` ile doğrulanır") and
|
|
5510
|
+
# review round 2 of plan 142 caught both refused. Above `_admitted`:
|
|
5511
|
+
# "doğrulanmadı" repairs an unverified claim, and here nothing was checked.
|
|
5512
|
+
if read_only and not any(passing_check(r) for r in rows):
|
|
5513
|
+
ran = _asserted(RAN_IT, t)
|
|
5514
|
+
if ran:
|
|
5515
|
+
return ("plan claim",
|
|
5516
|
+
PLAN_CLAIM % cut(" ".join(ran.group(0).split()), 64))
|
|
5434
5517
|
fold = _admitted(fold, t)
|
|
5435
5518
|
# The trigger is the turn's own evidence, not its words. The claim vocabulary
|
|
5436
5519
|
# below catches a claim-shaped reply; it missed the same unfounded state
|
|
@@ -5487,11 +5570,12 @@ def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None):
|
|
|
5487
5570
|
# shape that used to leave such a claim unjudged.
|
|
5488
5571
|
external = _external_claim(t)
|
|
5489
5572
|
if (not worked or read) and not (done or verified) and not external:
|
|
5490
|
-
return _residual_block(t, rows, shape, session_id=session_id, cwd=cwd
|
|
5573
|
+
return _residual_block(t, rows, shape, session_id=session_id, cwd=cwd,
|
|
5574
|
+
read_only=read_only)
|
|
5491
5575
|
if lost:
|
|
5492
5576
|
return _residual_block(t, rows, shape,
|
|
5493
5577
|
fold(rows, worked | {BEGAN_KIND}, external),
|
|
5494
|
-
session_id, cwd)
|
|
5578
|
+
session_id, cwd, read_only)
|
|
5495
5579
|
# A check that answered with an outcome nobody saw (exit 0 and nothing
|
|
5496
5580
|
# returned, a status a pipe owns: not `passing_check`) is no pass, and as a
|
|
5497
5581
|
# check it is no work row either. A done or tested claim resting on it owes
|
|
@@ -5524,7 +5608,7 @@ def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None):
|
|
|
5524
5608
|
if not external and not unseen and _settled(srows):
|
|
5525
5609
|
return _residual_block(t, rows, shape, fold(
|
|
5526
5610
|
_before_turn(srows), True, None, "this session"),
|
|
5527
|
-
session_id, cwd)
|
|
5611
|
+
session_id, cwd, read_only)
|
|
5528
5612
|
if (not worked or quiet) and (done or verified):
|
|
5529
5613
|
# A claim in a turn that did nothing: "Tamamlandı, tüm testler
|
|
5530
5614
|
# geçti" after a turn that edited and ended "doğrulanmadı" was
|
|
@@ -5545,7 +5629,7 @@ def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None):
|
|
|
5545
5629
|
if unseen:
|
|
5546
5630
|
worked = worked | {"verify_ok"}
|
|
5547
5631
|
return _residual_block(t, rows, shape, fold(judged, worked, external),
|
|
5548
|
-
session_id, cwd)
|
|
5632
|
+
session_id, cwd, read_only)
|
|
5549
5633
|
|
|
5550
5634
|
|
|
5551
5635
|
def _before_turn(rows):
|
|
@@ -6061,6 +6145,27 @@ OPEN_TODO = (
|
|
|
6061
6145
|
"{ACCESS}, {IRREVERSIBLE} or {ENV}, or marked blocked in the todo list "
|
|
6062
6146
|
"with its reason.")
|
|
6063
6147
|
|
|
6148
|
+
# The `handed step` class (plan 142): the reply tells the reader to run a
|
|
6149
|
+
# command this session ran commands beside (`handed_steps`). Filled with up to
|
|
6150
|
+
# three of those lines.
|
|
6151
|
+
HANDED_STEP = (
|
|
6152
|
+
"{HANDED_LEAD}The reply hands the user %d step(s) this session can run "
|
|
6153
|
+
"itself: %s. Run each one now and report what it printed. A step stays the "
|
|
6154
|
+
"user's only when it is outward with no grant from this session, when it "
|
|
6155
|
+
"cannot be undone, or when it needs {ACCESS} or {ENV} - and then its own "
|
|
6156
|
+
"line says which, with the command that failed. In a host plan mode, where "
|
|
6157
|
+
"nothing can run, the plan is the deliverable and the step runs in the turn "
|
|
6158
|
+
"after the approval.")
|
|
6159
|
+
|
|
6160
|
+
# The `plan claim` class (plan 142): a done/tested claim made in a turn the
|
|
6161
|
+
# host ran in a read-only mode (`read_only_turn`), where no command and no
|
|
6162
|
+
# write was possible, so nothing can be behind it.
|
|
6163
|
+
PLAN_CLAIM = (
|
|
6164
|
+
"{PLAN_LEAD}This turn ran in the host's read-only plan mode: no command "
|
|
6165
|
+
"and no write was possible, so \"%s\" has nothing behind it. Say what the "
|
|
6166
|
+
"plan is and which command will prove it, and claim the result in the turn "
|
|
6167
|
+
"that runs it, after the plan is approved.")
|
|
6168
|
+
|
|
6064
6169
|
|
|
6065
6170
|
def _tr_lower(text):
|
|
6066
6171
|
"""`text` lowercased the Turkish way (I -> ı, İ -> i): `str.lower` reads
|
|
@@ -6378,6 +6483,10 @@ NEVER_DEFAULT = re.compile(alts("ask", "never_default"), re.I)
|
|
|
6378
6483
|
# A reply that names a plan it wrote, or asks to build one: building a plan the
|
|
6379
6484
|
# turn wrote is class F (plan 105), so asking it is the policy, not a needless ask
|
|
6380
6485
|
PLAN_BUILD = re.compile(alts("ask", "plan_build"), re.I)
|
|
6486
|
+
# A last line asking the owner to pick a variant or option a research, analysis
|
|
6487
|
+
# or review recommends, to take a way around a tezgah refusal, or to decide
|
|
6488
|
+
# about another session's work (its line, plan or branch): class F (plan 136)
|
|
6489
|
+
OWNER_CHOICE = re.compile(alts("ask", "owner_choice"), re.I)
|
|
6381
6490
|
SPEND = re.compile(alts("ask", "spend"), re.I)
|
|
6382
6491
|
AMOUNT = re.compile(r"\$\s?(\d+(?:\.\d+)?)|(\d+(?:\.\d+)?)\s?(?:\$|usd\b|dolar)", re.I)
|
|
6383
6492
|
DEFAULT_MARK = re.compile(alts("ask", "default_mark"), re.I)
|
|
@@ -6398,10 +6507,12 @@ NEEDLESS_ASK = (
|
|
|
6398
6507
|
"merge, a run parameter within the standing spend cap - are taken and "
|
|
6399
6508
|
"stated in one line, and an outward step the user granted this session is "
|
|
6400
6509
|
"taken, not asked again. An outward step the user has not granted waits "
|
|
6401
|
-
"for the answer and never proceeds on a default, and so
|
|
6402
|
-
"plan this turn wrote
|
|
6403
|
-
"
|
|
6404
|
-
"
|
|
6510
|
+
"for the answer and never proceeds on a default, and so do building a "
|
|
6511
|
+
"plan this turn wrote, choosing a variant or option a research, analysis "
|
|
6512
|
+
"or review recommends, a way around a tezgah refusal and deciding about "
|
|
6513
|
+
"another session's work. Do it now, or, when it is a taste or intent fork "
|
|
6514
|
+
"only the user can decide, put it under a \"{APPROVAL_HEADING}\" heading "
|
|
6515
|
+
"with a \"{RECOMMENDED}:\" default and proceed on that default.")
|
|
6405
6516
|
|
|
6406
6517
|
|
|
6407
6518
|
def outward_classes(text):
|
|
@@ -6449,7 +6560,9 @@ def needless_ask(text, granted=frozenset):
|
|
|
6449
6560
|
"""The question the reply ends on when the agent can decide it, else None.
|
|
6450
6561
|
|
|
6451
6562
|
The last line asks (QUESTION_END), its paragraph carries no recommended
|
|
6452
|
-
default (DEFAULT_MARK) and it names no class-F step (`never_default
|
|
6563
|
+
default (DEFAULT_MARK) and it names no class-F step (`never_default`, or
|
|
6564
|
+
OWNER_CHOICE: a variant or option to pick, a way around a refusal, another
|
|
6565
|
+
session's work); it
|
|
6453
6566
|
is then needless when it names an outward step (OUTWARD) the user granted
|
|
6454
6567
|
this session, every one it names (`granted()`, called only then, so a reply
|
|
6455
6568
|
that ends on no question reads no ledger), or, naming none, when it asks a
|
|
@@ -6460,7 +6573,8 @@ def needless_ask(text, granted=frozenset):
|
|
|
6460
6573
|
if not lines or not QUESTION_END.search(lines[-1]):
|
|
6461
6574
|
return None
|
|
6462
6575
|
last = lines[-1]
|
|
6463
|
-
if (never_default(last) or
|
|
6576
|
+
if (never_default(last) or OWNER_CHOICE.search(_tr_lower(last))
|
|
6577
|
+
or PLAN_BUILD.search(_tr_lower(t))
|
|
6464
6578
|
or DEFAULT_MARK.search(_tr_lower(re.split(r"\n\s*\n", t)[-1]))):
|
|
6465
6579
|
return None
|
|
6466
6580
|
low = _tr_lower(last)
|
|
@@ -6472,6 +6586,56 @@ def needless_ask(text, granted=frozenset):
|
|
|
6472
6586
|
return None
|
|
6473
6587
|
|
|
6474
6588
|
|
|
6589
|
+
# The "you run it" shape: a line that tells the reader to run something
|
|
6590
|
+
# (`ask.hand_over`), beside a command in backticks (CODE_SPAN, the module's
|
|
6591
|
+
# own: a fence line carries no backtick span and never qualifies).
|
|
6592
|
+
HAND_OVER = re.compile(alts("ask", "hand_over"), re.I)
|
|
6593
|
+
# The steps the owner runs by design, whatever the session can do: tezgah's own
|
|
6594
|
+
# consent command, its installation and a machine-level `brew` step (the
|
|
6595
|
+
# session-scope rule). An outward or class-F step is read off the line itself
|
|
6596
|
+
# (`outward_classes`, `never_default`).
|
|
6597
|
+
OWNER_STEP = re.compile(r"(?i)\btezgah-(?:consent|setup|update)\b|\bconsent\b|\bbrew\b")
|
|
6598
|
+
|
|
6599
|
+
|
|
6600
|
+
def read_only_turn(mode):
|
|
6601
|
+
"""True when the host says this turn ran in a mode that can neither write
|
|
6602
|
+
nor run (READ_ONLY_MODES). A host that sends nothing reads as False."""
|
|
6603
|
+
return str(mode or "").strip().lower().replace("_", "-") in READ_ONLY_MODES
|
|
6604
|
+
|
|
6605
|
+
|
|
6606
|
+
def handed_steps(text, rows, read_only=False):
|
|
6607
|
+
"""The lines that hand the reader a step this session could have taken.
|
|
6608
|
+
|
|
6609
|
+
A line qualifies when it names a command in backticks and tells the reader
|
|
6610
|
+
to run it (HAND_OVER: the reader addressed as the one who runs it, never
|
|
6611
|
+
the "you can run X" of an answer about how something is done). A table row
|
|
6612
|
+
(TABLE_ROW) and a quoted line are not
|
|
6613
|
+
the reply speaking: a refusal or a log the reply quotes carries the same
|
|
6614
|
+
words. It is kept - the ask policy's, not the agent's - when it names an
|
|
6615
|
+
outward step (`outward_classes`), a class-F step
|
|
6616
|
+
(`never_default`), the owner's consent or tezgah's own install
|
|
6617
|
+
(OWNER_STEP), or when the line types its blocker. The turn must have run a
|
|
6618
|
+
command of its own (COMMAND_KINDS): a turn that ran none could not have run
|
|
6619
|
+
this one either, which keeps the class inert on a host that sends no mode.
|
|
6620
|
+
`read_only` closes it outright: in a plan mode every step waits for the
|
|
6621
|
+
approval, which is what the contract tells the turn to do."""
|
|
6622
|
+
if read_only or not any(str(r.get("kind")) in COMMAND_KINDS for r in rows):
|
|
6623
|
+
return []
|
|
6624
|
+
out = []
|
|
6625
|
+
for line in str(text or "").splitlines():
|
|
6626
|
+
said = line.strip()
|
|
6627
|
+
if said.startswith(("|", ">")) or TABLE_ROW.match(line):
|
|
6628
|
+
continue
|
|
6629
|
+
if not (CODE_SPAN.search(said) and HAND_OVER.search(said)):
|
|
6630
|
+
continue
|
|
6631
|
+
prose = _prose(said)
|
|
6632
|
+
if (outward_classes(said) or never_default(said) or OWNER_STEP.search(said)
|
|
6633
|
+
or BLOCKER_SAID.search(prose) or BLOCKER_TRIED.search(prose)):
|
|
6634
|
+
continue
|
|
6635
|
+
out.append(said)
|
|
6636
|
+
return out
|
|
6637
|
+
|
|
6638
|
+
|
|
6475
6639
|
def undefaulted(inp):
|
|
6476
6640
|
"""(questions, with a default) of an ask-tool payload: omp's `ask` marks
|
|
6477
6641
|
the default with a question's `recommended` index, Claude's
|
|
@@ -6527,14 +6691,18 @@ def unblocked_items(text, rows):
|
|
|
6527
6691
|
|
|
6528
6692
|
|
|
6529
6693
|
def _residual_block(text, rows, shape, verdict=(None, None), session_id=None,
|
|
6530
|
-
cwd=None):
|
|
6694
|
+
cwd=None, read_only=False):
|
|
6531
6695
|
"""`verdict` when it refuses; else, in a turn that did work (a STEP_KINDS
|
|
6532
6696
|
row), the `residual work` class when `unblocked_items` names one, or when a
|
|
6533
6697
|
subagent ended in the turn handing up items no type accounts for (the
|
|
6534
6698
|
`residual` count on its `subagent_end` row) and this reply types none; else
|
|
6535
6699
|
the `needless ask` class when `needless_ask` names the question the reply
|
|
6536
6700
|
ends on (any turn, worked or not: the plan proposal that ends "Başlayayım
|
|
6537
|
-
mı?" is its measured case)
|
|
6701
|
+
mı?" is its measured case) - except in a read-only turn, where the host's
|
|
6702
|
+
plan mode asks for that approval and the agent cannot take the step it
|
|
6703
|
+
would be asked about; else the `handed step` class when `handed_steps`
|
|
6704
|
+
names a step the reply left to the reader; else the `ready steps` class
|
|
6705
|
+
when `ready_left`
|
|
6538
6706
|
names one; else the `open todo` class when `open_left` names one; else the
|
|
6539
6707
|
`stopped slice` class when `_stopped_slice` finds one; else `verdict`. Not
|
|
6540
6708
|
asked for a subagent's end (`shape` false) or a nested session: both
|
|
@@ -6566,9 +6734,15 @@ def _residual_block(text, rows, shape, verdict=(None, None), session_id=None,
|
|
|
6566
6734
|
move = next(filter(None, (_limit_move(item, failed, spawn_failed)
|
|
6567
6735
|
for _label, item in left)), None)
|
|
6568
6736
|
return ("residual work", RESIDUAL_WORK % named + (LIMIT_MOVE % move if move else ""))
|
|
6569
|
-
asked =
|
|
6737
|
+
asked = None if read_only else needless_ask(
|
|
6738
|
+
text, lambda: grants_in_force(session_id))
|
|
6570
6739
|
if asked:
|
|
6571
6740
|
return ("needless ask", NEEDLESS_ASK % cut(asked, 80))
|
|
6741
|
+
passed = handed_steps(text, rows, read_only)
|
|
6742
|
+
if passed:
|
|
6743
|
+
return ("handed step", HANDED_STEP % (
|
|
6744
|
+
len(passed), "; ".join(cut(" ".join(line.split()), 64)
|
|
6745
|
+
for line in passed[:3])))
|
|
6572
6746
|
todo = _turn_todo(rows, session_id)
|
|
6573
6747
|
ready = _ready_in(text, todo, session_id, cwd)
|
|
6574
6748
|
if ready:
|