crewforth 0.0.1 → 3.0.0-rc.1

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 (168) hide show
  1. package/CHANGELOG.md +3784 -0
  2. package/LICENSE +21 -0
  3. package/README.md +37 -7
  4. package/VERSION +1 -0
  5. package/adopt.sh +1670 -0
  6. package/bin/cli.js +497 -0
  7. package/kit/AGENT_TEMPLATE.md +92 -0
  8. package/kit/CLAUDE.md +228 -0
  9. package/kit/README.md +107 -0
  10. package/kit/agents/crew-backend-expert.md +95 -0
  11. package/kit/agents/crew-commit-agent.md +96 -0
  12. package/kit/agents/crew-database-expert.md +84 -0
  13. package/kit/agents/crew-devops-expert.md +82 -0
  14. package/kit/agents/crew-frontend-expert.md +85 -0
  15. package/kit/agents/crew-performance-expert.md +78 -0
  16. package/kit/agents/crew-planner.md +60 -0
  17. package/kit/agents/crew-privacy-agent.md +71 -0
  18. package/kit/agents/crew-review-agent.md +95 -0
  19. package/kit/agents/crew-security-expert.md +73 -0
  20. package/kit/agents/crew-session-manager.md +77 -0
  21. package/kit/agents/crew-test-expert.md +64 -0
  22. package/kit/eval/doctor.sh +465 -0
  23. package/kit/eval/gate-report.sh +137 -0
  24. package/kit/eval/golden-holdout.txt +53 -0
  25. package/kit/eval/golden-routing.txt +199 -0
  26. package/kit/eval/lib/crew-env.sh +14 -0
  27. package/kit/eval/lib/settings-json.awk +154 -0
  28. package/kit/eval/lib/star.sh +70 -0
  29. package/kit/eval/parser-conformance.sh +612 -0
  30. package/kit/eval/preflight.sh +186 -0
  31. package/kit/eval/routing-eval.sh +209 -0
  32. package/kit/eval/scan-skill.sh +162 -0
  33. package/kit/eval/smoke-test.sh +6693 -0
  34. package/kit/eval/update-guard.sh +81 -0
  35. package/kit/eval/utilization.sh +130 -0
  36. package/kit/hooks/board-sync.sh +106 -0
  37. package/kit/hooks/board.sh +962 -0
  38. package/kit/hooks/commit-msg +49 -0
  39. package/kit/hooks/context-usage.sh +353 -0
  40. package/kit/hooks/floor-blocklist.txt +124 -0
  41. package/kit/hooks/guard-bash.sh +1431 -0
  42. package/kit/hooks/guard-commit-scan.sh +511 -0
  43. package/kit/hooks/guard-write.sh +513 -0
  44. package/kit/hooks/pre-commit +577 -0
  45. package/kit/hooks/route-hint.sh +185 -0
  46. package/kit/hooks/secret-blocklist.txt +82 -0
  47. package/kit/hooks/session-guard.sh +149 -0
  48. package/kit/hooks/session-rehydrate.sh +41 -0
  49. package/kit/hooks/session-stats.sh +252 -0
  50. package/kit/hooks/session-update-check.sh +236 -0
  51. package/kit/hooks/skill-trust.sh +150 -0
  52. package/kit/hooks/trace-blocklist.txt +53 -0
  53. package/kit/settings.json +132 -0
  54. package/kit/skills/a11y/SKILL.md +44 -0
  55. package/kit/skills/adr/SKILL.md +74 -0
  56. package/kit/skills/api-design/SKILL.md +50 -0
  57. package/kit/skills/automode-policy/SKILL.md +91 -0
  58. package/kit/skills/automode-policy/references/policy.json +13 -0
  59. package/kit/skills/automode-policy/scripts/apply.sh +84 -0
  60. package/kit/skills/automode-policy/scripts/check.sh +102 -0
  61. package/kit/skills/backend-architecture/SKILL.md +103 -0
  62. package/kit/skills/brainstorm/SKILL.md +47 -0
  63. package/kit/skills/ci-pipeline/SKILL.md +38 -0
  64. package/kit/skills/commit-message/SKILL.md +82 -0
  65. package/kit/skills/commit-message/references/examples.md +8 -0
  66. package/kit/skills/confidence-check/SKILL.md +86 -0
  67. package/kit/skills/crew-board/SKILL.md +55 -0
  68. package/kit/skills/crew-brainstorm/SKILL.md +19 -0
  69. package/kit/skills/crew-code-review/SKILL.md +151 -0
  70. package/kit/skills/crew-code-review/references/history-search.md +19 -0
  71. package/kit/skills/crew-code-review/references/panel-mode.md +32 -0
  72. package/kit/skills/crew-code-review/references/receiving.md +23 -0
  73. package/kit/skills/crew-doctor/SKILL.md +36 -0
  74. package/kit/skills/crew-gates/SKILL.md +25 -0
  75. package/kit/skills/crew-handoff/SKILL.md +14 -0
  76. package/kit/skills/crew-plan/SKILL.md +18 -0
  77. package/kit/skills/crew-review/SKILL.md +23 -0
  78. package/kit/skills/crew-ship/SKILL.md +19 -0
  79. package/kit/skills/crew-skill/SKILL.md +61 -0
  80. package/kit/skills/crew-studio/SKILL.md +129 -0
  81. package/kit/skills/crew-update/SKILL.md +43 -0
  82. package/kit/skills/db-migration/SKILL.md +120 -0
  83. package/kit/skills/db-migration/references/backup-restore.md +33 -0
  84. package/kit/skills/db-migration/references/raw-sql.md +30 -0
  85. package/kit/skills/db-migration/references/tool-matrix.md +35 -0
  86. package/kit/skills/dependency-audit/SKILL.md +61 -0
  87. package/kit/skills/dependency-upgrade/SKILL.md +82 -0
  88. package/kit/skills/deploy/SKILL.md +100 -0
  89. package/kit/skills/deploy/references/proxy-ssl.md +56 -0
  90. package/kit/skills/deploy/references/rollback.md +18 -0
  91. package/kit/skills/deploy/references/scripts.md +9 -0
  92. package/kit/skills/deploy/references/topology-a.md +128 -0
  93. package/kit/skills/deploy/references/topology-b.md +44 -0
  94. package/kit/skills/docs-writer/SKILL.md +53 -0
  95. package/kit/skills/docs-writer/references/coauthoring.md +34 -0
  96. package/kit/skills/eval-grader/SKILL.md +75 -0
  97. package/kit/skills/eval-grader/references/method.md +51 -0
  98. package/kit/skills/frontend/SKILL.md +56 -0
  99. package/kit/skills/frontend/references/verify-contract.md +41 -0
  100. package/kit/skills/frontend-design/SKILL.md +71 -0
  101. package/kit/skills/frontend-design/references/design-review.md +34 -0
  102. package/kit/skills/frontend-rn-expo/SKILL.md +25 -0
  103. package/kit/skills/handoff/SKILL.md +71 -0
  104. package/kit/skills/handoff/references/cold-reader.md +51 -0
  105. package/kit/skills/i18n-integrity/SKILL.md +31 -0
  106. package/kit/skills/incident-runbook/SKILL.md +40 -0
  107. package/kit/skills/incident-runbook/references/postmortem.md +16 -0
  108. package/kit/skills/iterate/SKILL.md +40 -0
  109. package/kit/skills/mcp-builder/SKILL.md +54 -0
  110. package/kit/skills/mcp-builder/references/building.md +71 -0
  111. package/kit/skills/observability/SKILL.md +50 -0
  112. package/kit/skills/performance/SKILL.md +38 -0
  113. package/kit/skills/performance/references/bottlenecks.md +10 -0
  114. package/kit/skills/privacy-compliance/SKILL.md +75 -0
  115. package/kit/skills/red-team/SKILL.md +48 -0
  116. package/kit/skills/reflect/SKILL.md +57 -0
  117. package/kit/skills/release/SKILL.md +44 -0
  118. package/kit/skills/security-scan/SKILL.md +99 -0
  119. package/kit/skills/security-scan/references/ai-agents.md +107 -0
  120. package/kit/skills/security-scan/references/fronts.md +99 -0
  121. package/kit/skills/security-scan/references/prompting.md +23 -0
  122. package/kit/skills/security-scan/references/reporting.md +75 -0
  123. package/kit/skills/security-scan/references/verify.md +131 -0
  124. package/kit/skills/sonarqube-check/SKILL.md +108 -0
  125. package/kit/skills/sonarqube-check/references/local-engines.md +32 -0
  126. package/kit/skills/sonarqube-check/references/stand-up-server.md +25 -0
  127. package/kit/skills/spec-planning/SKILL.md +92 -0
  128. package/kit/skills/systematic-debugging/SKILL.md +57 -0
  129. package/kit/skills/systematic-debugging/references/techniques.md +44 -0
  130. package/kit/skills/teamboard/SKILL.md +88 -0
  131. package/kit/skills/teamboard/references/setup.md +37 -0
  132. package/kit/skills/testing/SKILL.md +49 -0
  133. package/kit/skills/testing/references/flaky-triage.md +51 -0
  134. package/kit/skills/threat-model/SKILL.md +57 -0
  135. package/kit/skills/threat-model/references/interview.md +51 -0
  136. package/kit/skills/threat-model/references/schema.md +42 -0
  137. package/kit/skills/token-budget/SKILL.md +51 -0
  138. package/kit/skills/trace-scan/SKILL.md +43 -0
  139. package/kit/skills/worktree/SKILL.md +40 -0
  140. package/kit/studio/README.md +266 -0
  141. package/kit/studio/ensure-node.sh +471 -0
  142. package/kit/studio/package.json +14 -0
  143. package/kit/studio/server/hooks/studio-gate.sh +169 -0
  144. package/kit/studio/server/index.js +902 -0
  145. package/kit/studio/server/lib/crew-env.js +11 -0
  146. package/kit/studio/server/lib/fleet.js +146 -0
  147. package/kit/studio/server/lib/graph.js +533 -0
  148. package/kit/studio/server/lib/instance.js +135 -0
  149. package/kit/studio/server/lib/kit-telemetry.js +182 -0
  150. package/kit/studio/server/lib/kit.js +158 -0
  151. package/kit/studio/server/lib/palette.js +118 -0
  152. package/kit/studio/server/lib/peers.js +72 -0
  153. package/kit/studio/server/lib/permissions.js +150 -0
  154. package/kit/studio/server/lib/projects.js +294 -0
  155. package/kit/studio/server/lib/roster.js +191 -0
  156. package/kit/studio/server/lib/session.js +309 -0
  157. package/kit/studio/server/lib/terminal.js +91 -0
  158. package/kit/studio/server/lib/transcript.js +100 -0
  159. package/kit/studio/web/app.js +1098 -0
  160. package/kit/studio/web/canvas.js +1179 -0
  161. package/kit/studio/web/chat.js +543 -0
  162. package/kit/studio/web/index.html +86 -0
  163. package/kit/studio/web/md.js +110 -0
  164. package/kit/studio/web/storage-migrate.js +23 -0
  165. package/kit/studio/web/style.css +1453 -0
  166. package/package.json +46 -8
  167. package/start.sh +748 -0
  168. package/bin/crewforth.js +0 -10
