@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.
- package/CHANGELOG.md +140 -1
- package/MANIFEST +7 -0
- package/README.md +6 -3
- package/bin/tezgah-decisions +6 -14
- package/bin/tezgah-design +21 -5
- package/bin/tezgah-docs +18 -16
- package/bin/tezgah-doctor +152 -0
- package/bin/tezgah-lessons +4 -1
- package/bin/tezgah-research +249 -17
- package/bin/tezgah-route +70 -8
- package/bin/tezgah-setup +26 -13
- package/bin/tezgah-task +877 -60
- package/bin/tezgah-taste +56 -15
- package/docs/architecture.md +28 -11
- package/docs/clarity-baseline.json +2 -2
- package/docs/contract.md +108 -24
- package/docs/environment.md +3 -1
- package/docs/evidence.md +209 -28
- package/docs/feature-audit.md +1 -1
- package/docs/gate.md +78 -23
- package/docs/glossary.md +4 -4
- package/docs/hosts.md +8 -8
- package/docs/index.json +11 -5
- package/docs/judge.md +9 -3
- package/docs/models.md +75 -14
- package/docs/operations.md +133 -3
- package/docs/research.md +349 -15
- package/docs/skills.md +1 -1
- package/docs/status-line.md +5 -5
- package/docs/testing.md +20 -13
- package/docs/vision.md +12 -4
- package/hooks/hooks.json +2 -2
- package/hooks/lang/en.json +323 -0
- package/hooks/lang/tr.json +344 -0
- package/hooks/projects-auto-init.py +29 -1
- package/hooks/projects-posttooluse.py +24 -5
- package/hooks/projects-pretooluse.py +9 -4
- package/hooks/projects-stop.py +28 -16
- package/hooks/tezgah_agents.py +53 -17
- package/hooks/tezgah_clarity.py +7 -3
- package/hooks/tezgah_context.py +489 -438
- package/hooks/tezgah_gate.py +996 -80
- package/hooks/tezgah_integrity.py +1547 -197
- package/hooks/tezgah_models.py +427 -45
- package/hooks/tezgah_paths.py +147 -16
- package/hooks/tezgah_policy.py +261 -129
- package/hooks/tezgah_rank.py +29 -27
- package/hooks/tezgah_replay.py +200 -30
- package/hooks/tezgah_research.py +1303 -14
- package/hooks/tezgah_stopspec.py +7 -0
- package/hooks/tezgah_store.py +74 -27
- package/hooks/tezgah_task.py +1201 -40
- package/hooks/tezgah_taste.py +8 -4
- package/hosts/codex/hook.py +32 -13
- package/hosts/cursor/hook.py +32 -14
- package/hosts/dsh/hooks.json +2 -2
- package/hosts/omp/hook.py +44 -23
- package/hosts/omp/tezgah-hook.ts.in +93 -7
- package/hosts/opencode/plugins/tezgah.js +273 -6
- package/output-styles/tezgah.md +76 -42
- package/package.json +1 -1
- package/skills/i-have-adhd/EVALS.md +22 -2
- package/skills/i-have-adhd/SKILL.md +12 -5
- package/skills/plan-add/SKILL.md +27 -11
- package/skills/plan-add/render_table.py +8 -3
- package/skills/plan-sync/SKILL.md +5 -3
- package/skills/ponytail/EVALS.md +21 -2
- package/skills/ponytail/SKILL.md +4 -4
- package/skills/research/SKILL.md +89 -1
- package/skills/tezgah-contract/EVALS.md +52 -7
- 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
|
-
|
|
1854
|
-
|
|
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
|
|
package/output-styles/tezgah.md
CHANGED
|
@@ -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 `
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
In force from the first turn; levels
|
|
38
|
-
and ride the per-turn reminder when
|
|
39
|
-
from the router for the level table
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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,
|
|
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
|
|
73
|
-
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
- `--no-verify`, an env var that
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
# Evals
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
- [ ]
|
|
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
|
|
88
|
+
### 4. End with the one step that is the reader's to take
|
|
89
89
|
|
|
90
|
-
If
|
|
91
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|