@agentsfleet/orly 0.5.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 (112) hide show
  1. package/.githooks/pre-commit +42 -0
  2. package/.githooks/pre-push +67 -0
  3. package/AGENTS.md +348 -0
  4. package/LICENSE +21 -0
  5. package/README.md +249 -0
  6. package/SOUL.md +90 -0
  7. package/audits/agents-md.md +497 -0
  8. package/audits/agents-md.sh +353 -0
  9. package/audits/cross-tier-rates.sh +105 -0
  10. package/audits/data.sh +185 -0
  11. package/audits/deinit-pairs.sh +224 -0
  12. package/audits/design-tokens.sh +143 -0
  13. package/audits/doc-read.sh +183 -0
  14. package/audits/error-codes.sh +221 -0
  15. package/audits/fixtures/clean.diff +15 -0
  16. package/audits/fixtures/dirty.diff +16 -0
  17. package/audits/lifecycle-anchors.sh +22 -0
  18. package/audits/logging.sh +217 -0
  19. package/audits/msid-ui.sh +144 -0
  20. package/audits/parity-dispatch.sh +51 -0
  21. package/audits/rule-ledger-lib.sh +264 -0
  22. package/audits/rule-ledger.sh +225 -0
  23. package/audits/rule-paths.sh +108 -0
  24. package/audits/spec-template.sh +258 -0
  25. package/audits/sql-mod.sh +102 -0
  26. package/audits/ufs.sh +303 -0
  27. package/bin/orly +26 -0
  28. package/core/operating-model.md +256 -0
  29. package/dispatch/edit_rules.md +69 -0
  30. package/dispatch/lib.sh +213 -0
  31. package/dispatch/lifecycle.md +187 -0
  32. package/dispatch/name_architecture.md +93 -0
  33. package/dispatch/verify.md +111 -0
  34. package/dispatch/write_any.md +740 -0
  35. package/dispatch/write_any.sh +49 -0
  36. package/dispatch/write_auth.md +36 -0
  37. package/dispatch/write_changelog.md +64 -0
  38. package/dispatch/write_documentation.md +45 -0
  39. package/dispatch/write_go.md +24 -0
  40. package/dispatch/write_http.md +45 -0
  41. package/dispatch/write_mdx.md +7 -0
  42. package/dispatch/write_python.md +8 -0
  43. package/dispatch/write_rust.md +38 -0
  44. package/dispatch/write_shell.md +8 -0
  45. package/dispatch/write_spec.md +105 -0
  46. package/dispatch/write_sql.md +112 -0
  47. package/dispatch/write_sql.sh +36 -0
  48. package/dispatch/write_ts_adhere_bun.md +493 -0
  49. package/dispatch/write_ts_adhere_bun.sh +37 -0
  50. package/dispatch/write_zig.md +852 -0
  51. package/dispatch/write_zig.sh +40 -0
  52. package/docs/CHANGELOG_VOICE.md +18 -0
  53. package/docs/DISPATCH_ARCHITECTURE.md +664 -0
  54. package/docs/DISPATCH_REVIEW_DISPOSITION.md +65 -0
  55. package/docs/DOCUMENTATION_RULES.md +209 -0
  56. package/docs/EXECUTE_DOC_READS.md +29 -0
  57. package/docs/HARNESS_VERIFY_OUTPUT.md +40 -0
  58. package/docs/LIFECYCLE_PATTERNS.md +316 -0
  59. package/docs/LOGGING_STANDARD.md +397 -0
  60. package/docs/ORLY_ARCHITECTURE.md +129 -0
  61. package/docs/REST_API_DESIGN_GUIDELINES.md +783 -0
  62. package/docs/RULE_ENFORCEMENT.md +41 -0
  63. package/docs/SCHEMA_CONVENTIONS.md +93 -0
  64. package/docs/TEMPLATE.md +411 -0
  65. package/docs/VERIFY_TIERS.md +57 -0
  66. package/docs/greptile-learnings/README.md +51 -0
  67. package/docs/greptile-learnings/RULES.md +1045 -0
  68. package/fixtures/registry-invalid.json +1 -0
  69. package/fixtures/registry-valid.json +18 -0
  70. package/fixtures/unclosed-pack-block.md +8 -0
  71. package/package.json +63 -0
  72. package/packs/language/go/rules.md +24 -0
  73. package/packs/language/mdx/rules.md +7 -0
  74. package/packs/language/python/rules.md +8 -0
  75. package/packs/language/rust/rules.md +38 -0
  76. package/packs/language/shell/rules.md +8 -0
  77. package/registry.json +405 -0
  78. package/schemas/evidence.schema.json +43 -0
  79. package/schemas/profile.schema.json +12 -0
  80. package/schemas/registry.schema.json +12 -0
  81. package/skills/orly-babysit-prs/SKILL.md +326 -0
  82. package/skills/orly-spec-new/SKILL.md +157 -0
  83. package/skills/orly-write-integration-test/SKILL.md +470 -0
  84. package/skills/orly-write-unit-test/SKILL.md +520 -0
  85. package/src/cli.test.ts +70 -0
  86. package/src/cli.ts +287 -0
  87. package/src/config.test.ts +226 -0
  88. package/src/config.ts +288 -0
  89. package/src/criteria.test.ts +137 -0
  90. package/src/criteria.ts +166 -0
  91. package/src/criteria_spec.ts +127 -0
  92. package/src/criteria_support.ts +53 -0
  93. package/src/gates.test.ts +183 -0
  94. package/src/gates.ts +169 -0
  95. package/src/gates_spec.test.ts +98 -0
  96. package/src/gates_test_support.ts +194 -0
  97. package/src/git_env.ts +20 -0
  98. package/src/install.test.ts +514 -0
  99. package/src/install.ts +447 -0
  100. package/src/install_nested.test.ts +32 -0
  101. package/src/model.test.ts +31 -0
  102. package/src/model.ts +245 -0
  103. package/src/references.test.ts +47 -0
  104. package/src/references.ts +147 -0
  105. package/src/render.test.ts +60 -0
  106. package/src/render.ts +94 -0
  107. package/src/surfaces.test.ts +43 -0
  108. package/src/surfaces.ts +89 -0
  109. package/src/validation.test.ts +11 -0
  110. package/src/validation.ts +87 -0
  111. package/src/verify.test.ts +13 -0
  112. package/src/verify.ts +59 -0
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env bash
2
+ # deinit-pairs.sh — verify init/deinit pairing for Zig structs.
3
+ #
4
+ # Dispatch façade: dispatch/write_zig.md (lifecycle / LIFECYCLE GATE)
5
+ # Standalone tool — not wired into make lint or pre-commit (per Indy's
6
+ # direction, gates fire pre-emptively via printed proof-blocks during
7
+ # agent work; lint integration is intentionally absent).
8
+ #
9
+ # Lifecycle methods recognized (per LIFECYCLE_PATTERNS.md §10A.LC2):
10
+ # init / deinit / close / release / destroy / shutdown / dispose / free
11
+ #
12
+ # Refined pairing rule:
13
+ # A `pub fn init` requires a matching cleanup method (deinit / close /
14
+ # release / destroy / shutdown / dispose / free) IN THE SAME FILE only
15
+ # when the init either:
16
+ # (a) allocates inside its body
17
+ # (alloc/create/dupe/dupeZ/initCapacity/allocPrint/realloc, plus
18
+ # std.fs.File.open|create, std.Thread.spawn, etc.), OR
19
+ # (b) returns a heap pointer (`*T` or `!*T` or `*Self` or `!*Self`).
20
+ #
21
+ # Pure value-type init (e.g. `pub fn init() Self` returning a struct
22
+ # literal with no heap state) is permitted without deinit per
23
+ # LIFECYCLE_PATTERNS.md §4 decision matrix. Reviewer responsibility for
24
+ # the harder case where a struct stores ArrayList/HashMap fields whose
25
+ # first append/insert happens at use-site rather than init time.
26
+ #
27
+ # Heuristics implemented:
28
+ # 1. Init/deinit pair existence (with allocation refinement) — BLOCKING.
29
+ # 2. defer X.free(buf) + errdefer X.free(buf) on same target adjacent
30
+ # lines — BLOCKING (per §10A.LC4).
31
+ # Skips trivial optional bindings (`if (X) |v| free(v)`) where the
32
+ # captured name is a single 1-2 char identifier — those are loop-
33
+ # style bindings, not the same allocation.
34
+ #
35
+ # Heuristics deferred to reviewer:
36
+ # - Errdefer placement (last errdefer must precede last protected alloc).
37
+ # - Idempotency of deinit (requires test inspection).
38
+ # - Arena leakage into long-lived structs.
39
+ # - ArrayList/HashMap field with no init-time alloc but use-time growth.
40
+ #
41
+ # Modes:
42
+ # --staged diff-scope: only files in `git diff --cached`
43
+ # --all (default) src/ scan including *_test.zig
44
+ #
45
+ # Exits 0 clean, 1 on blocking findings.
46
+
47
+ set -euo pipefail
48
+
49
+ MODE="${1:-${SCOPE:-all}}"
50
+ ROOT="$(git rev-parse --show-toplevel)"
51
+ cd "$ROOT"
52
+
53
+ FAIL=0
54
+ fail() { printf "FAIL: %s\n" "$*" >&2; FAIL=1; }
55
+ ok() { printf "OK: %s\n" "$*"; }
56
+ note() { printf "NOTE: %s\n" "$*"; }
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # 1. Gather Zig files in scope. Tests INCLUDED — same lifecycle rules apply.
60
+ # ---------------------------------------------------------------------------
61
+ case "$MODE" in
62
+ --staged|staged)
63
+ mapfile -t FILES < <(git diff --cached --name-only --diff-filter=ACMRT \
64
+ | grep -E '^src/.*\.zig$' || true)
65
+ ;;
66
+ --all|all)
67
+ mapfile -t FILES < <(find src -type f -name '*.zig' 2>/dev/null || true)
68
+ ;;
69
+ *)
70
+ printf "usage: %s [--staged|--all]\n" "$0" >&2
71
+ exit 64
72
+ ;;
73
+ esac
74
+
75
+ if [[ ${#FILES[@]} -eq 0 ]]; then
76
+ ok "no zig source files in scope ($MODE)"
77
+ exit 0
78
+ fi
79
+
80
+ # ---------------------------------------------------------------------------
81
+ # Helper regexes.
82
+ # ---------------------------------------------------------------------------
83
+ CLEANUP_RE='pub fn (deinit|close|release|destroy|shutdown|dispose|free)\('
84
+ # Heap-pointer return on init signature: `) *T` or `) !*T` or returning *Self.
85
+ HEAP_RETURN_RE='\)[[:space:]]*!?\*([A-Z@]|Self\b)'
86
+ # Allocation patterns inside init body.
87
+ ALLOC_RE='\.(alloc|create|dupe|dupeZ|initCapacity|allocPrint|allocSentinel|reAlloc|realloc)\(|std\.fs\.(File|cwd\(\))\.(open|create)|Thread\.spawn|\.allocator\(\)|ArrayList(Unmanaged)?\(.*\)\.init|HashMap(Unmanaged)?\(.*\)\.init|StringHashMap(Unmanaged)?\(.*\)\.init'
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # 2. Per-init evaluation. Each `pub fn init` is checked individually:
91
+ # look at the signature for heap-return; look at next ~50 lines of body
92
+ # for allocation. Either positive triggers the deinit requirement.
93
+ # ---------------------------------------------------------------------------
94
+ files_scanned=0
95
+ inits_total=0
96
+ inits_value_type=0
97
+ inits_requires_cleanup=0
98
+ inits_unpaired=0
99
+
100
+ # Filter to existing files once.
101
+ present_files=()
102
+ for f in "${FILES[@]}"; do
103
+ [[ -f "$f" ]] && present_files+=("$f") && files_scanned=$((files_scanned + 1))
104
+ done
105
+
106
+ # M70 perf: pre-compute file sets in batched greps instead of one grep
107
+ # per file (~760 forks → 2 forks). Reads as "membership sets" for the
108
+ # per-init loop below.
109
+ declare -A files_with_cleanup=()
110
+ if [[ ${#present_files[@]} -gt 0 ]]; then
111
+ while IFS= read -r f; do
112
+ [[ -n "$f" ]] && files_with_cleanup["$f"]=1
113
+ done < <(grep -lE "$CLEANUP_RE" "${present_files[@]}" 2>/dev/null || true)
114
+ fi
115
+
116
+ # Enumerate every `pub fn init(` site in one batched grep. Output shape:
117
+ # `<file>:<line>:<sig>` — same as the per-file `grep -nE` produced, just
118
+ # with the filename prepended by `-H`.
119
+ inits_list=$(grep -nHE 'pub fn init\(' "${present_files[@]}" 2>/dev/null || true)
120
+
121
+ if [[ -n "$inits_list" ]]; then
122
+ while IFS= read -r row; do
123
+ [[ -z "$row" ]] && continue
124
+ f="${row%%:*}"
125
+ rest="${row#*:}"
126
+ ln="${rest%%:*}"
127
+ sig="${rest#*:}"
128
+ inits_total=$((inits_total + 1))
129
+
130
+ heap=0
131
+ if [[ "$sig" =~ \)[[:space:]]*\!?\*([A-Z@]|Self) ]]; then
132
+ heap=1
133
+ fi
134
+
135
+ # Body window: from sig line to next pub-fn / fn / EOF, capped at +50.
136
+ body=$(awk -v start="$ln" '
137
+ NR <= start { next }
138
+ /^[[:space:]]*(pub )?fn [a-zA-Z_]/ { exit }
139
+ NR > start + 50 { exit }
140
+ { print }
141
+ ' "$f")
142
+
143
+ allocates=0
144
+ if grep -qE "$ALLOC_RE" <<<"$body"; then
145
+ allocates=1
146
+ fi
147
+
148
+ if [[ $heap -eq 0 && $allocates -eq 0 ]]; then
149
+ inits_value_type=$((inits_value_type + 1))
150
+ continue
151
+ fi
152
+
153
+ inits_requires_cleanup=$((inits_requires_cleanup + 1))
154
+ if [[ -z "${files_with_cleanup[$f]+x}" ]]; then
155
+ inits_unpaired=$((inits_unpaired + 1))
156
+ reason=""
157
+ [[ $heap -eq 1 ]] && reason="heap-return"
158
+ [[ $allocates -eq 1 ]] && reason="${reason:+$reason+}allocates"
159
+ fail "$f:$ln — \`pub fn init\` requires cleanup ($reason) but no cleanup method in file"
160
+ printf " > %s\n" "$(echo "$sig" | sed 's/^[[:space:]]*//')" >&2
161
+ fi
162
+ done <<< "$inits_list"
163
+ fi
164
+
165
+ # ---------------------------------------------------------------------------
166
+ # 3. Detect defer/errdefer conflicts on the same allocation target.
167
+ # Skip trivial optional-binding names (1-2 char like `v`, `it`) which
168
+ # are local to each `if (x) |v|` scope and not actual conflicts.
169
+ # ---------------------------------------------------------------------------
170
+ # M70 perf: single awk across every file. File-local state (defers[]
171
+ # and errdefers[] arrays) resets on FNR == 1 via process_file() flush.
172
+ if [[ ${#present_files[@]} -gt 0 ]]; then
173
+ while IFS= read -r line; do
174
+ [[ -z "$line" ]] && continue
175
+ fail "defer/errdefer conflict (same target, ≤4 line window): $line"
176
+ done < <(awk '
177
+ function extract_target(line, s, p, q) {
178
+ p = index(line, ".free(");
179
+ if (p == 0) p = index(line, ".destroy(");
180
+ if (p == 0) return "";
181
+ s = substr(line, p);
182
+ p = index(s, "(");
183
+ q = index(s, ")");
184
+ if (p == 0 || q == 0 || q <= p + 1) return "";
185
+ return substr(s, p + 1, q - p - 1);
186
+ }
187
+ function is_optional_binding(line) {
188
+ return (line ~ /\|[a-z_]+\|/);
189
+ }
190
+ function flush() {
191
+ for (d in defers) {
192
+ for (e in errdefers) {
193
+ gap = d - e; if (gap < 0) gap = -gap;
194
+ if (defers[d] == errdefers[e] && gap <= 4) {
195
+ printf "%s:%d:%d: defer/errdefer conflict on %s\n", current_file, d, e, defers[d];
196
+ }
197
+ }
198
+ }
199
+ delete defers; delete errdefers
200
+ }
201
+ FNR == 1 && current_file != "" { flush() }
202
+ FNR == 1 { current_file = FILENAME }
203
+ /^[[:space:]]*defer[[:space:]].*\.(free|destroy)\(/ {
204
+ tgt = extract_target($0);
205
+ if (tgt != "" && length(tgt) > 2 && !is_optional_binding($0)) defers[FNR] = tgt;
206
+ }
207
+ /^[[:space:]]*errdefer[[:space:]].*\.(free|destroy)\(/ {
208
+ tgt = extract_target($0);
209
+ if (tgt != "" && length(tgt) > 2 && !is_optional_binding($0)) errdefers[FNR] = tgt;
210
+ }
211
+ END { if (current_file != "") flush() }
212
+ ' "${present_files[@]}")
213
+ fi
214
+
215
+ # ---------------------------------------------------------------------------
216
+ # 4. Verdict.
217
+ # ---------------------------------------------------------------------------
218
+ ok "scanned $files_scanned files; $inits_total init methods ($inits_value_type value-type, $inits_requires_cleanup require cleanup, $inits_unpaired unpaired)"
219
+ if [[ $FAIL -ne 0 ]]; then
220
+ printf "\n🔴 LIFECYCLE GATE: violations found. See dispatch/write_zig.md (lifecycle).\n" >&2
221
+ exit 1
222
+ fi
223
+ ok "LIFECYCLE GATE: clean"
224
+ exit 0
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env bash
2
+ # design-tokens.sh — enforce design-system token discipline across
3
+ # ui/packages/{app,website}/**/*.tsx.
4
+ #
5
+ # Dispatch façade: dispatch/write_ts_adhere_bun.md (Design Tokens / DESIGN TOKEN GATE)
6
+ # Fires in: CONFORM (via `make harness-verify` in `agentsfleet`).
7
+ #
8
+ # Rejects arbitrary Tailwind classes when an equivalent design-system
9
+ # token utility exists in ui/packages/design-system/src/theme.css.
10
+ # The token set is encoded below; when the design system adds a token,
11
+ # add the matching grep here in the same commit.
12
+ #
13
+ # TECHNICAL DEBT (acknowledged on migration to dotfiles, 2026-05-11):
14
+ # The PATTERNS array hard-codes agentsfleet-specific token names
15
+ # (text-fluid-hero, max-w-prose, tracking-display-md, etc.). If a second
16
+ # project adopts this dotfiles set, it must either:
17
+ # (a) share agentsfleet's exact token vocabulary, or
18
+ # (b) fork the PATTERNS array, or
19
+ # (c) wait for the parameterised version that reads token names from
20
+ # $REPO/ui/packages/design-system/src/theme.css at runtime.
21
+ # Path (c) is the right long-term shape; not done yet.
22
+ #
23
+ # Scope (M70):
24
+ # Walks the full working tree via `git ls-files` — sees staged content
25
+ # because the index is what `ls-files` reports. Pre-commit-safe: a fix
26
+ # staged but not yet committed satisfies the check on the same hook run.
27
+ # The previous `--diff` (BASE...HEAD) default was retired with M70
28
+ # because it was blind to the index at pre-commit time.
29
+ #
30
+ # Modes:
31
+ # (no flag) / --all full-codebase scan via `git ls-files` (default)
32
+ # --staged narrow to files in `git diff --cached --name-only`
33
+ # — opt-in fast loop for iterative dev
34
+ #
35
+ # Exits 0 clean, 1 on any violation. Test files exempt
36
+ # (snapshot/E2E assert on rendered DOM).
37
+
38
+ set -euo pipefail
39
+
40
+ MODE="${1:---all}"
41
+ ROOT="$(git rev-parse --show-toplevel)"
42
+ cd "$ROOT"
43
+
44
+ # ── Patterns ─────────────────────────────────────────────────────────────
45
+ #
46
+ # Each pattern flags an arbitrary CSS class that has a token equivalent.
47
+ # Format: <regex>|<suggestion>. The suggestion column is hint-only — the
48
+ # closest token depends on context; the audit just blocks the arbitrary.
49
+ #
50
+ # Tailwind base utilities (m-0, w-full, flex, gap-N, etc.) are not flagged
51
+ # — only arbitrary-value classes (the `text-[...]`, `leading-[...]`,
52
+ # `max-w-[...]`, `tracking-[...]` shape).
53
+
54
+ # Format: <regex>:::<suggestion>. The `:::` delimiter is unambiguous —
55
+ # regex content frequently contains `|` (alternation), so we can't use it.
56
+ PATTERNS=(
57
+ 'text-\[[0-9]+(px|rem)\]:::text-{label,eyebrow,body-sm,body,body-lg,heading,display-md,display-lg,display-xl}'
58
+ 'text-\[clamp\(:::text-fluid-{display-md,display-lg,hero}'
59
+ 'tracking-\[-?[0-9]:::tracking-{display-xl,display-lg,display-md,eyebrow,label}'
60
+ 'leading-\[[0-9]:::leading-{display-xl,display-lg,display-md,heading,eyebrow,body-lg,body,body-sm,label,mono,prose}'
61
+ 'max-w-\[[0-9]+(px|rem|ch)\]:::max-w-{trim,narrow,measure,form,wide,content,tagline,prose}'
62
+ '(text|bg|border)-(red|blue|green|yellow|purple|indigo|violet|pink|orange|gray|slate|zinc|neutral)-[0-9]:::use semantic tokens (text-destructive, text-error, text-success, …)'
63
+ )
64
+
65
+ # ── File scope ───────────────────────────────────────────────────────────
66
+ in_scope() {
67
+ local f="$1"
68
+ case "$f" in
69
+ *.test.tsx|*.test.jsx) return 1 ;;
70
+ *tests/e2e/*) return 1 ;;
71
+ ui/packages/app/*.tsx|ui/packages/app/*.jsx) return 0 ;;
72
+ ui/packages/website/*.tsx|ui/packages/website/*.jsx) return 0 ;;
73
+ ui/packages/app/**/*.tsx|ui/packages/app/**/*.jsx) return 0 ;;
74
+ ui/packages/website/**/*.tsx|ui/packages/website/**/*.jsx) return 0 ;;
75
+ *) return 1 ;;
76
+ esac
77
+ }
78
+
79
+ case "$MODE" in
80
+ --staged|staged)
81
+ FILES=$(git diff --cached --name-only --diff-filter=ACMRT 2>/dev/null || true)
82
+ ;;
83
+ --all|all)
84
+ # `git ls-files` reports the index, which includes staged content —
85
+ # so pre-commit-style invocations see staged-but-not-committed files
86
+ # without needing a `--staged` flag.
87
+ FILES=$(git ls-files -- 'ui/packages/app/*.tsx' 'ui/packages/app/*.jsx' 'ui/packages/website/*.tsx' 'ui/packages/website/*.jsx' 2>/dev/null || true)
88
+ ;;
89
+ *)
90
+ echo "usage: $0 [--all|--staged]" >&2
91
+ echo "note: --diff was retired in M70 — see dispatch/write_ts_adhere_bun.md (Design Tokens → Scope)." >&2
92
+ exit 2 ;;
93
+ esac
94
+
95
+ FAIL=0
96
+ SEEN_FILES_LIST=""
97
+
98
+ # Pre-filter FILES list once (drop blanks, non-existent, out-of-scope).
99
+ SCOPED_FILES=()
100
+ while IFS= read -r f; do
101
+ [ -z "$f" ] && continue
102
+ [ ! -f "$f" ] && continue
103
+ in_scope "$f" || continue
104
+ SCOPED_FILES+=("$f")
105
+ done <<< "$FILES"
106
+
107
+ if [ "${#SCOPED_FILES[@]}" -gt 0 ]; then
108
+ for entry in "${PATTERNS[@]}"; do
109
+ regex="${entry%%:::*}"
110
+ suggestion="${entry#*:::}"
111
+ # Single grep across all scoped files per pattern.
112
+ # `grep -nHE` always prefixes with filename + line; `-H` is required
113
+ # when a single file is supplied so the parser sees a uniform shape.
114
+ while IFS= read -r match; do
115
+ [ -z "$match" ] && continue
116
+ f="${match%%:*}"
117
+ rest="${match#*:}"
118
+ ln="${rest%%:*}"
119
+ # Honor inline override `// DESIGN TOKEN: SKIPPED per user override (reason: ...)`
120
+ # by checking the matched line and the line above.
121
+ ctx=$(awk -v n="$ln" 'NR>=n-1 && NR<=n' "$f" 2>/dev/null || true)
122
+ if printf '%s' "$ctx" | grep -q 'DESIGN TOKEN: SKIPPED'; then continue; fi
123
+ printf '%s\n -> %s\n' "$match" "$suggestion"
124
+ SEEN_FILES_LIST="${SEEN_FILES_LIST}${f}"$'\n'
125
+ FAIL=1
126
+ done < <(grep -nHE "$regex" "${SCOPED_FILES[@]}" 2>/dev/null || true)
127
+ done
128
+ fi
129
+
130
+ FILES_WITH_VIOLATIONS=$(printf '%s' "$SEEN_FILES_LIST" | sort -u | grep -c . || true)
131
+
132
+ if [ "$FAIL" = "0" ]; then
133
+ echo "OK: design-token discipline — no arbitraries that have a token equivalent (mode=$MODE)"
134
+ exit 0
135
+ fi
136
+
137
+ echo
138
+ echo "FAIL: design-token discipline ($FILES_WITH_VIOLATIONS file(s) with violations)"
139
+ echo " Replace each match with the suggested token utility, or add the inline override:"
140
+ echo " // DESIGN TOKEN: SKIPPED per user override (reason: <concrete reason>)"
141
+ echo " Token source: ui/packages/design-system/src/theme.css"
142
+ echo " Rule body: dispatch/write_ts_adhere_bun.md (Design Tokens)"
143
+ exit 1
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env bash
2
+ # doc-read.sh — turn the 📖 DOC READ proof-line into a set comparison.
3
+ #
4
+ # audits/doc-read.sh log <path> record that a file was read (hook target)
5
+ # audits/doc-read.sh check staged source vs recorded reads
6
+ #
7
+ # The DOC READ GATE asks an agent to read the façade its edit triggers, and
8
+ # then asks the same agent whether it did. That is a claim about itself
9
+ # compared against nothing. `log` records the read and `check` compares the
10
+ # record against the façades the staged files trigger.
11
+ #
12
+ # NOTHING HERE MAY DEPEND ON AN AGENT RUNTIME. `log` is a shell command any
13
+ # agent can run; a runtime that also has a read hook (Claude Code today) can
14
+ # call it automatically, and a runtime that removes that feature tomorrow
15
+ # changes nothing — the agent runs the command, the record is identical, the
16
+ # gate behaves the same. Vendor-shaped configuration stays out of this file and
17
+ # out of .githooks/, and an eval fails the build if it appears
18
+ # (evals/ledger/run.sh, ledger_doc_read_runtime_neutral).
19
+ #
20
+ # The record lives in the git directory, never in the tree: it is evidence
21
+ # about one working copy, not shared history. When it is absent — an agent
22
+ # runtime with no hook support — `check` warns and exits 0. Partial
23
+ # mechanization stated honestly beats a red that means nothing.
24
+ #
25
+ # Exit: 0 clean or unenforceable · 1 unread trigger · 2 usage error.
26
+ set -uo pipefail
27
+
28
+ # A hook exports GIT_DIR/GIT_INDEX_FILE at the repository it fired in. This
29
+ # script IS a hook target, so the scope dies before the first git call.
30
+ unset GIT_DIR GIT_INDEX_FILE GIT_WORK_TREE GIT_COMMON_DIR GIT_PREFIX \
31
+ GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES
32
+
33
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
34
+ # shellcheck source=audits/rule-ledger-lib.sh
35
+ source "$HERE/rule-ledger-lib.sh"
36
+
37
+ MODE_LOG="log"
38
+ MODE_CHECK="check"
39
+ LOG_RELATIVE_PATH="orly/doc-reads.jsonl"
40
+ USAGE="usage: $0 log <path> | $0 check"
41
+
42
+ if [[ -t 1 ]]; then G=$'\033[32m'; R=$'\033[31m'; Y=$'\033[33m'; X=$'\033[0m'
43
+ else G=''; R=''; Y=''; X=''; fi
44
+
45
+ # Where the record lives for this working copy. Empty when the caller is not
46
+ # inside a repository at all, which only `log` tolerates.
47
+ read_log_path() {
48
+ local git_dir
49
+ git_dir="$(git -C "$LEDGER_ROOT" rev-parse --absolute-git-dir 2>/dev/null)" || return 1
50
+ printf '%s/%s' "$git_dir" "$LOG_RELATIVE_PATH"
51
+ }
52
+
53
+ # Repo-relative form of a path, or nothing when it sits outside the tree — an
54
+ # agent reading its own notes elsewhere on disk is not a doc read.
55
+ repo_relative() {
56
+ local path="$1"
57
+ case "$path" in
58
+ "$LEDGER_ROOT"/*) printf '%s' "${path#"$LEDGER_ROOT"/}" ;;
59
+ /*) return 1 ;;
60
+ *) [ -e "$LEDGER_ROOT/$path" ] && printf '%s' "$path" ;;
61
+ esac
62
+ }
63
+
64
+ # One JSON Lines row per call, appended. Append-only with a single write per
65
+ # invocation is why two agents in two sessions cannot corrupt each other: the
66
+ # order rows land in does not change which paths the set contains.
67
+ run_log() {
68
+ local path="$1" relative log blob
69
+ relative="$(repo_relative "$path")" || return 0
70
+ [ -n "$relative" ] || return 0
71
+ log="$(read_log_path)" || return 0
72
+ mkdir -p "$(dirname "$log")" 2>/dev/null || return 0
73
+ blob="$(content_hash "$relative")"
74
+ # A hash we could not compute proves nothing was read at a known version.
75
+ # Recording it would let two failures compare equal and validate a read of a
76
+ # file that is no longer there.
77
+ [ -n "$blob" ] || return 0
78
+ printf '{"ts":%s,"path":"%s","blob":"%s"}\n' "$(date +%s)" "$(json_escape "$relative")" "$blob" >> "$log"
79
+ return 0
80
+ }
81
+
82
+ # Backslash and double quote are legal in a POSIX filename and illegal raw in a
83
+ # JSON string. Unescaped, `we"ird.md` writes a row no parser can read — and this
84
+ # one mis-splits it, reporting a path that was never read.
85
+ json_escape() {
86
+ printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
87
+ }
88
+
89
+ # The content a read actually saw. Validity is keyed to this rather than to a
90
+ # clock: an unchanged document does not need re-reading because a commit
91
+ # happened, and a changed one voids every prior read the same second it is
92
+ # edited — which a timestamp window silently fails to do.
93
+ content_hash() {
94
+ git -C "$LEDGER_ROOT" hash-object "$LEDGER_ROOT/$1" 2>/dev/null || printf ''
95
+ }
96
+
97
+ # Façade pages the staged files trigger, one per line. Same glob map the
98
+ # reachability report uses: a façade's scope is its dispatch_init line, so this
99
+ # check and the ledger can never disagree about what fires.
100
+ expected_facade_pages() {
101
+ local staged="$1" script stem globs
102
+ while IFS= read -r script; do
103
+ [ -n "$script" ] || continue
104
+ stem="$(basename "$script" .sh)"
105
+ [ -f "$LEDGER_ROOT/dispatch/$stem.md" ] || continue
106
+ globs="$(ledger_facade_globs "$script")"
107
+ [ -n "$globs" ] || continue
108
+ [ "$(ledger_match_count "$globs" < "$staged")" -gt 0 ] && printf 'dispatch/%s.md\n' "$stem"
109
+ done < <(ledger_facade_scripts)
110
+ }
111
+
112
+ json_unescape() {
113
+ printf '%s' "$1" | sed -e 's/\\"/"/g' -e 's/\\\\/\\/g'
114
+ }
115
+
116
+ # Paths whose recorded read saw the content that is on disk now. A row written
117
+ # against an older version of the document proves the agent read something
118
+ # else, so it does not count — and rows from before this milestone, which carry
119
+ # no blob, cannot prove anything and are ignored.
120
+ current_reads() {
121
+ local log="$1" line path blob
122
+ while IFS= read -r line; do
123
+ case "$line" in *'"blob":"'*) ;; *) continue ;; esac
124
+ blob="${line##*\"blob\":\"}"; blob="${blob%%\"*}"
125
+ [ -n "$blob" ] || continue
126
+ path="${line#*\"path\":\"}"; path="${path%\",\"blob\":\"$blob\"\}}"
127
+ path="$(json_unescape "$path")"
128
+ [ "$blob" = "$(content_hash "$path")" ] && printf '%s\n' "$path"
129
+ done < "$log"
130
+ }
131
+
132
+ # File-scope so the EXIT trap can still see them: a trap fires after a
133
+ # function's locals are gone, and `set -u` turns that into a spurious error on
134
+ # the way out of a clean run.
135
+ STAGED_FILE=""
136
+ EXPECTED_FILE=""
137
+ RECORDED_FILE=""
138
+ cleanup_temporaries() { rm -f "$STAGED_FILE" "$EXPECTED_FILE" "$RECORDED_FILE"; }
139
+
140
+ run_check() {
141
+ local log unread=0 page staged expected recorded
142
+ STAGED_FILE="$(mktemp)"; EXPECTED_FILE="$(mktemp)"; RECORDED_FILE="$(mktemp)"
143
+ staged="$STAGED_FILE"; expected="$EXPECTED_FILE"; recorded="$RECORDED_FILE"
144
+ trap cleanup_temporaries EXIT
145
+ git -C "$LEDGER_ROOT" diff --cached --name-only --diff-filter=ACMRT > "$staged" 2>/dev/null
146
+ expected_facade_pages "$staged" | sort -u > "$expected"
147
+
148
+ if [ ! -s "$expected" ]; then
149
+ printf ' %s🟢%s DOC READ: nothing staged triggers a façade\n' "$G" "$X"
150
+ return 0
151
+ fi
152
+ if ! log="$(read_log_path)" || [ ! -f "$log" ]; then
153
+ printf ' %s🟠%s DOC READ: no read record for this working copy — the agent runtime\n' "$Y" "$X"
154
+ printf ' has no Read hook, so the proof-line stays self-reported here\n'
155
+ return 0
156
+ fi
157
+
158
+ # Materialised once, and never piped into `grep -q`: under `pipefail` the
159
+ # producer takes SIGPIPE the moment grep matches and exits, and the pipeline
160
+ # then reports 141 — which reads exactly like "not found" and reds a doc that
161
+ # was read.
162
+ current_reads "$log" | sort -u > "$recorded"
163
+
164
+ while IFS= read -r page; do
165
+ grep -qxF "$page" "$recorded" && continue
166
+ printf ' %s🔴%s DOC READ: %s triggered by the staged diff, not read at its current content\n' "$R" "$X" "$page"
167
+ unread=1
168
+ done < "$expected"
169
+ [ "$unread" -eq 0 ] && printf ' %s🟢%s DOC READ: every triggered façade was read at its current content\n' "$G" "$X"
170
+ return "$unread"
171
+ }
172
+
173
+ case "${1:-}" in
174
+ "$MODE_LOG")
175
+ [ "$#" -eq 2 ] || { printf '%s\n' "$USAGE" >&2; exit 2; }
176
+ run_log "$2"
177
+ ;;
178
+ "$MODE_CHECK")
179
+ [ "$#" -eq 1 ] || { printf '%s\n' "$USAGE" >&2; exit 2; }
180
+ run_check
181
+ ;;
182
+ *) printf '%s\n' "$USAGE" >&2; exit 2 ;;
183
+ esac