@r1z4x/tezgah 1.0.0 → 1.2.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 +92 -1
- package/MANIFEST +10 -0
- package/bin/tezgah-agents +10 -1
- package/bin/tezgah-docs +1 -1
- package/bin/tezgah-doctor +11 -0
- package/bin/tezgah-research +31 -14
- package/bin/tezgah-setup +154 -29
- package/bin/tezgah-skill +59 -0
- package/bin/tezgah-skill.py +59 -0
- package/bin/tezgah-status +62 -1
- package/bin/tezgah-taste +504 -5
- package/bin/tezgah-triage +5 -3
- package/docs/README.md +1 -0
- package/docs/architecture.md +5 -2
- package/docs/citations-baseline.json +1 -1
- package/docs/clarity-baseline.json +9 -9
- package/docs/contract.md +68 -26
- package/docs/environment.md +6 -3
- package/docs/evidence.md +7 -0
- package/docs/gate.md +13 -7
- package/docs/glossary.md +1 -1
- package/docs/hosts.md +48 -1
- package/docs/index.json +34 -3
- package/docs/judge.md +75 -60
- package/docs/models.md +14 -8
- package/docs/operations.md +80 -25
- package/docs/orca.md +126 -0
- package/docs/research.md +38 -3
- package/docs/skills.md +74 -0
- package/docs/status-line.md +3 -1
- package/docs/testing.md +33 -21
- package/docs/vision.md +12 -8
- package/hooks/projects-posttooluse.py +7 -0
- package/hooks/projects-pretooluse.py +17 -5
- package/hooks/tezgah_agents.py +135 -13
- package/hooks/tezgah_components.py +3 -0
- package/hooks/tezgah_context.py +84 -22
- package/hooks/tezgah_gate.py +133 -17
- package/hooks/tezgah_integrity.py +60 -13
- package/hooks/tezgah_judge.py +288 -66
- package/hooks/tezgah_models.py +41 -5
- package/hooks/tezgah_orca.py +144 -0
- package/hooks/tezgah_paths.py +40 -6
- package/hooks/tezgah_policy.py +109 -35
- package/hooks/tezgah_research.py +104 -8
- package/hooks/tezgah_skill_pick.py +459 -8
- package/hooks/tezgah_stack.py +382 -0
- package/hooks/tezgah_stopspec.py +2 -2
- package/hooks/tezgah_taste.py +35 -9
- package/hooks/tezgah_taste_ledger.py +628 -0
- package/hosts/codex/hook.py +12 -1
- package/hosts/cursor/hook.py +12 -4
- package/hosts/omp/hook.py +18 -6
- package/hosts/omp/tezgah-hook.ts.in +20 -7
- package/hosts/opencode/plugins/tezgah.js +28 -15
- package/output-styles/tezgah.md +34 -19
- package/package.json +1 -1
- package/skills/i-have-adhd/SKILL.md +2 -2
- package/skills/research/SKILL.md +60 -17
- package/skills/tezgah-contract/SKILL.md +55 -13
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,95 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.2.0] - 2026-10-07
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Taste learns and applies, not only records.** Arm it with `taste-on`.
|
|
14
|
+
`tezgah-taste learn` then turns each correction after a writing turn into one
|
|
15
|
+
typed decision, and only TypeSafe may give it. The decision names a
|
|
16
|
+
preference, a defect or neither. It picks one of eleven fixed categories and
|
|
17
|
+
a scope, and relates the correction to each learning already held. A ledger
|
|
18
|
+
computes each learning's confidence from its evidence and forgets it over
|
|
19
|
+
time. A contradiction flags a learning and never overwrites it. A learning
|
|
20
|
+
goes active in its second session. Session start injects active learnings as
|
|
21
|
+
rules or hints. Every host notes the in-scope ones on a write, each once per
|
|
22
|
+
session. A rule needs a measured calibration bound. A defect never enters the
|
|
23
|
+
ledger. A before/after gate stops injection when corrections do not fall.
|
|
24
|
+
`list`, `accept`, `reject`, `edit` and `export` give the user control, and
|
|
25
|
+
`learn --from-transcripts` bootstraps from history. The judge seam gains
|
|
26
|
+
`only=` and a `text` question. Two commands never rewrite the ledger at once:
|
|
27
|
+
a second `learn`, `accept`, `reject` or `edit` exits 2 while one runs.
|
|
28
|
+
|
|
29
|
+
## [1.1.0] - 2026-10-07
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **Parallel by default.** A prompt with two or more independent items arms a
|
|
34
|
+
fan-out rule. One batch of subagents runs, one worktree per writing slice,
|
|
35
|
+
and the main thread integrates. Five specialist agents join the roster:
|
|
36
|
+
`tezgah-tester`, `tezgah-docs`, `tezgah-security`, `tezgah-ui` and
|
|
37
|
+
`tezgah-researcher`. `tezgah-agents --list` prints each with its tier,
|
|
38
|
+
skills and tools.
|
|
39
|
+
- **tezgah detects an Orca session** (`hooks/tezgah_orca.py`). Inside Orca a slice
|
|
40
|
+
checkout goes through `orca worktree create`, and `tezgah-status --orca`
|
|
41
|
+
lists the checkouts Orca tracks and the git worktrees it does not.
|
|
42
|
+
`docs/orca.md` maps what Orca owns and what tezgah owns.
|
|
43
|
+
- **`tezgah-doctor --stack`** prints each host's effective stack: context
|
|
44
|
+
files in load order, skill dirs and which copy wins, hooks per event, MCP
|
|
45
|
+
servers. It flags duplicate or stale skills, doubled hooks and rules, and
|
|
46
|
+
backups inside a scanned dir. `docs/hosts.md` holds the precedence table.
|
|
47
|
+
- **`tezgah-skill <words>`** searches every installed skill by section and
|
|
48
|
+
returns `skill://<name>:<start>-<end>` plus the file path. Every session is
|
|
49
|
+
told to use it. A local, model-free hint is on by default: when a prompt's
|
|
50
|
+
terms match one section well enough, the turn names that section once. The
|
|
51
|
+
armed judge hint names the section too, not only the skill.
|
|
52
|
+
- **`tezgah-status --judge`** shows what the judgement seam used last, and why
|
|
53
|
+
it was not the session's own model when it was not.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- **The judgement seam asks the session's own CLI first.** It used a third
|
|
58
|
+
party (TypeSafe, then OpenRouter) even when the session's frontier
|
|
59
|
+
credential was there. A third party now answers only as the `fallback`
|
|
60
|
+
setting in `config.json` allows. The default is `vendor`: no cross-vendor
|
|
61
|
+
stand-in while a session CLI exists. Every stand-in or refusal is said on
|
|
62
|
+
stderr and recorded. omp subagent chains stay inside the session vendor by
|
|
63
|
+
default too.
|
|
64
|
+
- **Research stays on the question asked.** A line records its scope (`--in`,
|
|
65
|
+
`--out`) and its method (`--method qualitative|measured`). A qualitative
|
|
66
|
+
line owes no metric. Every claim must trace to a scope item. An analysis no
|
|
67
|
+
longer owes three variants. A plain factual question gets an answer from
|
|
68
|
+
primary sources, with no experiment line.
|
|
69
|
+
- **Ponytail and adhd are in force from the first turn.** Their operative rules
|
|
70
|
+
ride the always-on contract instead of a "read the skill later" pointer.
|
|
71
|
+
- **The graph engages.** A stale index stamp now starts a background re-index.
|
|
72
|
+
It no longer tells the session to fall back to text search. omp's `find`,
|
|
73
|
+
`search` and `ast_grep` reach the gate. The graph nudge fires on
|
|
74
|
+
symbol-shaped searches only, not on plain words, and honours `.no-graph`.
|
|
75
|
+
The session line names the host's own graph tool.
|
|
76
|
+
- **A prose edit owes no check.** The Stop rule no longer counts a `.md`,
|
|
77
|
+
`.rst` or `.adoc` edit outside the instruction files as work. Such an edit
|
|
78
|
+
neither demands a check nor makes an earlier pass stale. Refusals ask for
|
|
79
|
+
the smallest check that proves the change.
|
|
80
|
+
- **`tests/impacted.py` maps a host file to the modules that name it**, not to
|
|
81
|
+
nearly the whole suite.
|
|
82
|
+
- **Host-file backups leave the dirs a host scans.** They go to
|
|
83
|
+
`~/.config/tezgah/backups/`, and `--install` sweeps the old ones. Cursor gets
|
|
84
|
+
each tezgah skill from one dir, not two. `--sync` refreshes only the plugin
|
|
85
|
+
copy Claude loads.
|
|
86
|
+
|
|
87
|
+
### Changed
|
|
88
|
+
|
|
89
|
+
- **A release run skips the serial suite.** `release.yml` calls CI with
|
|
90
|
+
`release: true`. One 3.10 `test` leg then runs compile, lint and the citation
|
|
91
|
+
audit, and `test-sharded` runs the suite.
|
|
92
|
+
- **CI runs less.** It runs on pushes to `main`, on pull requests and by hand.
|
|
93
|
+
A newer run cancels an older one on the same ref, and every job has a
|
|
94
|
+
timeout. A docs-only change runs only the citations audit and its impacted
|
|
95
|
+
tests. The `test` matrix is 3.10 and 3.14. A pull request runs the impacted
|
|
96
|
+
set. `main` and a release run the whole suite.
|
|
97
|
+
|
|
9
98
|
## [1.0.0] - 2026-10-07
|
|
10
99
|
|
|
11
100
|
### Upgrade note
|
|
@@ -734,5 +823,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
734
823
|
the copy could not find `hooks/` and died on import. It now links into the
|
|
735
824
|
installed tree, and also installs the command as `tezgah`.
|
|
736
825
|
|
|
737
|
-
[Unreleased]: https://github.com/r1z4x/tezgah/compare/v1.
|
|
826
|
+
[Unreleased]: https://github.com/r1z4x/tezgah/compare/v1.2.0...HEAD
|
|
827
|
+
[1.2.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.2.0
|
|
828
|
+
[1.1.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.1.0
|
|
738
829
|
[1.0.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.0.0
|
package/MANIFEST
CHANGED
|
@@ -74,6 +74,8 @@ bin/tezgah-route
|
|
|
74
74
|
bin/tezgah-route.py
|
|
75
75
|
bin/tezgah-setup
|
|
76
76
|
bin/tezgah-setup.py
|
|
77
|
+
bin/tezgah-skill
|
|
78
|
+
bin/tezgah-skill.py
|
|
77
79
|
bin/tezgah-status
|
|
78
80
|
bin/tezgah-status.py
|
|
79
81
|
bin/tezgah-task
|
|
@@ -101,6 +103,7 @@ docs/judge.md
|
|
|
101
103
|
docs/layers.md
|
|
102
104
|
docs/models.md
|
|
103
105
|
docs/operations.md
|
|
106
|
+
docs/orca.md
|
|
104
107
|
docs/research.md
|
|
105
108
|
docs/results/paired-outcome-2026-10.jsonl
|
|
106
109
|
docs/results/paired-outcome-2026-10.md
|
|
@@ -128,6 +131,7 @@ hooks/tezgah_judge.py
|
|
|
128
131
|
hooks/tezgah_lang.py
|
|
129
132
|
hooks/tezgah_lessons.py
|
|
130
133
|
hooks/tezgah_models.py
|
|
134
|
+
hooks/tezgah_orca.py
|
|
131
135
|
hooks/tezgah_paths.py
|
|
132
136
|
hooks/tezgah_policy.py
|
|
133
137
|
hooks/tezgah_rank.py
|
|
@@ -136,9 +140,11 @@ hooks/tezgah_research.py
|
|
|
136
140
|
hooks/tezgah_shapes.py
|
|
137
141
|
hooks/tezgah_skill_pick.py
|
|
138
142
|
hooks/tezgah_snapshot.py
|
|
143
|
+
hooks/tezgah_stack.py
|
|
139
144
|
hooks/tezgah_stopspec.py
|
|
140
145
|
hooks/tezgah_task.py
|
|
141
146
|
hooks/tezgah_taste.py
|
|
147
|
+
hooks/tezgah_taste_ledger.py
|
|
142
148
|
hooks/tezgah_untrusted.py
|
|
143
149
|
hooks/tezgah_update.py
|
|
144
150
|
hosts/codex/hook.py
|
|
@@ -719,6 +725,7 @@ tests/test_neuter_matrix.py
|
|
|
719
725
|
tests/test_omp_extension.py
|
|
720
726
|
tests/test_omp_hook.py
|
|
721
727
|
tests/test_opencode_plugin.py
|
|
728
|
+
tests/test_orca.py
|
|
722
729
|
tests/test_packaging.py
|
|
723
730
|
tests/test_packaging_scripts.py
|
|
724
731
|
tests/test_paths.py
|
|
@@ -735,8 +742,10 @@ tests/test_research_seal.py
|
|
|
735
742
|
tests/test_setup.py
|
|
736
743
|
tests/test_shapes.py
|
|
737
744
|
tests/test_skill_pick.py
|
|
745
|
+
tests/test_skill_search.py
|
|
738
746
|
tests/test_skills.py
|
|
739
747
|
tests/test_snapshot.py
|
|
748
|
+
tests/test_stack.py
|
|
740
749
|
tests/test_statusline.py
|
|
741
750
|
tests/test_stop_after_block.py
|
|
742
751
|
tests/test_stopspec.py
|
|
@@ -744,6 +753,7 @@ tests/test_switch_latching.py
|
|
|
744
753
|
tests/test_task.py
|
|
745
754
|
tests/test_taste.py
|
|
746
755
|
tests/test_taste_cli.py
|
|
756
|
+
tests/test_taste_ledger.py
|
|
747
757
|
tests/test_tezgah_mcp_wiring.py
|
|
748
758
|
tests/test_triage.py
|
|
749
759
|
tests/test_untrusted.py
|
package/bin/tezgah-agents
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""tezgah-agents [PATH] generate/refresh this repo's tezgah subagent set.
|
|
3
3
|
tezgah-agents --json [PATH] print the opencode `config.agent` entries as JSON.
|
|
4
|
+
tezgah-agents --list print every role: name, tier, read-only or writing,
|
|
5
|
+
and its description (skills and tools included).
|
|
4
6
|
|
|
5
7
|
The hook hosts run the plain form from SessionStart via the shared context
|
|
6
8
|
(tezgah_context calls tezgah_agents.sync_root). opencode has no such hook, so
|
|
@@ -14,7 +16,8 @@ import sys
|
|
|
14
16
|
|
|
15
17
|
HERE = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
|
16
18
|
sys.path.insert(0, os.path.join(HERE, "hooks"))
|
|
17
|
-
from tezgah_agents import opencode_agents_json, sync_root # noqa: E402
|
|
19
|
+
from tezgah_agents import ROLES, opencode_agents_json, sync_root # noqa: E402
|
|
20
|
+
from tezgah_models import AGENT_SLOT # noqa: E402
|
|
18
21
|
from tezgah_context import repo_root, under # noqa: E402
|
|
19
22
|
|
|
20
23
|
|
|
@@ -25,6 +28,12 @@ def main():
|
|
|
25
28
|
if "-h" in args or "--help" in args:
|
|
26
29
|
print(__doc__.strip())
|
|
27
30
|
return 0
|
|
31
|
+
if "--list" in args:
|
|
32
|
+
for name, desc, _cap, _body, readonly in ROLES:
|
|
33
|
+
print("%s tier=%s %s\n %s" % (
|
|
34
|
+
name, AGENT_SLOT.get(name, "-"),
|
|
35
|
+
"read-only" if readonly else "writes", desc))
|
|
36
|
+
return 0
|
|
28
37
|
as_json = "--json" in args
|
|
29
38
|
args = [a for a in args if a != "--json"]
|
|
30
39
|
cwd = os.path.realpath(args[0] if args else os.getcwd())
|
package/bin/tezgah-docs
CHANGED
|
@@ -106,7 +106,7 @@ RUN = re.compile(r"((?:`[A-Za-z_][A-Za-z0-9_]*(?:\(\))?`\s*[/,]?\s*){1,5})$")
|
|
|
106
106
|
CITE = re.compile(r"`([\w.-]+(?:/[\w.-]+)+|[\w-]+\.\w+):(\d+)(?:-(\d+))?`"
|
|
107
107
|
r"|`(:(\d+)(?:-(\d+))?)`")
|
|
108
108
|
# The symbol written after the citation instead of before it, the way a table
|
|
109
|
-
# cell reads: `hooks/tezgah_policy.py:
|
|
109
|
+
# cell reads: `hooks/tezgah_policy.py:667-928` (`CORE`).
|
|
110
110
|
AFTER = re.compile(r"\s*\(`([A-Za-z_][A-Za-z0-9_]*)(?:\(\))?`\)")
|
|
111
111
|
# Any `path:line`, backticked or not: what CITE does not match of these (a
|
|
112
112
|
# bare token, a comma list `x.py:12,40`) is counted as unjudged, so a citation
|
package/bin/tezgah-doctor
CHANGED
|
@@ -410,6 +410,10 @@ def main():
|
|
|
410
410
|
ap.add_argument("--json", action="store_true", help="machine-readable report")
|
|
411
411
|
ap.add_argument("--coverage", action="store_true",
|
|
412
412
|
help="the tracked files the graph index does not hold (exits 0)")
|
|
413
|
+
ap.add_argument("--stack", action="store_true",
|
|
414
|
+
help="per host: context files, skills and which copy wins, "
|
|
415
|
+
"hooks per event, MCP servers, and the conflicts "
|
|
416
|
+
"(exits 1 when any conflict is found)")
|
|
413
417
|
ap.add_argument("--repo", action="append", metavar="PATH",
|
|
414
418
|
help="a repository to account for instead of the configured "
|
|
415
419
|
"roots (repeatable; --coverage reports the first)")
|
|
@@ -435,6 +439,13 @@ def main():
|
|
|
435
439
|
repo = os.path.realpath(os.path.expanduser(args.repo[0]) if args.repo
|
|
436
440
|
else os.getcwd())
|
|
437
441
|
return coverage_report(repo, args.dump)
|
|
442
|
+
if args.stack:
|
|
443
|
+
import tezgah_stack
|
|
444
|
+
repo = os.path.realpath(os.path.expanduser(args.repo[0]) if args.repo
|
|
445
|
+
else os.getcwd())
|
|
446
|
+
data = tezgah_stack.report(repo)
|
|
447
|
+
print(json.dumps(data, indent=2) if args.json else tezgah_stack.render(data))
|
|
448
|
+
return 1 if any(s["conflicts"] for s in data.values()) else 0
|
|
438
449
|
|
|
439
450
|
pairs = targets(args.repo)
|
|
440
451
|
rep = collect(pairs)
|
package/bin/tezgah-research
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""tezgah-research - the research workspace CLI.
|
|
3
3
|
|
|
4
|
-
tezgah-research init <slug>
|
|
5
|
-
[--
|
|
4
|
+
tezgah-research init <slug> --ask "<the user's words>" [--question "..."]
|
|
5
|
+
[--method qualitative|measured] [--in "<covered>"]... [--out "<left alone>"]...
|
|
6
|
+
[--allow-open "<why>"] [--supersedes <slug>] scaffold a line
|
|
6
7
|
tezgah-research commit <slug> "<message>" commit the line in .tezgah's own repo
|
|
7
8
|
tezgah-research check [<slug>] [--json] [--strict] [--orx] [--all-lines] [-- <slug>]
|
|
8
9
|
the discipline checks
|
|
@@ -136,7 +137,7 @@ def cmd_init(args):
|
|
|
136
137
|
return misuse("init <slug>: %r is not a slug - lowercase letters, digits "
|
|
137
138
|
"and dashes" % args[0])
|
|
138
139
|
question, allow, supersedes = "", None, None
|
|
139
|
-
ask, tier = "", str(tr.DEFAULT_TIER)
|
|
140
|
+
ask, tier, method, scope_in, scope_out = "", str(tr.DEFAULT_TIER), "", [], []
|
|
140
141
|
rest = args[1:]
|
|
141
142
|
while rest:
|
|
142
143
|
flag = rest.pop(0)
|
|
@@ -146,13 +147,21 @@ def cmd_init(args):
|
|
|
146
147
|
ask = rest.pop(0)
|
|
147
148
|
elif flag == "--tier" and rest:
|
|
148
149
|
tier = rest.pop(0)
|
|
150
|
+
elif flag == "--method" and rest and rest[0] in tr.METHODS:
|
|
151
|
+
method = rest.pop(0)
|
|
152
|
+
elif flag == "--in" and rest:
|
|
153
|
+
scope_in.append(rest.pop(0))
|
|
154
|
+
elif flag == "--out" and rest:
|
|
155
|
+
scope_out.append(rest.pop(0))
|
|
149
156
|
elif flag == "--allow-open" and rest:
|
|
150
157
|
allow = rest.pop(0)
|
|
151
158
|
elif flag == "--supersedes" and rest:
|
|
152
159
|
supersedes = rest.pop(0)
|
|
153
160
|
else:
|
|
154
161
|
return misuse('init <slug> --ask "<the user\'s words>" [--tier quick|study|program]'
|
|
155
|
-
' [--
|
|
162
|
+
' [--method qualitative|measured] [--in "<covered>"]...'
|
|
163
|
+
' [--out "<left alone>"]... [--question "..."]'
|
|
164
|
+
' [--allow-open "<reason>"] [--supersedes <slug>]')
|
|
156
165
|
problems = tr.ask_problems(ask, tier)
|
|
157
166
|
if problems:
|
|
158
167
|
for problem in problems:
|
|
@@ -196,7 +205,8 @@ def cmd_init(args):
|
|
|
196
205
|
return 1
|
|
197
206
|
created = time.strftime("%Y-%m-%d")
|
|
198
207
|
made = tr.init(repo, slug, question=question, created=created,
|
|
199
|
-
supersedes=supersedes, ask=ask, tier=tier
|
|
208
|
+
supersedes=supersedes, ask=ask, tier=tier, method=method,
|
|
209
|
+
scope_in=scope_in, scope_out=scope_out)
|
|
200
210
|
if allow is not None:
|
|
201
211
|
problem = tr.note_open(repo, slug, allow, created)
|
|
202
212
|
if problem:
|
|
@@ -205,15 +215,22 @@ def cmd_init(args):
|
|
|
205
215
|
print("research line: %s" % tr.line_dir(repo, slug))
|
|
206
216
|
for path in made:
|
|
207
217
|
print(" wrote %s" % path)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
218
|
+
for problem in tr.scope_problems(tr.line_state(repo, slug)):
|
|
219
|
+
print(" owed: %s" % problem)
|
|
220
|
+
if method == "qualitative":
|
|
221
|
+
print("\nnext: read the primary sources for each state.json scope.in item "
|
|
222
|
+
"(`orx discover`, `orx paper`), save each to literature/ as you read "
|
|
223
|
+
"it, and record every claim with a `trace` naming the S<n> it answers "
|
|
224
|
+
"and the source it rests on. No experiment, no metric.")
|
|
225
|
+
else:
|
|
226
|
+
print("\nnext: write experiments/<hypothesis>/protocol.md - what the change "
|
|
227
|
+
"is, what it predicts, what result would falsify it, why - and COMMIT "
|
|
228
|
+
"it before the run: `tezgah-research commit %s \"protocol "
|
|
229
|
+
"<hypothesis>\"`. Then record the run in results.jsonl and commit it "
|
|
230
|
+
"the same way, in a later commit. The commits land in `.tezgah`'s own "
|
|
231
|
+
"git repository (the project ignores `.tezgah/`). Then analyse it in "
|
|
232
|
+
"analysis.md, and put the claim in claims.jsonl with its kind, "
|
|
233
|
+
"falsification criterion, evidence and the S<n> it traces to." % slug)
|
|
217
234
|
print("kill switch: research-off")
|
|
218
235
|
return 0
|
|
219
236
|
|
package/bin/tezgah-setup
CHANGED
|
@@ -54,6 +54,7 @@ _TREE = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
|
|
54
54
|
sys.path.insert(0, os.path.join(_TREE, "hooks"))
|
|
55
55
|
import tezgah_apps # noqa: E402
|
|
56
56
|
import tezgah_context # noqa: E402
|
|
57
|
+
import tezgah_orca # noqa: E402
|
|
57
58
|
import tezgah_paths as tp # noqa: E402
|
|
58
59
|
import tezgah_policy as policy # noqa: E402
|
|
59
60
|
import tezgah_agents # noqa: E402
|
|
@@ -465,8 +466,9 @@ def hook_entries(hosts):
|
|
|
465
466
|
|
|
466
467
|
def link(src, dst):
|
|
467
468
|
"""Idempotent symlink - or, where this platform refuses one, an entry
|
|
468
|
-
materialised in place. A real file in the way is kept as
|
|
469
|
-
and replaced, so an old shim
|
|
469
|
+
materialised in place. A real file in the way is kept as
|
|
470
|
+
`<backup_base(dst)>.tezgah-bak` and replaced, so an old shim
|
|
471
|
+
(codex/bin/consult) does not block the build."""
|
|
470
472
|
if is_tezgah_copy(dst):
|
|
471
473
|
# tezgah's own materialised entry from an earlier symlink-less run: it
|
|
472
474
|
# is replaced, not reported as a user's file and not left pointing at a
|
|
@@ -480,8 +482,10 @@ def link(src, dst):
|
|
|
480
482
|
os.unlink(dst)
|
|
481
483
|
elif os.path.exists(dst):
|
|
482
484
|
try:
|
|
483
|
-
|
|
484
|
-
|
|
485
|
+
bak = backup_base(dst) + ".tezgah-bak"
|
|
486
|
+
mkdirp(os.path.dirname(bak))
|
|
487
|
+
shutil.move(dst, bak)
|
|
488
|
+
except (OSError, shutil.Error) as exc:
|
|
485
489
|
return "%s %s (%s)" % (NO, dst, exc)
|
|
486
490
|
try:
|
|
487
491
|
mkdirp(os.path.dirname(dst))
|
|
@@ -681,32 +685,94 @@ def write_json(path, data):
|
|
|
681
685
|
# ponytail: a fixed cap, no setting.
|
|
682
686
|
BACKUP_CAP = 5
|
|
683
687
|
BACKUP_RE = re.compile(r"(.+)\.\d{8}T\d{12}(?:-\d+)?\.tezgah-bak")
|
|
688
|
+
# Where a host file's backups live. A host enumerates its own config dirs - omp
|
|
689
|
+
# 18.6's hooks capability lists every non-dot file in hooks/pre, so a
|
|
690
|
+
# `tezgah-hook.ts.<stamp>.tezgah-bak` beside the bridge was one more hook entry -
|
|
691
|
+
# so a copy laid beside a host file is one more thing that host discovers.
|
|
692
|
+
# tezgah's own state dir is scanned by no host and keeps its copies beside the
|
|
693
|
+
# file, where the uninstall that owns them finds them.
|
|
694
|
+
BACKUP_DIR = os.path.join(tp.CONFIG_DIR, "backups")
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def backup_base(path):
|
|
698
|
+
"""The path a backup of `path` is named after: `path` itself inside
|
|
699
|
+
tezgah's config dir, otherwise its mirror under BACKUP_DIR (relative to
|
|
700
|
+
HOME).
|
|
701
|
+
|
|
702
|
+
ponytail: a file outside HOME keeps its backup beside it - every host dir
|
|
703
|
+
lives under HOME on a real machine, and a mirror of an arbitrary absolute
|
|
704
|
+
path would let an in-process caller write into the real home."""
|
|
705
|
+
real = os.path.abspath(path)
|
|
706
|
+
if real.startswith(tp.CONFIG_DIR + os.sep) or not real.startswith(tp.HOME + os.sep):
|
|
707
|
+
return real
|
|
708
|
+
return os.path.join(BACKUP_DIR, os.path.relpath(real, tp.HOME))
|
|
684
709
|
|
|
685
710
|
|
|
686
711
|
def backup(path):
|
|
687
|
-
"""Copy `path` to `<
|
|
712
|
+
"""Copy `path` to `<backup_base>.<timestamp>.tezgah-bak` before a write
|
|
713
|
+
changes it.
|
|
688
714
|
|
|
689
715
|
The suffix stays `.tezgah-bak`, so managed() and the uninstall cleanup still
|
|
690
716
|
know it; past BACKUP_CAP the middle copies go, never the oldest."""
|
|
691
717
|
if not os.path.exists(path):
|
|
692
718
|
return
|
|
719
|
+
base = backup_base(path)
|
|
693
720
|
stamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S%f")
|
|
694
|
-
dst = "%s.%s.tezgah-bak" % (
|
|
721
|
+
dst = "%s.%s.tezgah-bak" % (base, stamp)
|
|
695
722
|
n = 0
|
|
696
723
|
while os.path.exists(dst):
|
|
697
724
|
n += 1
|
|
698
|
-
dst = "%s.%s-%d.tezgah-bak" % (
|
|
725
|
+
dst = "%s.%s-%d.tezgah-bak" % (base, stamp, n)
|
|
699
726
|
try:
|
|
727
|
+
mkdirp(os.path.dirname(dst))
|
|
700
728
|
shutil.copy2(path, dst)
|
|
701
|
-
d,
|
|
702
|
-
old = sorted(
|
|
703
|
-
if (m := BACKUP_RE.fullmatch(
|
|
704
|
-
for
|
|
705
|
-
os.remove(os.path.join(d,
|
|
729
|
+
d, name = os.path.split(base)
|
|
730
|
+
old = sorted(entry for entry in os.listdir(d or ".")
|
|
731
|
+
if (m := BACKUP_RE.fullmatch(entry)) and m.group(1) == name)
|
|
732
|
+
for entry in old[1:max(1, len(old) - BACKUP_CAP + 1)]:
|
|
733
|
+
os.remove(os.path.join(d, entry))
|
|
706
734
|
except OSError:
|
|
707
735
|
pass
|
|
708
736
|
|
|
709
737
|
|
|
738
|
+
# Dirs a backup sweep never enters: a host's transcripts, caches and plugin
|
|
739
|
+
# marketplaces hold no file tezgah ever backed up, and they are where the bytes are.
|
|
740
|
+
SWEEP_SKIP = frozenset((
|
|
741
|
+
".git", ".tmp", "blobs", "browser-profiles", "cache", "file-history",
|
|
742
|
+
"history", "log", "logs", "marketplaces", "memories", "node_modules", "packages",
|
|
743
|
+
"projects", "sessions", "shell-snapshots", "shell_snapshots", "tmp",
|
|
744
|
+
"vendor", "vendor_imports"))
|
|
745
|
+
SWEEP_DEPTH = 3
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def sweep_backups():
|
|
749
|
+
"""Move every `.tezgah-bak` an older release laid beside a host file under
|
|
750
|
+
BACKUP_DIR. Returns the moved destinations. A destination that already
|
|
751
|
+
exists is left alone rather than overwritten: it may be the only copy of the
|
|
752
|
+
user's original."""
|
|
753
|
+
roots = set(tp.HOST_DIRS.values()) | {os.path.join(tp.HOME, ".codex")}
|
|
754
|
+
moved = []
|
|
755
|
+
for root in sorted(roots):
|
|
756
|
+
depth0 = root.rstrip(os.sep).count(os.sep)
|
|
757
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
758
|
+
deep = dirpath.count(os.sep) - depth0 >= SWEEP_DEPTH
|
|
759
|
+
dirnames[:] = [] if deep else sorted(d for d in dirnames if d not in SWEEP_SKIP)
|
|
760
|
+
for name in filenames:
|
|
761
|
+
if not name.endswith(".tezgah-bak"):
|
|
762
|
+
continue
|
|
763
|
+
src = os.path.join(dirpath, name)
|
|
764
|
+
dst = backup_base(src)
|
|
765
|
+
if dst == os.path.abspath(src) or os.path.lexists(dst):
|
|
766
|
+
continue
|
|
767
|
+
try:
|
|
768
|
+
mkdirp(os.path.dirname(dst))
|
|
769
|
+
shutil.move(src, dst)
|
|
770
|
+
moved.append(dst)
|
|
771
|
+
except (OSError, shutil.Error):
|
|
772
|
+
pass
|
|
773
|
+
return moved
|
|
774
|
+
|
|
775
|
+
|
|
710
776
|
def set_managed_block(path, block, start="# tezgah:start", end="# tezgah:end"):
|
|
711
777
|
"""Replace the tezgah block in a text file, or append it. A YAML file that
|
|
712
778
|
is an empty sequence (`[]`, the dsh patch-layer default) loses that token
|
|
@@ -995,6 +1061,7 @@ def install_common(new_roots, hosts):
|
|
|
995
1061
|
("tezgah-decisions", os.path.join(HERE, "bin", "tezgah-decisions")),
|
|
996
1062
|
("tezgah-route", os.path.join(HERE, "bin", "tezgah-route")),
|
|
997
1063
|
("tezgah-lessons", os.path.join(HERE, "bin", "tezgah-lessons")),
|
|
1064
|
+
("tezgah-skill", os.path.join(HERE, "bin", "tezgah-skill")),
|
|
998
1065
|
# `tezgah` is this installer under the name `tezgah update` is typed
|
|
999
1066
|
# with (npm ships the same name). Not here on purpose: tezgah-dsh
|
|
1000
1067
|
# (wired as ~/.local/bin/dsh, see dsh_launcher) and
|
|
@@ -1821,6 +1888,37 @@ def cursor_hook_entries():
|
|
|
1821
1888
|
for ev, extra in CURSOR_HOOK_EVENTS.items()}
|
|
1822
1889
|
|
|
1823
1890
|
|
|
1891
|
+
# Cursor also reads ~/.codex/skills, ~/.claude/skills and ~/.agents/skills
|
|
1892
|
+
# (cursor.com/docs/context/skills) and documents no winner for a name found in
|
|
1893
|
+
# two of them. A tezgah skill Codex's install already linked into
|
|
1894
|
+
# ~/.codex/skills therefore reached Cursor twice once ~/.cursor/skills linked it
|
|
1895
|
+
# too: ~/.cursor/skills holds only the names no other dir Cursor reads resolves
|
|
1896
|
+
# to this tree. Cursor installs last (install_all), so it sees Codex's links.
|
|
1897
|
+
CURSOR_SHARED_SKILLS = tuple(os.path.join(HOME, d, "skills")
|
|
1898
|
+
for d in (".codex", ".claude", ".agents"))
|
|
1899
|
+
|
|
1900
|
+
|
|
1901
|
+
def cursor_shared_copy(s):
|
|
1902
|
+
"""The other dir Cursor reads that already links skill `s` to this tree."""
|
|
1903
|
+
want = os.path.realpath(os.path.join(HERE, "skills", s))
|
|
1904
|
+
return next((d for d in CURSOR_SHARED_SKILLS
|
|
1905
|
+
if os.path.realpath(os.path.join(d, s)) == want), None)
|
|
1906
|
+
|
|
1907
|
+
|
|
1908
|
+
def link_cursor_skills():
|
|
1909
|
+
shared = []
|
|
1910
|
+
for s in SKILLS:
|
|
1911
|
+
dst = os.path.join(CURSOR, "skills", s)
|
|
1912
|
+
if cursor_shared_copy(s):
|
|
1913
|
+
unlink_tezgah(dst)
|
|
1914
|
+
shared.append(s)
|
|
1915
|
+
else:
|
|
1916
|
+
print(link(os.path.join(HERE, "skills", s), dst))
|
|
1917
|
+
if shared:
|
|
1918
|
+
print("%s %d skills reach Cursor through %s, not linked twice"
|
|
1919
|
+
% (OK, len(shared), ", ".join(sorted({cursor_shared_copy(s) for s in shared}))))
|
|
1920
|
+
|
|
1921
|
+
|
|
1824
1922
|
def install_cursor():
|
|
1825
1923
|
print("\ncursor:")
|
|
1826
1924
|
path = os.path.join(CURSOR, "hooks.json")
|
|
@@ -1836,8 +1934,7 @@ def install_cursor():
|
|
|
1836
1934
|
arr.append(entry)
|
|
1837
1935
|
write_json(path, data)
|
|
1838
1936
|
print("%s %s" % (OK, path))
|
|
1839
|
-
|
|
1840
|
-
print(link(os.path.join(HERE, "skills", s), os.path.join(CURSOR, "skills", s)))
|
|
1937
|
+
link_cursor_skills()
|
|
1841
1938
|
cc = os.path.join(CURSOR, "cli-config.json")
|
|
1842
1939
|
cfg = read_config(cc, {})
|
|
1843
1940
|
if cfg is not None:
|
|
@@ -3038,8 +3135,9 @@ def remove_generated_state(full):
|
|
|
3038
3135
|
kept = []
|
|
3039
3136
|
for name in names:
|
|
3040
3137
|
path = os.path.join(tp.CONFIG_DIR, name)
|
|
3041
|
-
if name
|
|
3042
|
-
kept.append((path, "predecessor originals --adopt moved here"
|
|
3138
|
+
if name in ("adopted", "backups"):
|
|
3139
|
+
kept.append((path, "predecessor originals --adopt moved here"
|
|
3140
|
+
if name == "adopted" else "host-file backups, for a manual restore"))
|
|
3043
3141
|
continue
|
|
3044
3142
|
if name in GENERATED_STATE or name.endswith((".off", "-off")) \
|
|
3045
3143
|
or (name.endswith(".tezgah-bak")
|
|
@@ -3253,7 +3351,7 @@ def unwire_claude_settings():
|
|
|
3253
3351
|
changed = True
|
|
3254
3352
|
if changed:
|
|
3255
3353
|
write_json(sp, s)
|
|
3256
|
-
print("%s removed tezgah statusLine/attribution/enabledPlugins from %s (restore from its .tezgah-bak copies if wanted)" % (OK, sp))
|
|
3354
|
+
print("%s removed tezgah statusLine/attribution/enabledPlugins from %s (restore from its .tezgah-bak copies under %s if wanted)" % (OK, sp, BACKUP_DIR))
|
|
3257
3355
|
|
|
3258
3356
|
|
|
3259
3357
|
def unwire_opencode_config():
|
|
@@ -3484,8 +3582,8 @@ def uninstall(hosts, dry_run=False):
|
|
|
3484
3582
|
tezgah wrote outlives the command, and the verify pass at the end re-derives
|
|
3485
3583
|
that from the disk.
|
|
3486
3584
|
|
|
3487
|
-
What never goes, in any run: the user's own files,
|
|
3488
|
-
|
|
3585
|
+
What never goes, in any run: the user's own files, the host-file backups
|
|
3586
|
+
under `BACKUP_DIR`, the `adopted/` predecessor snapshots, and every
|
|
3489
3587
|
repository's `.tezgah/` research state. A partial run (some armed hosts
|
|
3490
3588
|
stay) also keeps config.json and the install tree, because the hosts still
|
|
3491
3589
|
armed read both at runtime."""
|
|
@@ -3523,6 +3621,9 @@ def uninstall(hosts, dry_run=False):
|
|
|
3523
3621
|
uninstall_common()
|
|
3524
3622
|
for h in hosts:
|
|
3525
3623
|
UNINSTALLERS[h]()
|
|
3624
|
+
if "codex" in hosts and "cursor" in (armed or []) and "cursor" not in hosts:
|
|
3625
|
+
print("\ncursor (its skills came through ~/.codex/skills):")
|
|
3626
|
+
link_cursor_skills()
|
|
3526
3627
|
if "claude" in hosts:
|
|
3527
3628
|
print("\nclaude plugin copy:")
|
|
3528
3629
|
remove_plugin_copies()
|
|
@@ -4304,7 +4405,7 @@ def dsh_servers(path):
|
|
|
4304
4405
|
|
|
4305
4406
|
|
|
4306
4407
|
def host_checks_claude():
|
|
4307
|
-
copies =
|
|
4408
|
+
copies = live_plugin_copies()
|
|
4308
4409
|
return [
|
|
4309
4410
|
("statusline wired (link or copy)",
|
|
4310
4411
|
is_tezgah_entry(os.path.join(CLAUDE, "statusline.py"))),
|
|
@@ -4560,7 +4661,9 @@ def host_checks_cursor():
|
|
|
4560
4661
|
("hooks.json wired", _hooks_has(os.path.join(CURSOR, "hooks.json"),
|
|
4561
4662
|
"tezgah-cursor-hook",
|
|
4562
4663
|
list(CURSOR_HOOK_EVENTS))),
|
|
4563
|
-
("skills linked",
|
|
4664
|
+
("skills linked", all(
|
|
4665
|
+
os.path.isfile(os.path.join(CURSOR, "skills", s, "SKILL.md"))
|
|
4666
|
+
or cursor_shared_copy(s) for s in SKILLS)),
|
|
4564
4667
|
("MCP in mcp.json", "codegraph" in (mcp.get("mcpServers") or {})),
|
|
4565
4668
|
] + mcp_rows(set(mcp.get("mcpServers") or {}), "mcp.json") + [
|
|
4566
4669
|
mcp_row(TEZGAH_MCP_NAME, (mcp.get("mcpServers") or {}).get(TEZGAH_MCP_NAME),
|
|
@@ -4870,6 +4973,19 @@ def plugin_copies():
|
|
|
4870
4973
|
return sorted(out)
|
|
4871
4974
|
|
|
4872
4975
|
|
|
4976
|
+
def live_plugin_copies():
|
|
4977
|
+
"""The plugin copies a refresh keeps current: the one installed_plugins.json
|
|
4978
|
+
names, or every copy when it names none. An older version dir is Claude's
|
|
4979
|
+
cache to collect; refreshing it rewrote a tree no session loads, and its
|
|
4980
|
+
staleness turned the report red for nothing."""
|
|
4981
|
+
copies = plugin_copies()
|
|
4982
|
+
rows = read_json(INSTALLED_PLUGINS, {}).get("plugins") or {}
|
|
4983
|
+
named = {os.path.realpath(r.get("installPath") or "")
|
|
4984
|
+
for k, v in rows.items() if k.split("@")[0] == "tezgah"
|
|
4985
|
+
for r in (v or [])[-1:] if isinstance(r, dict)}
|
|
4986
|
+
return [t for t in copies if os.path.realpath(t) in named] or copies
|
|
4987
|
+
|
|
4988
|
+
|
|
4873
4989
|
def copy_files(target):
|
|
4874
4990
|
"""Every managed relpath the copy holds.
|
|
4875
4991
|
|
|
@@ -5100,7 +5216,7 @@ def sync():
|
|
|
5100
5216
|
cache = os.path.join(HOME, ".claude", "plugins", "cache")
|
|
5101
5217
|
plugins = os.path.dirname(cache)
|
|
5102
5218
|
_recover_sync(plugins)
|
|
5103
|
-
targets =
|
|
5219
|
+
targets = live_plugin_copies()
|
|
5104
5220
|
if not targets:
|
|
5105
5221
|
print("no installed tezgah found under %s (nothing to sync)" % cache)
|
|
5106
5222
|
return 1
|
|
@@ -5176,12 +5292,12 @@ def refresh_plugin_copy(hosts):
|
|
|
5176
5292
|
`.mcp.json` is excluded from `plugin_copy_current` in both directions on
|
|
5177
5293
|
purpose - rendering into the copy must not make a current copy read stale -
|
|
5178
5294
|
which also means a copy carrying no tezgah row at all is still judged
|
|
5179
|
-
current. So a claude install always renders the file into every copy
|
|
5180
|
-
that copy as the root, the same half `sync()` performs."""
|
|
5295
|
+
current. So a claude install always renders the file into every live copy
|
|
5296
|
+
(`live_plugin_copies`) with that copy as the root, the same half `sync()` performs."""
|
|
5181
5297
|
if "claude" in hosts:
|
|
5182
|
-
for t in
|
|
5298
|
+
for t in live_plugin_copies():
|
|
5183
5299
|
write_mcp_json(path=os.path.join(t, MCP_JSON), selected=selection(), root=t)
|
|
5184
|
-
if any(not plugin_copy_current(t) for t in
|
|
5300
|
+
if any(not plugin_copy_current(t) for t in live_plugin_copies()):
|
|
5185
5301
|
print()
|
|
5186
5302
|
sync()
|
|
5187
5303
|
|
|
@@ -5322,7 +5438,8 @@ def wizard_plan(hosts, roots, deps_off, features, adopt_now):
|
|
|
5322
5438
|
if adopt_now:
|
|
5323
5439
|
print(" %s retire the predecessor wiring (moved aside, not deleted)" % OK)
|
|
5324
5440
|
print(" files tezgah rewrites are backed up as <file>.<timestamp>.tezgah-bak "
|
|
5325
|
-
"(oldest and newest four kept); an unparseable host config
|
|
5441
|
+
"under %s (oldest and newest four kept); an unparseable host config "
|
|
5442
|
+
"is refused" % BACKUP_DIR)
|
|
5326
5443
|
|
|
5327
5444
|
|
|
5328
5445
|
def wizard(default_hosts, default_roots, deps_off_default, default_features,
|
|
@@ -5374,7 +5491,7 @@ def upgrade(version, dry_run):
|
|
|
5374
5491
|
so its farm links would point back into it. The hosts and roots come from
|
|
5375
5492
|
config.json, so an upgrade does not re-ask the install questions. Nothing
|
|
5376
5493
|
else calls this: a flag never upgrades tezgah as a side effect of another
|
|
5377
|
-
(hooks/tezgah_policy.py:
|
|
5494
|
+
(hooks/tezgah_policy.py:792). Prints every step before running it, and
|
|
5378
5495
|
`--dry-run` stops after the printing."""
|
|
5379
5496
|
cfg = tp.config()
|
|
5380
5497
|
hosts = cfg.get("hosts") or detected_hosts()
|
|
@@ -5472,8 +5589,11 @@ def install_all(hosts, new_roots, deps_off, dry_run, redetect):
|
|
|
5472
5589
|
install_openresearch(hosts)
|
|
5473
5590
|
else:
|
|
5474
5591
|
print("\nopenresearch (orx):\n%s skipped: orx is not selected" % NO)
|
|
5475
|
-
|
|
5592
|
+
# cursor last: link_cursor_skills reads the skill links the other hosts made
|
|
5593
|
+
for h in sorted(hosts, key=lambda h: h == "cursor"):
|
|
5476
5594
|
INSTALLERS[h]()
|
|
5595
|
+
for dst in sweep_backups():
|
|
5596
|
+
print("%s moved a backup out of a host dir: %s" % (OK, dst))
|
|
5477
5597
|
refresh_plugin_copy(hosts)
|
|
5478
5598
|
record_hooks(hosts)
|
|
5479
5599
|
print()
|
|
@@ -5643,6 +5763,11 @@ def main(argv):
|
|
|
5643
5763
|
label, ok = hosts_row()
|
|
5644
5764
|
if not ok:
|
|
5645
5765
|
print("%s%s" % (NO, label))
|
|
5766
|
+
# Orca's presence, and whose codex home CODEX_HOME names: printed only
|
|
5767
|
+
# where Orca is installed or this shell runs inside it.
|
|
5768
|
+
orca = tezgah_orca.summary(CODEX)
|
|
5769
|
+
if orca:
|
|
5770
|
+
print(orca)
|
|
5646
5771
|
return 0
|
|
5647
5772
|
if args.deps and not args.install:
|
|
5648
5773
|
return 1 if install_deps(dry_run=args.dry_run) else 0
|