@r1z4x/tezgah 1.13.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +175 -1
- package/MANIFEST +18 -0
- package/NOTICE +152 -0
- package/README.de.md +41 -20
- package/README.es.md +39 -19
- package/README.fr.md +41 -20
- package/README.md +15 -8
- package/README.tr.md +39 -21
- package/bin/codegen +9 -2
- package/bin/consult +6 -0
- package/bin/tezgah-adhd +3 -0
- package/bin/tezgah-agents +3 -0
- package/bin/tezgah-capture +3 -0
- package/bin/tezgah-design +329 -54
- package/bin/tezgah-docs +157 -41
- package/bin/tezgah-dsh +1 -1
- package/bin/tezgah-gate +7 -2
- package/bin/tezgah-import-ai-research +16 -4
- package/bin/tezgah-index +3 -0
- package/bin/tezgah-lessons +13 -4
- package/bin/tezgah-mcp +4 -0
- package/bin/tezgah-migrate +3 -0
- package/bin/tezgah-ocr-review +2 -1
- package/bin/tezgah-optimize +4 -1
- package/bin/tezgah-pony +3 -0
- package/bin/tezgah-research +12 -6
- package/bin/tezgah-rollback +3 -0
- package/bin/tezgah-route +1 -1
- package/bin/tezgah-setup +596 -157
- package/bin/tezgah-status +11 -1
- package/bin/tezgah-task +3 -0
- package/bin/tezgah.js +10 -3
- package/commands/adhd.md +1 -1
- package/docs/README.md +19 -8
- package/docs/architecture.md +2 -2
- package/docs/citations-baseline.json +11 -17
- package/docs/clarity-baseline.json +3 -3
- package/docs/contract.md +77 -31
- package/docs/environment.md +58 -24
- package/docs/evidence.md +49 -19
- package/docs/gate.md +71 -21
- package/docs/hosts.md +65 -52
- package/docs/layers.md +1 -1
- package/docs/operations.md +67 -24
- package/docs/research.md +1 -1
- package/docs/skills.md +31 -8
- package/docs/status-line.md +33 -23
- package/docs/testing.md +48 -17
- package/docs/vision.md +6 -2
- package/hooks/lang/en.json +5 -2
- package/hooks/lang/tr.json +3 -2
- package/hooks/projects-posttooluse.py +11 -3
- package/hooks/projects-stop.py +1 -1
- package/hooks/tezgah_context.py +40 -23
- package/hooks/tezgah_gate.py +404 -106
- package/hooks/tezgah_integrity.py +367 -97
- package/hooks/tezgah_lessons.py +5 -2
- package/hooks/tezgah_paths.py +17 -6
- package/hooks/tezgah_policy.py +13 -5
- package/hooks/tezgah_skill_pick.py +1 -1
- package/hooks/tezgah_snapshot.py +6 -3
- package/hooks/tezgah_stopspec.py +2 -1
- package/hosts/amp/hook.py +1 -1
- package/hosts/amp/tezgah-plugin.ts.in +13 -3
- package/hosts/antigravity/hook.py +1 -1
- package/hosts/codex/hook.py +1 -1
- package/hosts/commandcode/hook.py +1 -1
- package/hosts/copilot/hook.py +1 -1
- package/hosts/cursor/hook.py +2 -1
- package/hosts/droid/hook.py +1 -1
- package/hosts/gemini/hook.py +1 -1
- package/hosts/goose/hook.py +1 -1
- package/hosts/omp/hook.py +1 -1
- package/hosts/omp/tezgah-hook.ts.in +127 -35
- package/hosts/opencode/plugins/tezgah.js +209 -63
- package/hosts/qwen/hook.py +1 -1
- package/output-styles/tezgah.md +1 -1
- package/package.json +6 -3
- package/packaging/build.sh +4 -4
- package/packaging/install.sh +11 -0
- package/packaging/upgrade.sh +3 -0
- package/skills/ai-research/LICENSE +21 -0
- package/skills/design-contract/SKILL.md +27 -6
- package/skills/design-library/LICENSE +21 -0
- package/skills/plan-add/render_table.py +7 -0
- package/skills/pm-frameworks/LICENSE +21 -0
- package/skills/rl-env/LICENSE +201 -0
- package/skills/tezgah-contract/SKILL.md +2 -2
- package/third_party/open-code-review/LICENSE +201 -0
- package/third_party/open-code-review/README.md +15 -0
- package/third_party/open-code-review/prompts/main_task_system.md +26 -0
- package/third_party/open-code-review/prompts/review_filter_task_system.md +13 -0
- package/workflows/graph-map.js +14 -5
|
@@ -190,17 +190,17 @@ const EXPLORE_DENY =
|
|
|
190
190
|
// as _CREDIT is there. The parity test in tests/test_opencode_plugin.py reads
|
|
191
191
|
// this literal and the Python one and fails if the two alternations differ.
|
|
192
192
|
const ATTRIB =
|
|
193
|
-
/co-authored-by\s
|
|
193
|
+
/co-authored-by\s*[:=]|generated with|made with|(?:generated|written|authored|built|assisted|drafted|produced|composed)\s+by\s+(?:(?:an?|the)\s+)?(?:ai|llm|claude|chatgpt|gpt|copilot|codex|cursor|gemini|deepseek|openai|anthropic)\b|(?:generated|written|authored|built|assisted|drafted|produced|composed)\s+(?:with|using)\s+(?:(?:an?|the)\s+)?(?:ai|llm|claude|chatgpt|gpt|copilot|codex|gemini|deepseek|openai|anthropic)(?:-[\w.]+)?(?:\s+(?:code|cli))?(?:\s*\([^)\n]*\))?(?=[.!)\]*_`]*(?:[\x22\x27,;]|\s+-\s|\s*(?:\n|$)))|noreply@anthropic|\u{1F916}/iu
|
|
194
194
|
// The same forms anchored to the start of a line, for the text a write/edit
|
|
195
195
|
// tool is about to land. Mirrors hooks/tezgah_gate.py ATTRIB_LINE: a credit owns
|
|
196
196
|
// its line, while prose naming the ban does not, and that anchor is what keeps
|
|
197
197
|
// the rule from denying the documentation that describes it.
|
|
198
198
|
const ATTRIB_LINE =
|
|
199
|
-
/^[\s>#*/<!+-]*(?:co-authored-by\s
|
|
199
|
+
/^[\s>#*/<!+-]*(?:co-authored-by\s*[:=]|generated with|made with|(?:generated|written|authored|built|assisted|drafted|produced|composed)\s+by\s+(?:(?:an?|the)\s+)?(?:ai|llm|claude|chatgpt|gpt|copilot|codex|cursor|gemini|deepseek|openai|anthropic)\b|(?:generated|written|authored|built|assisted|drafted|produced|composed)\s+(?:with|using)\s+(?:(?:an?|the)\s+)?(?:ai|llm|claude|chatgpt|gpt|copilot|codex|gemini|deepseek|openai|anthropic)(?:-[\w.]+)?(?:\s+(?:code|cli))?(?:\s*\([^)\n]*\))?(?=[.!)\]*_`]*(?:[\x22\x27,;]|\s+-\s|\s*(?:\n|$)))|noreply@anthropic|\u{1F916})/imu
|
|
200
200
|
const EDIT_TEXT = ["content", "new_string", "newString", "new_str", "file_text",
|
|
201
201
|
"patch", "text"]
|
|
202
202
|
const WRITE_CMD =
|
|
203
|
-
/(?:^|[|;&]\s*|\s)git\s+(?:-{1,2}\S+(?:\s+\S+)?\s+)*(?:commit|merge|tag|notes)\b|(?:^|[|;&]\s*|\s)gh\s+api\b|(?:^|[|;&]\s*|\s)gh\s+(?:pr|issue|release)\s+(?:create|edit|comment|review|merge|close)\b/i
|
|
203
|
+
/(?:^|[|;&]\s*|\s)(?:\S*\/)?git\s+(?:-{1,2}\S+(?:\s+\S+)?\s+)*(?:commit|merge|tag|notes)\b|(?:^|[|;&]\s*|\s)(?:\S*\/)?gh\s+api\b|(?:^|[|;&]\s*|\s)(?:\S*\/)?gh\s+(?:pr|issue|release)\s+(?:create|edit|comment|review|merge|close)\b/i
|
|
204
204
|
const ATTRIB_DENY =
|
|
205
205
|
"Attribution is banned in every artifact tezgah touches. Remove the " +
|
|
206
206
|
"Co-Authored-By / \"Generated with\" / robot-emoji / model-name credit from " +
|
|
@@ -314,12 +314,45 @@ const LEDGER_TAIL = 200
|
|
|
314
314
|
// hooks/tezgah_integrity.VERIFY).
|
|
315
315
|
const RUNNER_FLAGS =
|
|
316
316
|
"(?:(?:--filter|--prefix|--workspace|--cwd|--dir|-C|-w)\\s+\\S+\\s+|-\\S+\\s+)*"
|
|
317
|
+
// hooks/tezgah_integrity.WRAPPER_VALUE_OPTS and _wrapper_prefix: the wrappers
|
|
318
|
+
// a check may sit behind, each with its env assignments, options, the options'
|
|
319
|
+
// values (a short one may close a cluster, `sudo -Hu deploy`) and `timeout`'s
|
|
320
|
+
// duration, so `timeout 60 pytest` and `nice -n 5 pytest` are checks on both
|
|
321
|
+
// sides (B-121; VerifyParity pins the agreement). The core reads this prefix
|
|
322
|
+
// case-sensitively inside its `re.I` (`(?-i:...)`), which node cannot spell, so
|
|
323
|
+
// WRAPPER_RUN strips it from the text with a regex of its own before VERIFY
|
|
324
|
+
// reads it: `sudo -H true pytest` runs `true`, as there. A value never starts
|
|
325
|
+
// with `-` nor looks like an assignment, so a failed match stays linear.
|
|
326
|
+
const WRAPPER_VALUE_OPTS = {
|
|
327
|
+
sudo: ["-u", "--user", "-g", "--group", "-h", "--host", "-p", "--prompt",
|
|
328
|
+
"-C", "--close-from", "-D", "--chdir", "-r", "--role", "-t", "--type",
|
|
329
|
+
"-U", "--other-user", "-T", "--command-timeout"],
|
|
330
|
+
env: ["-u", "--unset", "-C", "--chdir", "-S", "-P", "-L", "-U"],
|
|
331
|
+
nice: ["-n", "--adjustment"], nohup: [], exec: ["-a"], command: [],
|
|
332
|
+
builtin: [], time: ["-f", "--format", "-o", "--output"],
|
|
333
|
+
timeout: ["-s", "--signal", "-k", "--kill-after"],
|
|
334
|
+
stdbuf: ["-i", "--input", "-o", "--output", "-e", "--error"],
|
|
335
|
+
ionice: ["-c", "--class", "-n", "--classdata", "-p", "--pid"],
|
|
336
|
+
caffeinate: ["-t", "-w"],
|
|
337
|
+
}
|
|
338
|
+
const reEscape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
339
|
+
const WRAPPER_VALUE = "(?!\\w+=)[^-\\s]\\S*\\s+"
|
|
340
|
+
const WRAPPER_PREFIX = "(?:(?:[\\w./-]*/)?(?:" + Object.entries(WRAPPER_VALUE_OPTS)
|
|
341
|
+
.map(([name, opts]) => {
|
|
342
|
+
const shorts = [...new Set(opts.filter((o) => o.length === 2 && o !== "-S")
|
|
343
|
+
.map((o) => o[1]))].sort().join("")
|
|
344
|
+
const valued = (shorts ? "-[A-Za-z]*[" + reEscape(shorts) + "]\\s+" + WRAPPER_VALUE + "|" : "") +
|
|
345
|
+
opts.filter((o) => o.startsWith("--"))
|
|
346
|
+
.map((o) => reEscape(o) + "\\s+" + WRAPPER_VALUE + "|").join("")
|
|
347
|
+
return reEscape(name) + "\\s+(?:\\w+=\\S+\\s+|" + valued + "-\\S+\\s+)*" +
|
|
348
|
+
(name === "timeout" ? "(?:\\d\\S*\\s+)?" : "")
|
|
349
|
+
}).join("|") + "))*"
|
|
350
|
+
const WRAPPER_RUN = new RegExp("((?:^|[|;&(\\n])\\s*)" + WRAPPER_PREFIX, "g")
|
|
317
351
|
const VERIFY = new RegExp(
|
|
318
352
|
// command position only: `echo pytest` is not a check, and a `\\s` prefix made
|
|
319
353
|
// the port read one where hooks/tezgah_integrity.VERIFY does not (review of
|
|
320
|
-
// plan 135)
|
|
321
|
-
"(?:^|[|;&(\\n])\\s*(?:
|
|
322
|
-
"(?:\\w+=\\S+\\s+)*(?:-\\S+\\s+)*)?(?:" +
|
|
354
|
+
// plan 135)
|
|
355
|
+
"(?:^|[|;&(\\n])\\s*(?:" +
|
|
323
356
|
"(?:python3?|uv run)\\s+(?:(?:--project|--with|--python|--extra|--group|-p|-w)" +
|
|
324
357
|
"\\s+\\S+\\s+|-\\S+\\s+)*(?:python3?\\s+-m\\s+)?" +
|
|
325
358
|
"(?:pytest|unittest|mypy|ruff|flake8|compileall)|" +
|
|
@@ -337,14 +370,27 @@ const VERIFY = new RegExp(
|
|
|
337
370
|
"bunx?|uvx)\\s+(?:-\\S+\\s+)*(?:playwright|cypress|storybook|chromatic|percy|" +
|
|
338
371
|
"lighthouse|pa11y|axe-core|backstop|reg-suit|vitest|jest|ava|mocha|eslint|" +
|
|
339
372
|
"prettier|tsc|pyright))\\b", "i")
|
|
340
|
-
|
|
373
|
+
// hooks/tezgah_integrity.NEUTER, the same alternatives: `|| true`, `|| :`,
|
|
374
|
+
// `|| exit 0`, `; true`, `; :` and `; exit 0` after a check. Also the bound on
|
|
375
|
+
// asking the core about a swallowed check: its own VERIFY decides the check.
|
|
376
|
+
const NEUTER = /\|\|\s*(?:true|:|exit\s+0)(?:\s|$|[|;&])|;\s*(?:true|:|exit\s+0)\s*(?:$|[|;&])/
|
|
377
|
+
// The bound on asking the core about the rest of the shortcut rule
|
|
378
|
+
// (hooks/tezgah_integrity.shortcut_command): a commit or push whose hooks a
|
|
379
|
+
// wrapper's options, an `eval` or an env var might hide, and an EXIT trap that
|
|
380
|
+
// could swallow a check's status. The core's readers step over the wrappers and
|
|
381
|
+
// open `eval`; this mirror's own (shortcutCommand) still runs after the core,
|
|
382
|
+
// and alone when the core cannot answer.
|
|
383
|
+
const SHORTCUT_CMD = /\b(?:commit|push|eval|trap)\b|--no-verify|\b(?:SKIP|HUSKY_SKIP_HOOKS|HUSKY)\s*=/i
|
|
341
384
|
const SKIP_ENV = /\b(?:SKIP|HUSKY_SKIP_HOOKS)\s*=|\bHUSKY=0\b/
|
|
342
385
|
const NO_VERIFY = /--no-verify\b/
|
|
343
386
|
const GITISH = /\b(?:git|commit|push|husky|pre-commit|npm|yarn|pnpm)\b/i
|
|
344
387
|
// mirrors _shell_segments/_hooks_redirect in hooks/tezgah_integrity.py: a
|
|
345
388
|
// core.hooksPath assignment in the same line as a git commit/push skips the
|
|
346
|
-
// hooks like --no-verify; read word by word through shellWords (shlex's reading)
|
|
347
|
-
|
|
389
|
+
// hooks like --no-verify; read word by word through shellWords (shlex's reading).
|
|
390
|
+
// HOOKS_KEYS is the core's: an include (`include.path`, `includeIf.<cond>.path`)
|
|
391
|
+
// can carry a hooksPath too.
|
|
392
|
+
const HOOKS_KEYS = /core\.hookspath|include\.path|includeif\..+\.path/
|
|
393
|
+
const HOOKS_KEYS_FULL = new RegExp("^(?:" + HOOKS_KEYS.source + ")$")
|
|
348
394
|
const GIT_CONFIG_ENV = /^GIT_CONFIG_(?:KEY_\d+|PARAMETERS)$/
|
|
349
395
|
const ENV_WORD = /^[A-Za-z_]\w*=/
|
|
350
396
|
const GIT_VALUE_OPTS = new Set(["-C", "-c", "--git-dir", "--work-tree",
|
|
@@ -548,13 +594,43 @@ function shellSegments(cmd) {
|
|
|
548
594
|
// the separators after a check that keep its status the line's
|
|
549
595
|
// (hooks/tezgah_integrity.OWNING_SEP)
|
|
550
596
|
const OWNING_SEP = /^[()]*(?:&&|\|&?)[()]*$/
|
|
597
|
+
// hooks/tezgah_integrity.trap_owns_status: a trap may own the line's status
|
|
598
|
+
// unless it is an EXIT (or 0) trap whose every command is plain cleanup
|
|
599
|
+
// (TRAP_CLEANUP). Any other action can end the shell with a status of its own,
|
|
600
|
+
// an ERR/DEBUG/RETURN trap runs in the middle of the check, and a `trap` word
|
|
601
|
+
// the line's own commands do not set (inside a string, an `eval`, a function
|
|
602
|
+
// body, behind a wrapper) counts the same way. The word is counted with quotes
|
|
603
|
+
// and backslashes taken out, so `t''rap` and `t\rap` are traps here too, and a
|
|
604
|
+
// trap on a line with ANSI-C quoting (`$'...'`) is never read as cleanup - a
|
|
605
|
+
// lost credit, never an invented one.
|
|
606
|
+
const TRAP_WORD = /(?<![\w.-])trap(?![\w.-])/g
|
|
607
|
+
const TRAP_CLEANUP = new Set(["rm", "echo", "printf", ":"])
|
|
608
|
+
function trapOwnsStatus(cmd) {
|
|
609
|
+
const seen = (cmd.replace(/['"\\]/g, "").match(TRAP_WORD) || []).length
|
|
610
|
+
const traps = shellSegments(cmd).map((words) => {
|
|
611
|
+
let i = 0
|
|
612
|
+
while (i < words.length && (ENV_WORD.test(words[i]) || SECRET_KEYWORDS.has(words[i]))) i++
|
|
613
|
+
return words.slice(i)
|
|
614
|
+
}).filter((words) => words[0] === "trap")
|
|
615
|
+
if (traps.length < seen || (traps.length && cmd.includes("$'"))) return true
|
|
616
|
+
for (const words of traps) {
|
|
617
|
+
const args = words.slice(1).filter((w) => w !== "--")
|
|
618
|
+
if (args.length < 2 || args[0] === "-") continue // a listing or a reset
|
|
619
|
+
if (args.slice(1).some((s) => !["EXIT", "0"].includes(s.toUpperCase().replace(/^SIG/, ""))))
|
|
620
|
+
return true
|
|
621
|
+
if (shellSegments(args[0]).some((w) => !w.length || !TRAP_CLEANUP.has(w[0]))) return true
|
|
622
|
+
}
|
|
623
|
+
return false
|
|
624
|
+
}
|
|
551
625
|
|
|
552
626
|
// True when the line's exit status is not its check's
|
|
553
627
|
// (hooks/tezgah_integrity.status_hidden): a pipe owns it unless the line opens
|
|
554
628
|
// with `set -o pipefail` and has no `||`, or a command after the check does -
|
|
555
629
|
// a `;`, a newline or a trailing `&` - unless that command is `exit $?`. A
|
|
556
|
-
// heredoc body and its terminator are data.
|
|
630
|
+
// heredoc body and its terminator are data. A trap may own it too
|
|
631
|
+
// (trapOwnsStatus).
|
|
557
632
|
function statusHidden(cmd) {
|
|
633
|
+
if (trapOwnsStatus(cmd)) return true
|
|
558
634
|
if (cmd.includes("|") && (cmd.includes("||") ||
|
|
559
635
|
!/^\s*set\s+(?:-\w+\s+)*-\w*o\s+pipefail\s*(?:;|&&|\n)/.test(cmd))) return true
|
|
560
636
|
const body = heredocBytes(cmd)
|
|
@@ -562,6 +638,10 @@ function statusHidden(cmd) {
|
|
|
562
638
|
body[k] && ch !== "\n" ? " " : ch).join(""))
|
|
563
639
|
for (let i = 0; i < cmds.length; i++) {
|
|
564
640
|
if (!verifyCommand(cmds[i][0].map(quoteWord).join(" "))) continue
|
|
641
|
+
// an `exit`, or an `exec` that runs a command, before the check ends the
|
|
642
|
+
// line with its own status (`exit 0; pytest` never runs pytest)
|
|
643
|
+
if (cmds.slice(0, i).some(([w]) => w[0] === "exit" || (w[0] === "exec" && w.length > 1)))
|
|
644
|
+
return true
|
|
565
645
|
for (let k = i; k < cmds.length - 1; k++) {
|
|
566
646
|
if (OWNING_SEP.test(cmds[k][1])) continue
|
|
567
647
|
const next = cmds[k + 1][0].join(" ")
|
|
@@ -580,7 +660,7 @@ function quoteWord(word) {
|
|
|
580
660
|
|
|
581
661
|
function namesHooksKey(setting) {
|
|
582
662
|
// a leading `$`: bash reads `$'core.hooksPath'` as the plain key, shlex keeps it
|
|
583
|
-
return setting.replace(/^\$+/, "").toLowerCase().split("=")[0]
|
|
663
|
+
return HOOKS_KEYS_FULL.test(setting.replace(/^\$+/, "").toLowerCase().split("=")[0])
|
|
584
664
|
}
|
|
585
665
|
|
|
586
666
|
function hooksRedirect(cmd) {
|
|
@@ -593,7 +673,7 @@ function hooksRedirect(cmd) {
|
|
|
593
673
|
if (ENV_WORD.test(words[i])) {
|
|
594
674
|
const eq = words[i].indexOf("=")
|
|
595
675
|
if (GIT_CONFIG_ENV.test(words[i].slice(0, eq)) &&
|
|
596
|
-
words[i].slice(eq + 1).toLowerCase()
|
|
676
|
+
HOOKS_KEYS.test(words[i].slice(eq + 1).toLowerCase())) assigns = true
|
|
597
677
|
}
|
|
598
678
|
i++
|
|
599
679
|
}
|
|
@@ -619,7 +699,7 @@ function hooksRedirect(cmd) {
|
|
|
619
699
|
CONFIG_READ_OPTS.has(a)) || (head.length > 0 && CONFIG_READ_SUBS.has(head[0]))
|
|
620
700
|
const positional = head.filter((a) => !a.startsWith("-"))
|
|
621
701
|
.concat(cut >= 0 ? args.slice(cut + 1) : [])
|
|
622
|
-
if (!reads && positional.length >= 2 && positional[0]
|
|
702
|
+
if (!reads && positional.length >= 2 && HOOKS_KEYS_FULL.test(positional[0])) assigns = true
|
|
623
703
|
}
|
|
624
704
|
}
|
|
625
705
|
return assigns && writes
|
|
@@ -657,13 +737,18 @@ const WRITE_TOOLS = new Set(["edit", "write", "multiedit", "notebookedit",
|
|
|
657
737
|
// hosts send.
|
|
658
738
|
const BASH_TOOLS = new Set(["bash", "shell", "command", "exec_command",
|
|
659
739
|
"run_command", "powershell", "pwsh"])
|
|
660
|
-
// The
|
|
661
|
-
//
|
|
662
|
-
//
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
|
|
740
|
+
// The shell lines the core is asked about first: every line with a write shape
|
|
741
|
+
// its rules read - a redirect (a descriptor copy such as `2>&1` names no file,
|
|
742
|
+
// as `shell_control` reads it), a program that writes, removes or moves a path
|
|
743
|
+
// (hooks/tezgah_gate.CONTROL_PROGRAMS, OPTION_TARGETS, `tar`), a git staging
|
|
744
|
+
// command (hooks/tezgah_gate.STAGING) or `git config` - and tezgah's own CLIs,
|
|
745
|
+
// however the program word is quoted, and hook entries. The control rule
|
|
746
|
+
// (`control_reason`) follows a `cd`, expands `~`/`$HOME`/XDG and refuses the
|
|
747
|
+
// removal of a directory that holds its state, and the workspace and credential
|
|
748
|
+
// rules read the write's target, so no path shape can stand in for them here:
|
|
749
|
+
// this is only the bound on asking, loose the same way IDENT_CMD is - a false
|
|
750
|
+
// hit costs one spawn whose answer comes back empty.
|
|
751
|
+
const CONTROL_CMD = />(?!&(?:\d+|-)(?=$|[\s;&|)]))|\b(?:touch|rm|rmdir|unlink|shred|truncate|chmod|chown|chgrp|mv|mkdir|tee|cp|ln|install|rsync|dd|sed|perl|sqlite3|curl|wget|unzip|tar|ditto|mkfifo|mknod)\b|\bgit\b[\s\S]*\b(?:add|config|stage|commit|update-index)\b|\btezgah-(?:gate(?:\.py)?\b[^|;&\n]*\bdecide\b|capture\b|pony\b|adhd\b|context(?:\.py)?\b[^|;&\n]*\b(?:attest|user_prompt)\b|[\w-]+-hook\b)|\bprojects-(?:auto-init|pretooluse|posttooluse|stop)\b|\bhook\.py\b/i
|
|
667
752
|
// The same bound for the language rule: the command shapes that dream up an
|
|
668
753
|
// identifier which outlives the session - `git commit` (its message), `git
|
|
669
754
|
// checkout -b`/`-B`/`--branch` and `git switch -c`/`-C`/`--create` (a branch),
|
|
@@ -682,7 +767,7 @@ const CONTROL_CMD = /(?:\b(?:touch|rm|rmdir|unlink|shred|truncate|chmod|chown|ch
|
|
|
682
767
|
// comes first and the language rule comes first there too
|
|
683
768
|
// (hooks/tezgah_gate.decision order).
|
|
684
769
|
const IDENT_CMD =
|
|
685
|
-
/\bgit\s+(?:-{1,2}\S+(?:\s+\S+)?\s+)*(?:commit\b|checkout\s+(?:-\S+\s+)*(?:-b|-B|--branch)\s+\S|switch\s+(?:-\S+\s+)*(?:-c|-C|--create)\s+\S|branch\s+(?!-)\S)|(?:^|[|;&(]\s*|\s)gh\s+(?:pr|issue)\s+(?:create|edit)\b/i
|
|
770
|
+
/\bgit\s+(?:-{1,2}\S+(?:\s+\S+)?\s+)*(?:commit\b|checkout\s+(?:-\S+\s+)*(?:(?:-b|-B|--branch)\s+\S|-[bB](?=[^\s;&|]))|switch\s+(?:-\S+\s+)*(?:-c|-C|--create)\s+\S|branch\s+(?!-)\S)|(?:^|[|;&(]\s*|\s)gh\s+(?:pr|issue)\s+(?:create|edit)\b/i
|
|
686
771
|
// Mirrors hooks/tezgah_integrity.py READ_TOOLS: known calls that do no step of
|
|
687
772
|
// work and that no rule reads a row for. A name missing from this set is not
|
|
688
773
|
// silently dropped - it records as `unknown`, which would fill the trace with
|
|
@@ -942,7 +1027,7 @@ function verifyCommand(cmd) {
|
|
|
942
1027
|
// the masked text, the convention shortcutCommand already follows: a check
|
|
943
1028
|
// named inside a quoted string or a heredoc body is text ABOUT a command, not
|
|
944
1029
|
// one, and reading it as one records a passing check nobody ran
|
|
945
|
-
const text = maskText(cmd)
|
|
1030
|
+
const text = maskText(cmd).replace(WRAPPER_RUN, "$1")
|
|
946
1031
|
for (const m of text.matchAll(VERIFY_ALL)) {
|
|
947
1032
|
const words = m[0].trim().split(/\s+/)
|
|
948
1033
|
const tool = words[words.length - 1].toLowerCase()
|
|
@@ -2025,12 +2110,47 @@ function orderReason(args, rows) {
|
|
|
2025
2110
|
return ORDER_DENY.replace("%s", failedCheck(rows))
|
|
2026
2111
|
}
|
|
2027
2112
|
|
|
2113
|
+
// A write or shell call's first row, as hooks/tezgah_gate.decision writes it
|
|
2114
|
+
// before any rule runs (tezgah_integrity.BEGAN_KIND): tool.execute.after writes
|
|
2115
|
+
// the second with the same id, a refusal answers it with its `deny`, and one
|
|
2116
|
+
// never answered is a call whose outcome nobody saw. BEGAN holds the args
|
|
2117
|
+
// object of each call whose row is written - by the core when it answered
|
|
2118
|
+
// (gateReason), else here - so a call gets exactly one, ahead of its `deny`.
|
|
2119
|
+
const BEGAN = new WeakSet()
|
|
2120
|
+
// The directory the plugin was loaded for, the `cwd` the core is handed: a
|
|
2121
|
+
// `began` row's `target` resolves against it as the core's does.
|
|
2122
|
+
let HOOK_DIR = null
|
|
2123
|
+
async function noteBegan(sessionID, tool, args, workspace) {
|
|
2124
|
+
const t = String(tool || "").toLowerCase()
|
|
2125
|
+
if (!sessionID || !args || typeof args !== "object" || BEGAN.has(args)
|
|
2126
|
+
|| !(WRITE_TOOLS.has(t) || BASH_TOOLS.has(t))) return
|
|
2127
|
+
BEGAN.add(args)
|
|
2128
|
+
const command = String(args.command || args.cmd || "")
|
|
2129
|
+
const written = writtenPath(args)
|
|
2130
|
+
// `~` and `$HOME` expanded as the core's write_paths does
|
|
2131
|
+
const target = written
|
|
2132
|
+
? absTarget(written.replace(/^(?:~|\$\{HOME\}|\$HOME)(?=\/|$)/, HOME), HOOK_DIR) : null
|
|
2133
|
+
await appendRow(sessionID, {
|
|
2134
|
+
kind: "began", ts: Math.floor(Date.now() / 1000),
|
|
2135
|
+
detail: redact(command || written || "").slice(0, DETAIL_MAX),
|
|
2136
|
+
id: actionID(t, args), tool: t, workspace,
|
|
2137
|
+
// decided from the whole command, as hooks/tezgah_gate.decision does: the
|
|
2138
|
+
// detail is cut at DETAIL_MAX
|
|
2139
|
+
...(BASH_TOOLS.has(t) && verifyCommand(command) ? { check: 1 } : {}),
|
|
2140
|
+
// the file it writes, as the race rule matches it: a `began` row nothing
|
|
2141
|
+
// answered yet is a write in flight (hooks/tezgah_gate.writers_elsewhere)
|
|
2142
|
+
...(target ? { target } : {}),
|
|
2143
|
+
})
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2028
2146
|
// The refusal recorded before it is returned, as the Python gate's `_deny` does:
|
|
2029
2147
|
// a deny nobody counts is a rule whose effect can never be argued about, and the
|
|
2030
2148
|
// row says which action the rule stopped and where. `extra` appends a fact the
|
|
2031
|
-
// row must keep beyond the 80 characters of the reason it truncates.
|
|
2149
|
+
// row must keep beyond the 80 characters of the reason it truncates. The call's
|
|
2150
|
+
// `began` row goes first, as the core writes it before any rule.
|
|
2032
2151
|
async function noteDeny(sessionID, rule, reason, tool, args, workspace, extra) {
|
|
2033
2152
|
if (!sessionID) return
|
|
2153
|
+
await noteBegan(sessionID, tool, args, workspace)
|
|
2034
2154
|
let detail = rule + ": " + String(reason).slice(0, 80)
|
|
2035
2155
|
if (extra) detail += "; " + extra
|
|
2036
2156
|
await appendRow(sessionID, {
|
|
@@ -2222,7 +2342,7 @@ function secretPrinter(text) {
|
|
|
2222
2342
|
}
|
|
2223
2343
|
|
|
2224
2344
|
// `words` past keywords, env assignments and the wrappers
|
|
2225
|
-
// (hooks/
|
|
2345
|
+
// (hooks/tezgah_integrity._past_wrappers)
|
|
2226
2346
|
function secretPastWrappers(words) {
|
|
2227
2347
|
let i = 0
|
|
2228
2348
|
while (i < words.length) {
|
|
@@ -2674,8 +2794,15 @@ const SWITCHES = new Set(["adhd-off", "consult-off", "design-off", "docs-judge-o
|
|
|
2674
2794
|
// switch file whose ctime or mtime is newer than the session ledger's first row
|
|
2675
2795
|
// is ignored unless an `authorized` row names it or it is the full uninstall's
|
|
2676
2796
|
// stand-down (STAND_DOWN); without one the file alone answers. This process
|
|
2677
|
-
// serves many sessions, so the id is an argument, not process state.
|
|
2797
|
+
// serves many sessions, so the id is an argument, not process state. A child
|
|
2798
|
+
// session latches on the earlier of its own and its parent's first row, and
|
|
2799
|
+
// its parent's `authorized` rows count for it (B-036). The parent is the one
|
|
2800
|
+
// opencode named on the child's session.created (PARENTS), never a ledger row:
|
|
2801
|
+
// a `spawned` row is bytes an agent can write (review 165).
|
|
2802
|
+
// ponytail: held in this process only - a plugin restarted while a child runs
|
|
2803
|
+
// forgets its parent, and the child then latches on its own first row as before.
|
|
2678
2804
|
const STAND_DOWN = "tezgah-setup --uninstall --full\n"
|
|
2805
|
+
const PARENTS = new Map()
|
|
2679
2806
|
|
|
2680
2807
|
async function off(name, sessionID) {
|
|
2681
2808
|
if (!SWITCHES.has(name)) return false
|
|
@@ -2691,15 +2818,24 @@ async function honored(path, name, sessionID) {
|
|
|
2691
2818
|
let stats
|
|
2692
2819
|
try { stats = [statSync(path), lstatSync(path)] } catch { return false }
|
|
2693
2820
|
if (!sessionID) return true
|
|
2694
|
-
const
|
|
2695
|
-
const
|
|
2821
|
+
const parent = PARENTS.get(String(sessionID))
|
|
2822
|
+
const ids = parent && parent !== String(sessionID) ? [sessionID, parent] : [sessionID]
|
|
2823
|
+
const firsts = []
|
|
2824
|
+
for (const id of ids) {
|
|
2825
|
+
const [first] = await ledgerRead(id, "first")
|
|
2826
|
+
if (typeof first?.ts === "number") firsts.push(first.ts)
|
|
2827
|
+
}
|
|
2828
|
+
const since = firsts.length ? Math.min(...firsts) : null
|
|
2696
2829
|
const newest = Math.max(...stats.flatMap((s) => [s.mtimeMs, s.ctimeMs])) / 1000
|
|
2697
2830
|
if (since === null || newest < since + 1) return true
|
|
2698
2831
|
try {
|
|
2699
2832
|
if (name === "pretooluse-off" && readFileSync(path, "utf8") === STAND_DOWN) return true
|
|
2700
2833
|
} catch {}
|
|
2701
|
-
|
|
2702
|
-
(
|
|
2834
|
+
for (const id of ids) {
|
|
2835
|
+
if ((await ledgerRead(id, "kind", "authorized")).some(
|
|
2836
|
+
(row) => Array.isArray(row.authorized) && row.authorized.includes(name))) return true
|
|
2837
|
+
}
|
|
2838
|
+
return false
|
|
2703
2839
|
}
|
|
2704
2840
|
|
|
2705
2841
|
function slug(p) {
|
|
@@ -2945,18 +3081,33 @@ function builderText(event, directory, payload) {
|
|
|
2945
3081
|
// guards. What the silence costs is one row, never a refusal.
|
|
2946
3082
|
// `core.answered` says the core did answer: its live `decide` has then kept the
|
|
2947
3083
|
// snapshot of an allowed write itself, and this file must not keep a second one.
|
|
3084
|
+
// It has written the call's `began` row too, and its `deny` when `core.denied`.
|
|
3085
|
+
// A core that failed after its first line (a rule raised, the deadline hit) may
|
|
3086
|
+
// have written that `began` already: a row for this call since the ask began
|
|
3087
|
+
// counts as written, so the call is not opened twice.
|
|
2948
3088
|
async function gateReason(tool, args, dir, sessionID, core = {}) {
|
|
2949
3089
|
const state = {}
|
|
3090
|
+
const since = Math.floor(Date.now() / 1000)
|
|
2950
3091
|
const reason = await collect(
|
|
2951
3092
|
[GATE_BIN, "decide"], { stdio: ["pipe", "pipe", "ignore"] },
|
|
2952
3093
|
JSON.stringify({
|
|
2953
|
-
tool, input: args, cwd: dir, session_id: sessionID || null
|
|
3094
|
+
tool, input: args, cwd: dir, session_id: sessionID || null,
|
|
3095
|
+
parent: PARENTS.get(String(sessionID)) }),
|
|
2954
3096
|
(code, out, failure) => {
|
|
2955
3097
|
state.failure = failure
|
|
2956
3098
|
return code === 0 ? out.trim() : ""
|
|
2957
3099
|
})
|
|
2958
|
-
if (state.failure)
|
|
2959
|
-
|
|
3100
|
+
if (state.failure) {
|
|
3101
|
+
await noteDelegation(sessionID, state.failure)
|
|
3102
|
+
const digest = actionID(tool, args)
|
|
3103
|
+
if (sessionID && args && typeof args === "object" &&
|
|
3104
|
+
(await ledgerTail(sessionID, LEDGER_TAIL)).some((r) => r.kind === "began"
|
|
3105
|
+
&& r.id === digest && Number(r.ts) >= since)) BEGAN.add(args)
|
|
3106
|
+
} else {
|
|
3107
|
+
core.answered = true
|
|
3108
|
+
core.denied = Boolean(reason)
|
|
3109
|
+
if (args && typeof args === "object") BEGAN.add(args)
|
|
3110
|
+
}
|
|
2960
3111
|
return reason
|
|
2961
3112
|
}
|
|
2962
3113
|
|
|
@@ -3101,6 +3252,7 @@ export const Tezgah = async ({ directory }) => {
|
|
|
3101
3252
|
if (globalThis.__tezgahPluginLoaded) return {}
|
|
3102
3253
|
globalThis.__tezgahPluginLoaded = true
|
|
3103
3254
|
const dir = directory || process.cwd()
|
|
3255
|
+
HOOK_DIR = dir
|
|
3104
3256
|
|
|
3105
3257
|
return {
|
|
3106
3258
|
// Register the repo's generated subagents at config load so opencode sees
|
|
@@ -3149,8 +3301,8 @@ export const Tezgah = async ({ directory }) => {
|
|
|
3149
3301
|
|
|
3150
3302
|
"tool.execute.before": async (input, output) => {
|
|
3151
3303
|
let deny = null
|
|
3152
|
-
// whether the core was asked
|
|
3153
|
-
//
|
|
3304
|
+
// whether the core was asked, so it is asked at most once per call; what
|
|
3305
|
+
// it answered (and so which rows it wrote) is in `core`
|
|
3154
3306
|
let asked = false
|
|
3155
3307
|
const core = {}
|
|
3156
3308
|
try {
|
|
@@ -3166,11 +3318,15 @@ export const Tezgah = async ({ directory }) => {
|
|
|
3166
3318
|
// `verify-off` removes; attribution and explore are other rules and stay
|
|
3167
3319
|
const shortcuts = !(await off("verify-off", sessionID))
|
|
3168
3320
|
// The control rule is the Python gate's first (hooks/tezgah_gate.decision),
|
|
3169
|
-
// so a shell line CONTROL_CMD says could
|
|
3170
|
-
//
|
|
3171
|
-
//
|
|
3321
|
+
// so a shell line CONTROL_CMD says could write - or one NEUTER or
|
|
3322
|
+
// SHORTCUT_CMD says could swallow a check's status or skip the hooks - is
|
|
3323
|
+
// put to the core before any rule here: its answer is the whole gate's,
|
|
3324
|
+
// in the gate's own order, and this file keeps no copy of the path table,
|
|
3325
|
+
// the credential's `git add` reader, the workspace rule or the shortcut
|
|
3326
|
+
// readers' wrapper table and `eval` opening.
|
|
3172
3327
|
const line = BASH_TOOLS.has(tool) ? String(args.command || args.cmd || "") : ""
|
|
3173
|
-
if (line && CONTROL_CMD.test(line)
|
|
3328
|
+
if (line && (CONTROL_CMD.test(line) ||
|
|
3329
|
+
(shortcuts && (NEUTER.test(line) || SHORTCUT_CMD.test(line))))) {
|
|
3174
3330
|
asked = true
|
|
3175
3331
|
deny = await gateReason(tool, args, dir, sessionID, core)
|
|
3176
3332
|
}
|
|
@@ -3207,10 +3363,10 @@ export const Tezgah = async ({ directory }) => {
|
|
|
3207
3363
|
}
|
|
3208
3364
|
}
|
|
3209
3365
|
// A refusal this file decided is counted the way the Python gate counts
|
|
3210
|
-
// its own (hooks/tezgah_gate._deny); one the core answered
|
|
3211
|
-
//
|
|
3212
|
-
//
|
|
3213
|
-
if (deny && !
|
|
3366
|
+
// its own (hooks/tezgah_gate._deny); one the core answered is in the
|
|
3367
|
+
// ledger already. Without the row, `--report --live` saw the shortcut
|
|
3368
|
+
// refused and nothing recorded.
|
|
3369
|
+
if (deny && !core.denied) {
|
|
3214
3370
|
await noteDeny(sessionID, deny === ATTRIB_DENY ? "attribution"
|
|
3215
3371
|
: deny === EXPLORE_DENY ? "explorer" : "shortcut", deny, tool, args, base)
|
|
3216
3372
|
}
|
|
@@ -3292,24 +3448,9 @@ export const Tezgah = async ({ directory }) => {
|
|
|
3292
3448
|
await captureSnapshot("write", { file_path: target }, dir, sessionID)
|
|
3293
3449
|
}
|
|
3294
3450
|
}
|
|
3295
|
-
// The call's
|
|
3296
|
-
//
|
|
3297
|
-
|
|
3298
|
-
// outcome nobody saw. A core that was asked and did not answer wrote
|
|
3299
|
-
// none, so this file writes it then too.
|
|
3300
|
-
if (!deny && !core.answered && sessionID &&
|
|
3301
|
-
(WRITE_TOOLS.has(tool) || BASH_TOOLS.has(tool))) {
|
|
3302
|
-
await appendRow(sessionID, {
|
|
3303
|
-
kind: "began", ts: Math.floor(Date.now() / 1000),
|
|
3304
|
-
detail: redact(String(args.command || args.cmd || writtenPath(args)
|
|
3305
|
-
|| "")).slice(0, DETAIL_MAX),
|
|
3306
|
-
id: actionID(tool, args), tool, workspace: base,
|
|
3307
|
-
// decided from the whole command, as hooks/tezgah_gate.decision
|
|
3308
|
-
// does: the detail is cut at DETAIL_MAX
|
|
3309
|
-
...(BASH_TOOLS.has(tool) && verifyCommand(String(args.command || args.cmd || ""))
|
|
3310
|
-
? { check: 1 } : {}),
|
|
3311
|
-
})
|
|
3312
|
-
}
|
|
3451
|
+
// The call's `began` row on its allow path (noteBegan): written here
|
|
3452
|
+
// unless the core answered, or a refusal above already wrote it.
|
|
3453
|
+
if (!deny) await noteBegan(sessionID, tool, args, base)
|
|
3313
3454
|
} catch {}
|
|
3314
3455
|
if (deny) throw new Error(deny)
|
|
3315
3456
|
},
|
|
@@ -3326,6 +3467,7 @@ export const Tezgah = async ({ directory }) => {
|
|
|
3326
3467
|
const ev = input && input.event
|
|
3327
3468
|
const info = ev && ev.type === "session.created" && ev.properties && ev.properties.info
|
|
3328
3469
|
if (!info || !info.id || !info.parentID) return
|
|
3470
|
+
PARENTS.set(String(info.id), String(info.parentID))
|
|
3329
3471
|
if (!(await rootFor(dir))) return
|
|
3330
3472
|
const title = String(info.title || "")
|
|
3331
3473
|
const typed = /\(@([^\s()]+) subagent\)\s*$/.exec(title)
|
|
@@ -3345,7 +3487,6 @@ export const Tezgah = async ({ directory }) => {
|
|
|
3345
3487
|
const env = output.env && typeof output.env === "object" ? output.env : (output.env = {})
|
|
3346
3488
|
const rs = await roots()
|
|
3347
3489
|
if (rs.length) env.TEZGAH_ROOTS = rs.join(delimiter)
|
|
3348
|
-
env.TEZGAH_HOME = CONFIG
|
|
3349
3490
|
env.TEZGAH_STATUS_BIN = STATUS_BIN
|
|
3350
3491
|
if (input && input.sessionID) env.TEZGAH_SESSION = String(input.sessionID)
|
|
3351
3492
|
} catch {}
|
|
@@ -3363,7 +3504,8 @@ export const Tezgah = async ({ directory }) => {
|
|
|
3363
3504
|
const context = Array.isArray(output.context) ? output.context : (output.context = [])
|
|
3364
3505
|
const sessionID = String(input?.sessionID || "")
|
|
3365
3506
|
const text = await builderText("post_compact", dir,
|
|
3366
|
-
sessionID ? { session_id: sessionID
|
|
3507
|
+
sessionID ? { session_id: sessionID,
|
|
3508
|
+
parent: PARENTS.get(sessionID) } : {})
|
|
3367
3509
|
if (text) context.push(text)
|
|
3368
3510
|
} catch {}
|
|
3369
3511
|
},
|
|
@@ -3389,8 +3531,12 @@ export const Tezgah = async ({ directory }) => {
|
|
|
3389
3531
|
const prompt = parts
|
|
3390
3532
|
.filter((p) => p && p.type === "text" && typeof p.text === "string")
|
|
3391
3533
|
.map((p) => p.text).join("\n")
|
|
3534
|
+
// a child session's prompt is its parent's brief, never the user's
|
|
3535
|
+
// grant (B-086), and its latch counts from its parent too (B-036)
|
|
3536
|
+
const parent = PARENTS.get(sessionID)
|
|
3392
3537
|
const text = await builderText("user_prompt", dir,
|
|
3393
|
-
{ prompt, session_id: sessionID, host: "opencode"
|
|
3538
|
+
{ prompt, session_id: sessionID, host: "opencode",
|
|
3539
|
+
...(parent ? { subagent: true, parent } : {}) })
|
|
3394
3540
|
if (text) {
|
|
3395
3541
|
const anchor = output.message && typeof output.message === "object"
|
|
3396
3542
|
? output.message : {}
|
package/hosts/qwen/hook.py
CHANGED
|
@@ -151,7 +151,7 @@ def main():
|
|
|
151
151
|
event = payload.get("hook_event_name") or ""
|
|
152
152
|
cwd = payload.get("cwd") or os.getcwd()
|
|
153
153
|
session_id = payload.get("session_id")
|
|
154
|
-
safe(session_id, bind_session, session_id) # the switch latch
|
|
154
|
+
safe(session_id, bind_session, session_id, payload.get("parent")) # the switch latch
|
|
155
155
|
out = {}
|
|
156
156
|
if event == "PreToolUse":
|
|
157
157
|
reason = safe(session_id, decision, gate_name(payload.get("tool_name", "")),
|
package/output-styles/tezgah.md
CHANGED
|
@@ -38,7 +38,7 @@ first; write what was cut only when you consciously cut something the user asked
|
|
|
38
38
|
for, in at most three lines. In force from the first turn; levels
|
|
39
39
|
lite|full|ultra are set with `~/.config/tezgah/bin/tezgah-pony` and ride the per-turn reminder when
|
|
40
40
|
not `full`; read the full `ponytail` skill from the router for the level table
|
|
41
|
-
and examples. Off: "stop ponytail".
|
|
41
|
+
and examples. Off: "stop ponytail", `ponytail-auto.off` or `.no-ponytail`.
|
|
42
42
|
|
|
43
43
|
**Output shape: ADHD-friendly.** The answer or the next action is on the first
|
|
44
44
|
line - a command, path, snippet or the decision itself, never context or the
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@r1z4x/tezgah",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "One shared working contract for every AI coding assistant you run: Claude Code, Codex, Cursor, opencode, dsh and
|
|
3
|
+
"version": "1.14.0",
|
|
4
|
+
"description": "One shared working contract for every AI coding assistant you run, across fourteen hosts: omp, Claude Code, Codex, Cursor, opencode, dsh, Qwen Code, Factory Droid, Command Code, GitHub Copilot CLI, Gemini CLI, Antigravity CLI, Goose and Amp.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -23,7 +23,10 @@
|
|
|
23
23
|
"MANIFEST",
|
|
24
24
|
"CHANGELOG.md",
|
|
25
25
|
"LICENSE",
|
|
26
|
-
"
|
|
26
|
+
"NOTICE",
|
|
27
|
+
"third_party/",
|
|
28
|
+
"packaging/",
|
|
29
|
+
"!**/__pycache__"
|
|
27
30
|
],
|
|
28
31
|
"engines": {
|
|
29
32
|
"node": ">=18"
|
package/packaging/build.sh
CHANGED
|
@@ -79,12 +79,12 @@ def version():
|
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
def changelog_version():
|
|
82
|
-
"""The newest
|
|
82
|
+
"""The newest `## [x.y.z]` heading, as `RELEASE` in hooks/tezgah_context.py."""
|
|
83
83
|
try:
|
|
84
84
|
with open(os.path.join(ROOT, "CHANGELOG.md")) as fh:
|
|
85
|
-
for
|
|
86
|
-
if
|
|
87
|
-
return
|
|
85
|
+
for v in (ln[4:].split("]")[0] for ln in fh if ln.startswith("## [")):
|
|
86
|
+
if v.count(".") == 2 and all(p.isdigit() for p in v.split(".")):
|
|
87
|
+
return v
|
|
88
88
|
except OSError:
|
|
89
89
|
pass
|
|
90
90
|
return ""
|
package/packaging/install.sh
CHANGED
|
@@ -26,6 +26,17 @@ while [ $# -gt 0 ]; do case $1 in
|
|
|
26
26
|
*) echo "tezgah: unknown option $1" >&2; exit 2 ;;
|
|
27
27
|
esac; done
|
|
28
28
|
|
|
29
|
+
# Python 3.10+ is the floor (README; CI tests nothing older): refused here, by
|
|
30
|
+
# the version found, before anything is fetched.
|
|
31
|
+
py=$(command -v python3 2>/dev/null) || { echo "tezgah: needs Python 3.10 or newer, and no python3 is on PATH" >&2; exit 1; }
|
|
32
|
+
pyver=$("$py" --version 2>&1 | sed -n 's/^Python \([0-9][0-9]*\.[0-9][0-9.]*\).*/\1/p')
|
|
33
|
+
pymajor=0; pyminor=0
|
|
34
|
+
if [ -n "$pyver" ]; then pymajor=${pyver%%.*}; pyminor=${pyver#*.}; pyminor=${pyminor%%.*}; fi
|
|
35
|
+
if [ "$pymajor" -lt 3 ] || { [ "$pymajor" -eq 3 ] && [ "$pyminor" -lt 10 ]; }; then
|
|
36
|
+
echo "tezgah: needs Python 3.10 or newer; $py is ${pyver:-of unknown version}" >&2
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
29
40
|
# The default upgrade.sh resolves: the installer is reached through the tree it picks.
|
|
30
41
|
PREFIX=${PREFIX_ARG:-${TEZGAH_PREFIX:-${XDG_DATA_HOME:-$HOME/.local/share}/tezgah}}
|
|
31
42
|
finish() { [ -z "$work" ] || rm -rf "$work"; }
|
package/packaging/upgrade.sh
CHANGED
|
@@ -41,6 +41,9 @@ die() {
|
|
|
41
41
|
exit 1
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
# First: a missing curl would otherwise read as a fetch failure blaming a token.
|
|
45
|
+
for t in curl tar; do command -v "$t" >/dev/null 2>&1 || die "$t is required (README: Install)"; done
|
|
46
|
+
|
|
44
47
|
# A private repository answers 404 to `releases/download/...` from a session without
|
|
45
48
|
# a token (measured 2026-10-17, with the token as URL userinfo and as a header
|
|
46
49
|
# alike), while the REST asset endpoint returns the blob for `Accept:
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Claude AI Research Skills Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|