@@ -0,0 +1,612 @@
1
+ #!/usr/bin/env bash
2
+ # PARSER CONFORMANCE — the gate's payload reader must agree with a real JSON parser.
3
+ #
4
+ # WHY THIS FILE EXISTS. guard-bash.sh read its payload through a ladder: jq, then python3, then a pure-bash
5
+ # slice. Three parsers meant three behaviours, and every parser incident Crewforth has had came from the tiers
6
+ # DIVERGING rather than from any one of them being weak:
7
+ # * tier 3 was once `CMD="$INPUT"`, so every `git push` was hard-blocked as a force-push on any session id
8
+ # containing `-f8`. CI never saw it, because CI has jq.
9
+ # * Windows ships a python3 Store stub that satisfies `command -v` and cannot run, so stock desktops took
10
+ # tier 2 and FAILED OPEN.
11
+ # * a Windows-native jq writes stdout in TEXT mode, so tier 1 received `\` + CR + CR + LF where tier 3
12
+ # received JSON's two-character escapes, and an ordinary commit was refused on one platform only.
13
+ # Each was found by a single assertion going red long after the change that caused it.
14
+ #
15
+ # TWO MODES, AND THE FILE CHOOSES BETWEEN THEM BY LOOKING AT THE HOOK.
16
+ # LADDER PRESENT — the payload can still be read two ways, so the property is "both tiers reach the same
17
+ # VERDICT". Exit codes are compared, not bytes: the tiers legitimately hand the rules different bytes and a
18
+ # byte comparison would report those as defects and bury the one that is not.
19
+ # LADDER ABSENT — there is only one reader in production, so a tier comparison would be the slice against
20
+ # itself. The property becomes "the slice DECODES what a real parser decodes", measured against a reference
21
+ # that never goes through the hook at all, plus "the gate still reaches the verdict each row declares".
22
+ # The DECODE check runs in BOTH modes on purpose. It costs nothing while the ladder is there and it means the
23
+ # normalisations below are already pinned by the time the ladder goes, rather than being written the same day
24
+ # they become the only thing standing.
25
+ #
26
+ # THE MODE DETECTOR IS ITSELF CALIBRATED, IN BOTH DIRECTIONS. It is the one place where this whole file can go
27
+ # quiet: a detector that wrongly says "ladder absent" against a ladder-ful hook stops exercising tier selection
28
+ # and says nothing about it. So two fixtures are built and the detector must answer each correctly before any
29
+ # mode runs.
30
+ #
31
+ # THE DECODE RULES ARE TAKEN FROM THE HOOK'S CASE ARMS, NOT FROM A MEASUREMENT. Pinning whatever the slice
32
+ # happens to do today would make the pin agree with any future change. _json_unescape maps:
33
+ # \n -> LF \t -> TAB \r -> DROPPED \b and \f -> one space
34
+ # \uXXXX matching 00[2-7][0-9a-fA-F] (printable ASCII) -> that character
35
+ # any other \uXXXX -> a single `?` \<anything else> -> that character literally
36
+ # A row whose expected decode is `=` must match the reference EXACTLY; a row that exercises one of those
37
+ # normalisations carries the literal it must produce, so each normalisation is a stated fact and not an excuse.
38
+ #
39
+ # FIXTURES ARE BYTE-CHECKED BEFORE THEY ARE TRUSTED. A corpus of escapes is the easiest thing in this repo to
40
+ # measure vacuously: write `\\u002e` and the payload carries an escaped BACKSLASH followed by the letters
41
+ # u002e — inert text every reader agrees about while proving nothing; write it through a shell that expands
42
+ # escapes and the payload carries a real `.` and no parser ever sees an escape. Both shapes happened while this
43
+ # corpus was being built, on two different machines. So every escape row declares the escape it needs, the
44
+ # payload's bytes are searched for it, and a row whose fixture came out inert is reported UNMEASURED, never as
45
+ # a pass. Every backslash in a fixture is produced at RUN TIME by printf rather than written literally here,
46
+ # because a literal one has to survive every layer between an author and this file and in this repo it has not.
47
+ #
48
+ # Exit 0 = everything agreed. Exit 1 = a divergence, a broken fixture, or a calibration that did not answer.
49
+ # Exit 3 = nothing could be compared on this machine (no hook, or no reference parser). Never a pass.
50
+
51
+ set -u
52
+ LC_ALL=C
53
+
54
+ HERE="$(cd "$(dirname "$0")" && pwd)"
55
+ HOOK="$HERE/../hooks/guard-bash.sh"
56
+ [ -f "$HOOK" ] || HOOK="$HERE/../../kit/hooks/guard-bash.sh"
57
+ if [ ! -f "$HOOK" ]; then
58
+ # Not a divergence and not a broken fixture — there was nothing to measure at all, which is what rc 3 means
59
+ # everywhere else in this file. Exiting 1 here would report "the readers disagree" for a file that was merely
60
+ # copied somewhere else, and a caller that maps 1 to "gate failed" would chase a bug that does not exist.
61
+ echo "parser-conformance: guard-bash.sh bulunamadı ($HOOK) — HİÇBİR ŞEY ÖLÇÜLMEDİ, bu bir geçiş değildir"
62
+ exit 3
63
+ fi
64
+
65
+ PASS=0; FAILED=0; UNMEASURED=0; KNOWN_OPEN=0
66
+
67
+ # AN ASSERTION THAT RAN IN A SUBSHELL IS INVISIBLE, and this file is as exposed to it as any other. `ok`/`bad`
68
+ # move shell VARIABLES, so inside `( … )` or a pipeline they move a copy in a child: the row prints, the total
69
+ # does not, and a `bad` there cannot fail the run. Colour cannot find it — only the DELTA can.
70
+ # So every assertion also appends the counter's value to a file. A file survives a subshell; a variable does
71
+ # not. Walking that log afterwards, each line must show the counter one higher than the line before, and any
72
+ # line that repeats means the assertion BEFORE it was lost. The append is a redirect, not a process.
73
+ # `$BASHPID != $$` is shorter and exact, and is not used: it does not exist on bash 3.2, which is what macOS
74
+ # runs, so it would detect nothing on one of the three supported platforms while looking like it worked.
75
+ # The label is stripped of line endings first — a decoded value can contain a real LF, which would split one
76
+ # log line into two and manufacture a repeat that is the harness's and not the suite's.
77
+ ASSERTLOG="$(mktemp)"
78
+ ok(){ PASS=$((PASS+1)); _al "OK $1"; printf ' \033[32mOK\033[0m %s\n' "$1"; }
79
+ bad(){ FAILED=$((FAILED+1)); _al "BAD $1"; printf ' \033[31mFAIL\033[0m %s\n' "$1"; }
80
+ unm(){ UNMEASURED=$((UNMEASURED+1)); _al "UNM $1"; printf ' \033[33m????\033[0m %s\n' "$1"; }
81
+ note(){ printf ' · %s\n' "$1"; }
82
+ _al(){ local l="${1//$'\n'/ }"; l="${l//$'\r'/ }"; printf '%s\t%s\n' "$((PASS+FAILED+UNMEASURED))" "$l" >> "$ASSERTLOG"; }
83
+
84
+ # THE RULE: the PARENT's logged values are exactly total, total-1, … 1, so walk the log BACKWARD expecting
85
+ # that chain. A line on the chain is a parent line; every line that is not was executed in a child.
86
+ #
87
+ # The first version used "a repeat at N means N-1 was lost", which is only true when each loss sits in its OWN
88
+ # subshell. Several assertions inside ONE `( … )` — the shape that actually occurred in this repo, five rows
89
+ # at once — make the counter ADVANCE inside the child (P+1, P+2, P+3) and then DROP when the parent resumes;
90
+ # that rule saw one drop and named one row, missing the rest. A synthetic fixture had modelled separate
91
+ # subshells, so it agreed with itself. Running the real shape is what showed the difference.
92
+ # The backward walk needs no special case for runs, for separate subshells, or for a loss at the very end.
93
+ _analyse(){ # $1 log, $2 visible total -> prints offenders in file order, then HITS=n
94
+ awk -F'\t' -v final="$2" '
95
+ { n[NR]=$1; lab[NR]=$2 }
96
+ END { e=final+0; h=0
97
+ for (i=NR; i>=1; i--) { if (n[i]+0 == e) e--; else out[++h]=lab[i] }
98
+ for (j=h; j>=1; j--) printf " >> %s\n", out[j]
99
+ printf "HITS=%d\n", h }' "$1"; }
100
+
101
+ # Calibrated on a SYNTHETIC log before it is trusted, because a detector that quietly stopped working would
102
+ # report "0 findings" — a clean bill of health, and the same failure it exists to catch. The fixture carries
103
+ # the shape that broke the previous rule (a RUN inside one subshell) plus a loss at the very end, and a clean
104
+ # tail that must not be accused.
105
+ _cal="$(mktemp)"
106
+ printf '1\tp1\n2\tp2\n3\tRUN-1\n4\tRUN-2\n5\tRUN-3\n3\tp3\n4\tp4\n5\tSON\n' > "$_cal"
107
+ _co="$(_analyse "$_cal" 4)"
108
+ if [ "$(printf '%s' "$_co" | sed -n 's/^HITS=//p')" = 4 ] \
109
+ && printf '%s' "$_co" | grep -q 'RUN-1' && printf '%s' "$_co" | grep -q 'RUN-2' \
110
+ && printf '%s' "$_co" | grep -q 'RUN-3' && printf '%s' "$_co" | grep -q 'SON' \
111
+ && ! printf '%s' "$_co" | grep -q 'p[1-4]'; then
112
+ _ALZ=1
113
+ else
114
+ _ALZ=0; printf ' \033[33m????\033[0m %s\n' "alt-kabuk dedektörü kendi kalibrasyonunu geçemedi — bu koşuda iddia kaybı ARANMAYACAK"
115
+ fi
116
+ rm -f "$_cal"
117
+
118
+ W="$(mktemp -d)"
119
+ # ASSERTLOG is created above, outside $W, so the trap names it too: every early exit (no reference parser,
120
+ # no extractable slice) used to leave one temp file behind per run.
121
+ trap 'cd /; rm -rf "$W" "$ASSERTLOG"' EXIT INT TERM
122
+ P="$W/payload.json"
123
+ BS="$(printf '\\')" # one real backslash, never written literally into this file
124
+
125
+ # ---------------------------------------------------------------------------------------------------------
126
+ # THE MODE DETECTOR, and its calibration in both directions.
127
+ # ---------------------------------------------------------------------------------------------------------
128
+ has_ladder(){ grep -q 'command -v jq' "$1"; }
129
+
130
+ mkdir -p "$W/det"
131
+ printf '%s\n' '#!/usr/bin/env bash' 'CMD=""' \
132
+ 'if command -v jq >/dev/null 2>&1 && CMD="$(jq -r .x)"; then :; fi' > "$W/det/with.sh"
133
+ printf '%s\n' '#!/usr/bin/env bash' 'CMD=""' 'CMD="$(_json_slice "$INPUT" command)"' > "$W/det/without.sh"
134
+
135
+ echo "== mod seçici, iki yönde de kalibre ediliyor =="
136
+ DETOK=1
137
+ if has_ladder "$W/det/with.sh"; then ok "merdivenli bir kapıda seçici 'merdiven VAR' diyor"
138
+ else bad "seçici merdivenli kapıyı kaçırdı — kip seçimi güvenilmez"; DETOK=0; fi
139
+ if has_ladder "$W/det/without.sh"; then bad "seçici merdivensiz kapıda 'VAR' dedi — sessizleşmenin tersi, yine de yanlış"; DETOK=0
140
+ else ok "merdivensiz bir kapıda seçici 'merdiven YOK' diyor"; fi
141
+ [ "$DETOK" = 1 ] || { echo; echo "SEÇİCİ KALİBRASYONU BAŞARISIZ — hangi kipin koşacağı bilinemez, hiçbir şey raporlanmıyor"; exit 1; }
142
+
143
+ LADDER=0; has_ladder "$HOOK" && LADDER=1
144
+ note "kapıda okuma merdiveni: $( [ "$LADDER" = 1 ] && echo 'VAR — iki katman karşılaştırılacak' || echo 'YOK — tek okuyucu, referansa karşı çözüm karşılaştırılacak' )"
145
+ echo
146
+
147
+ # ---------------------------------------------------------------------------------------------------------
148
+ # A REFERENCE PARSER. Real jq is preferred; otherwise a shim named `jq` is placed on PATH so the hook can still
149
+ # take its tier-1 branch while the ladder exists. The shim writes in binary: the Windows text-mode flavour is a
150
+ # separate, already-fixed defect and not what this file measures. perl comes before python3 because Git Bash
151
+ # ships perl with JSON::PP in core, while the python3 on PATH there is a Store stub that cannot run.
152
+ # ---------------------------------------------------------------------------------------------------------
153
+ REFKIND=""
154
+ if command -v jq >/dev/null 2>&1 && printf '{"a":"b"}' | jq -r '.a' 2>/dev/null | grep -qx b; then REFKIND=jq
155
+ elif command -v perl >/dev/null 2>&1 && printf '{"a":"b"}' | perl -MJSON::PP -e 'local $/; print decode_json(<STDIN>)->{a}' 2>/dev/null | grep -qx b; then REFKIND=perl
156
+ elif command -v python3 >/dev/null 2>&1 && printf '{"a":"b"}' | python3 -c 'import sys,json;sys.stdout.write(json.load(sys.stdin)["a"])' 2>/dev/null | grep -qx b; then REFKIND=python3
157
+ elif command -v node >/dev/null 2>&1 && printf '{"a":"b"}' | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(JSON.parse(s).a))' 2>/dev/null | grep -qx b; then REFKIND=node
158
+ fi
159
+ if [ -z "$REFKIND" ]; then
160
+ echo " bu makinede JSON çözebilen ikinci bir araç yok (jq, perl, python3, node)."
161
+ echo " HİÇBİR ŞEY KARŞILAŞTIRILMADI. Bu bir geçiş değildir."
162
+ exit 3
163
+ fi
164
+ note "referans çözücü: $REFKIND"
165
+
166
+ # THE REFERENCE'S TRANSPORT IS PART OF THE INSTRUMENT, and it was the one thing here taken on faith. Every
167
+ # fixture is byte-checked and the reader's decode is compared byte-exact, but what the reference HANDED BACK
168
+ # was never calibrated. A Windows-native jq opens stdout in TEXT mode and turns every LF it writes into CRLF,
169
+ # so the one row whose decoded value contains an LF diverges by a byte the READER never had — an instrument
170
+ # fault reported as a product divergence, in the file whose whole purpose is to refuse exactly that.
171
+ #
172
+ # The fix is not to normalise the difference away: a CR that a decode genuinely produced and a CR the pipe
173
+ # added look identical after normalisation, so stripping them would hide a real divergence to spare a fake one.
174
+ # Instead the reference is asked for BASE64, which is a single line with no embedded newline, so the platform's
175
+ # line-ending translation has nothing to act on and is out of the path for every reference equally. bash does
176
+ # the decoding, and both sides of the comparison are captured the same way so `$( )` treats them alike.
177
+ B64=0
178
+ if command -v base64 >/dev/null 2>&1 && [ "$(printf 'a\nb' | base64 | tr -d '\n' | base64 -d 2>/dev/null | od -An -tx1 | tr -d ' \n')" = "610a62" ]; then B64=1; fi
179
+
180
+ ref_encode(){ # the reference's answer for .tool_input.command, base64 on one line
181
+ case "$REFKIND" in
182
+ jq) jq -r '.tool_input.command // empty | @base64' < "$P" 2>/dev/null ;;
183
+ # Encode::encode first: JSON::PP hands back a CHARACTER string and encode_base64 wants octets, so an
184
+ # astral character made it return nothing at all. That failure was silent in the worst way — the
185
+ # known-divergence calibration compares the reference against the slice, and empty differs from the
186
+ # slice, so the row passed while the reference was carrying nothing.
187
+ perl) perl -MJSON::PP -MMIME::Base64 -MEncode -e 'local $/; my $j=decode_json(<STDIN>); print encode_base64(Encode::encode("UTF-8", ($j->{tool_input}{command} // "")), "");' < "$P" 2>/dev/null ;;
188
+ python3) python3 -c 'import sys,json,base64;sys.stdout.write(base64.b64encode(json.load(sys.stdin).get("tool_input",{}).get("command","").encode()).decode())' < "$P" 2>/dev/null ;;
189
+ node) node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{let o;try{o=JSON.parse(s)}catch(e){process.exit(0)}process.stdout.write(Buffer.from(((o.tool_input||{}).command)||"").toString("base64"))})' < "$P" 2>/dev/null ;;
190
+ esac; }
191
+
192
+ ref_raw(){ # the direct read, kept as the fallback when nothing can decode base64
193
+ case "$REFKIND" in
194
+ jq) jq -r '.tool_input.command // empty' < "$P" 2>/dev/null ;;
195
+ perl) perl -MJSON::PP -e 'local $/; my $j=decode_json(<STDIN>); binmode(STDOUT); print(($j->{tool_input}{command} // ""));' < "$P" 2>/dev/null ;;
196
+ python3) python3 -c 'import sys,json;sys.stdout.write(json.load(sys.stdin).get("tool_input",{}).get("command",""))' < "$P" 2>/dev/null ;;
197
+ node) node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{let o;try{o=JSON.parse(s)}catch(e){process.exit(0)}process.stdout.write(((o.tool_input||{}).command)||"")})' < "$P" 2>/dev/null ;;
198
+ esac; }
199
+
200
+ ref_decode(){ if [ "$B64" = 1 ]; then ref_encode | tr -d '\r\n' | base64 -d 2>/dev/null; else ref_raw; fi; }
201
+
202
+ SHIM="$W/shim"; mkdir -p "$SHIM"; T1PATH=""
203
+ if [ "$REFKIND" != jq ]; then
204
+ case "$REFKIND" in
205
+ perl) RUN='perl -MJSON::PP -e '\''local $/; my $j=decode_json(<STDIN>); binmode(STDOUT); my $k=$ENV{K}; print(($k eq "cmd" ? ($j->{tool_input}{command} // "") : ($j->{permission_mode} // "")), "\n");'\''' ;;
206
+ python3) RUN='python3 -c '\''import sys,os,json;d=json.load(sys.stdin);k=os.environ["K"];sys.stdout.write((d.get("tool_input",{}).get("command","") if k=="cmd" else d.get("permission_mode",""))+"\n")'\''' ;;
207
+ node) RUN='node -e '\''let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{let o;try{o=JSON.parse(s)}catch(e){process.exit(5)}const k=process.env.K;process.stdout.write((k==="cmd"?((o.tool_input||{}).command||""):(o.permission_mode||""))+"\n")})'\''' ;;
208
+ esac
209
+ { echo '#!/usr/bin/env bash'
210
+ echo 'case "$2" in'
211
+ echo " '.tool_input.command // empty') K=cmd ;;"
212
+ echo " '.permission_mode // empty') K=pm ;;"
213
+ echo ' *) exit 3 ;;'
214
+ echo 'esac'
215
+ echo "export K; $RUN"; } > "$SHIM/jq"
216
+ chmod +x "$SHIM/jq"; T1PATH="$SHIM"
217
+ fi
218
+
219
+ # ---------------------------------------------------------------------------------------------------------
220
+ # THE SLICE, EXTRACTED. This is what production reads with once the ladder is gone, so it is compared against
221
+ # the reference directly rather than through the hook — a comparison routed through the hook would be the
222
+ # slice against itself the moment the ladder disappears.
223
+ # ---------------------------------------------------------------------------------------------------------
224
+ { echo '#!/usr/bin/env bash'; echo 'INPUT="$(cat)"'
225
+ awk '/^_json_slice\(\)/{f=1} f{print} f&&/^}$/{exit}' "$HOOK"
226
+ awk '/^_json_unescape\(\)/{f=1} f{print} f&&/^}$/{exit}' "$HOOK"
227
+ echo '_json_unescape "$(_json_slice "$INPUT" command)"'; } > "$W/dec.sh"
228
+ cp "$W/dec.sh" "$W/dec.good.sh"
229
+ if ! bash -n "$W/dec.sh" 2>/dev/null; then
230
+ echo " _json_slice / _json_unescape kapıdan çıkarılamadı — çözüm karşılaştırması yapılamaz"; exit 3; fi
231
+ slice_decode(){ bash "$W/dec.sh" < "$P" 2>/dev/null; }
232
+
233
+ # ---------------------------------------------------------------------------------------------------------
234
+ # A REPO FOR THE GATE TO JUDGE. §4.6 reads a review record, so without one every commit blocks for a reason
235
+ # that has nothing to do with parsing and the corpus would compare two identical refusals.
236
+ # ---------------------------------------------------------------------------------------------------------
237
+ R="$W/repo"; mkdir -p "$R"
238
+ ( cd "$R" && git init -q . && git config user.email t@example.com && git config user.name t \
239
+ && echo one > a.txt && git add a.txt && git commit -qm init && echo two >> a.txt && git add a.txt \
240
+ && mkdir -p .claude ) >/dev/null 2>&1
241
+ ( cd "$R" && printf '{"diff_oid":"%s","head":"%s","ts":"t"}\n' \
242
+ "$(git diff --cached | git hash-object --stdin)" "$(git rev-parse --verify --quiet HEAD)" \
243
+ > .claude/review-pass.json )
244
+
245
+ # THE PURE-BASH COLUMN HAS TO BE PURE-BASH ON EVERY MACHINE, not only on one that happens to lack jq. Run on
246
+ # the ambient PATH, `t3` is whatever rung the machine can reach — so on a jq box the "tier 3" column IS rung
247
+ # one, both columns mean the same thing, and a ladder-split row records `t3=0/t1=0` and reports that the
248
+ # divergence moved. That is the same defect as a row that only checks rc: the measurement's meaning depended on
249
+ # the environment rather than on what it claimed to measure. The interpreters are shadowed with stubs that
250
+ # EXIST and FAIL, which is the shape a stock Windows desktop already has (the Store python3), so the fallthrough
251
+ # being exercised is the real one and not "the binary is absent".
252
+ NOINT="$W/noint"; mkdir -p "$NOINT"
253
+ for _i in jq python3 python perl node; do
254
+ printf '%s\n' '#!/usr/bin/env bash' 'exit 1' > "$NOINT/$_i"; chmod +x "$NOINT/$_i"
255
+ done
256
+ t3(){ ( cd "$R" && PATH="$NOINT:$PATH" bash "$HOOK" < "$P" >/dev/null 2>&1; printf '%s' "$?" ); }
257
+ t1(){ if [ -n "$T1PATH" ]; then ( cd "$R" && PATH="$T1PATH:$PATH" bash "$HOOK" < "$P" >/dev/null 2>&1; printf '%s' "$?" )
258
+ else ( cd "$R" && bash "$HOOK" < "$P" >/dev/null 2>&1; printf '%s' "$?" ); fi; }
259
+
260
+ mk(){ printf '{"cwd":"/tmp","permission_mode":"default","tool_name":"Bash","tool_input":{"command":"%s"},"tool_use_id":"x"}' "$1" > "$P"; }
261
+ mkraw(){ printf '%s' "$1" > "$P"; }
262
+ hexof(){ printf '%s' "$1" | od -An -tx1 | tr -d ' \n'; }
263
+ bytes(){ od -An -tx1 "$P" | tr -d ' \n'; }
264
+
265
+ # ---------------------------------------------------------------------------------------------------------
266
+ # CALIBRATION. Nothing below is reported unless every question here answers.
267
+ # ---------------------------------------------------------------------------------------------------------
268
+ echo
269
+ echo "-- kalibrasyon --"
270
+ CAL=1
271
+ mk 'rm -rf /'; a="$(t3)"
272
+ if [ "$a" = 2 ]; then ok "yıkıcı bir komut bloklanıyor (kapı gerçekten koşuyor)"; else bad "kalibrasyon: 'rm -rf /' rc=$a"; CAL=0; fi
273
+ mk 'ls -la'; a="$(t3)"
274
+ if [ "$a" = 0 ]; then ok "zararsız bir komut geçiyor (kapı aşırı bloklamıyor)"; else bad "kalibrasyon: 'ls -la' rc=$a"; CAL=0; fi
275
+
276
+ if [ "$LADDER" = 1 ]; then
277
+ # BOTH COLUMNS MUST MEAN WHAT THEY SAY, and each needs its own control because they fail in opposite ways.
278
+ #
279
+ # The rung-one column must really reach an interpreter: a stub that returns a DIFFERENT command for a
280
+ # harmless payload flips rc 0 -> 2 only if the branch is taken. Without this the second column can silently
281
+ # be the first column twice.
282
+ if [ -n "$T1PATH" ]; then
283
+ cp "$SHIM/jq" "$W/jq.real"
284
+ { echo '#!/usr/bin/env bash'
285
+ echo "case \"\$2\" in '.tool_input.command // empty') printf '%s\\n' 'rm -rf /' ;; '.permission_mode // empty') printf '%s\\n' default ;; *) exit 3 ;; esac"
286
+ } > "$SHIM/jq"; chmod +x "$SHIM/jq"
287
+ mk 'ls -la'; c="$(t1)"
288
+ cp "$W/jq.real" "$SHIM/jq"; chmod +x "$SHIM/jq"
289
+ if [ "$c" = 2 ]; then ok "rung-1 sütunu gerçekten bir yorumlayıcıya ulaşıyor (stub başka komut döndürünce rc 0->2)"
290
+ else bad "kalibrasyon: rung-1 dalına girilmiyor — ikinci sütun birincinin kopyası (rc=$c)"; CAL=0; fi
291
+ else
292
+ mk 'ls -la'
293
+ if jq -r '.tool_input.command // empty' < "$P" 2>/dev/null | grep -qx 'ls -la'; then
294
+ ok "rung-1 sütunu gerçek jq ile koşuluyor"
295
+ else bad "kalibrasyon: gerçek jq beklendiği gibi cevap vermiyor"; CAL=0; fi
296
+ fi
297
+
298
+ # The MIRROR control, and it is the one this file did not have: the pure-bash column must IGNORE a working
299
+ # interpreter that is reachable. Run on the ambient PATH, `t3` was whatever rung the machine could reach — so
300
+ # on a jq box the "pure bash" column WAS rung one, both columns meant the same thing, and a ladder-split row
301
+ # recorded no divergence and reported that the divergence had moved. Same class as a row that only checks rc:
302
+ # the measurement's meaning depended on which binaries happened to be installed.
303
+ # A WORKING jq shim is placed BEHIND the shadow on the path t3 uses. If the shadow holds, `command -v jq`
304
+ # finds the failing stub first, the rung falls through and the slice answers `ls -la` for rc=0. If the shim is
305
+ # reached it returns `rm -rf /` and the row reads 2, which IS the shadow not taking.
306
+ DECOYBIN="$W/decoybin"; mkdir -p "$DECOYBIN"
307
+ { echo '#!/usr/bin/env bash'
308
+ echo "case \"\$2\" in '.tool_input.command // empty') printf '%s\\n' 'rm -rf /' ;; '.permission_mode // empty') printf '%s\\n' default ;; *) exit 3 ;; esac"
309
+ } > "$DECOYBIN/jq"; chmod +x "$DECOYBIN/jq"
310
+ mk 'ls -la'
311
+ c="$( cd "$R" && PATH="$NOINT:$DECOYBIN:$PATH" bash "$HOOK" < "$P" >/dev/null 2>&1; printf '%s' "$?" )"
312
+ if [ "$c" = 0 ]; then ok "saf-bash sütunu erişilebilir bir yorumlayıcıyı GÖRMEZDEN geliyor (gölge tutuyor)"
313
+ else bad "kalibrasyon: gölge tutmuyor — saf-bash sütunu bir yorumlayıcıya ulaştı (rc=$c); bu sütun ortamın kurulu ikililerine bağlı demektir"; CAL=0; fi
314
+ else
315
+ # With one reader the shadow is a no-op, so asserting it would prove nothing. What is worth asserting instead
316
+ # is WHY it is a no-op: the hook names no interpreter on any line that also touches the payload. A rung that
317
+ # wrote $INPUT to a temp file and parsed the FILE would not name INPUT on the parsing line, so this looks for
318
+ # the interpreter and the payload variable independently and reports the pair rather than requiring them to
319
+ # share a line.
320
+ _INTERP="$(grep -nE '(^|[^A-Za-z_])(jq|python3?|perl|node|ruby)([^A-Za-z0-9_]|$)' "$HOOK" | grep -v '^[0-9]*: *#' | grep -vc '^$' || true)"
321
+ if [ "${_INTERP:-0}" = 0 ]; then ok "tek okuyucu: kapıda hiçbir yorumlayıcı adı geçmiyor (gölgeye gerek yok)"
322
+ else note "tek okuyucu: kapıda $_INTERP satırda yorumlayıcı adı geçiyor — payload'ı okumuyorlarsa sorun değil, ama yapısal kontrol onları adlandırmalı"; fi
323
+ fi
324
+
325
+ # the decode comparison must be able to SEE a difference, and must be able to FAIL
326
+ mk "echo a${BS}ud83d${BS}ude00b"
327
+ d_s="$(slice_decode)"; d_r="$(ref_decode)"
328
+ # BOTH SIDES have to be checked, not just their inequality. An empty reference differs from the slice too, so
329
+ # "they differ" passed while the reference was carrying nothing at all — which is how a broken transport hid
330
+ # itself here for one run. The reference must produce the astral character; the slice must produce `??`.
331
+ if [ -z "$d_r" ]; then bad "kalibrasyon: referans vekil çift için BOŞ döndü — taşıma bozuk, 'farklılar' yanlış sebepten geçerdi"; CAL=0
332
+ elif [ "$d_s" = "$d_r" ]; then bad "kalibrasyon: bilinen fark görünmüyor — çözüm karşılaştırması boş olabilir"; CAL=0
333
+ elif [ "$d_s" = 'echo a??b' ]; then ok "bilinen çözüm farkı görünüyor (dilim '$d_s' · referans '$d_r')"
334
+ else bad "kalibrasyon: dilim vekil çift için 'echo a??b' vermedi ('$d_s') — belgelenen normalleştirme değişmiş"; CAL=0; fi
335
+
336
+ # MUTATION MUST-FAIL: break the extracted slice and require the comparison to notice
337
+ printf '%s\n' '#!/usr/bin/env bash' 'cat >/dev/null' "printf '%s' 'MUTASYON'" > "$W/dec.sh"
338
+ mk 'ls -la'; d_s="$(slice_decode)"; d_r="$(ref_decode)"
339
+ cp "$W/dec.good.sh" "$W/dec.sh"
340
+ if [ "$d_s" != "$d_r" ]; then ok "bozulmuş bir dilim çözüm karşılaştırmasında YAKALANIYOR (mutasyon testi)"
341
+ else bad "kalibrasyon: dilim kasten bozuldu ve karşılaştırma fark etmedi — kontrol ölü"; CAL=0; fi
342
+ mk 'ls -la'
343
+ if [ "$(slice_decode)" = "ls -la" ]; then ok "mutasyondan sonra gerçek dilim geri geldi"
344
+ else bad "kalibrasyon: mutasyon geri alınamadı, kalan satırlar bozuk dilimle ölçülürdü"; CAL=0; fi
345
+
346
+ # THE REFERENCE'S TRANSPORT, calibrated on values whose bytes are known before they are sent. This is the one
347
+ # the file did not have, and its absence produced a CI failure that named the reader for a fault in the
348
+ # measuring path. A reference that cannot carry a line ending intact cannot be used to judge one.
349
+ REF_LOSSY=0
350
+ mk "a${BS}nb"; _t="$(ref_decode | od -An -tx1 | tr -d ' \n')"
351
+ if [ "$_t" = "610a62" ]; then ok "referans gömülü bir LF'i bozmadan taşıyor ($( [ "$B64" = 1 ] && echo 'base64 taşıma' || echo 'doğrudan okuma' ))"
352
+ else REF_LOSSY=1; bad "referans TAŞIMASI BOZUK: LF için 610a62 beklenirken $_t döndü — bu ölçü aletinin kusuru, kapının değil"; fi
353
+ mk "a${BS}rb"; _t="$(ref_decode | od -An -tx1 | tr -d ' \n')"
354
+ if [ "$_t" = "610d62" ]; then ok "referans gömülü bir CR'i bozmadan taşıyor"
355
+ else REF_LOSSY=1; bad "referans TAŞIMASI BOZUK: CR için 610d62 beklenirken $_t döndü"; fi
356
+ # Deliberately NOT normalised away: a CR the decode really produced and a CR the pipe added are the same byte
357
+ # afterwards, so stripping them would hide a real divergence in order to spare a fake one. A lossy transport
358
+ # makes the CR/LF-bearing rows UNMEASURED instead, which is a result the next reader can act on.
359
+ [ "$REF_LOSSY" = 0 ] || note "taşıma kayıplı — satır sonu taşıyan satırlar ölçülemedi sayılacak, normalleştirilmeyecek"
360
+
361
+ if [ "$CAL" != 1 ]; then
362
+ echo; echo "KALİBRASYON BAŞARISIZ — korpus raporlanmıyor, çünkü sonucu okunamaz."; exit 1; fi
363
+
364
+ # ---------------------------------------------------------------------------------------------------------
365
+ # THE CORPUS. Every row declares the verdict it expects, so a divergence is reported with its DIRECTION: a
366
+ # reader answering 0 where 2 was expected is a BLIND GATE; 2 where 0 was expected is an OVER-BLOCK, which
367
+ # teaches the user to reach for --no-verify and disarms all of §4.
368
+ # $3 `need` : the escape the fixture must really carry (`-` none, `hex:…` a whole byte sequence)
369
+ # $5 `dec` : `=` the slice must decode exactly what the reference decodes
370
+ # anything else: the literal the documented normalisation must produce
371
+ # ---------------------------------------------------------------------------------------------------------
372
+ echo
373
+ echo "-- korpus --"
374
+
375
+ check_decode(){ # $1 label $2 expectation
376
+ local s r; s="$(slice_decode)"
377
+ # A lossy transport cannot judge a line ending, so those rows say so rather than being compared against a
378
+ # value the instrument mangled. The rows WITHOUT a line ending are unaffected and still run.
379
+ if [ "$REF_LOSSY" = 1 ]; then
380
+ case "$2$s" in
381
+ *$'\n'*|*$'\r'*) unm "$1 · satır sonu taşıyor ve referans taşıması kayıplı — ÖLÇÜLEMEDİ"; return ;;
382
+ esac
383
+ fi
384
+ if [ "$2" = "=" ]; then
385
+ r="$(ref_decode)"
386
+ if [ "$s" = "$r" ]; then ok "$1 · çözüm referansla aynı"
387
+ else bad "$1 · ÇÖZÜM AYRIŞIYOR: dilim [$s] · referans [$r]"; fi
388
+ else
389
+ if [ "$s" = "$2" ]; then ok "$1 · çözüm belgelenen normalleştirmeyi veriyor [$2]"
390
+ else bad "$1 · NORMALLEŞTİRME BOZULDU: beklenen [$2] · ölçülen [$s]"; fi
391
+ fi; }
392
+
393
+ run_row(){ # $1 label $2 expected rc $3 need $4 command $5 decode expectation
394
+ local label="$1" exp="$2" need="$3" cmd="$4" dec="$5" a b h nh dh
395
+ mk "$cmd"
396
+ if [ "$need" != "-" ]; then
397
+ h="$(bytes)"
398
+ case "$need" in
399
+ hex:*) nh="${need#hex:}"; dh="" ;;
400
+ *) nh="5c$(hexof "$need")"; dh="5c5c$(hexof "$need")" ;;
401
+ esac
402
+ if [ -n "$dh" ]; then case "$h" in *"$dh"*) unm "$label — fixture ATIL: kaçış çift yazılmış ($dh)"; return ;; esac; fi
403
+ case "$h" in *"$nh"*) ;; *) unm "$label — fixture ATIL: payload'da $nh baytları yok"; return ;; esac
404
+ fi
405
+ check_decode "$label" "$dec"
406
+ a="$(t3)"
407
+ if [ "$LADDER" = 1 ]; then
408
+ b="$(t1)"
409
+ if [ "$a" != "$b" ]; then
410
+ if [ "$a" = "$exp" ]; then bad "$label · AYRIŞIYOR: tier1 rc=$b, beklenen $exp $( [ "$exp" = 2 ] && echo '(tier1 KÖR)' || echo '(tier1 AŞIRI BLOKLUYOR)' )"
411
+ else bad "$label · AYRIŞIYOR: tier3 rc=$a, beklenen $exp $( [ "$exp" = 2 ] && echo '(tier3 KÖR)' || echo '(tier3 AŞIRI BLOKLUYOR)' )"; fi
412
+ return
413
+ fi
414
+ fi
415
+ if [ "$a" = "$exp" ]; then ok "$label · hüküm rc=$a"
416
+ else bad "$label · HÜKÜM YANLIŞ: rc=$a, beklenen $exp $( [ "$exp" = 2 ] && echo '(KÖR KAPI)' || echo '(AŞIRI BLOK)' )"; fi; }
417
+
418
+ U="${BS}u" # the two characters a JSON unicode escape starts with, built at run time
419
+ run_row 'düz commit' 2 - 'git commit -m c' '='
420
+ run_row 'pathspec commit' 2 - 'git commit -m c -- a.txt' '='
421
+ run_row 'devam + LF' 2 hex:5c5c5c6e "git commit ${BS}${BS}${BS}n -m c" '='
422
+ run_row 'devam + CRLF' 2 hex:5c5c5c72 "git commit ${BS}${BS}${BS}r${BS}n -m c" "git commit ${BS}"$'\n'" -m c"
423
+ run_row 'CRLF ayraç' 2 r "git commit -m c${BS}r${BS}necho done" "git commit -m c"$'\n'"echo done"
424
+ run_row 'yalnız CR' 2 r "git commit -m c${BS}recho done" 'git commit -m cecho done'
425
+ run_row 'sekme ayraçlı' 2 t "git commit${BS}t-m${BS}tc" "git commit"$'\t'"-m"$'\t'"c"
426
+ run_row 'kaçışlı tırnak' 2 '"' "git commit -m ${BS}\"a b${BS}\"" 'git commit -m "a b"'
427
+ run_row 'rm -rf kök' 2 - 'rm -rf /' '='
428
+ run_row 'force push' 2 - 'git push --force' '='
429
+ run_row 'git add -f' 2 - 'git add -f secrets.env' '='
430
+ run_row 'PowerShell özyinelemeli sil' 2 - "Remove-Item -Recurse -Force C:${BS}${BS}x" "Remove-Item -Recurse -Force C:${BS}x"
431
+ run_row 'gate betiği (düz)' 2 - 'echo x > .claude/hooks/guard-bash.sh' '='
432
+ run_row 'gate betiği (\u kaçışlı)' 2 u "echo x > ${U}002eclaude/hooks/guard-bash.sh" 'echo x > .claude/hooks/guard-bash.sh'
433
+ run_row 'gate yolunu okuma (\u)' 0 u "cat ${U}002eclaude/hooks/guard-bash.sh" 'cat .claude/hooks/guard-bash.sh'
434
+ run_row 'latin kaçışları -> ?' 0 u "echo ${U}00e7${U}011f${U}0131" 'echo ???'
435
+ run_row 'vekil çift -> ??' 0 u "echo a${U}d83d${U}de00b" 'echo a??b'
436
+ run_row 'NUL kaçışı -> ?' 0 u "echo a${U}0000b" 'echo a?b'
437
+ run_row 'backspace -> boşluk' 0 b "echo a${BS}bb" 'echo a b'
438
+ run_row 'formfeed -> boşluk' 0 f "echo a${BS}fc" 'echo a c'
439
+ run_row 'kaçışlı bölü' 0 / "cat a${BS}/b${BS}/c.txt" 'cat a/b/c.txt'
440
+ run_row 'zararsız komut' 0 - 'echo not-a-command' '='
441
+ run_row 'boş komut' 0 - '' '='
442
+
443
+ # ---------------------------------------------------------------------------------------------------------
444
+ # ADVERSARIAL SHAPES — whole objects rather than commands, because the question is which VALUE each reader
445
+ # takes for a key, not what the command says. RFC 8259 leaves duplicate names undefined, so neither "first
446
+ # wins" nor "last wins" is wrong on its own; what matters is that the gate does not judge one value while
447
+ # showing the user another.
448
+ # ---------------------------------------------------------------------------------------------------------
449
+ echo
450
+ echo "-- düşmanca biçimler --"
451
+ # AN EXIT CODE IS NOT A REASON. A row asserting rc=2 passes while the gate refuses for a rule that has nothing
452
+ # to do with the shape under test, and then keeps passing until the fixture changes. That happened here twice:
453
+ # * a payload whose only `"command"` sat in a SIBLING object was read as "the refusal closed the hole" — the
454
+ # slice had in fact read the sibling value and §4.5's `destructive rm -rf` fired on it. The harmless twin
455
+ # (`ls -la` in the same place) returns 0, which is what makes the real behaviour visible: the gate judges a
456
+ # string that is not the command that will run.
457
+ # * every adversarial payload carried `"cwd":"/tmp"`, where no review record exists, so each row whose
458
+ # command was a commit was refused by §4.6 before its own rule was ever reached.
459
+ # So: the payload's cwd is the prepared repo, each row may name the RULE it expects to fire, and the string the
460
+ # slice actually read is printed on every failure.
461
+ # EVERYTHING UP TO THE CLOSING PAREN, rather than a class of the characters a tag is expected to contain. A
462
+ # refusal protecting two sections is tagged `GUARD (§4.4/§4.5)`, and two narrower attempts both failed on it:
463
+ # `§[0-9.]+` stopped at the `/` and captured `GUARD (§4.4` with no closing paren, then `§[0-9./]+` stopped at
464
+ # the SECOND `§` and matched nothing at all, so the row reported "no rule fired" for a refusal that names two.
465
+ # Adding `§` to the class would also be fragile under `LC_ALL=C`, where it is two bytes. `[^)]*` has no opinion
466
+ # about what a tag may contain, which is the right amount of opinion for something whose job is to read one.
467
+ t3err(){ ( cd "$R" && PATH="$NOINT:$PATH" bash "$HOOK" < "$P" 2>&1 >/dev/null ) | tr -d '\r' | grep -oE 'GUARD \([^)]*\)' | head -1; }
468
+ slice_reads(){ bash "$W/dec.sh" < "$P" 2>/dev/null; }
469
+
470
+ adv(){ # $1 label $2 expected rc $3 rule $4 raw payload [$5 known-open:<t3>/<t1>]
471
+ # $3 = `-` do not check why it answered
472
+ # `§4.x` the hook's own tag must contain this
473
+ # An ambiguity refusal is named this way rather than by the destructive rule that used
474
+ # to fire: Crewforth refuses a payload it cannot read unambiguously instead of picking a
475
+ # winner, so the refusal comes FIRST and no command is judged at all. A row naming the
476
+ # destructive rule would be asserting a last-wins policy Crewforth does not have.
477
+ # `ladder-split` while a reader ladder exists this payload is EXPECTED to diverge, because the
478
+ # divergence IS the ladder's defect; with one reader the row must reach $2 instead.
479
+ local label="$1" exp="$2" want="$3" a b r known="${5:-}"
480
+ mkraw "${4//@CWD@/$R}"; a="$(t3)"
481
+ if [ "$LADDER" = 1 ]; then b="$(t1)"; else b="$a"; fi
482
+ if [ -n "$known" ] && [ "${CREW_CONFORMANCE_KNOWN_OPEN:-0}" = 1 ]; then
483
+ # A known-open row is a divergence this file FOUND that nothing has fixed yet. It is pinned to the exact
484
+ # shape observed so it cannot rot: if the shape moves, or if the row starts passing, this FAILS and says so.
485
+ local want="${known#known-open:}"
486
+ if [ "$a" = "$b" ] && [ "$a" = "$exp" ]; then bad "$label — ARTIK GEÇİYOR: bulgu düzeltilmiş, known-open işareti kaldırılmalı"; return; fi
487
+ if [ "$a/$b" = "$want" ]; then KNOWN_OPEN=$((KNOWN_OPEN+1))
488
+ printf ' \033[33mAÇIK\033[0m %s — bilinen ayrışma, kayıtlı: %s (beklenen %s)\n' "$label" "$a/$b" "$exp"; return; fi
489
+ bad "$label — bilinen ayrışma DEĞİŞTİ: kayıt $want, ölçülen $a/$b"; return
490
+ fi
491
+ if [ "$want" = "ladder-split" ]; then
492
+ # Two readers, one of which cannot see this shape at all — that IS the ladder's defect, so while the ladder
493
+ # exists the row asserts the divergence rather than pretending it is absent. Pinned to the exact pair, so a
494
+ # change in the shape fails, and so does the divergence DISAPPEARING while the ladder is still there: that
495
+ # would mean something else moved and the row is no longer describing what it says it describes.
496
+ if [ "$LADDER" = 1 ]; then
497
+ if [ "$a" = 2 ] && [ "$b" = 0 ]; then ok "$label · merdiven dururken AYRIŞIYOR (t3=2 t1=0) — merdivenin kusuru, silinince kapanmalı"
498
+ else bad "$label · merdivenli ayrışma DEĞİŞTİ: kayıt t3=2/t1=0, ölçülen t3=$a/t1=$b · dilim okudu [$(slice_reads)]"; fi
499
+ return
500
+ fi
501
+ if [ "$a" = "$exp" ]; then ok "$label · tek okuyucuda kapandı (rc=$a)"
502
+ else bad "$label · merdiven gitti ama HÂLÂ AÇIK: rc=$a, beklenen $exp · dilim okudu [$(slice_reads)]"; fi
503
+ return
504
+ fi
505
+ if [ "$LADDER" = 1 ] && [ "$a" != "$b" ]; then
506
+ bad "$label — AYRIŞIYOR: tier3 rc=$a · tier1 rc=$b (beklenen $exp) · dilim okudu [$(slice_reads)]"; return; fi
507
+ if [ "$a" != "$exp" ]; then
508
+ bad "$label — rc=$a, beklenen $exp $( [ "$exp" = 2 ] && echo '(KÖR KAPI)' || echo '(AŞIRI BLOK)' ) · dilim okudu [$(slice_reads)]"; return; fi
509
+ if [ "$want" != "-" ]; then
510
+ # the hook prints `GUARD (§4.5): …`, the row names `§4.5`, so this is a containment test and not equality —
511
+ # written as equality first, which reported "beklenen §4.5, ateşlenen GUARD (§4.5)" on rows that were right.
512
+ r="$(t3err)"
513
+ case "$r" in
514
+ *"$want"*) ok "$label (rc=$a, $want)"; return ;;
515
+ *) bad "$label — rc=$exp DOĞRU AMA SEBEP YANLIŞ: beklenen $want, ateşlenen ${r:-hiçbir kural} · dilim okudu [$(slice_reads)]"; return ;;
516
+ esac
517
+ fi
518
+ ok "$label (rc=$a)"; }
519
+
520
+ adv 'command anahtarı iki kez' 2 '§4.4' \
521
+ '{"cwd":"@CWD@","permission_mode":"default","tool_name":"Bash","tool_input":{"command":"ls -la","command":"rm -rf /"},"tool_use_id":"x"}'
522
+ adv 'gövdede sahte command anahtarı' 2 '§4.5' \
523
+ '{"cwd":"@CWD@","permission_mode":"default","tool_name":"Bash","tool_input":{"description":"\"command\":\"ls\"","command":"rm -rf /"},"tool_use_id":"x"}'
524
+ adv 'gerçek anahtardan önce iç içe nesne' 2 '§4.4' \
525
+ '{"cwd":"@CWD@","permission_mode":"default","tool_name":"Bash","tool_input":{"meta":{"command":"ls -la"},"command":"rm -rf /"},"tool_use_id":"x"}'
526
+ adv 'kapanıştan önce kaçışlı tırnak' 2 '§4.5' \
527
+ '{"cwd":"@CWD@","permission_mode":"default","tool_name":"Bash","tool_input":{"command":"rm -rf / --no-preserve-root \"x\""},"tool_use_id":"x"}'
528
+ adv 'kapanıştan önce kaçışlı ters bölü' 0 '-' \
529
+ '{"cwd":"@CWD@","permission_mode":"default","tool_name":"Bash","tool_input":{"command":"echo C:\\\\"},"tool_use_id":"x"}'
530
+ adv 'anahtar sırası: tool_input önce' 2 '§4.5' \
531
+ '{"tool_input":{"command":"rm -rf /"},"tool_name":"Bash","cwd":"@CWD@","permission_mode":"default","tool_use_id":"x"}'
532
+
533
+ # THE SLICE MUST NOT READ A VALUE FROM OUTSIDE tool_input, and the pair below is what makes that visible. The
534
+ # dangerous shape alone proves nothing: it is refused, but by §4.5 firing on the string the slice pulled out of
535
+ # the SIBLING object — the gate judging text that is not the command that will run. The harmless twin is the
536
+ # row that shows it, because there is nothing in `ls -la` for any rule to catch.
537
+ # Reported once as "the refusal closes this hole". It does not. It was the twin that was missing.
538
+ adv 'kardeş nesnede yıkıcı komut' 2 'ladder-split' \
539
+ '{"cwd":"@CWD@","permission_mode":"default","tool_name":"Bash","tool_input":{"foo":1},"meta":{"command":"rm -rf /"},"tool_use_id":"x"}'
540
+ adv 'kardeş nesnede zararsız komut (İKİZ)' 0 '-' \
541
+ '{"cwd":"@CWD@","permission_mode":"default","tool_name":"Bash","tool_input":{"foo":1},"meta":{"command":"ls -la"},"tool_use_id":"x"}'
542
+ adv 'sade payload hâlâ geçiyor (ikiz)' 0 '-' \
543
+ '{"cwd":"@CWD@","permission_mode":"default","tool_name":"Bash","tool_input":{"command":"ls -la"},"tool_use_id":"x"}'
544
+
545
+ # THE KEY NAME AS A VALUE. `_json_slice` searches for the bytes `"key"` and does not require the colon that
546
+ # follows a real key, so a payload carrying the key's NAME as a VALUE relocates the parse — and the ambiguity
547
+ # gates that count `"key":` do not see the same thing the parser found. Here the slice reads `ls` while the
548
+ # command that will actually run is `rm -rf /`.
549
+ adv 'anahtar adı DEĞER olarak (command)' 2 '§4.5' \
550
+ '{"cwd":"@CWD@","a":"command","ls":1,"permission_mode":"default","tool_name":"Bash","tool_input":{"command":"rm -rf /"},"tool_use_id":"x"}'
551
+ adv 'kontrol: aynı komut, gölgesiz (ikiz)' 2 '§4.5' \
552
+ '{"cwd":"@CWD@","permission_mode":"default","tool_name":"Bash","tool_input":{"command":"rm -rf /"},"tool_use_id":"x"}'
553
+
554
+ # WHITESPACE BEFORE THE COLON is legal JSON and the parser and its guard must agree about it. The unspaced twin
555
+ # sits next to it so a red row can be read: if both are red the cause is the nested key, if only the spaced one
556
+ # is red the cause is the spacing.
557
+ adv 'iki noktadan önce boşluk' 2 '§4.5' \
558
+ '{"cwd":"@CWD@","permission_mode":"default","tool_name":"Bash","tool_input":{"command" : "rm -rf /"},"tool_use_id":"x"}'
559
+ adv 'kontrol: boşluksuz ikiz' 2 '§4.5' \
560
+ '{"cwd":"@CWD@","permission_mode":"default","tool_name":"Bash","tool_input":{"command":"rm -rf /"},"tool_use_id":"x"}'
561
+
562
+ # permission_mode: §4.4 fails CLOSED under bypassPermissions, so a key that shadows the real one moves the gate
563
+ # from refuse to ask, and under bypass the harness answers ask with allow. The cwd is the prepared repo, not
564
+ # /tmp: with no review record §4.6 refuses a commit before §4.4 is ever reached, and every one of these rows
565
+ # would have passed on that unrelated rule. That is exactly what they did before this was fixed.
566
+ adv 'permission_mode iki kez (gölgeli bypass)' 2 '§4.4' \
567
+ '{"cwd":"@CWD@","meta":{"permission_mode":"default"},"permission_mode":"bypassPermissions","tool_name":"Bash","tool_input":{"command":"git commit -m x"},"tool_use_id":"x"}'
568
+ adv 'permission_mode DEĞER olarak' 2 '§4.4' \
569
+ '{"cwd":"@CWD@","a":"permission_mode","default":1,"permission_mode":"bypassPermissions","tool_name":"Bash","tool_input":{"command":"git commit -m x"},"tool_use_id":"x"}'
570
+ adv 'dürüst bypass (ikiz)' 2 '§4.4' \
571
+ '{"cwd":"@CWD@","permission_mode":"bypassPermissions","tool_name":"Bash","tool_input":{"command":"git commit -m x"},"tool_use_id":"x"}'
572
+ adv 'permission_mode yok (ikiz, meşru)' 2 '§4.4' \
573
+ '{"cwd":"@CWD@","tool_name":"Bash","tool_input":{"command":"git commit -m x"},"tool_use_id":"x"}'
574
+ adv 'dürüst default: ask, blok değil (ikiz)' 0 '-' \
575
+ '{"cwd":"@CWD@","permission_mode":"default","tool_name":"Bash","tool_input":{"command":"git commit -m x"},"tool_use_id":"x"}'
576
+
577
+ # The key NAME written as a unicode escape. A reader that decodes key names finds `command` and judges
578
+ # `rm -rf /`; one that searches for the literal bytes finds nothing and the gate has no command to judge. The
579
+ # safe answer is the refusal either way, so 2 is expected and a reader answering 0 is BLIND, not lenient.
580
+ # The backslash is built at run time and the payload's BYTES are checked before the row is allowed to count:
581
+ # written literally, the escape has arrived here as an escaped backslash — inert text every reader agrees
582
+ # about — and the row would have reported a pass while measuring nothing.
583
+ ESCKEY="{\"cwd\":\"@CWD@\",\"permission_mode\":\"default\",\"tool_name\":\"Bash\",\"tool_input\":{\"${BS}u0063ommand\":\"rm -rf /\"},\"tool_use_id\":\"x\"}"
584
+ case "$(printf '%s' "$ESCKEY" | od -An -tx1 | tr -d ' \n')" in
585
+ *5c5c7530303633*) bad 'anahtar adı \u kaçışlı — fixture ATIL: çift ters bölü' ;;
586
+ *5c7530303633*) adv 'anahtar adı \u kaçışlı' 2 '-' "$ESCKEY" ;;
587
+ *) bad 'anahtar adı \u kaçışlı — fixture kurulamadı' ;;
588
+ esac
589
+
590
+ echo
591
+ # Every assertion must have been visible to the parent. A row lost to a subshell is worse than a red one: a
592
+ # `bad` there prints and cannot fail the run, so the gate is silently always-green from that line onward.
593
+ if [ "$_ALZ" = 1 ]; then
594
+ _ao="$(_analyse "$ASSERTLOG" "$((PASS+FAILED+UNMEASURED))")"
595
+ _ah="$(printf '%s' "$_ao" | sed -n 's/^HITS=//p')"
596
+ if [ "${_ah:-0}" != 0 ]; then
597
+ printf ' \033[31mFAIL\033[0m %s\n' "$_ah iddia ALT KABUKTA koştu — sayaca ulaşmadılar, bir 'bad' orada görünmez olurdu:"
598
+ printf '%s\n' "$_ao" | grep '>>'
599
+ FAILED=$((FAILED+1))
600
+ fi
601
+ fi
602
+ rm -f "$ASSERTLOG"
603
+ printf 'PARSER-CONFORMANCE: %s geçti · %s başarısız · %s ölçülemedi · %s bilinen-açık (kip: %s · referans: %s)\n' \
604
+ "$PASS" "$FAILED" "$UNMEASURED" "$KNOWN_OPEN" \
605
+ "$( [ "$LADDER" = 1 ] && echo 'merdivenli' || echo 'tek okuyucu' )" "$REFKIND"
606
+ if [ "$KNOWN_OPEN" -gt 0 ]; then
607
+ echo "$KNOWN_OPEN bilinen ayrışma AÇIK ve bu koşuda başarısızlık sayılmadı (CREW_CONFORMANCE_KNOWN_OPEN=1)."
608
+ fi
609
+ if [ "$FAILED" -gt 0 ]; then echo "Okuyucu referanstan ayrıştı ya da bir hüküm yanlış. Bu kapatılmalı."; exit 1; fi
610
+ if [ "$UNMEASURED" -gt 0 ]; then echo "Bazı satırlar ölçülemedi — bu bir geçiş değildir, fixture'ları düzeltin."; exit 1; fi
611
+ echo "Kapının okuyucusu her vakada referansla aynı sonuca vardı."
612
+ exit 0