@r1z4x/tezgah 1.5.0 → 1.6.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 +96 -1
- package/MANIFEST +6 -1
- package/README.md +6 -4
- package/bin/consult +13 -10
- package/bin/tezgah-docs +1 -1
- package/bin/tezgah-research +53 -3
- package/bin/tezgah-route +8 -5
- package/bin/tezgah-setup +45 -25
- package/bin/tezgah-task +194 -41
- package/docs/architecture.md +1 -1
- package/docs/citations-baseline.json +1 -1
- package/docs/clarity-baseline.json +4 -4
- package/docs/contract.md +4 -1
- package/docs/feature-audit.md +24 -3
- package/docs/gate.md +10 -11
- package/docs/hosts.md +9 -0
- package/docs/index.json +21 -7
- package/docs/judge.md +3 -1
- package/docs/layers.md +1 -1
- package/docs/models.md +17 -7
- package/docs/operations.md +0 -1
- package/docs/research.md +46 -0
- package/docs/skills.md +23 -5
- package/docs/testing.md +1 -1
- package/hooks/tezgah_agents.py +130 -96
- package/hooks/tezgah_components.py +2 -1
- package/hooks/tezgah_context.py +67 -15
- package/hooks/tezgah_gate.py +62 -76
- package/hooks/tezgah_judge.py +15 -8
- package/hooks/tezgah_models.py +50 -3
- package/hooks/tezgah_paths.py +26 -11
- package/hooks/tezgah_policy.py +47 -19
- package/hooks/tezgah_product.py +107 -0
- package/hooks/tezgah_research.py +3 -3
- package/hooks/tezgah_skill_pick.py +58 -17
- package/hooks/tezgah_task.py +16 -8
- package/hosts/opencode/plugins/tezgah.js +17 -37
- package/package.json +1 -2
- package/skills/analyze-app/SKILL.md +57 -0
- package/skills/design-contract/EVALS.md +24 -1
- package/skills/design-contract/SKILL.md +17 -1
- package/skills/design-contract/data-views.md +86 -0
- package/skills/feature-audit/SKILL.md +32 -3
- package/skills/plan-add/EVALS.md +8 -8
- package/skills/plan-add/SKILL.md +20 -21
- package/skills/plan-sync/EVALS.md +2 -2
- package/skills/plan-sync/SKILL.md +11 -7
- package/skills/pm-frameworks/SKILL.md +6 -0
- package/skills/product-analysis/EVALS.md +23 -1
- package/skills/product-analysis/SKILL.md +50 -11
- package/skills/product-flow/EVALS.md +81 -0
- package/skills/product-flow/SKILL.md +91 -0
- package/skills/research/SKILL.md +10 -1
- package/skills/tezgah-contract/SKILL.md +67 -41
- package/agents/tezgah-reviewer.md +0 -79
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""The product-flow playbook (plan 074, ADR 025): the one place its steps are
|
|
2
|
+
written. `next_steps` computes each step's status from the topic's files under
|
|
3
|
+
`.tezgah/analysis/<topic>/` - `flow.md`, one `## <step>` heading per step, and
|
|
4
|
+
`spec.md` - and nothing stores it."""
|
|
5
|
+
import datetime
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
import tezgah_paths as tp
|
|
10
|
+
|
|
11
|
+
# A topic becomes a path below, so anything outside kebab-case reads no file.
|
|
12
|
+
TOPIC = re.compile(r"(?=.{1,64}\Z)[a-z0-9]+(?:-[a-z0-9]+)*")
|
|
13
|
+
OBSERVE = re.compile(r"observe after:\s*(\d{4}-\d{2}-\d{2})", re.I)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _step(sid, skill, first=""):
|
|
17
|
+
"""A step whose artifact is its `## <sid>` note in flow.md; `first` is what
|
|
18
|
+
the session does before writing it."""
|
|
19
|
+
return {"id": sid, "skill": skill,
|
|
20
|
+
"leaves": "## %s in .tezgah/analysis/<topic>/flow.md" % sid,
|
|
21
|
+
"command": "%swrite ## %s in .tezgah/analysis/{topic}/flow.md"
|
|
22
|
+
% (first, sid)}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# `{topic}` in a command is filled per topic. The design check and the Stop UI
|
|
26
|
+
# rule judge design and build; the flow only asks for their note.
|
|
27
|
+
_STEPS = (
|
|
28
|
+
_step("research", "research"),
|
|
29
|
+
_step("metrics", "product-analysis"),
|
|
30
|
+
_step("assumptions", "product-analysis"),
|
|
31
|
+
_step("decision", "product-analysis"),
|
|
32
|
+
_step("instrumentation", "product-analysis"),
|
|
33
|
+
{"id": "forward-spec", "skill": "feature-audit",
|
|
34
|
+
"leaves": ".tezgah/analysis/<topic>/spec.md",
|
|
35
|
+
"command": "feature-audit forward mode: write .tezgah/analysis/{topic}/spec.md "
|
|
36
|
+
"naming the decision"},
|
|
37
|
+
_step("design", "design-contract", "tezgah-design check, then "),
|
|
38
|
+
_step("build", "design-contract", "tezgah-design check, then "),
|
|
39
|
+
_step("re-measure", "analyze-app", "measure the decision's metric again, then "),
|
|
40
|
+
)
|
|
41
|
+
FLOWS = {
|
|
42
|
+
"metric-dashboard": _STEPS,
|
|
43
|
+
# A feature may reuse a metric another topic owns: its own metric steps join
|
|
44
|
+
# the flow only when its flow.md holds `## metrics` (`next_steps`).
|
|
45
|
+
"feature": tuple(s for s in _STEPS if s["id"] not in ("metrics", "instrumentation")),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _sections(text):
|
|
50
|
+
"""{step: body} of the `## <step>` headings in `text`, lower-cased."""
|
|
51
|
+
parts = re.split(r"^##[ \t]+(.+?)[ \t]*$", text, flags=re.M)
|
|
52
|
+
return {parts[i].lower(): parts[i + 1] for i in range(1, len(parts) - 1, 2)}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _read(path):
|
|
56
|
+
try:
|
|
57
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
58
|
+
return fh.read()
|
|
59
|
+
except OSError:
|
|
60
|
+
return ""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def next_steps(repo, topic, flow="metric-dashboard", today=""):
|
|
64
|
+
"""[{"id", "skill", "status", "detail", "command"}] for `topic` under `flow`,
|
|
65
|
+
status one of done | missing | due, read from `.tezgah/analysis/<topic>/` of
|
|
66
|
+
`repo`. A topic outside TOPIC or with no folder lists every step missing."""
|
|
67
|
+
if flow not in FLOWS:
|
|
68
|
+
raise ValueError("unknown flow %r (%s)" % (flow, ", ".join(FLOWS)))
|
|
69
|
+
try:
|
|
70
|
+
day = datetime.date.fromisoformat(today) if today else datetime.date.today()
|
|
71
|
+
except ValueError:
|
|
72
|
+
raise ValueError("today %r is not YYYY-MM-DD" % (today,)) from None
|
|
73
|
+
safe = isinstance(topic, str) and TOPIC.fullmatch(topic)
|
|
74
|
+
rel = os.path.join(".tezgah", "analysis", topic if safe else "<topic>")
|
|
75
|
+
folder = os.path.join(tp.workspace(repo), "analysis", topic) if safe else ""
|
|
76
|
+
notes = _sections(_read(os.path.join(folder, "flow.md"))) if folder else {}
|
|
77
|
+
flow_rel = os.path.join(rel, "flow.md")
|
|
78
|
+
spec_rel = os.path.join(rel, "spec.md")
|
|
79
|
+
steps = FLOWS["metric-dashboard"] if flow == "feature" and "metrics" in notes \
|
|
80
|
+
else FLOWS[flow]
|
|
81
|
+
|
|
82
|
+
def status(sid):
|
|
83
|
+
if sid == "forward-spec":
|
|
84
|
+
return ("done", "%s exists" % spec_rel) \
|
|
85
|
+
if folder and os.path.isfile(os.path.join(folder, "spec.md")) \
|
|
86
|
+
else ("missing", "%s does not exist" % spec_rel)
|
|
87
|
+
if sid in notes:
|
|
88
|
+
return "done", "%s ## %s" % (flow_rel, sid)
|
|
89
|
+
if sid == "re-measure":
|
|
90
|
+
after = OBSERVE.search(notes.get("decision", ""))
|
|
91
|
+
try:
|
|
92
|
+
when = datetime.date.fromisoformat(after.group(1)) if after else None
|
|
93
|
+
except ValueError:
|
|
94
|
+
when = None
|
|
95
|
+
if when is not None:
|
|
96
|
+
return ("due", "observe after %s has passed" % when) if day > when \
|
|
97
|
+
else ("missing", "observe after %s, not yet" % when)
|
|
98
|
+
return "missing", "no observe after: in %s ## decision" % flow_rel
|
|
99
|
+
return "missing", "no ## %s in %s" % (sid, flow_rel)
|
|
100
|
+
|
|
101
|
+
out = []
|
|
102
|
+
for s in steps:
|
|
103
|
+
state, detail = status(s["id"])
|
|
104
|
+
out.append({"id": s["id"], "skill": s["skill"], "status": state,
|
|
105
|
+
"detail": detail, "command": s["command"].format(
|
|
106
|
+
topic=topic if safe else "<topic>")})
|
|
107
|
+
return out
|
package/hooks/tezgah_research.py
CHANGED
|
@@ -2842,9 +2842,9 @@ def _check_append_only(repo, label, path, errors, warnings, hard):
|
|
|
2842
2842
|
old = [line for line in older.splitlines() if line.strip()]
|
|
2843
2843
|
new = [line for line in newer.splitlines() if line.strip()]
|
|
2844
2844
|
if new[:len(old)] != old:
|
|
2845
|
-
_soft(errors, warnings, hard, "%s was rewritten after the run: a
|
|
2846
|
-
"row changed or went missing, and results are
|
|
2847
|
-
"correction is a new row" % label)
|
|
2845
|
+
_soft(errors, warnings, hard, "%s was rewritten after the run: a "
|
|
2846
|
+
"committed row changed or went missing, and results are "
|
|
2847
|
+
"append-only - a correction is a new row" % label)
|
|
2848
2848
|
return
|
|
2849
2849
|
|
|
2850
2850
|
|
|
@@ -347,10 +347,40 @@ def skill_roots(extra=()):
|
|
|
347
347
|
return out
|
|
348
348
|
|
|
349
349
|
|
|
350
|
+
# One routing line of a skill's own INDEX.md: `name - what it makes - path`.
|
|
351
|
+
_INDEX_ROW = re.compile(r"^\S+ - .+ - (\S+/SKILL\.md)\s*$")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _entries(root, name):
|
|
355
|
+
"""[(skill, path)] for one skill directory: its SKILL.md, then every entry its
|
|
356
|
+
INDEX.md routes to. design-library keeps 39 entries under
|
|
357
|
+
`<plugin>/skills/<entry>/SKILL.md` and reaches them only through that index,
|
|
358
|
+
so a search over top-level files alone never saw a word of them. A nested
|
|
359
|
+
entry is addressed `<skill>/<path>`, the form `skill://` resolves."""
|
|
360
|
+
base = os.path.join(root, name)
|
|
361
|
+
out = [(name, os.path.join(base, "SKILL.md"))]
|
|
362
|
+
try:
|
|
363
|
+
with open(os.path.join(base, "INDEX.md"), encoding="utf-8") as fh:
|
|
364
|
+
lines = fh.read().splitlines()
|
|
365
|
+
except (OSError, UnicodeDecodeError):
|
|
366
|
+
return out
|
|
367
|
+
# A row is followed only inside the skill's own directory: an absolute path,
|
|
368
|
+
# a `..` or a symlink out would index a file the skill does not ship.
|
|
369
|
+
inside = os.path.realpath(base) + os.sep
|
|
370
|
+
for line in lines:
|
|
371
|
+
hit = _INDEX_ROW.match(line)
|
|
372
|
+
if not hit or os.path.isabs(hit.group(1)):
|
|
373
|
+
continue
|
|
374
|
+
path = os.path.join(base, hit.group(1))
|
|
375
|
+
if os.path.realpath(path).startswith(inside):
|
|
376
|
+
out.append(("%s/%s" % (name, hit.group(1)), path))
|
|
377
|
+
return out
|
|
378
|
+
|
|
379
|
+
|
|
350
380
|
def _rows(roots=None):
|
|
351
381
|
"""[(skill, path, title, start, end, text)] over every installed skill's
|
|
352
|
-
sections,
|
|
353
|
-
is one entry, not five."""
|
|
382
|
+
sections, and the entries a skill's INDEX.md routes to, deduplicated on the
|
|
383
|
+
file's realpath so a skill five hosts link is one entry, not five."""
|
|
354
384
|
out, seen = [], set()
|
|
355
385
|
for root in (skill_roots() if roots is None else roots):
|
|
356
386
|
try:
|
|
@@ -358,14 +388,23 @@ def _rows(roots=None):
|
|
|
358
388
|
except OSError:
|
|
359
389
|
continue
|
|
360
390
|
for name in names:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
for title, start, end, text in sections(path):
|
|
365
|
-
if (path, start) in seen:
|
|
391
|
+
for skill, raw in _entries(root, name):
|
|
392
|
+
path = os.path.realpath(raw)
|
|
393
|
+
if not os.path.isfile(path):
|
|
366
394
|
continue
|
|
367
|
-
|
|
368
|
-
|
|
395
|
+
secs = sections(path)
|
|
396
|
+
if "/" in skill and secs:
|
|
397
|
+
# A routed entry is one row, the whole file: its short
|
|
398
|
+
# sections out-scored whole skills on two common words
|
|
399
|
+
# ("give", "thing") and silenced the right hint, while the
|
|
400
|
+
# entry as a whole still carries its topic's words.
|
|
401
|
+
secs = [(secs[0][0], secs[0][1], secs[-1][2],
|
|
402
|
+
"\n".join(s[3] for s in secs))]
|
|
403
|
+
for title, start, end, text in secs:
|
|
404
|
+
if (path, start) in seen:
|
|
405
|
+
continue
|
|
406
|
+
seen.add((path, start))
|
|
407
|
+
out.append((skill, path, title, start, end, text))
|
|
369
408
|
return out
|
|
370
409
|
|
|
371
410
|
|
|
@@ -490,9 +529,10 @@ def rank_postings(query, posts, lengths, k):
|
|
|
490
529
|
|
|
491
530
|
|
|
492
531
|
def _signature():
|
|
493
|
-
"""[[root, skill, mtime_ns, size]] per installed SKILL.md
|
|
494
|
-
keys on. The file's own stat, not its
|
|
495
|
-
removing one skill is exactly the
|
|
532
|
+
"""[[root, skill, mtime_ns, size]] per installed SKILL.md and per entry an
|
|
533
|
+
INDEX.md routes to: what a rebuild keys on. The file's own stat, not its
|
|
534
|
+
directory's - installing, editing or removing one skill is exactly the
|
|
535
|
+
change the cache must notice."""
|
|
496
536
|
sig = []
|
|
497
537
|
for root in skill_roots():
|
|
498
538
|
try:
|
|
@@ -500,11 +540,12 @@ def _signature():
|
|
|
500
540
|
except OSError:
|
|
501
541
|
continue
|
|
502
542
|
for name in names:
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
543
|
+
for skill, path in _entries(root, name):
|
|
544
|
+
try:
|
|
545
|
+
st = os.stat(path)
|
|
546
|
+
except OSError:
|
|
547
|
+
continue
|
|
548
|
+
sig.append([root, skill, st.st_mtime_ns, st.st_size])
|
|
508
549
|
return sig
|
|
509
550
|
|
|
510
551
|
|
package/hooks/tezgah_task.py
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"""The active task: one plan file's phase and its path allowlist.
|
|
3
3
|
|
|
4
4
|
A task is not a second record beside the plan - it IS the plan file under
|
|
5
|
-
`.tezgah/plans/open/`, with two optional frontmatter keys that
|
|
6
|
-
(bin/tezgah-task
|
|
5
|
+
`.tezgah/plans/open/`, with two optional frontmatter keys that only its CLI
|
|
6
|
+
(bin/tezgah-task, run by the orchestrating session itself) writes:
|
|
7
7
|
|
|
8
8
|
phase: implementation # discovery | implementation | verification
|
|
9
9
|
allowed_paths: # globs, relative to the repo root
|
|
@@ -158,6 +158,12 @@ _MARKER = re.compile(r"unverifiable(?![-\w])", re.I)
|
|
|
158
158
|
# why") has no separator there and is not a declaration.
|
|
159
159
|
_SEPARATORS = ":,;(-"
|
|
160
160
|
_SPAN = re.compile(r"`([^`\n]+)`")
|
|
161
|
+
# A runner whose test entry point is a bare subcommand: `pnpm test`, `cargo
|
|
162
|
+
# test`, `uv run pytest` carry no option and no path, yet each is the command.
|
|
163
|
+
# The runner has to be the first word, so `run the tests` stays prose.
|
|
164
|
+
_RUNNERS = ("npm", "pnpm", "yarn", "bun", "deno", "cargo", "go", "make", "mix",
|
|
165
|
+
"dotnet", "gradle", "mvn", "poetry", "uv")
|
|
166
|
+
_SUBCOMMANDS = ("test", "run")
|
|
161
167
|
|
|
162
168
|
|
|
163
169
|
def _argument(token):
|
|
@@ -179,14 +185,16 @@ def _argument(token):
|
|
|
179
185
|
|
|
180
186
|
def names_command(text):
|
|
181
187
|
"""Does this acceptance item name the command that proves it? A command is a
|
|
182
|
-
backticked run of two or more words carrying an option, a path or a script
|
|
183
|
-
`bin/tezgah-docs --citations`, `python3 -m unittest tests/test_task.py
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
188
|
+
backticked run of two or more words carrying an option, a path or a script
|
|
189
|
+
(`bin/tezgah-docs --citations`, `python3 -m unittest tests/test_task.py`), or
|
|
190
|
+
a known runner followed by `test` or `run` (`pnpm test`, `npm run test`). A
|
|
191
|
+
citation (`hooks/tezgah_task.py`, `plan:line`) is not one, and any other
|
|
192
|
+
invocation with no argument is not recognised either - both are reported,
|
|
193
|
+
which is the safe direction for a report a person reads."""
|
|
187
194
|
for span in _SPAN.findall(text):
|
|
188
195
|
tokens = span.split()
|
|
189
|
-
if len(tokens) > 1 and any(_argument(token) for token in tokens[1:])
|
|
196
|
+
if len(tokens) > 1 and (any(_argument(token) for token in tokens[1:]) or
|
|
197
|
+
(tokens[0] in _RUNNERS and tokens[1] in _SUBCOMMANDS)):
|
|
190
198
|
return True
|
|
191
199
|
return False
|
|
192
200
|
|
|
@@ -21,10 +21,7 @@
|
|
|
21
21
|
// this host asks for its answer rather than keeping a JS copy of it (the
|
|
22
22
|
// rule-by-rule port below is documented as incomplete and divergent, which
|
|
23
23
|
// is exactly why a new rule never enters through it). The same ask covers
|
|
24
|
-
// the
|
|
25
|
-
// the allowlist - through the same CLI and the same rule, asked only on a
|
|
26
|
-
// command that names the task CLI (see TASK_CLI for the bound), and the
|
|
27
|
-
// language rule's own shell route: a command that would create a branch, a
|
|
24
|
+
// the language rule's own shell route: a command that would create a branch, a
|
|
28
25
|
// commit subject or a PR/issue title is put to the same CLI under the same
|
|
29
26
|
// kind of bound (see IDENT_CMD), so the word list stays in hooks/tezgah_lang
|
|
30
27
|
// and a non-English identifier is refused here with the core's own answer.
|
|
@@ -249,9 +246,10 @@ const CLASS_NOTE = {
|
|
|
249
246
|
transient: "it names a failure the host's own client may retry - a " +
|
|
250
247
|
"timeout, a connection error, a rate limit, a 5xx - and an identical " +
|
|
251
248
|
"repeat by the agent adds nothing to that",
|
|
252
|
-
user: "it names something only the user can
|
|
253
|
-
"an access grant - so
|
|
254
|
-
"the
|
|
249
|
+
user: "it names something only the user can supply - a credential, a " +
|
|
250
|
+
"login, an access grant - so take a route that needs none first " +
|
|
251
|
+
"(another provider or member, a local run, a public source) and ask the " +
|
|
252
|
+
"user once, naming what it unlocks, only when none is left",
|
|
255
253
|
permanent: "an assertion or a bad argument does not change by re-running it",
|
|
256
254
|
}
|
|
257
255
|
const NO_CLASS_NOTE = "the host reported no error text for it, so the class " +
|
|
@@ -548,25 +546,12 @@ const WRITE_TOOLS = new Set(["edit", "write", "multiedit", "notebookedit",
|
|
|
548
546
|
// hosts send.
|
|
549
547
|
const BASH_TOOLS = new Set(["bash", "shell", "command", "exec_command",
|
|
550
548
|
"run_command", "powershell", "pwsh"])
|
|
551
|
-
// The core's task rule has a shell half no write tool takes: a command that
|
|
552
|
-
// moves the active task's own record (`hooks/tezgah_gate.TASK_CHANGE`), which is
|
|
553
|
-
// how an agent moves the phase or the allowlist it is being held to instead of
|
|
554
|
-
// retyping the record file. The rule, and the masking that lets a command merely
|
|
555
|
-
// naming the CLI through, are the core's, so its answer is asked for and used
|
|
556
|
-
// verbatim; what stays here is only the pre-test that decides whether asking is
|
|
557
|
-
// worth a spawn. A spawn per bash call would tax every command in the session
|
|
558
|
-
// for a rule about one, so the core is asked when the raw command names the CLI
|
|
559
|
-
// and carries an argument - loose on purpose in that direction, because a
|
|
560
|
-
// commit message or a comment that mentions the CLI costs one spawn whose answer
|
|
561
|
-
// comes back empty, while a pre-test tight enough to miss nothing would be the
|
|
562
|
-
// second implementation this host exists not to keep.
|
|
563
|
-
const TASK_CLI = /\btezgah-task\b\s+\S/
|
|
564
549
|
// The control rule's shell half (`hooks/tezgah_gate.control_reason`): a line
|
|
565
550
|
// that could change tezgah's own control plane - a program that writes, removes
|
|
566
551
|
// or moves a path, or a redirect, before a name that plane holds; or one of its
|
|
567
552
|
// CLIs with an argument. The rule, its path table and its `~`/`$HOME`/XDG
|
|
568
553
|
// expansion are the core's; this is only the bound on asking, loose the same
|
|
569
|
-
// way
|
|
554
|
+
// way IDENT_CMD is: a false hit costs one spawn whose answer comes back empty.
|
|
570
555
|
const CONTROL_CMD = /(?:\b(?:touch|rm|rmdir|unlink|shred|truncate|chmod|chown|chgrp|mv|mkdir|tee|cp|ln|install|rsync|dd|sed|perl|git|sh|bash|zsh)\b|>)[\s\S]*(?:tezgah|\.no-|\.husky|\.git\/hooks|\.claude|\.codex|\.cursor|opencode|\.omp|\.local\/share)|\btezgah-(?:gate(?:\.py)?\s+decide|(?:capture|pony|adhd)(?:\.py)?\s+\S|context(?:\.py)?\s+(?:attest|user_prompt)\b|(?:codex|cursor)-hook\b)|\bprojects-(?:auto-init|pretooluse|posttooluse|stop)\b|\bhook\.py\b/
|
|
571
556
|
// The same bound for the language rule: the command shapes that dream up an
|
|
572
557
|
// identifier which outlives the session - `git commit` (its message), `git
|
|
@@ -2709,26 +2694,21 @@ export const Tezgah = async ({ directory }) => {
|
|
|
2709
2694
|
await noteDeny(sessionID, deny === ATTRIB_DENY ? "attribution"
|
|
2710
2695
|
: deny === EXPLORE_DENY ? "explorer" : "shortcut", deny, tool, args, base)
|
|
2711
2696
|
}
|
|
2712
|
-
//
|
|
2713
|
-
//
|
|
2714
|
-
//
|
|
2715
|
-
//
|
|
2716
|
-
//
|
|
2717
|
-
//
|
|
2718
|
-
//
|
|
2719
|
-
//
|
|
2720
|
-
//
|
|
2721
|
-
// comes back empty and falls through to the rules below. Asked ahead of
|
|
2722
|
-
// shell rules for the same reason the Python gate orders them there
|
|
2723
|
-
// (hooks/tezgah_gate.decision: shortcut, attribution, lang, task, secret,
|
|
2724
|
-
// repeat): a call another rule would refuse is counted as that rule and
|
|
2725
|
-
// never as a repeat.
|
|
2697
|
+
// Shell routes asked of the core in one spawn (IDENT_CMD
|
|
2698
|
+
// carries which commands are worth asking about, and why its pre-test is
|
|
2699
|
+
// loose): a command that creates an identifier may be putting a branch,
|
|
2700
|
+
// a commit subject or a PR/issue title into the repository's history for
|
|
2701
|
+
// good. The refusal is the core's own text and this file keeps no copy
|
|
2702
|
+
// of the rule. Asked ahead of shell rules for the same reason the Python
|
|
2703
|
+
// gate orders them there (hooks/tezgah_gate.decision: shortcut,
|
|
2704
|
+
// attribution, lang, task, secret, repeat): a call another rule would
|
|
2705
|
+
// refuse is counted as that rule and never as a repeat.
|
|
2726
2706
|
const cmd = BASH_TOOLS.has(tool)
|
|
2727
2707
|
? String(args.command || args.cmd || "") : ""
|
|
2728
|
-
// A piped check (hooks/tezgah_integrity.piped_check) is the
|
|
2708
|
+
// A piped check (hooks/tezgah_integrity.piped_check) is the second: the
|
|
2729
2709
|
// core decides whether the pipe hides the check's status, and it rides
|
|
2730
2710
|
// `verify-off` like the other integrity denials.
|
|
2731
|
-
if (!deny && !asked && cmd && (
|
|
2711
|
+
if (!deny && !asked && cmd && (IDENT_CMD.test(cmd) ||
|
|
2732
2712
|
(shortcuts && cmd.includes("|") && verifyCommand(cmd)))) {
|
|
2733
2713
|
asked = true
|
|
2734
2714
|
deny = await gateReason(tool, args, dir, sessionID, core)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@r1z4x/tezgah",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "One shared working contract for every AI coding assistant you run: Claude Code, Codex, Cursor, opencode, dsh and oh-my-pi (omp).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"output-styles/",
|
|
20
20
|
"commands/",
|
|
21
21
|
"docs/",
|
|
22
|
-
"agents/",
|
|
23
22
|
"statusline.py",
|
|
24
23
|
"MANIFEST",
|
|
25
24
|
"CHANGELOG.md",
|
|
@@ -136,6 +136,63 @@ Screenshots, traces and tree dumps go to the app artifact directory
|
|
|
136
136
|
(`PLAYWRIGHT_MCP_OUTPUT_DIR`, default `~/.cache/tezgah/apps`). Return the file
|
|
137
137
|
path; keep image tokens out of the context.
|
|
138
138
|
|
|
139
|
+
## Measurement for design-contract
|
|
140
|
+
|
|
141
|
+
`tezgah-design check` judges a measurement, and this loop is what writes it:
|
|
142
|
+
`.tezgah/design/measurement.json`, one entry per component the repository's
|
|
143
|
+
`.tezgah/design-contract.md` lists. Read the computed styles with `browser_evaluate`,
|
|
144
|
+
one selector per component:
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
() => {
|
|
148
|
+
const PROPS = ["font-size", "padding", "gap", "color", "background-color",
|
|
149
|
+
"width", "height"];
|
|
150
|
+
const LIST = [["Button", "interactive", "button.primary"],
|
|
151
|
+
["RevenueTile", "data", "[data-widget=revenue]"]];
|
|
152
|
+
return {components: LIST.map(([name, kind, selector]) => {
|
|
153
|
+
const style = getComputedStyle(document.querySelector(selector));
|
|
154
|
+
return {name, kind, states: [],
|
|
155
|
+
styles: Object.fromEntries(PROPS.map(p => [p, style.getPropertyValue(p)]))};
|
|
156
|
+
})};
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
For a focusable component, read `outline-style`, `outline-width` and `box-shadow`
|
|
161
|
+
only after focusing it from the keyboard (`browser_press_key` Tab until it has
|
|
162
|
+
focus, so `:focus-visible` applies): at rest they are `none`, and the
|
|
163
|
+
`focus-visible` rule reads a resting `none` as a missing focus ring. For an animated
|
|
164
|
+
component add `transition-duration`, which the `reduced-motion` rule reads.
|
|
165
|
+
`states` is filled from what was observed, never from source: a computed
|
|
166
|
+
style shows only the state the page is in, so each state name goes in after the
|
|
167
|
+
component was driven or routed into it and read again. The file is this shape - an
|
|
168
|
+
object with a `components` list, or the bare list:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{"components": [
|
|
172
|
+
{"name": "Button", "kind": "interactive",
|
|
173
|
+
"states": ["default", "hover", "focus", "active", "disabled", "loading",
|
|
174
|
+
"error"],
|
|
175
|
+
"styles": {"font-size": "16px", "padding": "8px 12px", "gap": "8px",
|
|
176
|
+
"color": "rgb(17, 17, 17)", "background-color": "rgb(255, 255, 255)",
|
|
177
|
+
"width": "120px", "height": "40px"}},
|
|
178
|
+
{"name": "RevenueTile", "kind": "data",
|
|
179
|
+
"states": ["empty", "loading", "skeleton", "error", "offline", "partial",
|
|
180
|
+
"long-text", "permission-denied"],
|
|
181
|
+
"styles": {"font-size": "14px", "padding": "16px",
|
|
182
|
+
"color": "rgb(17, 17, 17)", "background-color": "rgb(255, 255, 255)"}}]}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`kind` is `interactive` or `data`, and it decides which half of the contract's state
|
|
186
|
+
set the component owes; a component without one is unjudged.
|
|
187
|
+
|
|
188
|
+
## The re-measure of a product flow
|
|
189
|
+
|
|
190
|
+
Under `product-flow`, what the running app showed after a decision's observe-after
|
|
191
|
+
date is the topic's re-measure: write it under `## re-measure` in
|
|
192
|
+
`.tezgah/analysis/<topic>/flow.md` with the decision it measures, the value, the
|
|
193
|
+
verdict (met, missed or unobservable), the evidence class `ui-observed` and the
|
|
194
|
+
screenshot or tree dump path. The `product-flow` skill gives the order of the steps.
|
|
195
|
+
|
|
139
196
|
## Looking at the screen
|
|
140
197
|
|
|
141
198
|
The tree answers structure; the image answers hierarchy, rhythm, colour and feel.
|
|
@@ -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
|
|
|
@@ -74,6 +74,29 @@ finding", and `states.data` is a field "The contract must carry ... exactly".
|
|
|
74
74
|
|
|
75
75
|
---
|
|
76
76
|
|
|
77
|
+
## Case 4 - The trap: a dashboard with only its happy state
|
|
78
|
+
|
|
79
|
+
**Input**
|
|
80
|
+
> Bu veriye uygun bir metric dashboard tasarla. (attached: `signups.csv` -
|
|
81
|
+
> date, plan, country, signups, activations)
|
|
82
|
+
|
|
83
|
+
**Must contain**
|
|
84
|
+
- [ ] Names the dashboard type, operational or analytical, before the layout
|
|
85
|
+
- [ ] Picks each chart by the question it answers (comparison, trend, part-to-whole, distribution, relationship), with length or position for quantity and colour only as a redundant cue
|
|
86
|
+
- [ ] Shows every number with its comparison (target, previous period or benchmark), its window and its unit, and 3-5 key metrics by default
|
|
87
|
+
- [ ] Lists per widget the states `check` requires of a data view (empty, loading, skeleton, error, offline, partial, long-text, permission-denied) plus configuration-needed and stale with an "as of" time
|
|
88
|
+
- [ ] Takes the metrics from `product-analysis`'s metric record (goal, definition, window, baseline, logged) instead of inventing them
|
|
89
|
+
|
|
90
|
+
**Must not**
|
|
91
|
+
- Encode a magnitude in colour, or use a pie or gauge for a comparison
|
|
92
|
+
- Design only the loaded state
|
|
93
|
+
|
|
94
|
+
**Why this case** - `data-views.md` says "Colour never encodes a magnitude", that
|
|
95
|
+
"A bare number cannot be judged", and that every widget "owes each of these
|
|
96
|
+
states". A data file pulls toward drawing its columns in their happy state.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
77
100
|
## What these do not prove
|
|
78
101
|
|
|
79
102
|
These are trap cases for a human or judge rater, not a behavioural measurement.
|
|
@@ -70,6 +70,9 @@ opens. Field names and prose stay English.
|
|
|
70
70
|
owns. A component the measurement shows that is not listed is a
|
|
71
71
|
`component-inventory` violation, so a new component is added here, in the same
|
|
72
72
|
turn it is written.
|
|
73
|
+
- `components[].decision` - optional, the product decision that asked for the
|
|
74
|
+
component: the topic and the decision as written under `## decision` in
|
|
75
|
+
`.tezgah/analysis/<topic>/flow.md`.
|
|
73
76
|
|
|
74
77
|
```json
|
|
75
78
|
{
|
|
@@ -101,6 +104,18 @@ wrong - and a component whose measurement does not show a state it owes gets a
|
|
|
101
104
|
`state-coverage` violation naming the states it is missing. `kind` decides which
|
|
102
105
|
half applies, so a component measured without one is unjudged, never passed.
|
|
103
106
|
|
|
107
|
+
A dashboard, chart or metric tile also follows [data-views.md](data-views.md): the
|
|
108
|
+
dashboard type, chart choice by question, context and window per number, and the
|
|
109
|
+
widget states.
|
|
110
|
+
|
|
111
|
+
## The product decision behind a component
|
|
112
|
+
|
|
113
|
+
A component built for a product decision names it in its `decision` field, such
|
|
114
|
+
as `{"name": "RevenueTile", "kind": "data", "decision": "signup-dashboard: ship
|
|
115
|
+
the revenue tile"}`, the decision written under `## decision` in
|
|
116
|
+
`.tezgah/analysis/<topic>/flow.md`. A component with no decision behind it leaves
|
|
117
|
+
the field out. The `product-flow` skill gives the order of the steps.
|
|
118
|
+
|
|
104
119
|
## Derivation
|
|
105
120
|
|
|
106
121
|
Read the repository's own token source. In this order, and nothing else:
|
|
@@ -144,7 +159,8 @@ repository owns, so list them in the turn that writes a new one.
|
|
|
144
159
|
```
|
|
145
160
|
|
|
146
161
|
`--measured` takes the JSON the running-app read produces - the per-component
|
|
147
|
-
computed styles and observed states - or a dash for stdin
|
|
162
|
+
computed styles and observed states - or a dash for stdin; `analyze-app`'s
|
|
163
|
+
"Measurement for design-contract" section writes it with `browser_evaluate`:
|
|
148
164
|
|
|
149
165
|
```json
|
|
150
166
|
{"components": [
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Data views
|
|
2
|
+
|
|
3
|
+
The rules a dashboard, a chart or a metric tile is held to, on top of the state set
|
|
4
|
+
in `SKILL.md`. The metrics themselves - which ones, their definition, window,
|
|
5
|
+
baseline and whether they are logged - come from `product-analysis`'s Metrics
|
|
6
|
+
section; this file decides how they are shown. Each rule names its source, and a
|
|
7
|
+
rule with no primary source found says so.
|
|
8
|
+
|
|
9
|
+
## The dashboard type comes first
|
|
10
|
+
|
|
11
|
+
Decide whether the screen is **operational** - critical information read quickly
|
|
12
|
+
during a time-sensitive task - or **analytical** - at-a-glance information for
|
|
13
|
+
analysis and decision making (NN/g). Carbon draws the same line as presentation
|
|
14
|
+
against exploration dashboards. The type sets the density, the refresh rate and
|
|
15
|
+
how much interaction the screen owes; a dashboard that has not named its type has
|
|
16
|
+
not named its user.
|
|
17
|
+
|
|
18
|
+
## Chart choice follows the question
|
|
19
|
+
|
|
20
|
+
Name the question each view answers, then pick the encoding:
|
|
21
|
+
|
|
22
|
+
| The question | Encoding | Source |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| comparison - which is bigger? | bars on a common baseline | NN/g (length on a common scale) |
|
|
25
|
+
| trend - how did it move over time? | a line over a time axis | NN/g (position) |
|
|
26
|
+
| part-to-whole - what share is each? | a stacked bar, or a bar of shares | tezgah's own: no primary source read |
|
|
27
|
+
| distribution - how is it spread? | a histogram or a box plot | tezgah's own: no primary source read |
|
|
28
|
+
| relationship - does one move with another? | a scatter plot | NN/g (position on two axes) |
|
|
29
|
+
|
|
30
|
+
Length and position carry quantity, because people compare them accurately; area
|
|
31
|
+
and angle (pie, gauge, radar) do not (NN/g, after Cleveland and McGill). Colour
|
|
32
|
+
never encodes a magnitude: it is a redundant grouping cue beside a label, a shape
|
|
33
|
+
or a position, and every chart passes the greyscale test - it still reads with the
|
|
34
|
+
colour removed (NN/g; the design library's `colour-independence` entry).
|
|
35
|
+
|
|
36
|
+
## Every number has context and a window
|
|
37
|
+
|
|
38
|
+
A bare number cannot be judged: "Compared to what? Is this good or bad?" (Few,
|
|
39
|
+
*Common Pitfalls in Dashboard Design*). Every figure shown carries its comparison
|
|
40
|
+
- a target, the previous period or a benchmark - and the window it covers, and is
|
|
41
|
+
formatted with its unit and no more precision than the decision needs (Few's
|
|
42
|
+
"excessive detail or precision" pitfall). The record behind each comes from the
|
|
43
|
+
metric's `baseline` and `window` fields.
|
|
44
|
+
|
|
45
|
+
## Three to five metrics by default
|
|
46
|
+
|
|
47
|
+
The default view shows 3-5 key metrics and the primary actions; the rest sit
|
|
48
|
+
behind a fuller view the user opens (the design library's `simplified-views`
|
|
49
|
+
entry). Carbon's "Limit the number of metrics" and Few's "exceeding the
|
|
50
|
+
boundaries of a single screen" say the same. The most important metric gets the
|
|
51
|
+
largest area and the highest contrast (Carbon).
|
|
52
|
+
|
|
53
|
+
## The widget state set
|
|
54
|
+
|
|
55
|
+
Every widget - tile, chart, table - owes each of these states, and a state that
|
|
56
|
+
does not exist is a finding:
|
|
57
|
+
|
|
58
|
+
| State | What it shows | Source |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| loading | that data is on the way, in the widget's own frame | Carbon's loading pattern, seen but not read at source |
|
|
61
|
+
| no-data | that the query returned nothing, and what would fill it | Carbon empty states |
|
|
62
|
+
| error | that the system failed, per widget, with a text-only message when many fail | Carbon empty states ("Systems issue") |
|
|
63
|
+
| permission-denied | that data exists but this user may not see it | Carbon empty states ("Permissions issue") |
|
|
64
|
+
| configuration-needed | that the widget cannot run until something is set up | Carbon empty states ("Configuration required") |
|
|
65
|
+
| stale | the last good data, with its "as of" time | tezgah's own rule: no primary source found |
|
|
66
|
+
| partial | which part of the data is missing, beside what arrived | tezgah's own rule: no primary source found |
|
|
67
|
+
|
|
68
|
+
Against the contract: loading, error, partial and permission-denied are already in
|
|
69
|
+
`states.data`, and no-data is its `empty`; configuration-needed and stale are
|
|
70
|
+
reported as observed state names beside the shipped set, and their absence is a
|
|
71
|
+
finding the reviewer names, since `check` does not require them.
|
|
72
|
+
|
|
73
|
+
## Sources
|
|
74
|
+
|
|
75
|
+
- Stephen Few, *Common Pitfalls in Dashboard Design* (2006),
|
|
76
|
+
http://www.perceptualedge.com/articles/Whitepapers/Common_Pitfalls.pdf
|
|
77
|
+
- NN/g, *Dashboards: Making Charts and Graphs Easier to Understand* (2017),
|
|
78
|
+
https://www.nngroup.com/articles/dashboards-preattentive/
|
|
79
|
+
- Carbon Design System, Dashboards,
|
|
80
|
+
https://www.carbondesignsystem.com/data-visualization/dashboards
|
|
81
|
+
- Carbon Design System, Empty states,
|
|
82
|
+
https://www.carbondesignsystem.com/building-blocks/core/patterns/empty-states
|
|
83
|
+
- The design library: [colour-independence](../design-library/adaptive-interfaces/skills/colour-independence/SKILL.md)
|
|
84
|
+
("Always test charts in greyscale") and
|
|
85
|
+
[simplified-views](../design-library/adaptive-interfaces/skills/simplified-views/SKILL.md)
|
|
86
|
+
(its Dashboard Simplification patterns: a default of three to five key metrics).
|