@r1z4x/tezgah 1.6.0 → 1.8.2

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 (71) hide show
  1. package/CHANGELOG.md +140 -1
  2. package/MANIFEST +7 -0
  3. package/README.md +6 -3
  4. package/bin/tezgah-decisions +6 -14
  5. package/bin/tezgah-design +21 -5
  6. package/bin/tezgah-docs +18 -16
  7. package/bin/tezgah-doctor +152 -0
  8. package/bin/tezgah-lessons +4 -1
  9. package/bin/tezgah-research +249 -17
  10. package/bin/tezgah-route +70 -8
  11. package/bin/tezgah-setup +26 -13
  12. package/bin/tezgah-task +877 -60
  13. package/bin/tezgah-taste +56 -15
  14. package/docs/architecture.md +28 -11
  15. package/docs/clarity-baseline.json +2 -2
  16. package/docs/contract.md +108 -24
  17. package/docs/environment.md +3 -1
  18. package/docs/evidence.md +209 -28
  19. package/docs/feature-audit.md +1 -1
  20. package/docs/gate.md +78 -23
  21. package/docs/glossary.md +4 -4
  22. package/docs/hosts.md +8 -8
  23. package/docs/index.json +11 -5
  24. package/docs/judge.md +9 -3
  25. package/docs/models.md +75 -14
  26. package/docs/operations.md +133 -3
  27. package/docs/research.md +349 -15
  28. package/docs/skills.md +1 -1
  29. package/docs/status-line.md +5 -5
  30. package/docs/testing.md +20 -13
  31. package/docs/vision.md +12 -4
  32. package/hooks/hooks.json +2 -2
  33. package/hooks/lang/en.json +323 -0
  34. package/hooks/lang/tr.json +344 -0
  35. package/hooks/projects-auto-init.py +29 -1
  36. package/hooks/projects-posttooluse.py +24 -5
  37. package/hooks/projects-pretooluse.py +9 -4
  38. package/hooks/projects-stop.py +28 -16
  39. package/hooks/tezgah_agents.py +53 -17
  40. package/hooks/tezgah_clarity.py +7 -3
  41. package/hooks/tezgah_context.py +489 -438
  42. package/hooks/tezgah_gate.py +996 -80
  43. package/hooks/tezgah_integrity.py +1547 -197
  44. package/hooks/tezgah_models.py +427 -45
  45. package/hooks/tezgah_paths.py +147 -16
  46. package/hooks/tezgah_policy.py +261 -129
  47. package/hooks/tezgah_rank.py +29 -27
  48. package/hooks/tezgah_replay.py +200 -30
  49. package/hooks/tezgah_research.py +1303 -14
  50. package/hooks/tezgah_stopspec.py +7 -0
  51. package/hooks/tezgah_store.py +74 -27
  52. package/hooks/tezgah_task.py +1201 -40
  53. package/hooks/tezgah_taste.py +8 -4
  54. package/hosts/codex/hook.py +32 -13
  55. package/hosts/cursor/hook.py +32 -14
  56. package/hosts/dsh/hooks.json +2 -2
  57. package/hosts/omp/hook.py +44 -23
  58. package/hosts/omp/tezgah-hook.ts.in +93 -7
  59. package/hosts/opencode/plugins/tezgah.js +273 -6
  60. package/output-styles/tezgah.md +76 -42
  61. package/package.json +1 -1
  62. package/skills/i-have-adhd/EVALS.md +22 -2
  63. package/skills/i-have-adhd/SKILL.md +12 -5
  64. package/skills/plan-add/SKILL.md +27 -11
  65. package/skills/plan-add/render_table.py +8 -3
  66. package/skills/plan-sync/SKILL.md +5 -3
  67. package/skills/ponytail/EVALS.md +21 -2
  68. package/skills/ponytail/SKILL.md +4 -4
  69. package/skills/research/SKILL.md +89 -1
  70. package/skills/tezgah-contract/EVALS.md +52 -7
  71. package/skills/tezgah-contract/SKILL.md +154 -59
@@ -40,7 +40,8 @@
40
40
  // second and third halves - the channel on the call's own ledger row (with
41
41
  // the `external` row an MCP answer or a fetched page earns), the provenance
42
42
  // label in front of the result itself, and the taint notice on the first
43
- // effect a turn makes after such a read (hooks/tezgah_untrusted.marks).
43
+ // effect a turn makes after such a read (hooks/tezgah_untrusted.marks); a
44
+ // `task` call also writes its slice record (noteSlice, bin/tezgah-task).
44
45
  //
45
46
  // Known gap: every rule of hooks/tezgah_gate.py is enforced here, but the ledger
46
47
  // the counters read is complete only for the rules ported last - secret, loop,
@@ -227,6 +228,48 @@ const SECRET_SINK =
227
228
  // message about the rule rather than a credential in a write. `|` is NOT a
228
229
  // boundary here but a carrier, and the split is read off the masked text.
229
230
  const SEGMENT = /\|\||&&|[;&\n]/g
