@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +126 -1
  2. package/MANIFEST +11 -0
  3. package/README.md +6 -3
  4. package/bin/consult +13 -10
  5. package/bin/tezgah-docs +18 -16
  6. package/bin/tezgah-doctor +152 -0
  7. package/bin/tezgah-research +286 -12
  8. package/bin/tezgah-route +48 -1
  9. package/bin/tezgah-setup +27 -14
  10. package/bin/tezgah-task +771 -46
  11. package/bin/tezgah-taste +48 -10
  12. package/docs/architecture.md +7 -4
  13. package/docs/citations-baseline.json +1 -1
  14. package/docs/clarity-baseline.json +5 -5
  15. package/docs/contract.md +112 -25
  16. package/docs/evidence.md +201 -28
  17. package/docs/feature-audit.md +25 -4
  18. package/docs/gate.md +70 -33
  19. package/docs/glossary.md +4 -4
  20. package/docs/hosts.md +8 -8
  21. package/docs/index.json +30 -10
  22. package/docs/judge.md +9 -3
  23. package/docs/models.md +35 -2
  24. package/docs/operations.md +103 -3
  25. package/docs/research.md +392 -13
  26. package/docs/skills.md +24 -6
  27. package/docs/status-line.md +5 -5
  28. package/docs/testing.md +2 -2
  29. package/docs/vision.md +12 -4
  30. package/hooks/hooks.json +2 -2
  31. package/hooks/lang/en.json +323 -0
  32. package/hooks/lang/tr.json +344 -0
  33. package/hooks/projects-posttooluse.py +24 -5
  34. package/hooks/projects-pretooluse.py +9 -4
  35. package/hooks/projects-stop.py +28 -16
  36. package/hooks/tezgah_agents.py +29 -9
  37. package/hooks/tezgah_clarity.py +7 -3
  38. package/hooks/tezgah_components.py +2 -1
  39. package/hooks/tezgah_context.py +460 -390
  40. package/hooks/tezgah_gate.py +640 -104
  41. package/hooks/tezgah_integrity.py +1480 -197
  42. package/hooks/tezgah_models.py +337 -15
  43. package/hooks/tezgah_paths.py +103 -10
  44. package/hooks/tezgah_policy.py +286 -130
  45. package/hooks/tezgah_product.py +107 -0
  46. package/hooks/tezgah_rank.py +29 -27
  47. package/hooks/tezgah_replay.py +200 -30
  48. package/hooks/tezgah_research.py +1297 -12
  49. package/hooks/tezgah_skill_pick.py +58 -17
  50. package/hooks/tezgah_stopspec.py +7 -0
  51. package/hooks/tezgah_store.py +74 -27
  52. package/hooks/tezgah_task.py +917 -8
  53. package/hosts/codex/hook.py +9 -10
  54. package/hosts/cursor/hook.py +21 -14
  55. package/hosts/dsh/hooks.json +2 -2
  56. package/hosts/omp/hook.py +44 -23
  57. package/hosts/omp/tezgah-hook.ts.in +93 -7
  58. package/hosts/opencode/plugins/tezgah.js +119 -39
  59. package/output-styles/tezgah.md +76 -42
  60. package/package.json +1 -1
  61. package/skills/analyze-app/SKILL.md +57 -0
  62. package/skills/design-contract/EVALS.md +24 -1
  63. package/skills/design-contract/SKILL.md +17 -1
  64. package/skills/design-contract/data-views.md +86 -0
  65. package/skills/feature-audit/SKILL.md +32 -3
  66. package/skills/i-have-adhd/EVALS.md +22 -2
  67. package/skills/i-have-adhd/SKILL.md +12 -5
  68. package/skills/plan-add/EVALS.md +8 -8
  69. package/skills/plan-add/SKILL.md +22 -21
  70. package/skills/plan-sync/EVALS.md +2 -2
  71. package/skills/plan-sync/SKILL.md +14 -8
  72. package/skills/pm-frameworks/SKILL.md +6 -0
  73. package/skills/ponytail/EVALS.md +21 -2
  74. package/skills/ponytail/SKILL.md +4 -4
  75. package/skills/product-analysis/EVALS.md +23 -1
  76. package/skills/product-analysis/SKILL.md +50 -11
  77. package/skills/product-flow/EVALS.md +81 -0
  78. package/skills/product-flow/SKILL.md +91 -0
  79. package/skills/research/SKILL.md +99 -2
  80. package/skills/tezgah-contract/EVALS.md +52 -7
  81. 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 record's shell route - a bash command that would move the phase or
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 fix - a credential, a login, " +
253
- "an access grant - so stop and ask the user for it rather than change " +
254
- "the approach",
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 TASK_CLI is: a false hit costs one spawn whose answer comes back empty.
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
- // Two rules with a shell route, asked of the core in one spawn (TASK_CLI
2713
- // and IDENT_CMD carry which commands are worth asking about, and why
2714
- // their pre-tests are loose): a command that names the task CLI may be
2715
- // moving the phase or the allowlist this session is held to, and a
2716
- // command that creates an identifier may be putting a branch, a commit
2717
- // subject or a PR/issue title into the repository's history for good.
2718
- // Both refusals are the core's own text - the same ones a write to the
2719
- // record and a non-English artifact get - and this file keeps neither
2720
- // rule. The core masks the command, so one that only mentions the CLI
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 third: 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 && (TASK_CLI.test(cmd) || IDENT_CMD.test(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
 
@@ -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 `reply_lang` sets in ~/.config/tezgah/config.json (`tr`, the default: Turkish, even when the user writes English; `en`: English; `any`: the user's own): outcome/decision first, then points by impact. Code, commits,
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 claims are dropped or marked "doğrulanmadı". Never
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
- callers route through. A deliberate corner cut gets a `ponytail:` comment
33
- naming the ceiling. Non-trivial logic leaves ONE runnable check (an assert
34
- self-check or one small test); a one-liner needs none. A complex ask ships the
35
- lazy version and names the fuller one in the same reply instead of stalling.
36
- Reply: code first, then at most three lines - what was skipped, when to add it.
37
- In force from the first turn; levels lite|full|ultra are set with `tezgah-pony`
38
- and ride the per-turn reminder when not `full`; read the full `ponytail` skill
39
- from the router for the level table and examples. Off: "stop ponytail".
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 one
46
- concrete next step. Finish the issue in hand before raising a second one; an
47
- error states location, cause and fix with no drama, naming a cause only when the
48
- evidence identifies it (else what is known and what would confirm it); after a
49
- change say what now works. A list shows at most five items, ranked, the rest
50
- kept in reserve rather than dropped - presentation only, never the analysis. An
51
- estimate is in concrete units and marked as an estimate, never presented as a
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, STOP and ask - with a recommended default - never decide it yourself.
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 why. Never placate: a reply never
73
- opens with agreement, praise or an apology ("haklısın", "you're right", "good
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 "doğrulanmadı".** A "done/tested/fixed/passing"
78
- claim is true only if the check ran in THIS session and its output was seen;
79
- otherwise mark it "doğrulanmadı" instead of asserting it. The gate enforces the
80
- mechanical half and cannot be argued with: a check made unable to fail is denied
81
- - `--no-verify`, an env var that skips the hooks, `pytest || true` / `; true`,
82
- a check piped into `tail`/`grep` (keep the check last with its output in a file,
83
- then read the file in a separate call, or open the line with `set -o pipefail;`),
84
- and a newly added skip/xfail/`.only` on a test - and a Stop hook (Claude,
85
- Codex, Cursor, omp) refuses to end a turn that claims done/tested with no successful
86
- check recorded in the session. Never describe a check you did not run as if it
87
- ran, never report a failed check as passing, and never present a plan, stub or
88
- TODO as a delivered result. **Say what a number was measured on.** A figure
89
- produced by a fixture - a temp HOME, a generated repository, a stand-in, a
90
- hand-written sample - is evidence about the code path, never a property of the
91
- running system, and it is reported as the fixture it is: a demo on synthetic
92
- input is not progress on the product. Off: `verify-off`.
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: while working, run only the tests of what changed; run the
99
- full suite once, on the final tree, right before it ships - not again on the
100
- same revision after a merge or a scratch file outside the repo. Small asks of
101
- one kind (a color, an icon, a label) are batched into one change and one release.
131
+ work. Check cadence: in a turn, run only the impacted tests of what changed plus
132
+ compileall and ruff; the full suite, neuter, the docs audits and e2e run once,
133
+ on the final tree, at the merge step and in CI - not again on the same revision
134
+ after a merge or a scratch file outside the repo. Small asks of one kind (a
135
+ color, an icon, a label) are batched into one change and one release.
102
136
 
103
137
  **Lessons ledger: stop repeating mistakes.** A repo may keep
104
138
  `.tezgah/lessons.md` (one per line; injected - recent per session, relevant
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r1z4x/tezgah",
3
- "version": "1.5.1",
3
+ "version": "1.7.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": {
@@ -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
- Three inputs a session can be handed where this skill could misfire, and what a
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": [