@r1z4x/tezgah 1.5.1 → 1.7.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 +126 -1
- package/MANIFEST +11 -0
- package/README.md +6 -3
- package/bin/consult +13 -10
- package/bin/tezgah-docs +18 -16
- package/bin/tezgah-doctor +152 -0
- package/bin/tezgah-research +286 -12
- package/bin/tezgah-route +48 -1
- package/bin/tezgah-setup +27 -14
- package/bin/tezgah-task +771 -46
- package/bin/tezgah-taste +48 -10
- package/docs/architecture.md +7 -4
- package/docs/citations-baseline.json +1 -1
- package/docs/clarity-baseline.json +5 -5
- package/docs/contract.md +112 -25
- package/docs/evidence.md +201 -28
- package/docs/feature-audit.md +25 -4
- package/docs/gate.md +70 -33
- package/docs/glossary.md +4 -4
- package/docs/hosts.md +8 -8
- package/docs/index.json +30 -10
- package/docs/judge.md +9 -3
- package/docs/models.md +35 -2
- package/docs/operations.md +103 -3
- package/docs/research.md +392 -13
- package/docs/skills.md +24 -6
- package/docs/status-line.md +5 -5
- package/docs/testing.md +2 -2
- package/docs/vision.md +12 -4
- package/hooks/hooks.json +2 -2
- package/hooks/lang/en.json +323 -0
- package/hooks/lang/tr.json +344 -0
- package/hooks/projects-posttooluse.py +24 -5
- package/hooks/projects-pretooluse.py +9 -4
- package/hooks/projects-stop.py +28 -16
- package/hooks/tezgah_agents.py +29 -9
- package/hooks/tezgah_clarity.py +7 -3
- package/hooks/tezgah_components.py +2 -1
- package/hooks/tezgah_context.py +460 -390
- package/hooks/tezgah_gate.py +640 -104
- package/hooks/tezgah_integrity.py +1480 -197
- package/hooks/tezgah_models.py +337 -15
- package/hooks/tezgah_paths.py +103 -10
- package/hooks/tezgah_policy.py +286 -130
- package/hooks/tezgah_product.py +107 -0
- package/hooks/tezgah_rank.py +29 -27
- package/hooks/tezgah_replay.py +200 -30
- package/hooks/tezgah_research.py +1297 -12
- package/hooks/tezgah_skill_pick.py +58 -17
- package/hooks/tezgah_stopspec.py +7 -0
- package/hooks/tezgah_store.py +74 -27
- package/hooks/tezgah_task.py +917 -8
- package/hosts/codex/hook.py +9 -10
- package/hosts/cursor/hook.py +21 -14
- package/hosts/dsh/hooks.json +2 -2
- package/hosts/omp/hook.py +44 -23
- package/hosts/omp/tezgah-hook.ts.in +93 -7
- package/hosts/opencode/plugins/tezgah.js +119 -39
- package/output-styles/tezgah.md +76 -42
- package/package.json +1 -1
- 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/i-have-adhd/EVALS.md +22 -2
- package/skills/i-have-adhd/SKILL.md +12 -5
- package/skills/plan-add/EVALS.md +8 -8
- package/skills/plan-add/SKILL.md +22 -21
- package/skills/plan-sync/EVALS.md +2 -2
- package/skills/plan-sync/SKILL.md +14 -8
- package/skills/pm-frameworks/SKILL.md +6 -0
- package/skills/ponytail/EVALS.md +21 -2
- package/skills/ponytail/SKILL.md +4 -4
- 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 +99 -2
- package/skills/tezgah-contract/EVALS.md +52 -7
- package/skills/tezgah-contract/SKILL.md +204 -85
|
@@ -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.
|
|
@@ -43,7 +40,8 @@
|
|
|
43
40
|
// second and third halves - the channel on the call's own ledger row (with
|
|
44
41
|
// the `external` row an MCP answer or a fetched page earns), the provenance
|
|
45
42
|
// label in front of the result itself, and the taint notice on the first
|
|
46
|
-
// effect a turn makes after such a read (hooks/tezgah_untrusted.marks)
|
|
43
|
+
// effect a turn makes after such a read (hooks/tezgah_untrusted.marks); a
|
|
44
|
+
// `task` call also writes its slice record (noteSlice, bin/tezgah-task).
|
|
47
45
|
//
|
|
48
46
|
// Known gap: every rule of hooks/tezgah_gate.py is enforced here, but the ledger
|
|
49
47
|
// the counters read is complete only for the rules ported last - secret, loop,
|
|
@@ -249,9 +247,10 @@ const CLASS_NOTE = {
|
|
|
249
247
|
transient: "it names a failure the host's own client may retry - a " +
|
|
250
248
|
"timeout, a connection error, a rate limit, a 5xx - and an identical " +
|
|
251
249
|
"repeat by the agent adds nothing to that",
|
|
252
|
-
user: "it names something only the user can
|
|
253
|
-
"an access grant - so
|
|
254
|
-
"the
|
|
250
|
+
user: "it names something only the user can supply - a credential, a " +
|
|
251
|
+
"login, an access grant - so take a route that needs none first " +
|
|
252
|
+
"(another provider or member, a local run, a public source) and ask the " +
|
|
253
|
+
"user once, naming what it unlocks, only when none is left",
|
|
255
254
|
permanent: "an assertion or a bad argument does not change by re-running it",
|
|
256
255
|
}
|
|
257
256
|
const NO_CLASS_NOTE = "the host reported no error text for it, so the class " +
|
|
@@ -548,25 +547,12 @@ const WRITE_TOOLS = new Set(["edit", "write", "multiedit", "notebookedit",
|
|
|
548
547
|
// hosts send.
|
|
549
548
|
const BASH_TOOLS = new Set(["bash", "shell", "command", "exec_command",
|
|
550
549
|
"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
550
|
// The control rule's shell half (`hooks/tezgah_gate.control_reason`): a line
|
|
565
551
|
// that could change tezgah's own control plane - a program that writes, removes
|
|
566
552
|
// or moves a path, or a redirect, before a name that plane holds; or one of its
|
|
567
553
|
// CLIs with an argument. The rule, its path table and its `~`/`$HOME`/XDG
|
|
568
554
|
// expansion are the core's; this is only the bound on asking, loose the same
|
|
569
|
-
// way
|
|
555
|
+
// way IDENT_CMD is: a false hit costs one spawn whose answer comes back empty.
|
|
570
556
|
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
557
|
// The same bound for the language rule: the command shapes that dream up an
|
|
572
558
|
// identifier which outlives the session - `git commit` (its message), `git
|
|
@@ -1173,6 +1159,49 @@ const DETAIL_MAX = 200
|
|
|
1173
1159
|
// agree on the credential itself; they differ only for an astral character
|
|
1174
1160
|
// inside a quoted named value.
|
|
1175
1161
|
|
|
1162
|
+
// The `todo` row for `todowrite` (hooks/tezgah_integrity.note_todo): the list's
|
|
1163
|
+
// open, ready and blocked counts, the ready heads in `heads` and `detail`. Typed
|
|
1164
|
+
// blockers: BLOCKER_SAID/BLOCKER_TRIED's tokens - every hooks/lang/<code>.json
|
|
1165
|
+
// pack's `blockers.<kind>.pattern`, DECISION and KARAR alike - on Unicode word
|
|
1166
|
+
// boundaries (JS `\b` is ASCII-only); no pack found matches none, as `alts`
|
|
1167
|
+
// does. Read once, on the first todo call. ponytail: without the Python
|
|
1168
|
+
// reader's negation rule.
|
|
1169
|
+
const TODO_CLOSED = new Set(["completed", "done", "cancelled", "abandoned"])
|
|
1170
|
+
let todoTyped = null
|
|
1171
|
+
function todoTypedPattern() {
|
|
1172
|
+
if (todoTyped) return todoTyped
|
|
1173
|
+
const kinds = { decision: [], irreversible: [], access: [], env: [] }
|
|
1174
|
+
const dir = hookScript("lang")
|
|
1175
|
+
try {
|
|
1176
|
+
for (const name of dir ? readdirSync(dir).sort() : []) {
|
|
1177
|
+
if (!name.endsWith(".json")) continue
|
|
1178
|
+
const blockers = JSON.parse(readFileSync(join(dir, name), "utf8")).blockers || {}
|
|
1179
|
+
for (const kind of Object.keys(kinds)) {
|
|
1180
|
+
if (blockers[kind]?.pattern) kinds[kind].push(blockers[kind].pattern)
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
} catch {}
|
|
1184
|
+
const alt = (...names) => names.flatMap((k) => kinds[k]).join("|") || "(?!)"
|
|
1185
|
+
todoTyped = new RegExp("(?<![\\p{L}\\p{N}_])(?:(?:" + alt("decision", "irreversible")
|
|
1186
|
+
+ ")[A-ZÇĞİIÖŞÜ]*|" + alt("access", "env") + ")(?![\\p{L}\\p{N}_])", "u")
|
|
1187
|
+
return todoTyped
|
|
1188
|
+
}
|
|
1189
|
+
async function recordTodo(sessionID, tool, args, workspace) {
|
|
1190
|
+
if (!Array.isArray(args?.todos)) return
|
|
1191
|
+
const open = args.todos.filter((i) => i && String(i.content || "").trim()
|
|
1192
|
+
&& !TODO_CLOSED.has(String(i.status || "pending").toLowerCase()))
|
|
1193
|
+
const typed = (i) => todoTypedPattern().test(i.content) || /\bblocked\b/i.test(i.content)
|
|
1194
|
+
const heads = open.filter((i) => String(i.status || "pending").toLowerCase() === "pending"
|
|
1195
|
+
&& !typed(i)).map((i) => redact(String(i.content).split(/\s+/).join(" ").trim())
|
|
1196
|
+
.slice(0, 60))
|
|
1197
|
+
await appendRow(sessionID, {
|
|
1198
|
+
kind: "todo", ts: Math.floor(Date.now() / 1000),
|
|
1199
|
+
detail: heads.join("\n").slice(0, DETAIL_MAX),
|
|
1200
|
+
id: actionID(tool, args), workspace: workspace || null, tool,
|
|
1201
|
+
open: open.length, ready: heads.length, blocked: open.filter(typed).length, heads,
|
|
1202
|
+
})
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1176
1205
|
// Evidence ledger, the store the Python gate and Stop hook read. Written
|
|
1177
1206
|
// per tool call so a "done/tested" claim can be checked against what ran.
|
|
1178
1207
|
// The bash tool returns `metadata.exit` (the process exit code), so a check's
|
|
@@ -1196,6 +1225,7 @@ async function recordEvidence(sessionID, tool, args, result, workspace, cwd,
|
|
|
1196
1225
|
source) {
|
|
1197
1226
|
if (!sessionID) return
|
|
1198
1227
|
const t = String(tool || "").toLowerCase()
|
|
1228
|
+
if (t === "todowrite") return await recordTodo(sessionID, tool, args, workspace)
|
|
1199
1229
|
const exit = result?.metadata?.exit
|
|
1200
1230
|
let kind = null
|
|
1201
1231
|
let detail = String(args?.command || writtenPath(args) || "")
|
|
@@ -1274,6 +1304,60 @@ async function recordEvidence(sessionID, tool, args, result, workspace, cwd,
|
|
|
1274
1304
|
}
|
|
1275
1305
|
}
|
|
1276
1306
|
|
|
1307
|
+
// The slice journal (hooks/tezgah_task.py, plan 081). opencode's `task` call
|
|
1308
|
+
// runs its child to the end before this hook sees it, so one call opens the
|
|
1309
|
+
// record and closes it - done, or stopped with the error's class - through
|
|
1310
|
+
// bin/tezgah-task, the records' one writer. The id is the child session opencode
|
|
1311
|
+
// reports (`metadata.sessionId`), else the first 12 hex digits of the brief's
|
|
1312
|
+
// sha1 (tezgah_task.slice_id). Awaited under the core deadline and never thrown:
|
|
1313
|
+
// a journal that cannot be written costs the record, not the call.
|
|
1314
|
+
const TASK_BIN = join(CONFIG, "bin", "tezgah-task")
|
|
1315
|
+
const SLICE_ID = /^[A-Za-z0-9._-]{1,80}$/
|
|
1316
|
+
const SLICE_BRIEF = 4000
|
|
1317
|
+
// tezgah_task._CLASSES, in its order: the class a stopped record carries, and
|
|
1318
|
+
// `error` for any other failure text.
|
|
1319
|
+
const SLICE_CLASSES = [
|
|
1320
|
+
["rate_limit", /\b429\b|rate[ _-]?limit/i],
|
|
1321
|
+
["quota", /quota|key limit exceeded|usage limit|\b40[23]\b.*\blimit|\blimit\b.*\b40[23]\b/i],
|
|
1322
|
+
["aborted", /abort|cancell?ed/i],
|
|
1323
|
+
["timeout", /timed out|timeout/i],
|
|
1324
|
+
["cut_off", /max[ _]turns|max_tokens|cut off|context[ _]length/i],
|
|
1325
|
+
]
|
|
1326
|
+
|
|
1327
|
+
function sliceClass(text) {
|
|
1328
|
+
const hit = SLICE_CLASSES.find(([, re]) => re.test(text))
|
|
1329
|
+
return hit ? hit[0] : "error"
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// The error a `task` result reports, as text, or "" for none.
|
|
1333
|
+
function sliceError(result) {
|
|
1334
|
+
for (const e of [result?.error, result?.metadata?.error]) {
|
|
1335
|
+
const text = typeof e === "string" ? e : String(e?.message || e?.name || "")
|
|
1336
|
+
if (e && text.trim()) return text.trim()
|
|
1337
|
+
}
|
|
1338
|
+
return ""
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
async function noteSlice(sessionID, args, result, cwd) {
|
|
1342
|
+
if (!existsSync(TASK_BIN) || await off("slices-off", sessionID)) return
|
|
1343
|
+
const brief = [args?.description, args?.prompt].map((p) => String(p || "").trim())
|
|
1344
|
+
.filter(Boolean).join("\n\n").slice(0, SLICE_BRIEF)
|
|
1345
|
+
if (!brief) return
|
|
1346
|
+
const child = String(result?.metadata?.sessionId || result?.metadata?.session_id || "")
|
|
1347
|
+
const id = SLICE_ID.test(child) ? child
|
|
1348
|
+
: createHash("sha1").update(brief).digest("hex").slice(0, 12)
|
|
1349
|
+
const run = (argv) => collect([TASK_BIN, "slice", ...argv],
|
|
1350
|
+
{ cwd, stdio: ["ignore", "pipe", "ignore"] }, null,
|
|
1351
|
+
(code) => code === 0)
|
|
1352
|
+
const opened = await run(["open", "--id", id, "--kind", "agent", "--host", "opencode",
|
|
1353
|
+
"--agent", String(args?.subagent_type || ""),
|
|
1354
|
+
"--brief", brief, "--parent", String(sessionID || ""),
|
|
1355
|
+
"--cwd", cwd])
|
|
1356
|
+
if (!opened) return
|
|
1357
|
+
const error = sliceError(result)
|
|
1358
|
+
await run(error ? ["stop", id, "--class", sliceClass(error)] : ["done", id])
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1277
1361
|
// How far back the pre-state search reads: the gate writes its snapshot row in
|
|
1278
1362
|
// the call immediately before the write, so the newest rows are where it is
|
|
1279
1363
|
// (SNAPSHOT_TAIL in hooks/tezgah_integrity).
|
|
@@ -2239,7 +2323,7 @@ const SWITCHES = new Set(["adhd-off", "consult-off", "docs-judge-off",
|
|
|
2239
2323
|
"exec-mode.off", "judge-off", "lang-off", "orchestrate-off",
|
|
2240
2324
|
"ponytail-auto.off", "pretooluse-off", "reminder-off", "research-off",
|
|
2241
2325
|
"spec-off", "task-off", "triage-off", "verify-off", "workspace-off",
|
|
2242
|
-
"agents-off", "skill-suggest-on", "taste-on", "update-check-off"])
|
|
2326
|
+
"agents-off", "skill-suggest-on", "slices-off", "taste-on", "update-check-off"])
|
|
2243
2327
|
|
|
2244
2328
|
// The session latch, mirrored from hooks/tezgah_paths.off: with a session id, a
|
|
2245
2329
|
// switch file whose ctime or mtime is newer than the session ledger's first row
|
|
@@ -2709,26 +2793,21 @@ export const Tezgah = async ({ directory }) => {
|
|
|
2709
2793
|
await noteDeny(sessionID, deny === ATTRIB_DENY ? "attribution"
|
|
2710
2794
|
: deny === EXPLORE_DENY ? "explorer" : "shortcut", deny, tool, args, base)
|
|
2711
2795
|
}
|
|
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.
|
|
2796
|
+
// Shell routes asked of the core in one spawn (IDENT_CMD
|
|
2797
|
+
// carries which commands are worth asking about, and why its pre-test is
|
|
2798
|
+
// loose): a command that creates an identifier may be putting a branch,
|
|
2799
|
+
// a commit subject or a PR/issue title into the repository's history for
|
|
2800
|
+
// good. The refusal is the core's own text and this file keeps no copy
|
|
2801
|
+
// of the rule. Asked ahead of shell rules for the same reason the Python
|
|
2802
|
+
// gate orders them there (hooks/tezgah_gate.decision: shortcut,
|
|
2803
|
+
// attribution, lang, task, secret, repeat): a call another rule would
|
|
2804
|
+
// refuse is counted as that rule and never as a repeat.
|
|
2726
2805
|
const cmd = BASH_TOOLS.has(tool)
|
|
2727
2806
|
? String(args.command || args.cmd || "") : ""
|
|
2728
|
-
// A piped check (hooks/tezgah_integrity.piped_check) is the
|
|
2807
|
+
// A piped check (hooks/tezgah_integrity.piped_check) is the second: the
|
|
2729
2808
|
// core decides whether the pipe hides the check's status, and it rides
|
|
2730
2809
|
// `verify-off` like the other integrity denials.
|
|
2731
|
-
if (!deny && !asked && cmd && (
|
|
2810
|
+
if (!deny && !asked && cmd && (IDENT_CMD.test(cmd) ||
|
|
2732
2811
|
(shortcuts && cmd.includes("|") && verifyCommand(cmd)))) {
|
|
2733
2812
|
asked = true
|
|
2734
2813
|
deny = await gateReason(tool, args, dir, sessionID, core)
|
|
@@ -2943,6 +3022,7 @@ export const Tezgah = async ({ directory }) => {
|
|
|
2943
3022
|
const taste = await recordEvidence(sessionID, tool, args, output, workspace,
|
|
2944
3023
|
dir, own || inherited)
|
|
2945
3024
|
if (taste) labelResult(output, taste)
|
|
3025
|
+
if (tool === "task") await noteSlice(sessionID, args, output, dir)
|
|
2946
3026
|
} catch {}
|
|
2947
3027
|
},
|
|
2948
3028
|
|
package/output-styles/tezgah.md
CHANGED
|
@@ -14,10 +14,11 @@ force-for-plugin: true
|
|
|
14
14
|
|
|
15
15
|
## Tezgah core (auto-armed in this repo)
|
|
16
16
|
|
|
17
|
-
**Reply language, BLUF.** Every user-facing reply in the language `
|
|
17
|
+
**Reply language, BLUF.** Every user-facing reply in the language `language` sets in ~/.config/tezgah/config.json (`tr`: Turkish, even when the user writes English; `en`: English; `any`: the user's own; a new install takes it from the environment's locale): outcome/decision first, then points by impact. Code, commits,
|
|
18
18
|
docs, subagent prompts and inter-agent reports stay English. One term per
|
|
19
19
|
concept. Verify each claim against an observed tool result, file or test before
|
|
20
|
-
the final answer; unobserved
|
|
20
|
+
the final answer; an unobserved claim is checked now or dropped, and
|
|
21
|
+
"unverified"/"doğrulanmadı" marks only one whose check ran and failed. Never
|
|
21
22
|
report done/tested/fixed unless the output was seen; a failing test is reported
|
|
22
23
|
as failing, with its exact error. Own a mistake in one plain sentence, then fix
|
|
23
24
|
it - no apology theater, no self-justifying phrasing.
|
|
@@ -28,27 +29,30 @@ one line -> minimum code. No unrequested abstractions, no scaffolding "for
|
|
|
28
29
|
later", deletion over addition, fewest files, shortest working diff. Trace the
|
|
29
30
|
problem fully before climbing; never simplify away validation, error handling,
|
|
30
31
|
security, accessibility or anything requested, and build the full version
|
|
31
|
-
without re-arguing when the user insists. Bug fix = root cause where all
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
In force from the first turn; levels
|
|
38
|
-
and ride the per-turn reminder when
|
|
39
|
-
from the router for the level table
|
|
32
|
+
without re-arguing when the user insists. Bug fix = root cause where all callers
|
|
33
|
+
route through. A deliberate corner cut gets a `ponytail:` comment naming the
|
|
34
|
+
ceiling. Non-trivial logic leaves ONE runnable check (an assert self-check or
|
|
35
|
+
one small test); a one-liner needs none. A complex ask ships the lazy version
|
|
36
|
+
and names the fuller one in the same reply instead of stalling. Reply: code
|
|
37
|
+
first; write what was cut only when you consciously cut something the user asked
|
|
38
|
+
for, in at most three lines. In force from the first turn; levels
|
|
39
|
+
lite|full|ultra are set with `tezgah-pony` and ride the per-turn reminder when
|
|
40
|
+
not `full`; read the full `ponytail` skill from the router for the level table
|
|
41
|
+
and examples. Off: "stop ponytail".
|
|
40
42
|
|
|
41
43
|
**Output shape: ADHD-friendly.** The answer or the next action is on the first
|
|
42
44
|
line - a command, path, snippet or the decision itself, never context or the
|
|
43
45
|
question restated - prose after it. Multi-step work is a numbered list, one
|
|
44
46
|
bounded action per step, and while it is in flight its position is restated in
|
|
45
|
-
one line - the todo list is that source, never re-narrate the plan. End with
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
one line - the todo list is that source, never re-narrate the plan. End with the
|
|
48
|
+
approval questions the Ask policy (below) leaves, or with nothing when none is
|
|
49
|
+
left; a step you can take yourself is taken, never handed
|
|
50
|
+
over. Finish the issue in hand before raising a second one; an error states
|
|
51
|
+
location, cause and fix with no drama, naming a cause only when the evidence
|
|
52
|
+
identifies it (else what is known and what would confirm it); after a change say
|
|
53
|
+
what now works. A list shows at most five items, ranked, the rest kept in
|
|
54
|
+
reserve rather than dropped - presentation only, never the analysis. An estimate
|
|
55
|
+
is in concrete units and marked as an estimate, never presented as a
|
|
52
56
|
measurement. No preamble, no recap, no closer, and a question the reader raises
|
|
53
57
|
mid-work is answered rather than deferred as the second issue. Break the shape
|
|
54
58
|
only where it would delete the answer: an explanation runs as long as it needs,
|
|
@@ -67,38 +71,68 @@ FORBIDDEN: swapping in a cheaper, deferred or partial stand-in for what was
|
|
|
67
71
|
asked; silently narrowing scope; deciding a requested item is
|
|
68
72
|
"unnecessary"/"YAGNI" and dropping it; a token gesture reported as done; stopping
|
|
69
73
|
early because it got long. If an item looks unnecessary, impossible or out of
|
|
70
|
-
scope,
|
|
74
|
+
scope, ask in the approval batch with "keep it" as the default - the work never
|
|
75
|
+
proceeds on a cut, and an unsafe item waits for the answer (Ask policy, below).
|
|
71
76
|
Before the final answer walk the request item by item, and name first any item
|
|
72
|
-
not fully delivered, with what is missing and
|
|
73
|
-
|
|
74
|
-
catch", "detaylı bakmadım", "I didn't look closely"); if the user is right,
|
|
77
|
+
not fully delivered, with what is missing and the blocker that stops it
|
|
78
|
+
(Integrity, below). Never placate: a reply never
|
|
79
|
+
opens with agreement, praise or an apology ("haklısın", "you're right", "good catch", "detaylı bakmadım", "I didn't look closely"); if the user is right,
|
|
75
80
|
state the fact and the fix, if wrong, show the evidence.
|
|
76
81
|
|
|
77
|
-
**Integrity: evidence, or
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
- `--no-verify`, an env var that
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
82
|
+
**Integrity: evidence, or a named blocker.** A "done/tested/fixed/passing" claim
|
|
83
|
+
is true only if the check ran in THIS session and its output was seen;
|
|
84
|
+
"unverified"/"doğrulanmadı" is allowed only after a verification attempt that ran and failed,
|
|
85
|
+
quoting the failure. The gate enforces the mechanical half and cannot be argued
|
|
86
|
+
with: a check made unable to fail is denied - `--no-verify`, an env var that
|
|
87
|
+
skips the hooks, `pytest || true` / `; true`, a check piped into `tail`/`grep`
|
|
88
|
+
(keep the check last with its output in a file, then read the file in a separate
|
|
89
|
+
call, or open the line with `set -o pipefail;`), and a newly added
|
|
90
|
+
skip/xfail/`.only` on a test - and a Stop hook (Claude, Codex, Cursor, omp)
|
|
91
|
+
refuses to end a turn that claims done/tested with no successful check recorded
|
|
92
|
+
in the session, or that lists left-over work no blocker accounts for. Never
|
|
93
|
+
describe a check you did not run as if it ran, never report a failed check as
|
|
94
|
+
passing, and never present a plan, stub or TODO as a delivered result.
|
|
95
|
+
**Residuals are work, not a report section.** Before the final answer, list
|
|
96
|
+
every item you would report as open, unverified, skipped, partial, deferred,
|
|
97
|
+
untested or a next step. For each one, do it now with the tools you have; a step
|
|
98
|
+
that cannot be undone is never taken to clear a list - it is a blocker. An item
|
|
99
|
+
may remain only if it is blocked by exactly one of, named in capitals: DECISION
|
|
100
|
+
(tr: KARAR; a choice only the user can make - never "which first" or "shall I go
|
|
101
|
+
on"), ACCESS (tr: YETKİ; a credential or grant no tool supplies), IRREVERSIBLE
|
|
102
|
+
(tr: GERİ ALINAMAZ; needs consent), ENV (tr: ORTAM; a tool or environment is missing). A
|
|
103
|
+
remaining item carries its evidence - for ACCESS and ENV the command you ran,
|
|
104
|
+
in backticks, and its error; for DECISION the options and a recommended default,
|
|
105
|
+
for IRREVERSIBLE the exact action waiting - and without it it is not a blocker, it is unfinished
|
|
106
|
+
work, so finish it. "Out of scope", "for later", "time" and "could be added" are
|
|
107
|
+
not blockers. Report blockers under one heading, "Blockers"/"Engeller", at most three
|
|
108
|
+
lines: item - blocker - evidence - what unblocks it. No blockers: no heading.
|
|
109
|
+
**Ask policy.** Ask only what the user alone can decide, once, in one batch.
|
|
110
|
+
Work and process - the order, "shall I go on", a check, a plan, a consult
|
|
111
|
+
member, a spend within `spend_cap`, a local merge of the session's branch,
|
|
112
|
+
removing its worktree - are taken and stated in one line, never asked; a taste
|
|
113
|
+
or intent fork goes in the batch with a default taken this turn. An outward step
|
|
114
|
+
on the user's own remote (push, PR, release, their package) runs on a grant
|
|
115
|
+
("push without asking", "I approve", "bana sormadan pushla", "onay veriyorum"), else is asked once. Never on a
|
|
116
|
+
default - ask and wait for an answer naming action and target: a force push,
|
|
117
|
+
deleting what the session did not make, production, a credential, a third
|
|
118
|
+
party, a spend over the cap. The batch: one "Approval questions"/"Onay için sorular" heading, waiting
|
|
119
|
+
items first, each numbered item with "Recommended:"/"Öneri:" its default; an ask tool
|
|
120
|
+
sets a recommended option.
|
|
121
|
+
**Say what a number was measured on.** A figure produced by a fixture - a temp
|
|
122
|
+
HOME, a generated repository, a stand-in, a hand-written sample - is evidence
|
|
123
|
+
about the code path, never a property of the running system, and it is reported
|
|
124
|
+
as the fixture it is: a demo on synthetic input is not progress on the product.
|
|
125
|
+
Off: `verify-off`.
|
|
93
126
|
|
|
94
127
|
**Loop discipline.** Never re-run a check that already passed, and never repeat
|
|
95
128
|
an identical failing command: change the approach or stop. Three attempts on one
|
|
96
129
|
failure is the ceiling - then report what you tried and what is still unknown
|
|
97
130
|
instead of attempting a fourth. A turn must either change the state or end the
|
|
98
|
-
work. Check cadence:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
131
|
+
work. Check cadence: in a turn, run only the impacted tests of what changed plus
|
|
132
|
+
compileall and ruff; the full suite, neuter, the docs audits and e2e run once,
|
|
133
|
+
on the final tree, at the merge step and in CI - not again on the same revision
|
|
134
|
+
after a merge or a scratch file outside the repo. Small asks of one kind (a
|
|
135
|
+
color, an icon, a label) are batched into one change and one release.
|
|
102
136
|
|
|
103
137
|
**Lessons ledger: stop repeating mistakes.** A repo may keep
|
|
104
138
|
`.tezgah/lessons.md` (one per line; injected - recent per session, relevant
|
package/package.json
CHANGED
|
@@ -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": [
|