231
+ // Plan 096, as hooks/tezgah_gate reads it (SECRET_PREFIX, SECRET_RUNNER, ...,
232
+ // _secret_runner): a token held only by a program's leading `NAME=value` words
233
+ // is in that program's env, so its redirected output carries no key - but only
234
+ // for a script runner named by itself, given a script file, with no option that
235
+ // runs its own text, stdin or a preload, or traces. Any other program holds.
236
+ const SECRET_PREFIX =
237
+ /^[\s;&|]*(?:env\s+)?(?:[A-Za-z_]\w*=(?:'[^']*'|"(?:\\.|[^"\\])*"|\$\([^()]*\)|`[^`]*`|\\.|[^\s;&|()<>'"`\\])*\s+)+/
238
+ // the sinks that carry a program's output and nothing but it (SECRET_OUTPUT)
239
+ const SECRET_OUTPUT = />>?(?![&=])|\|\s*tee\b/g
240
+ const SECRET_RUNNER = /^(?:python[\d.]*|node|bun|deno|bash|sh|zsh)$/
241
+ const SECRET_SCRIPT = /^[^$`\s]+\.(?:py|js|mjs|ts|sh)$/
242
+ const SECRET_INLINE = {python: /^-[A-Za-z]*[cmi]/,
243
+ sh: /^(?:-[A-Za-z]*[csxv]|-o$|--(?:verbose|debugger))/,
244
+ js: /^(?:-[A-Za-z]*[epri]|--(?:eval|print|require|import|loader|experimental-loader|preload|interactive)(?:=|$))/}
245
+ // the line holds (SECRET_ENVIRON, SECRET_STARTUP, SECRET_TRACE, SECRET_FUNCTION)
246
+ const SECRET_ENVIRON = /\/proc\/[^/\s]*\/environ/
247
+ const SECRET_STARTUP =
248
+ /(?<![\w$])(?:SHELLOPTS|BASHOPTS|BASH_ENV|ENV|PS4|ZDOTDIR|NODE_OPTIONS|PYTHONSTARTUP|PYTHONINSPECT|PERL5OPT|PERL5DB|RUBYOPT|LD_PRELOAD|DYLD_\w*)=/
249
+ const SECRET_TRACE = /(?:^|[\s;&|(])set\s+(?:-[A-Za-z]*[xv]|-o\s+(?:xtrace|verbose))/
250
+ const SECRET_FUNCTION =
251
+ /(?:^|[\s;&|(])(?:function\s+[^\s(){};&|]+|[^\s(){};&|$=]+\s*\(\s*\))/
252
+ const SECRET_UNEXPANDED = /\$[A-Za-z_{(]|`/
253
+ // the programs a carried name is printed by when they own a sink
254
+ const SECRET_PRINTERS = new Set(["echo", "printf", "cat", "eval", "awk", "perl",
255
+ "ruby"])
256
+ // a versioned python is read by /^python[\d.]+$/: only pythonBin() spells the
257
+ // interpreter's name (tests/test_interpreter.py)
258
+ const SECRET_INTERPRETERS = new Set(["python", "sh", "bash", "zsh", "dash"])
259
+ const SECRET_KEYWORDS = new Set(["if", "then", "elif", "else", "do", "while",
260
+ "until", "!", "{", "time"])
261
+ // the wrappers `_program` reads past, with the options that take a value
262
+ const SECRET_WRAPPERS = new Map([
263
+ ["sudo", ["-u", "-g", "-h", "-p", "-C", "-D", "-r", "-t", "-U"]],
264
+ ["env", ["-u", "--unset", "-C", "--chdir"]], ["nice", ["-n", "--adjustment"]],
265
+ ["nohup", []], ["exec", ["-a"]], ["time", ["-f", "-o"]], ["command", []],
266
+ ["timeout", ["-s", "--signal", "-k", "--kill-after"]], ["stdbuf", ["-i", "-o", "-e"]]])
267
+ const SECRET_INTERPRETER_OPTS = ["-X", "-W", "-o", "+o", "-O"]
268
+ // `NAME=value` at a word start, and the `$NAME`/`${NAME}` a later word reads
269
+ // (hooks/tezgah_gate.SHELL_ASSIGN, SHELL_VAR)
270
+ const SECRET_ASSIGN =
271
+ /(?:^|(?<=[\s;&|(]))([A-Za-z_][A-Za-z0-9_]*)=((?:'[^']*'|"[^"]*"|[^\s;&|)'"])*)/g
272
+ const SECRET_VAR = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g
230
273
  const SECRET_DENY =
231
274
  "Credential write denied: this command would land a credential in a file " +
232
275
  "(`>`/`>>`, `tee`, `git add` or a curl trace). Record the credential's " +
@@ -1158,6 +1201,49 @@ const DETAIL_MAX = 200
1158
1201
  // agree on the credential itself; they differ only for an astral character
1159
1202
  // inside a quoted named value.
1160
1203
 
1204
+ // The `todo` row for `todowrite` (hooks/tezgah_integrity.note_todo): the list's
1205
+ // open, ready and blocked counts, the ready heads in `heads` and `detail`. Typed
1206
+ // blockers: BLOCKER_SAID/BLOCKER_TRIED's tokens - every hooks/lang/<code>.json
1207
+ // pack's `blockers.<kind>.pattern`, DECISION and KARAR alike - on Unicode word
1208
+ // boundaries (JS `\b` is ASCII-only); no pack found matches none, as `alts`
1209
+ // does. Read once, on the first todo call. ponytail: without the Python
1210
+ // reader's negation rule.
1211
+ const TODO_CLOSED = new Set(["completed", "done", "cancelled", "abandoned"])
1212
+ let todoTyped = null
1213
+ function todoTypedPattern() {
1214
+ if (todoTyped) return todoTyped
1215
+ const kinds = { decision: [], irreversible: [], access: [], env: [] }
1216
+ const dir = hookScript("lang")
1217
+ try {
1218
+ for (const name of dir ? readdirSync(dir).sort() : []) {
1219
+ if (!name.endsWith(".json")) continue
1220
+ const blockers = JSON.parse(readFileSync(join(dir, name), "utf8")).blockers || {}
1221
+ for (const kind of Object.keys(kinds)) {
1222
+ if (blockers[kind]?.pattern) kinds[kind].push(blockers[kind].pattern)
1223
+ }
1224
+ }
1225
+ } catch {}
1226
+ const alt = (...names) => names.flatMap((k) => kinds[k]).join("|") || "(?!)"
1227
+ todoTyped = new RegExp("(?<![\\p{L}\\p{N}_])(?:(?:" + alt("decision", "irreversible")
1228
+ + ")[A-ZÇĞİIÖŞÜ]*|" + alt("access", "env") + ")(?![\\p{L}\\p{N}_])", "u")
1229
+ return todoTyped
1230
+ }
1231
+ async function recordTodo(sessionID, tool, args, workspace) {
1232
+ if (!Array.isArray(args?.todos)) return
1233
+ const open = args.todos.filter((i) => i && String(i.content || "").trim()
1234
+ && !TODO_CLOSED.has(String(i.status || "pending").toLowerCase()))
1235
+ const typed = (i) => todoTypedPattern().test(i.content) || /\bblocked\b/i.test(i.content)
1236
+ const heads = open.filter((i) => String(i.status || "pending").toLowerCase() === "pending"
1237
+ && !typed(i)).map((i) => redact(String(i.content).split(/\s+/).join(" ").trim())
1238
+ .slice(0, 60))
1239
+ await appendRow(sessionID, {
1240
+ kind: "todo", ts: Math.floor(Date.now() / 1000),
1241
+ detail: heads.join("\n").slice(0, DETAIL_MAX),
1242
+ id: actionID(tool, args), workspace: workspace || null, tool,
1243
+ open: open.length, ready: heads.length, blocked: open.filter(typed).length, heads,
1244
+ })
1245
+ }
1246
+
1161
1247
  // Evidence ledger, the store the Python gate and Stop hook read. Written
1162
1248
  // per tool call so a "done/tested" claim can be checked against what ran.
1163
1249
  // The bash tool returns `metadata.exit` (the process exit code), so a check's
@@ -1181,6 +1267,7 @@ async function recordEvidence(sessionID, tool, args, result, workspace, cwd,
1181
1267
  source) {
1182
1268
  if (!sessionID) return
1183
1269
  const t = String(tool || "").toLowerCase()
1270
+ if (t === "todowrite") return await recordTodo(sessionID, tool, args, workspace)
1184
1271
  const exit = result?.metadata?.exit
1185
1272
  let kind = null
1186
1273
  let detail = String(args?.command || writtenPath(args) || "")
@@ -1259,6 +1346,60 @@ async function recordEvidence(sessionID, tool, args, result, workspace, cwd,
1259
1346
  }
1260
1347
  }
1261
1348
 
1349
+ // The slice journal (hooks/tezgah_task.py, plan 081). opencode's `task` call
1350
+ // runs its child to the end before this hook sees it, so one call opens the
1351
+ // record and closes it - done, or stopped with the error's class - through
1352
+ // bin/tezgah-task, the records' one writer. The id is the child session opencode
1353
+ // reports (`metadata.sessionId`), else the first 12 hex digits of the brief's
1354
+ // sha1 (tezgah_task.slice_id). Awaited under the core deadline and never thrown:
1355
+ // a journal that cannot be written costs the record, not the call.
1356
+ const TASK_BIN = join(CONFIG, "bin", "tezgah-task")
1357
+ const SLICE_ID = /^[A-Za-z0-9._-]{1,80}$/
1358
+ const SLICE_BRIEF = 4000
1359
+ // tezgah_task._CLASSES, in its order: the class a stopped record carries, and
1360
+ // `error` for any other failure text.
1361
+ const SLICE_CLASSES = [
1362
+ ["rate_limit", /\b429\b|rate[ _-]?limit/i],
1363
+ ["quota", /quota|key limit exceeded|usage limit|\b40[23]\b.*\blimit|\blimit\b.*\b40[23]\b/i],
1364
+ ["aborted", /abort|cancell?ed/i],
1365
+ ["timeout", /timed out|timeout/i],
1366
+ ["cut_off", /max[ _]turns|max_tokens|cut off|context[ _]length/i],
1367
+ ]
1368
+
1369
+ function sliceClass(text) {
1370
+ const hit = SLICE_CLASSES.find(([, re]) => re.test(text))
1371
+ return hit ? hit[0] : "error"
1372
+ }
1373
+
1374
+ // The error a `task` result reports, as text, or "" for none.
1375
+ function sliceError(result) {
1376
+ for (const e of [result?.error, result?.metadata?.error]) {
1377
+ const text = typeof e === "string" ? e : String(e?.message || e?.name || "")
1378
+ if (e && text.trim()) return text.trim()
1379
+ }
1380
+ return ""
1381
+ }
1382
+
1383
+ async function noteSlice(sessionID, args, result, cwd) {
1384
+ if (!existsSync(TASK_BIN) || await off("slices-off", sessionID)) return
1385
+ const brief = [args?.description, args?.prompt].map((p) => String(p || "").trim())
1386
+ .filter(Boolean).join("\n\n").slice(0, SLICE_BRIEF)
1387
+ if (!brief) return
1388
+ const child = String(result?.metadata?.sessionId || result?.metadata?.session_id || "")
1389
+ const id = SLICE_ID.test(child) ? child
1390
+ : createHash("sha1").update(brief).digest("hex").slice(0, 12)
1391
+ const run = (argv) => collect([TASK_BIN, "slice", ...argv],
1392
+ { cwd, stdio: ["ignore", "pipe", "ignore"] }, null,
1393
+ (code) => code === 0)
1394
+ const opened = await run(["open", "--id", id, "--kind", "agent", "--host", "opencode",
1395
+ "--agent", String(args?.subagent_type || ""),
1396
+ "--brief", brief, "--parent", String(sessionID || ""),
1397
+ "--cwd", cwd])
1398
+ if (!opened) return
1399
+ const error = sliceError(result)
1400
+ await run(error ? ["stop", id, "--class", sliceClass(error)] : ["done", id])
1401
+ }
1402
+
1262
1403
  // How far back the pre-state search reads: the gate writes its snapshot row in
1263
1404
  // the call immediately before the write, so the newest rows are where it is
1264
1405
  // (SNAPSHOT_TAIL in hooks/tezgah_integrity).
@@ -1842,23 +1983,148 @@ function realPath(p) {
1842
1983
  // The refusal when this command would write a credential into a file, or null.
1843
1984
  // Reading an env var or running a tool with a key in its env is the normal work
1844
1985
  // this must not touch, so a token only counts next to a write sink, and a sink
1845
- // only carries the text of its own simple command.
1986
+ // only carries the text of its own simple command. A token held only by the
1987
+ // leading `NAME=value` words of an allowlisted program is in that program's env
1988
+ // (envPrefixOnly), unless the line reads /proc's environ or defines a function.
1989
+ // A name an earlier command assigned a token is carried, and a printer owning a
1990
+ // sink that reads it still writes it (printerOwnsSink; plan 096).
1846
1991
  function secretCommand(command) {
1847
1992
  const c = String(command || "")
1848
1993
  if (!c || !SECRET_TOKEN.test(c)) return null
1849
1994
  const masked = maskText(c)
1995
+ const relax = !(SECRET_ENVIRON.test(c) || SECRET_STARTUP.test(c)
1996
+ || SECRET_FUNCTION.test(masked) || SECRET_TRACE.test(masked))
1850
1997
  let start = 0
1998
+ const carried = new Set()
1851
1999
  const ends = [...masked.matchAll(SEGMENT)].map((m) => m.index)
1852
2000
  for (const end of ends.concat(c.length)) {
1853
- if (SECRET_TOKEN.test(c.slice(start, end))
1854
- && SECRET_SINK.test(masked.slice(start, end))) {
1855
- return SECRET_DENY
2001
+ const text = c.slice(start, end)
2002
+ if (SECRET_SINK.test(masked.slice(start, end))) {
2003
+ if (SECRET_TOKEN.test(text) && !(relax && envPrefixOnly(text))) return SECRET_DENY
2004
+ if ([...text.matchAll(SECRET_VAR)].some((v) => carried.has(v[1] || v[2]))
2005
+ && printerOwnsSink(text, masked.slice(start, end))) {
2006
+ return SECRET_DENY
2007
+ }
2008
+ }
2009
+ for (const a of text.matchAll(SECRET_ASSIGN)) {
2010
+ if (SECRET_TOKEN.test(a[0])) carried.add(a[1])
1856
2011
  }
1857
2012
  start = end
1858
2013
  }
1859
2014
  return null
1860
2015
  }
1861
2016
 
2017
+ // True when every token in this simple command's text sits in its leading
2018
+ // `NAME=value` words, no sink runs inside them, the sinks after them carry only
2019
+ // the program's output (a `git add` or a curl trace holds), and the program is
2020
+ // on the allowlist (hooks/tezgah_gate._env_prefix_only).
2021
+ function envPrefixOnly(text) {
2022
+ const m = text.match(SECRET_PREFIX)
2023
+ if (!m) return false
2024
+ const rest = text.slice(m[0].length)
2025
+ return !(SECRET_TOKEN.test(rest) || SECRET_SINK.test(m[0])
2026
+ || SECRET_SINK.test(rest.replace(SECRET_OUTPUT, " "))
2027
+ || !secretRunner(rest))
2028
+ }
2029
+
2030
+ // the words of the first command `text` runs, a `$( )` body skipped
2031
+ function firstCommand(text) {
2032
+ const first = shellCommands(text.replace(/^[ \t\n;&|]+/, ""))
2033
+ .find((cmd) => cmd[1] !== "$)")
2034
+ return first ? first[0] : []
2035
+ }
2036
+
2037
+ // True when the first program `text` runs, past the wrappers, is a script
2038
+ // runner named by itself (a path could be any program) given a script file,
2039
+ // with no `-` or inline, stdin, preload, interactive or trace option before it
2040
+ // (hooks/tezgah_gate._secret_runner).
2041
+ function secretRunner(text) {
2042
+ const words = secretPastWrappers(firstCommand(text))
2043
+ if (!words.length || !SECRET_RUNNER.test(words[0])) return false
2044
+ const name = words[0]
2045
+ let args = words.slice(1)
2046
+ if (name === "deno") {
2047
+ if (args[0] !== "run") return false
2048
+ args = args.slice(1)
2049
+ }
2050
+ const inline = SECRET_INLINE[name.startsWith("python") ? "python"
2051
+ : ["node", "bun", "deno"].includes(name) ? "js" : "sh"]
2052
+ for (let i = 0; i < args.length;) {
2053
+ const arg = args[i]
2054
+ if (arg === "-" || inline.test(arg)) return false
2055
+ if (!/^[-+]/.test(arg)) return SECRET_SCRIPT.test(arg)
2056
+ i += SECRET_INTERPRETER_OPTS.includes(arg) ? 2 : 1
2057
+ }
2058
+ return false
2059
+ }
2060
+
2061
+ // True when a pipeline stage that owns a file sink - a `>` to a file other than
2062
+ // /dev/null, or a pipe into `tee` - is a printer
2063
+ // (hooks/tezgah_gate._printer_owns_sink).
2064
+ function printerOwnsSink(text, masked) {
2065
+ const cuts = [...masked.matchAll(/(?<!>)\|/g)].map((m) => m.index)
2066
+ const starts = [0, ...cuts.map((k) => k + 1)]
2067
+ const stops = [...cuts, text.length]
2068
+ for (let k = 0; k < starts.length; k++) {
2069
+ const a = starts[k], b = stops[k]
2070
+ let owns = [...masked.slice(a, b).matchAll(/>>?(?![&=])/g)].some((m) =>
2071
+ text.slice(a + m.index + m[0].length, b).trim().split(/\s+/)[0] !== "/dev/null")
2072
+ if (k + 1 < starts.length && /^\s*tee\b/.test(masked.slice(starts[k + 1]))) owns = true
2073
+ if (owns && secretPrinter(text.slice(a, b))) return true
2074
+ }
2075
+ return false
2076
+ }
2077
+
2078
+ // True when the first program `text` runs prints a carried name
2079
+ // (hooks/tezgah_gate._secret_printer).
2080
+ function secretPrinter(text) {
2081
+ const program = secretProgram(firstCommand(text))[0]
2082
+ return SECRET_PRINTERS.has(program) || SECRET_INTERPRETERS.has(program)
2083
+ || /^python[\d.]+$/.test(program) || SECRET_UNEXPANDED.test(program)
2084
+ }
2085
+
2086
+ // `words` past keywords, env assignments and the wrappers
2087
+ // (hooks/tezgah_gate._past_wrappers)
2088
+ function secretPastWrappers(words) {
2089
+ let i = 0
2090
+ while (i < words.length) {
2091
+ const word = words[i]
2092
+ if (/^[A-Za-z_]\w*=/.test(word) || SECRET_KEYWORDS.has(word)) { i += 1; continue }
2093
+ const wrapper = basename(word)
2094
+ const opts = SECRET_WRAPPERS.get(wrapper)
2095
+ if (!opts) break
2096
+ i += 1
2097
+ while (i < words.length && words[i].startsWith("-")) i += opts.includes(words[i]) ? 2 : 1
2098
+ if (wrapper === "timeout" && i < words.length) i += 1
2099
+ }
2100
+ return words.slice(i)
2101
+ }
2102
+
2103
+ // [program basename without `.py`, its arguments] for one simple command, past
2104
+ // the wrappers and an interpreter's options before its script
2105
+ // (hooks/tezgah_gate._program).
2106
+ function secretProgram(words) {
2107
+ words = secretPastWrappers(words)
2108
+ if (words[0] === "[") {
2109
+ const close = words.indexOf("]")
2110
+ words = words.slice(close < 0 ? words.length : close + 1)
2111
+ }
2112
+ if (!words.length) return ["", []]
2113
+ let name = basename(words[0])
2114
+ if (SECRET_INTERPRETERS.has(name) || /^python[\d.]+$/.test(name)) {
2115
+ const own = name.startsWith("python") ? /^-[A-Za-z]*[cm]$/ : /^-[A-Za-z]*c[A-Za-z]*$/
2116
+ let j = 1
2117
+ while (j < words.length && /^[-+]/.test(words[j]) && words[j] !== "-") {
2118
+ if (own.test(words[j])) { j = words.length + 1; break }
2119
+ j += SECRET_INTERPRETER_OPTS.includes(words[j]) ? 2 : 1
2120
+ }
2121
+ if (j < words.length) words = words.slice(j)
2122
+ }
2123
+ if (!words.length) return ["", []]
2124
+ name = basename(words[0])
2125
+ return [name.endsWith(".py") ? name.slice(0, -3) : name, words.slice(1)]
2126
+ }
2127
+
1862
2128
  // --- untrusted content: the channel and the label ---------------------------
1863
2129
  // The two halves the Python hosts get from
1864
2130
  // hooks/tezgah_integrity.untrusted_source and hooks/tezgah_untrusted.marks,
@@ -2224,7 +2490,7 @@ const SWITCHES = new Set(["adhd-off", "consult-off", "docs-judge-off",
2224
2490
  "exec-mode.off", "judge-off", "lang-off", "orchestrate-off",
2225
2491
  "ponytail-auto.off", "pretooluse-off", "reminder-off", "research-off",
2226
2492
  "spec-off", "task-off", "triage-off", "verify-off", "workspace-off",
2227
- "agents-off", "skill-suggest-on", "taste-on", "update-check-off"])
2493
+ "agents-off", "skill-suggest-on", "slices-off", "taste-on", "update-check-off"])
2228
2494
 
2229
2495
  // The session latch, mirrored from hooks/tezgah_paths.off: with a session id, a
2230
2496
  // switch file whose ctime or mtime is newer than the session ledger's first row
@@ -2923,6 +3189,7 @@ export const Tezgah = async ({ directory }) => {
2923
3189
  const taste = await recordEvidence(sessionID, tool, args, output, workspace,
2924
3190
  dir, own || inherited)
2925
3191
  if (taste) labelResult(output, taste)
3192
+ if (tool === "task") await noteSlice(sessionID, args, output, dir)
2926
3193
  } catch {}
2927
3194
  },
2928
3195
 
@@ -14,10 +14,11 @@ force-for-plugin: true
14
14
 
15
15
  ## Tezgah core (auto-armed in this repo)
16
16
 
17
- **Reply language, BLUF.** Every user-facing reply in the language `reply_lang` sets in ~/.config/tezgah/config.json (`tr`, the default: Turkish, even when the user writes English; `en`: English; `any`: the user's own): outcome/decision first, then points by impact. Code, commits,
17
+ **Reply language, BLUF.** Every user-facing reply in the language `language` sets in ~/.config/tezgah/config.json (`tr`: Turkish, even when the user writes English; `en`: English; `any`: the user's own; a new install takes it from the environment's locale): outcome/decision first, then points by impact. Code, commits,
18
18
  docs, subagent prompts and inter-agent reports stay English. One term per
19
19
  concept. Verify each claim against an observed tool result, file or test before
20
- the final answer; unobserved claims are dropped or marked "doğrulanmadı". Never
20
+ the final answer; an unobserved claim is checked now or dropped, and
21
+ "unverified"/"doğrulanmadı" marks only one whose check ran and failed. Never
21
22
  report done/tested/fixed unless the output was seen; a failing test is reported
22
23
  as failing, with its exact error. Own a mistake in one plain sentence, then fix
23
24
  it - no apology theater, no self-justifying phrasing.
@@ -28,27 +29,30 @@ one line -> minimum code. No unrequested abstractions, no scaffolding "for
28
29
  later", deletion over addition, fewest files, shortest working diff. Trace the
29
30
  problem fully before climbing; never simplify away validation, error handling,
30
31
  security, accessibility or anything requested, and build the full version
31
- without re-arguing when the user insists. Bug fix = root cause where all
32
- callers route through. A deliberate corner cut gets a `ponytail:` comment
33
- naming the ceiling. Non-trivial logic leaves ONE runnable check (an assert
34
- self-check or one small test); a one-liner needs none. A complex ask ships the
35
- lazy version and names the fuller one in the same reply instead of stalling.
36
- Reply: code first, then at most three lines - what was skipped, when to add it.
37
- In force from the first turn; levels lite|full|ultra are set with `tezgah-pony`
38
- and ride the per-turn reminder when not `full`; read the full `ponytail` skill
39
- from the router for the level table and examples. Off: "stop ponytail".
32
+ without re-arguing when the user insists. Bug fix = root cause where all callers
33
+ route through. A deliberate corner cut gets a `ponytail:` comment naming the
34
+ ceiling. Non-trivial logic leaves ONE runnable check (an assert self-check or
35
+ one small test); a one-liner needs none. A complex ask ships the lazy version
36
+ and names the fuller one in the same reply instead of stalling. Reply: code
37
+ first; write what was cut only when you consciously cut something the user asked
38
+ for, in at most three lines. In force from the first turn; levels
39
+ lite|full|ultra are set with `tezgah-pony` and ride the per-turn reminder when
40
+ not `full`; read the full `ponytail` skill from the router for the level table
41
+ and examples. Off: "stop ponytail".
40
42
 
41
43
  **Output shape: ADHD-friendly.** The answer or the next action is on the first
42
44
  line - a command, path, snippet or the decision itself, never context or the
43
45
  question restated - prose after it. Multi-step work is a numbered list, one
44
46
  bounded action per step, and while it is in flight its position is restated in
45
- one line - the todo list is that source, never re-narrate the plan. End with one
46
- concrete next step. Finish the issue in hand before raising a second one; an
47
- error states location, cause and fix with no drama, naming a cause only when the
48
- evidence identifies it (else what is known and what would confirm it); after a
49
- change say what now works. A list shows at most five items, ranked, the rest
50
- kept in reserve rather than dropped - presentation only, never the analysis. An
51
- estimate is in concrete units and marked as an estimate, never presented as a
47
+ one line - the todo list is that source, never re-narrate the plan. End with the
48
+ approval questions the Ask policy (below) leaves, or with nothing when none is
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
51
+ location, cause and fix with no drama, naming a cause only when the evidence
52
+ identifies it (else what is known and what would confirm it); after a change say
53
+ what now works. A list shows at most five items, ranked, the rest kept in
54
+ reserve rather than dropped - presentation only, never the analysis. An estimate
55
+ is in concrete units and marked as an estimate, never presented as a
52
56
  measurement. No preamble, no recap, no closer, and a question the reader raises
53
57
  mid-work is answered rather than deferred as the second issue. Break the shape
54
58
  only where it would delete the answer: an explanation runs as long as it needs,
@@ -67,38 +71,68 @@ FORBIDDEN: swapping in a cheaper, deferred or partial stand-in for what was
67
71
  asked; silently narrowing scope; deciding a requested item is
68
72
  "unnecessary"/"YAGNI" and dropping it; a token gesture reported as done; stopping
69
73
  early because it got long. If an item looks unnecessary, impossible or out of
70
- scope, STOP and ask - with a recommended default - never decide it yourself.
74
+ scope, ask in the approval batch with "keep it" as the default - the work never
75
+ proceeds on a cut, and an unsafe item waits for the answer (Ask policy, below).
71
76
  Before the final answer walk the request item by item, and name first any item
72
- not fully delivered, with what is missing and why. Never placate: a reply never
73
- opens with agreement, praise or an apology ("haklısın", "you're right", "good
74
- catch", "detaylı bakmadım", "I didn't look closely"); if the user is right,
77
+ not fully delivered, with what is missing and the blocker that stops it
78
+ (Integrity, below). Never placate: a reply never
79
+ opens with agreement, praise or an apology ("haklısın", "you're right", "good catch", "detaylı bakmadım", "I didn't look closely"); if the user is right,
75
80
  state the fact and the fix, if wrong, show the evidence.
76
81
 
77
- **Integrity: evidence, or "doğrulanmadı".** A "done/tested/fixed/passing"
78
- claim is true only if the check ran in THIS session and its output was seen;
79
- otherwise mark it "doğrulanmadı" instead of asserting it. The gate enforces the
80
- mechanical half and cannot be argued with: a check made unable to fail is denied
81
- - `--no-verify`, an env var that skips the hooks, `pytest || true` / `; true`,
82
- a check piped into `tail`/`grep` (keep the check last with its output in a file,
83
- then read the file in a separate call, or open the line with `set -o pipefail;`),
84
- and a newly added skip/xfail/`.only` on a test - and a Stop hook (Claude,
85
- Codex, Cursor, omp) refuses to end a turn that claims done/tested with no successful
86
- check recorded in the session. Never describe a check you did not run as if it
87
- ran, never report a failed check as passing, and never present a plan, stub or
88
- TODO as a delivered result. **Say what a number was measured on.** A figure
89
- produced by a fixture - a temp HOME, a generated repository, a stand-in, a
90
- hand-written sample - is evidence about the code path, never a property of the
91
- running system, and it is reported as the fixture it is: a demo on synthetic
92
- input is not progress on the product. Off: `verify-off`.
82
+ **Integrity: evidence, or a named blocker.** A "done/tested/fixed/passing" claim
83
+ is true only if the check ran in THIS session and its output was seen;
84
+ "unverified"/"doğrulanmadı" is allowed only after a verification attempt that ran and failed,
85
+ quoting the failure. The gate enforces the mechanical half and cannot be argued
86
+ with: a check made unable to fail is denied - `--no-verify`, an env var that
87
+ skips the hooks, `pytest || true` / `; true`, a check piped into `tail`/`grep`
88
+ (keep the check last with its output in a file, then read the file in a separate
89
+ call, or open the line with `set -o pipefail;`), and a newly added
90
+ skip/xfail/`.only` on a test - and a Stop hook (Claude, Codex, Cursor, omp)
91
+ refuses to end a turn that claims done/tested with no successful check recorded
92
+ in the session, or that lists left-over work no blocker accounts for. Never
93
+ describe a check you did not run as if it ran, never report a failed check as
94
+ passing, and never present a plan, stub or TODO as a delivered result.
95
+ **Residuals are work, not a report section.** Before the final answer, list
96
+ every item you would report as open, unverified, skipped, partial, deferred,
97
+ untested or a next step. For each one, do it now with the tools you have; a step
98
+ that cannot be undone is never taken to clear a list - it is a blocker. An item
99
+ may remain only if it is blocked by exactly one of, named in capitals: DECISION
100
+ (tr: KARAR; a choice only the user can make - never "which first" or "shall I go
101
+ on"), ACCESS (tr: YETKİ; a credential or grant no tool supplies), IRREVERSIBLE
102
+ (tr: GERİ ALINAMAZ; needs consent), ENV (tr: ORTAM; a tool or environment is missing). A
103
+ remaining item carries its evidence - for ACCESS and ENV the command you ran,
104
+ in backticks, and its error; for DECISION the options and a recommended default,
105
+ for IRREVERSIBLE the exact action waiting - and without it it is not a blocker, it is unfinished
106
+ work, so finish it. "Out of scope", "for later", "time" and "could be added" are
107
+ not blockers. Report blockers under one heading, "Blockers"/"Engeller", at most three
108
+ lines: item - blocker - evidence - what unblocks it. No blockers: no heading.
109
+ **Ask policy.** Ask only what the user alone can decide, once, in one batch.
110
+ Work and process - the order, "shall I go on", a check, a plan, a consult
111
+ member, a spend within `spend_cap`, a local merge of the session's branch,
112
+ removing its worktree - are taken and stated in one line, never asked; a taste
113
+ or intent fork goes in the batch with a default taken this turn. An outward step
114
+ on the user's own remote (push, PR, release, their package) runs on a grant
115
+ ("push without asking", "I approve", "bana sormadan pushla", "onay veriyorum"), else is asked once. Never on a
116
+ default - ask and wait for an answer naming action and target: a force push,
117
+ deleting what the session did not make, production, a credential, a third
118
+ party, a spend over the cap. The batch: one "Approval questions"/"Onay için sorular" heading, waiting
119
+ items first, each numbered item with "Recommended:"/"Öneri:" its default; an ask tool
120
+ sets a recommended option.
121
+ **Say what a number was measured on.** A figure produced by a fixture - a temp
122
+ HOME, a generated repository, a stand-in, a hand-written sample - is evidence
123
+ about the code path, never a property of the running system, and it is reported
124
+ as the fixture it is: a demo on synthetic input is not progress on the product.
125
+ Off: `verify-off`.
93
126
 
94
127
  **Loop discipline.** Never re-run a check that already passed, and never repeat
95
128
  an identical failing command: change the approach or stop. Three attempts on one
96
129
  failure is the ceiling - then report what you tried and what is still unknown
97
130
  instead of attempting a fourth. A turn must either change the state or end the
98
- work. Check cadence: while working, run only the tests of what changed; run the
99
- full suite once, on the final tree, right before it ships - not again on the
100
- same revision after a merge or a scratch file outside the repo. Small asks of
101
- one kind (a color, an icon, a label) are batched into one change and one release.
131
+ work. Check cadence: in a turn, run only the impacted tests of what changed plus
132
+ compileall and ruff; the full suite, neuter, the docs audits and e2e run once,
133
+ on the final tree, at the merge step and in CI - not again on the same revision
134
+ after a merge or a scratch file outside the repo. Small asks of one kind (a
135
+ color, an icon, a label) are batched into one change and one release.
102
136
 
103
137
  **Lessons ledger: stop repeating mistakes.** A repo may keep
104
138
  `.tezgah/lessons.md` (one per line; injected - recent per session, relevant
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r1z4x/tezgah",
3
- "version": "1.6.0",
3
+ "version": "1.8.2",
4
4
  "description": "One shared working contract for every AI coding assistant you run: Claude Code, Codex, Cursor, opencode, dsh and oh-my-pi (omp).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  # Evals
2
2
 
3
- Three inputs a session can be handed where this skill could misfire, and what a
3
+ Four inputs a session can be handed where this skill could misfire, and what a
4
4
  good answer has to contain. Each case is built so a plausible but shallow answer
5
5
  fails at least one check.
6
6
 
@@ -41,7 +41,7 @@ long as the topic needs". The user states that completeness is the point.
41
41
  **Must contain**
42
42
  - [ ] States the location (the connection to `10.0.4.12:5432`) and what is known from the line
43
43
  - [ ] Separates what the line shows (a timeout to the database port) from guesses about why
44
- - [ ] Names the one check that would confirm the leading guess, as the next action
44
+ - [ ] Runs the check that would confirm the leading guess when this machine can reach the host; when it cannot, names that check as the one step the reader takes, with the attempt and its error
45
45
 
46
46
  **Must not**
47
47
  - State a single root cause (firewall, wrong password, database down) as fact
@@ -74,6 +74,26 @@ needs". The output-shaping rules would otherwise delete the answer.
74
74
 
75
75
  ---
76
76
 
77
+ ## Case 4 - The trap: a next step the session can take itself
78
+
79
+ **Input**
80
+ > Fix the off-by-one in the pagination helper and tell me when it is ready.
81
+
82
+ **Must contain**
83
+ - [ ] Fixes the off-by-one and runs the helper's test itself, reporting its output
84
+ - [ ] Ends on what now works, or on ONE step only the reader can take (a decision, a grant, a consent)
85
+
86
+ **Must not**
87
+ - End with "Next: run `npm test`" or any other step the session could run itself
88
+ - End on a "Sıradaki adım" list
89
+
90
+ **Why this case** - Rule 4 reads "End with the one step that is the reader's to
91
+ take": "A step you can take yourself is not a next step: take it." Running the
92
+ test is the session's own step, and "tell me when it is ready" invites handing it
93
+ over.
94
+
95
+ ---
96
+
77
97
  ## What these do not prove
78
98
 
79
99
  These are trap cases for a human or judge rater, not a behavioural measurement.
@@ -85,13 +85,19 @@ restating. Never also narrate the plan as prose: two copies of the same state is
85
85
  the ceremony this rule exists to remove, and it is the one place where this rule
86
86
  would otherwise fight the exec contract's ban on summaries.
87
87
 
88
- ### 4. End with one concrete next step
88
+ ### 4. End with the one step that is the reader's to take
89
89
 
90
- If anything is open, name ONE thing the reader can do, small enough to start
91
- now. "Open the file" counts.
90
+ If something is left that only the reader can do - a decision, a credential or
91
+ grant, consent for a step that cannot be undone - name that ONE step, small
92
+ enough to start now. Several such decisions go in one numbered approval batch,
93
+ each with a recommended default, and the work proceeds on the defaults (the
94
+ tezgah contract's Ask policy). A step you can take yourself is not a next step:
95
+ take it, and never end on asking whether to. When nothing is left, end on what
96
+ now works.
92
97
 
93
98
  Bad: "Hope that helps. Let me know if you want to dig deeper."
94
- Good: "Next: run `npm test` and paste the first failing line."
99
+ Bad: "Next: run `npm test` and paste the first failing line." (you can run it)
100
+ Good: "Blockers: the push to `main` - IRREVERSIBLE - `git push origin main` waits - your yes." (tr: "Engeller", GERİ ALINAMAZ)
95
101
 
96
102
  ### 5. Suppress tangents
97
103
 
@@ -121,7 +127,8 @@ worse than a named unknown.
121
127
 
122
128
  Say what now works, not what was touched. "Login works with magic links; try
123
129
  `npm run dev` and open `/login`." A claim about what works is still a claim: it
124
- carries the evidence that was actually observed, or it is marked unverified.
130
+ carries the evidence that was actually observed. Try the check first; a claim is
131
+ marked unverified only after that check ran and failed, with the failure quoted.
125
132
 
126
133
  ### 8. Cap a list at five items
127
134