@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,51 @@
1
+ #!/usr/bin/env bash
2
+ # parity-dispatch.sh — the Stage-2 replacement for agents-md.sh's gate parity.
3
+ #
4
+ # Model B dissolves docs/gates/ (20 cards) into dispatch/ (10 entries). At the
5
+ # Stage-2 switchover, agents-md.sh's gate-parity block (`index ↔ disk ↔
6
+ # REQUIRED_GATES`, over docs/gates/) is deleted and this is sourced + called in
7
+ # its place — line-neutral, so the ≤350 length gate on agents-md.sh still holds.
8
+ #
9
+ # Until then it is exercised only by evals/test-dispatch-parity.sh against a
10
+ # model-B sandbox, proving the switchover audit goes GREEN and BITES before any
11
+ # card is deleted. It does not run on the live (gate-mode) tree.
12
+ #
13
+ # check_dispatch_parity <ROOT> # echoes PASS/FAIL lines, returns 0/1
14
+ #
15
+ # Sources of truth (must agree):
16
+ # (a) disk — dispatch/*.md (each entry has exactly one latent .md)
17
+ # (b) index — AGENTS.md dispatch-table rows (2nd cell a backticked entry)
18
+ # (c) expected — REQUIRED_DISPATCH (audits/data.sh)
19
+
20
+ check_dispatch_parity() {
21
+ local root="$1" rc=0 e
22
+ local agents="$root/AGENTS.md" # separate line: $root must be set before it expands here
23
+ local bt='`' # backtick held in a var (literal ` is fine, but keep it explicit)
24
+
25
+ local disk_count table_rows required_count
26
+ disk_count=$(find "$root/dispatch" -maxdepth 1 -name '*.md' 2>/dev/null | wc -l | tr -d ' ')
27
+ table_rows=$(grep -cE "^\|[^|]*\| *${bt}[a-z_]+${bt} *\|" "$agents" 2>/dev/null)
28
+ required_count=${#REQUIRED_DISPATCH[@]}
29
+
30
+ if [[ "$disk_count" -ne "$table_rows" || "$table_rows" -ne "$required_count" ]]; then
31
+ echo "FAIL: dispatch parity — disk=$disk_count ↔ table=$table_rows ↔ REQUIRED=$required_count (all three must match)"
32
+ rc=1
33
+ fi
34
+
35
+ for e in "${REQUIRED_DISPATCH[@]}"; do
36
+ [[ -f "$root/dispatch/$e.md" ]] \
37
+ || { echo "FAIL: dispatch entry has no body: dispatch/$e.md"; rc=1; }
38
+ grep -qE "^\|[^|]*\| *${bt}$e${bt} *\|" "$agents" 2>/dev/null \
39
+ || { echo "FAIL: dispatch entry absent from AGENTS.md table: $e"; rc=1; }
40
+ done
41
+
42
+ # docs/gates/ must be EMPTY in the dispatch model (the old empty-set guard
43
+ # inverts: a leftover card means the switchover is half-done).
44
+ local leftover
45
+ leftover=$(find "$root/docs/gates" -maxdepth 1 -name '*.md' 2>/dev/null | wc -l | tr -d ' ')
46
+ [[ "$leftover" -eq 0 ]] \
47
+ || { echo "FAIL: docs/gates/ still holds $leftover card(s) — switchover incomplete"; rc=1; }
48
+
49
+ [[ $rc -eq 0 ]] && echo "PASS: dispatch parity (disk=$disk_count ↔ table=$table_rows ↔ REQUIRED=$required_count; docs/gates/ empty)"
50
+ return $rc
51
+ }
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env bash
2
+ # rule-ledger-lib.sh — shared extractors for audits/rule-ledger.sh.
3
+ #
4
+ # Sourced, never executed. Split from the leaf per dispatch/write_any.md
5
+ # §File & Function Length Gate so both stay well under the cap.
6
+ #
7
+ # The docs/ rule tier is the half of the corpus that dispatch/'s coverage audit
8
+ # cannot see: evals/dispatch/coverage.sh globs dispatch/*.md only. This library
9
+ # supplies the same kind of extraction for docs/, plus the UNENFORCED class that
10
+ # makes "prose nobody checks" a counted number instead of an unknown.
11
+
12
+ # Rule documents whose enforcement this ledger reports. A docs/*.md cited as a
13
+ # rule read by a dispatch façade but absent from BOTH lists is a parity red:
14
+ # either it belongs here, or it is not a rule doc and belongs in EXCLUDED.
15
+ REGISTERED_DOCS=(
16
+ "docs/LOGGING_STANDARD.md"
17
+ "docs/REST_API_DESIGN_GUIDELINES.md"
18
+ "docs/SCHEMA_CONVENTIONS.md"
19
+ "docs/DOCUMENTATION_RULES.md"
20
+ "docs/LIFECYCLE_PATTERNS.md"
21
+ "docs/CHANGELOG_VOICE.md"
22
+ "docs/VERIFY_TIERS.md"
23
+ "docs/greptile-learnings/RULES.md"
24
+ )
25
+
26
+ # Cited by a façade but deliberately not a rule doc: architecture (describes
27
+ # shape, enforces nothing), the spec template (a form, gated by
28
+ # audits/spec-template.sh), the trigger index, an output-format reference, and
29
+ # product-repo docs that do not exist in this checkout at all.
30
+ EXCLUDED_DOCS=(
31
+ "docs/DISPATCH_ARCHITECTURE.md"
32
+ "docs/ORLY_ARCHITECTURE.md"
33
+ "docs/TEMPLATE.md"
34
+ "docs/EXECUTE_DOC_READS.md"
35
+ "docs/HARNESS_VERIFY_OUTPUT.md"
36
+ "docs/AUTH.md"
37
+ "docs/changelog.md"
38
+ )
39
+
40
+ # Prefix-matched exclusions for open-ended trees owned by other repositories.
41
+ EXCLUDED_PREFIXES=(
42
+ "docs/architecture/"
43
+ )
44
+
45
+ # A normative clause is a line that binds behaviour. This is a keyword
46
+ # heuristic, not a parse: counts INFORM the reader and never gate a build, so a
47
+ # keyword inside an example block costs nothing but a slightly high number.
48
+ NORMATIVE_PATTERN='\b(MUST|NEVER|ALWAYS|Forbidden|Required|SHALL|Do not|Don.t)\b'
49
+
50
+ # Markdown table rows are excluded from the clause count. In a rule doc a table
51
+ # is usually a field reference — `| ts_ms | u64 | always | ... |` — where the
52
+ # keyword is a column VALUE, not an obligation, and counting those makes a
53
+ # fully-triaged document look permanently unfinished. Rules stated inside a
54
+ # table restate prose stated elsewhere; they are still read, just not counted.
55
+ TABLE_ROW_PATTERN='^[[:space:]]*\|'
56
+
57
+ # Enforcement classes. The first two already exist in dispatch/*.md
58
+ # (docs/DISPATCH_ARCHITECTURE.md §6); UNENFORCED is added by this milestone so
59
+ # an acknowledged-prose clause is distinguishable from one nobody has triaged.
60
+ DETERMINISTIC_TAG='\[DETERMINISTIC → [A-Za-z0-9_:-]+\]'
61
+ JUDGMENT_TAG='\[JUDGMENT → [A-Za-z0-9_:-]+\]'
62
+ UNENFORCED_TAG='\[UNENFORCED → [^]]+\]'
63
+
64
+ LEDGER_ROOT="${ORLY_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
65
+
66
+ # A tag alone on its line (the `> [DETERMINISTIC → FLL]` form dispatch/*.md
67
+ # already uses) covers every clause under it until the next heading. A tag at
68
+ # the end of a sentence covers that sentence only. Both are needed: a rule doc
69
+ # states some rules as a tagged section and others as one-line asides, and
70
+ # forcing either shape onto the other would mean editing prose to satisfy a
71
+ # counter.
72
+ STANDALONE_TAG_PATTERN='^[[:space:]]*>?[[:space:]]*\[(DETERMINISTIC|JUDGMENT|UNENFORCED) → [^]]+\][[:space:]]*$'
73
+ HEADING_PATTERN='^#{1,6} '
74
+ CLASS_DETERMINISTIC="det"
75
+ CLASS_JUDGMENT="judgment"
76
+ CLASS_UNENFORCED="unenforced"
77
+
78
+ # Line numbers of every line in a file matching a pattern, one per line.
79
+ ledger_line_numbers() {
80
+ grep -nE "$2" "$1" 2>/dev/null | cut -d: -f1
81
+ }
82
+
83
+ # Clause line numbers: keyword lines that are not table rows. Case-insensitive,
84
+ # because a rule reads the same whether the author shouted it or not.
85
+ ledger_clause_lines() {
86
+ grep -niE "$NORMATIVE_PATTERN" "$1" 2>/dev/null \
87
+ | grep -vE '^[0-9]+:[[:space:]]*\|' | cut -d: -f1
88
+ }
89
+
90
+ # The five census numbers for one document, space-separated:
91
+ # clauses det judgment unenforced untagged
92
+ # Returns 1 without output when the path is absent, so a caller reports the
93
+ # missing registration rather than printing zeroes that look like a clean doc.
94
+ #
95
+ # One pass down the file carrying the section's block tag: a heading clears it,
96
+ # a standalone tag sets it, and every clause takes the tag on its own line
97
+ # first, the block's second, nothing third. Nothing third is the backlog.
98
+ ledger_doc_counts() {
99
+ local path="$1"
100
+ local line total block="" class
101
+ local clauses=0 det=0 judgment=0 unenforced=0 untagged=0
102
+ [ -f "$path" ] || return 1
103
+ local is_heading=() is_clause=() line_class=() standalone=()
104
+ for line in $(ledger_line_numbers "$path" "$HEADING_PATTERN"); do is_heading[$line]=1; done
105
+ for line in $(ledger_clause_lines "$path"); do is_clause[$line]=1; done
106
+ for line in $(ledger_line_numbers "$path" "$STANDALONE_TAG_PATTERN"); do standalone[$line]=1; done
107
+ for line in $(ledger_line_numbers "$path" "$DETERMINISTIC_TAG"); do line_class[$line]="$CLASS_DETERMINISTIC"; done
108
+ for line in $(ledger_line_numbers "$path" "$JUDGMENT_TAG"); do line_class[$line]="$CLASS_JUDGMENT"; done
109
+ for line in $(ledger_line_numbers "$path" "$UNENFORCED_TAG"); do line_class[$line]="$CLASS_UNENFORCED"; done
110
+
111
+ total="$(wc -l < "$path" | tr -d ' ')"
112
+ for (( line = 1; line <= total; line++ )); do
113
+ [ -n "${is_heading[$line]:-}" ] && { block=""; continue; }
114
+ [ -n "${standalone[$line]:-}" ] && { block="${line_class[$line]:-}"; continue; }
115
+ [ -n "${is_clause[$line]:-}" ] || continue
116
+ class="${line_class[$line]:-$block}"
117
+ clauses=$(( clauses + 1 ))
118
+ case "$class" in
119
+ "$CLASS_DETERMINISTIC") det=$(( det + 1 )) ;;
120
+ "$CLASS_JUDGMENT") judgment=$(( judgment + 1 )) ;;
121
+ "$CLASS_UNENFORCED") unenforced=$(( unenforced + 1 )) ;;
122
+ *) untagged=$(( untagged + 1 )) ;;
123
+ esac
124
+ done
125
+ printf '%s %s %s %s %s' "$clauses" "$det" "$judgment" "$unenforced" "$untagged"
126
+ }
127
+
128
+ # The DETERMINISTIC codes a document's tags name, deduplicated, in the order
129
+ # they appear. This is the actionable half of the scoreboard: not "how many
130
+ # clauses exist" but "which rules here are decided by a script".
131
+ ledger_doc_codes() {
132
+ grep -oE "$DETERMINISTIC_TAG" "$1" 2>/dev/null \
133
+ | sed -E 's/.* → ([A-Za-z0-9_:-]+)\]/\1/' | sort -u
134
+ }
135
+
136
+ # The leaf audit a dispatch façade runs for a code, e.g. LOG → logging.sh. The
137
+ # .sh row is the single source of truth for which script decides a code, so a
138
+ # code that loses its helper stops claiming enforcement here the same commit.
139
+ ledger_code_script() {
140
+ local code="$1" script
141
+ script="$(grep -hoE "^dispatch_run_helper +\"$code\" +\"[^\"]+\"" "$LEDGER_ROOT"/dispatch/*.sh 2>/dev/null \
142
+ | sed -E 's/.*"([^"]+)"$/\1/' | head -1)"
143
+ printf '%s' "${script:-$UNWIRED_CODE_LABEL}"
144
+ }
145
+
146
+ # A DETERMINISTIC tag whose code has no helper row is a claim with nothing
147
+ # behind it — named rather than hidden, because that is exactly the drift this
148
+ # ledger exists to surface.
149
+ UNWIRED_CODE_LABEL="no helper row"
150
+
151
+ # docs/*.md paths any dispatch façade tells the agent to read. Both the bare
152
+ # form and the ~/Projects/dotfiles/-anchored form appear in the corpus, so the
153
+ # anchor is stripped before comparison.
154
+ ledger_cited_docs() {
155
+ grep -rhoE '(~/Projects/dotfiles/)?docs/[A-Za-z0-9_/-]+\.md' "$LEDGER_ROOT"/dispatch/*.md 2>/dev/null \
156
+ | sed -E 's|^~/Projects/dotfiles/||' \
157
+ | sort -u
158
+ }
159
+
160
+ # Membership test against a registry array passed by name-expansion.
161
+ ledger_in_list() {
162
+ local needle="$1"; shift
163
+ local item
164
+ for item in "$@"; do [ "$item" = "$needle" ] && return 0; done
165
+ return 1
166
+ }
167
+
168
+ # True when a cited path sits under a tree another repository owns.
169
+ ledger_has_prefix() {
170
+ local doc="$1" prefix
171
+ for prefix in "${EXCLUDED_PREFIXES[@]}"; do
172
+ case "$doc" in "$prefix"*) return 0 ;; esac
173
+ done
174
+ return 1
175
+ }
176
+
177
+ # --- trigger scope ------------------------------------------------------------
178
+ #
179
+ # A rule can only fire when something the agent edits matches its façade's
180
+ # scope, and that scope is declared exactly once: the `dispatch_init` line of
181
+ # the façade's executable. Reading it here means the ledger, the doc-read
182
+ # check, and the dispatcher itself can never disagree about what fires.
183
+
184
+ # dispatch/lib.sh is the shared sourcing target rather than a façade: it
185
+ # declares no scope of its own and must never be read as a glob-less one.
186
+ DISPATCH_LIB_BASENAME="lib.sh"
187
+
188
+ # The executable façades: dispatch/*.sh minus the shared library.
189
+ ledger_facade_scripts() {
190
+ local file
191
+ for file in "$LEDGER_ROOT"/dispatch/*.sh; do
192
+ [ -f "$file" ] || continue
193
+ [ "$(basename "$file")" = "$DISPATCH_LIB_BASENAME" ] && continue
194
+ printf '%s\n' "$file"
195
+ done
196
+ }
197
+
198
+ # The file globs a façade declares, space-separated. dispatch_init quotes its
199
+ # language double and every glob single, so the single-quoted run IS the scope.
200
+ # Empty output means the façade declares no scope — the caller reds on that.
201
+ ledger_facade_globs() {
202
+ grep -hE '^[[:space:]]*dispatch_init[[:space:]]' "$1" 2>/dev/null \
203
+ | head -1 | grep -oE "'[^']+'" | tr -d "'" | tr '\n' ' '
204
+ }
205
+
206
+ # How many paths on stdin fall inside a glob set. The unquoted `case` pattern
207
+ # is the match itself — the same expansion dispatch_resolve_files applies to
208
+ # explicit file arguments, so a fire count means what the dispatcher means.
209
+ ledger_match_count() {
210
+ local globs="$1" path glob matches=0
211
+ while IFS= read -r path; do
212
+ [ -n "$path" ] || continue
213
+ for glob in $globs; do
214
+ # shellcheck disable=SC2254 # the glob is data; expansion performs the match
215
+ case "$path" in $glob) matches=$((matches + 1)); break ;; esac
216
+ done
217
+ done
218
+ printf '%d' "$matches"
219
+ }
220
+
221
+ # The façade stems whose page cites a rule doc. Substring matching catches both
222
+ # the bare and the ~/Projects/dotfiles/-anchored spelling of the same path.
223
+ ledger_facades_citing() {
224
+ local doc="$1" page
225
+ for page in "$LEDGER_ROOT"/dispatch/*.md; do
226
+ [ -f "$page" ] || continue
227
+ grep -qF "$doc" "$page" 2>/dev/null && printf '%s\n' "$(basename "$page" .md)"
228
+ done
229
+ }
230
+
231
+ # Whether anything can carry a rule doc into a diff mechanically:
232
+ # mechanical — a citing façade declares a file scope
233
+ # latent — every citing façade is prose the agent must choose to read
234
+ # uncited — no façade page names the doc at all
235
+ # Tree-only, with no history in it, so the scoreboard stays a pure function of
236
+ # the checkout.
237
+ TRIGGER_MECHANICAL="mechanical"
238
+ TRIGGER_LATENT="latent"
239
+ TRIGGER_UNCITED="uncited"
240
+
241
+ ledger_doc_trigger() {
242
+ local doc="$1" stem script cited=1
243
+ while IFS= read -r stem; do
244
+ [ -n "$stem" ] || continue
245
+ cited=0
246
+ script="$LEDGER_ROOT/dispatch/$stem.sh"
247
+ [ -f "$script" ] || continue
248
+ [ -n "$(ledger_facade_globs "$script")" ] || continue
249
+ printf '%s' "$TRIGGER_MECHANICAL"
250
+ return
251
+ done < <(ledger_facades_citing "$doc")
252
+ [ "$cited" -eq 0 ] && { printf '%s' "$TRIGGER_LATENT"; return; }
253
+ printf '%s' "$TRIGGER_UNCITED"
254
+ }
255
+
256
+ # The citing façade stems as one comma-joined field, or an em dash when none.
257
+ ledger_doc_facade_list() {
258
+ local doc="$1" stem stems=""
259
+ while IFS= read -r stem; do
260
+ [ -n "$stem" ] || continue
261
+ stems="${stems:+$stems,}$stem"
262
+ done < <(ledger_facades_citing "$doc")
263
+ printf '%s' "${stems:-—}"
264
+ }
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env bash
2
+ # rule-ledger.sh — what each rule document actually enforces.
3
+ #
4
+ # audits/rule-ledger.sh --census one row per registered rule doc
5
+ #
6
+ # Reports the enforcement shape of the docs/ rule tier: how many normative
7
+ # clauses a document carries, and how many of those are machine-checked
8
+ # (DETERMINISTIC), agent-judged (JUDGMENT), acknowledged prose (UNENFORCED), or
9
+ # untriaged (untagged). Counts INFORM and never fail the build. The only census
10
+ # failure is STRUCTURAL: a rule doc cited by a dispatch façade that appears in
11
+ # neither the registry nor the exclusion list, or a registered path missing from
12
+ # disk — both mean the ledger is lying about coverage.
13
+ #
14
+ # The census answers the other half too: a rule that reads perfectly still
15
+ # governs nothing if its façade declares no scope, because nothing it says can
16
+ # ever reach a diff. That is structural, and answerable from the tree.
17
+ #
18
+ # Exit: 0 clean · 1 structural violation · 2 usage error. Writes nothing.
19
+ set -uo pipefail
20
+
21
+ # `sort` orders by locale, and this script's output is byte-compared by
22
+ # `make audit`. Without a pinned collation the scoreboard is a function of the
23
+ # tree AND the contributor's LC_ALL: `LC_ALL=C` sorts `A-Z B-X a_y`, en_US.UTF-8
24
+ # sorts `a_y A-Z B-X`. Invisible while every document carries one code, and a
25
+ # guaranteed spurious red the moment one carries two.
26
+ export LC_ALL=C
27
+
28
+ # A hook exports GIT_DIR/GIT_INDEX_FILE at the repository it fired in; any git
29
+ # a child runs would silently retarget there. The library resolves LEDGER_ROOT
30
+ # with git, so the scope dies here, before it is sourced.
31
+ unset GIT_DIR GIT_INDEX_FILE GIT_WORK_TREE GIT_COMMON_DIR GIT_PREFIX \
32
+ GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES
33
+
34
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
35
+ # shellcheck source=audits/rule-ledger-lib.sh
36
+ source "$HERE/rule-ledger-lib.sh"
37
+
38
+ MODE_CENSUS="census"
39
+ MODE_WRITE="write"
40
+ MODE_CHECK="check"
41
+ SCOREBOARD_PATH="docs/RULE_ENFORCEMENT.md"
42
+ SCOREBOARD_FIX_COMMAND="bash audits/rule-ledger.sh --write $SCOREBOARD_PATH"
43
+ USAGE="usage: $0 --census | --write <file> | --check"
44
+
45
+ MODE=""
46
+ WRITE_TARGET=""
47
+ while [ "$#" -gt 0 ]; do
48
+ case "$1" in
49
+ --census) MODE="$MODE_CENSUS" ;;
50
+ --check) MODE="$MODE_CHECK" ;;
51
+ --write)
52
+ shift
53
+ WRITE_TARGET="${1:-}"
54
+ [ -n "$WRITE_TARGET" ] || { printf '%s\n--write takes a target file\n' "$USAGE" >&2; exit 2; }
55
+ MODE="$MODE_WRITE"
56
+ ;;
57
+ -h|--help) printf '%s\n' "$USAGE"; exit 0 ;;
58
+ *) printf 'unknown arg: %s\n%s\n' "$1" "$USAGE" >&2; exit 2 ;;
59
+ esac
60
+ shift
61
+ done
62
+ [ -n "$MODE" ] || { printf '%s\n' "$USAGE" >&2; exit 2; }
63
+
64
+ if [[ -t 1 ]]; then G=$'\033[32m'; R=$'\033[31m'; BO=$'\033[1m'; X=$'\033[0m'
65
+ else G=''; R=''; BO=''; X=''; fi
66
+ RC=0
67
+ fail() { printf ' %s🔴 %s%s\n' "$R" "$1" "$X" >&2; RC=1; }
68
+ okln() { printf ' %s🟢%s %s\n' "$G" "$X" "$1"; }
69
+
70
+ # One machine-parseable row per registered document. Field order is stable and
71
+ # every value is a bare integer, so a caller greps `untagged=0` rather than
72
+ # parsing a table.
73
+ census_row() {
74
+ local doc="$1"
75
+ local counts clauses det judgment unenforced untagged
76
+ if ! counts="$(ledger_doc_counts "$LEDGER_ROOT/$doc")"; then
77
+ fail "doc=$doc MISSING from disk — registered in rule-ledger-lib.sh REGISTERED_DOCS"
78
+ return
79
+ fi
80
+ read -r clauses det judgment unenforced untagged <<< "$counts"
81
+ printf ' doc=%-44s clauses=%-4s det=%-4s judgment=%-4s unenforced=%-4s untagged=%s\n' \
82
+ "$doc" "$clauses" "$det" "$judgment" "$unenforced" "$untagged"
83
+ }
84
+
85
+ # A façade that tells the agent to read a rule doc the ledger does not know
86
+ # about means the scoreboard under-reports the corpus. Either register it or
87
+ # declare it a non-rule document — silence is the one outcome not allowed.
88
+ check_registry_parity() {
89
+ local doc
90
+ while IFS= read -r doc; do
91
+ [ -n "$doc" ] || continue
92
+ ledger_in_list "$doc" "${REGISTERED_DOCS[@]}" && continue
93
+ ledger_in_list "$doc" "${EXCLUDED_DOCS[@]}" && continue
94
+ ledger_has_prefix "$doc" && continue
95
+ fail "doc=$doc cited by a dispatch façade but in neither REGISTERED_DOCS nor EXCLUDED_DOCS (audits/rule-ledger-lib.sh)"
96
+ done < <(ledger_cited_docs)
97
+ }
98
+
99
+ # A façade executable that declares no `dispatch_init` scope carries rules no
100
+ # diff can ever reach. It is the one trigger question worth failing on, and it
101
+ # is answerable from the tree — unlike "has this fired lately", which depends on
102
+ # what happened to get committed recently and gates nothing.
103
+ check_facade_scopes() {
104
+ local script stem
105
+ while IFS= read -r script; do
106
+ [ -n "$script" ] || continue
107
+ [ -n "$(ledger_facade_globs "$script")" ] && continue
108
+ stem="$(basename "$script" .sh)"
109
+ fail "facade=$stem declares no dispatch_init scope — dispatch/$stem.sh (nothing it carries can ever fire)"
110
+ done < <(ledger_facade_scripts)
111
+ }
112
+
113
+ run_census() {
114
+ printf '%sRULE ENFORCEMENT LEDGER — census%s\n' "$BO" "$X"
115
+ for entry in "${REGISTERED_DOCS[@]}"; do census_row "$entry"; done
116
+ printf '\n%sstructural checks%s\n' "$BO" "$X"
117
+ check_registry_parity
118
+ check_facade_scopes
119
+ [ "$RC" -eq 0 ] && okln "every cited rule doc is registered; every façade declares a scope"
120
+ return 0
121
+ }
122
+
123
+ scoreboard_legend() {
124
+ cat <<'LEGEND'
125
+ ## How to read this
126
+
127
+ - **enforced by** — the rules in this document a script decides, and the script
128
+ that decides each. This is the column worth acting on: it is the difference
129
+ between a rule that holds and a rule that is merely written down. A code
130
+ reading `no helper row` is a `[DETERMINISTIC → …]` tag with nothing behind it.
131
+ - **judged / acknowledged** — clauses an agent must weigh (`[JUDGMENT → CODE]`)
132
+ and clauses nobody checks on purpose (`[UNENFORCED → reason]`, the reason
133
+ stated where a reader meets it).
134
+ - **classified** — how much of the document has been triaged at all. The
135
+ denominator counts lines carrying a normative keyword outside headings and
136
+ tables — a keyword heuristic, not a parse. It informs; it never gates.
137
+ - **trigger** — `mechanical` when a citing façade declares a file scope,
138
+ `latent` when every citing façade is prose the agent must choose to read,
139
+ `uncited` when no façade names the document at all.
140
+
141
+ A tag alone on its line covers every clause under it until the next heading; a
142
+ tag at the end of a sentence covers that sentence. Same grammar the
143
+ `dispatch/*.md` façades use, read by the same code.
144
+
145
+ Everything here is a function of the tree alone — no timestamps, no commit
146
+ hashes, no history — so the file reproduces from a checkout and any diff means
147
+ the corpus moved.
148
+ LEGEND
149
+ }
150
+
151
+ # The scoreboard body: what each document actually enforces, then how much of it
152
+ # has been triaged. Deterministic by construction — registry order in, content
153
+ # out — so `--check` can regenerate and byte-compare.
154
+ render_scoreboard() {
155
+ local doc counts clauses det judgment unenforced untagged code codes
156
+ local sum_clauses=0 sum_classified=0
157
+ printf '# Rule enforcement ledger\n\n'
158
+ printf 'Generated by `audits/rule-ledger.sh`. Never hand-edit: `make audit`\n'
159
+ printf 'regenerates this file and fails on any difference. To update it, run\n'
160
+ printf '`%s`.\n\n' "$SCOREBOARD_FIX_COMMAND"
161
+ printf '| Rule document | enforced by | judged | acknowledged | classified | trigger |\n'
162
+ printf '|---|---|--:|--:|--:|---|\n'
163
+ for doc in "${REGISTERED_DOCS[@]}"; do
164
+ counts="$(ledger_doc_counts "$LEDGER_ROOT/$doc")" || return 1
165
+ read -r clauses det judgment unenforced untagged <<< "$counts"
166
+ codes=""
167
+ while IFS= read -r code; do
168
+ [ -n "$code" ] || continue
169
+ codes="${codes:+$codes<br>}\`$code\` → \`$(ledger_code_script "$code")\`"
170
+ done < <(ledger_doc_codes "$LEDGER_ROOT/$doc")
171
+ printf '| `%s` | %s | %s | %s | %s/%s | %s |\n' \
172
+ "$doc" "${codes:-—}" "$judgment" "$unenforced" \
173
+ "$(( clauses - untagged ))" "$clauses" "$(ledger_doc_trigger "$doc")"
174
+ sum_clauses=$(( sum_clauses + clauses ))
175
+ sum_classified=$(( sum_classified + clauses - untagged ))
176
+ done
177
+ printf '| **corpus** | | | | **%s/%s** | |\n\n' "$sum_classified" "$sum_clauses"
178
+ scoreboard_legend
179
+ }
180
+
181
+ run_write() {
182
+ local target="$1" rendered
183
+ case "$target" in /*) ;; *) target="$LEDGER_ROOT/$target" ;; esac
184
+ rendered="$(mktemp)"
185
+ trap 'rm -f "$rendered"' EXIT
186
+ if render_scoreboard > "$rendered"; then
187
+ cat "$rendered" > "$target"
188
+ printf 'wrote %s\n' "$target"
189
+ else
190
+ fail "render aborted — a registered doc is missing from disk; run --census for the name"
191
+ fi
192
+ rm -f "$rendered"
193
+ trap - EXIT
194
+ return 0
195
+ }
196
+
197
+ # Currency, not content: the committed file must equal what this tree renders.
198
+ # A stale scoreboard is a red because a wrong number read as fact is worse than
199
+ # no number at all.
200
+ run_check() {
201
+ local target="$LEDGER_ROOT/$SCOREBOARD_PATH" rendered
202
+ rendered="$(mktemp)"
203
+ trap 'rm -f "$rendered"' EXIT
204
+ printf '%sRULE ENFORCEMENT LEDGER — scoreboard currency%s\n' "$BO" "$X"
205
+ if ! render_scoreboard > "$rendered"; then
206
+ fail "a registered doc is missing from disk — run --census for the name"
207
+ elif [ ! -f "$target" ]; then
208
+ fail "$SCOREBOARD_PATH does not exist — regenerate: $SCOREBOARD_FIX_COMMAND"
209
+ elif ! cmp -s "$rendered" "$target"; then
210
+ fail "$SCOREBOARD_PATH is stale — regenerate: $SCOREBOARD_FIX_COMMAND"
211
+ else
212
+ okln "$SCOREBOARD_PATH matches the tree"
213
+ fi
214
+ rm -f "$rendered"
215
+ trap - EXIT
216
+ return 0
217
+ }
218
+
219
+ case "$MODE" in
220
+ "$MODE_CENSUS") run_census ;;
221
+ "$MODE_WRITE") run_write "$WRITE_TARGET" ;;
222
+ "$MODE_CHECK") run_check ;;
223
+ esac
224
+
225
+ exit "$RC"
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env bash
2
+ # rule-paths.sh — residence checks: rule citations resolve inside the repository
3
+ # that installed them.
4
+ #
5
+ # The incident this encodes (M143): an agent in an agentsfleet worktree
6
+ # resolved "reads docs/REST_API_DESIGN_GUIDELINES.md" against its own
7
+ # repository, found nothing — under the old thin-distribution model, consumer
8
+ # repos carried no rule copies at all — and designed an HTTP surface from
9
+ # memory instead of the guide.
10
+ #
11
+ # M03_001 closed that gap by materialising rule docs into every consuming
12
+ # repository (`orly init`/`orly update`, keyed by the profile's selected
13
+ # packs). A citation now resolving to a real local file IS the fix, so the
14
+ # invariant inverts: the ~/Projects/dotfiles/ anchor becomes wrong wherever
15
+ # it appears in a surface a materialised repository also carries — it names a
16
+ # path that will not exist on any machine but this one.
17
+ #
18
+ # residence — dotfiles-resident rule docs are cited relative to the
19
+ # installing repository, never through the
20
+ # ~/Projects/dotfiles/ anchor, in every surface `orly init`
21
+ # materialises. AGENTS.md (byte-capped) carries the new
22
+ # resolution doctrine, zero CWD-relative hrefs, and no
23
+ # residence-contradicting rows. Files that never leave this
24
+ # checkout (RULE_PATH_ENGINE_ONLY — governance-editing
25
+ # façades genuinely about the engine's own source) are
26
+ # exempt; their absolute citations are correct.
27
+ # (Reachability was the other half of this file: a settings allow-rule
28
+ # granting Read(~/Projects/dotfiles/**) so an agent could read rule docs out
29
+ # of a dotfiles checkout. M03_001 inverted the model — rules resolve inside
30
+ # the repository that installed them — and the engine now lives in its own
31
+ # repository, where there is no dotfiles anchor to reach and no personal
32
+ # settings file to assert against. It went with the split.)
33
+ #
34
+ # Sourced by audits/agents-md.sh (labels: "rule-path residence",
35
+ # Classifier: DOTFILES_RESIDENT in audits/data.sh.
36
+ #
37
+ # check_rule_residence <ROOT> # echoes FAIL lines, returns 0/1
38
+
39
+ # Non-dispatch surfaces read from consumer-repo CWDs; dispatch/*.md is globbed.
40
+ RULE_PATH_SURFACES=(
41
+ "docs/TEMPLATE.md"
42
+ "docs/EXECUTE_DOC_READS.md"
43
+ "docs/greptile-learnings/RULES.md"
44
+ )
45
+
46
+ # Governance-editing façades describe how to edit THIS engine's own source
47
+ # (core/**, packs/**, schemas/**, src/**, the registry, the rendered AGENTS.md) — a workflow that only ever
48
+ # runs inside this checkout. Their absolute citations are correct, not a
49
+ # residual of the old model; a materialised copy would never satisfy them
50
+ # because there is nothing to materialise (a consumer never receives
51
+ # core/, registry.json, or this checkout's own dispatch/edit_rules.md).
52
+ RULE_PATH_ENGINE_ONLY=(
53
+ "dispatch/edit_rules.md"
54
+ )
55
+
56
+ check_rule_residence() {
57
+ local root="$1" rc=0
58
+ local agents="$root/AGENTS.md" # separate line: $root must be set before it expands here
59
+
60
+ # (a) resolution doctrine present in the rendered rules.
61
+ grep -qF 'Rule paths resolve relative to this repository' "$agents" \
62
+ || { echo "FAIL: resolution doctrine missing from AGENTS.md"; rc=1; }
63
+
64
+ # (b) zero CWD-relative markdown hrefs — AGENTS.md is served as
65
+ # ~/.claude/CLAUDE.md, so ](./…) and ](../…) resolve against garbage.
66
+ if grep -qE '\]\((\./|\.\./)' "$agents"; then
67
+ echo "FAIL: AGENTS.md carries $(grep -cE '\]\((\./|\.\./)' "$agents") CWD-relative markdown href(s)"
68
+ rc=1
69
+ fi
70
+
71
+ # Resident docs/… names derived from DOTFILES_RESIDENT (single classifier).
72
+ local d names=()
73
+ for d in "${DOTFILES_RESIDENT[@]}"; do
74
+ [[ "$d" == docs/* ]] && names+=("${d#docs/}")
75
+ done
76
+
77
+ # (c) no AGENTS.md line may pair a resident doc with "(product repo)" — the
78
+ # misread that sent the M143 agent to the wrong repository. Still a real
79
+ # mistake under the new model: a doc this array names IS materialised here.
80
+ local alt
81
+ alt=$(IFS='|'; printf '%s' "${names[*]%.md}")
82
+ if grep -E "docs/(${alt})\.md" "$agents" | grep -qF '(product repo)'; then
83
+ echo "FAIL: AGENTS.md line pairs a dotfiles-resident doc with '(product repo)'"
84
+ rc=1
85
+ fi
86
+
87
+ # (d) surfaces materialised by orly init must cite resident docs relative to
88
+ # the installing repository — never through the ~/Projects/dotfiles/ anchor,
89
+ # which names a path absent on every machine but this one. Engine-only
90
+ # façades (RULE_PATH_ENGINE_ONLY) are exempt: their absolute citations are
91
+ # correct because nothing is ever materialised to satisfy them.
92
+ local f rel is_exempt e hits
93
+ for f in "$agents" "$root"/dispatch/*.md "${RULE_PATH_SURFACES[@]/#/${root}/}"; do
94
+ [[ -f "$f" ]] || continue
95
+ rel="${f#"$root"/}"
96
+ is_exempt=0
97
+ for e in "${RULE_PATH_ENGINE_ONLY[@]}"; do [[ "$e" == "$rel" ]] && is_exempt=1; done
98
+ [[ "$is_exempt" -eq 1 ]] && continue
99
+ hits=$(LC_ALL=C grep -noE '~/Projects/dotfiles/(docs|dispatch|audits)/[A-Za-z0-9_./-]+\.(md|sh)' "$f" | head -5)
100
+ if [[ -n "$hits" ]]; then
101
+ echo "FAIL: anchored citation in $rel (should resolve relative to the installing repository):"
102
+ printf ' %s\n' "$hits"
103
+ rc=1
104
+ fi
105
+ done
106
+
107
+ return $rc
108
+ }