create-afe-project 0.0.0-stage → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +458 -0
- package/LICENSE +21 -0
- package/README.md +77 -2
- package/bin/create-afe-project.js +123 -0
- package/package.json +43 -4
- package/src/answers.js +147 -0
- package/src/config.js +151 -0
- package/src/project.js +217 -0
- package/src/render.js +51 -0
- package/template/.claude/harness.conf +50 -0
- package/template/.claude/hooks/post-edit-check.sh +41 -0
- package/template/.claude/hooks/protect-docs.sh +315 -0
- package/template/.claude/hooks/session-start.sh +81 -0
- package/template/.claude/hooks/stop-check-decisions.sh +25 -0
- package/template/.claude/hooks/stop-check-docs.sh +29 -0
- package/template/.claude/hooks/stop-check-fast.sh +33 -0
- package/template/.claude/hooks/stop-check-template.sh +23 -0
- package/template/.claude/hooks/stop-run-project-hooks.sh +27 -0
- package/template/.claude/path-classes +102 -0
- package/template/.claude/settings.project.json +5 -0
- package/template/.claude/settings.template.json +92 -0
- package/template/.claude/skills/afe-init/SKILL.md +104 -0
- package/template/.claude/skills/create-pr/SKILL.md.jinja +110 -0
- package/template/.claude/skills/update-template/SKILL.md +40 -0
- package/template/.claude/skills/{% if code_review %}review-code{% endif %}/SKILL.md.jinja +50 -0
- package/template/.claude/skills/{% if docs_review %}docs-review{% endif %}/SKILL.md +60 -0
- package/template/.claude/skills/{% if dual_review %}dual-review{% endif %}/SKILL.md +58 -0
- package/template/.github/workflows/decisions.yml.jinja +57 -0
- package/template/.github/workflows/docs.yml.jinja +40 -0
- package/template/.github/workflows/{% if code_review %}code-review.yml{% endif %}.jinja +54 -0
- package/template/.github/workflows/{% if docs_review %}docs-review.yml{% endif %}.jinja +52 -0
- package/template/.github/workflows/{% if fail_without_change %}fail-without-change.yml{% endif %}.jinja +42 -0
- package/template/.github/workflows/{% if secret_scan %}secret-scan.yml{% endif %}.jinja +47 -0
- package/template/.markdownlint-cli2.jsonc +27 -0
- package/template/.template/owned.txt.jinja +65 -0
- package/template/AGENTS.md.jinja +91 -0
- package/template/CLAUDE.md +3 -0
- package/template/README.md.jinja +53 -0
- package/template/_gitignore +14 -0
- package/template/docs/README.md.jinja +44 -0
- package/template/docs/contributing.md.jinja +206 -0
- package/template/docs/decisions/adr-template.md +38 -0
- package/template/docs/documentation-rules.md.jinja +200 -0
- package/template/docs/testing-rules.md +80 -0
- package/template/scripts/check-decisions.sh +382 -0
- package/template/scripts/check-docs.sh +20 -0
- package/template/scripts/check-links.mjs +142 -0
- package/template/scripts/check-project.sh +27 -0
- package/template/scripts/check-template.sh +88 -0
- package/template/scripts/check.sh +21 -0
- package/template/scripts/env.sh +29 -0
- package/template/scripts/gen-decisions-index.sh +57 -0
- package/template/scripts/gen-settings.sh +14 -0
- package/template/scripts/lib/classes.sh +89 -0
- package/template/scripts/lib/git-base.sh +25 -0
- package/template/scripts/lib/harness-conf.sh +48 -0
- package/template/scripts/lib/json.mjs +160 -0
- package/template/scripts/pr-metrics.sh +80 -0
- package/template/scripts/protect-branch.sh +101 -0
- package/template/scripts/{% if acceptance_check %}check-acceptance.sh{% endif %} +47 -0
- package/template/scripts/{% if fail_without_change %}fail-without-change.sh{% endif %} +88 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PreToolUse hook (Edit, Write, MultiEdit, Bash): a change to a path of class approval, and an
|
|
3
|
+
# irreversible action, need an explicit owner instruction (AGENTS.md, section 4;
|
|
4
|
+
# docs/documentation-rules.md, sections 8 and 9). Class approval are the template-owned files of
|
|
5
|
+
# .template/owned.txt, the generated ownership manifest, answers file, and .claude/settings.json,
|
|
6
|
+
# the documentation map, .claude/path-classes and its `approval <glob>` lines,
|
|
7
|
+
# .claude/settings.project.json, .mcp.json, the glob lines of a legacy .claude/protected-paths,
|
|
8
|
+
# ADRs that exist on main, and ADRs that are no longer `proposed`. The irreversible actions are the
|
|
9
|
+
# commands that irreversible() below names. A target path is judged by the git repository that holds it when that
|
|
10
|
+
# repository has .template/owned.txt, so a sibling worktree or another generated project is
|
|
11
|
+
# guarded by its own paths; otherwise by the session's project (CLAUDE_PROJECT_DIR) when the path
|
|
12
|
+
# is inside it; any other path is not guarded. A relative path of a shell command resolves
|
|
13
|
+
# against the hook input's `cwd`. The hook turns such a change into a
|
|
14
|
+
# permission prompt for the owner (`permissionDecision: ask`); in headless runs Claude Code denies
|
|
15
|
+
# it. DOCS_GUARD=off in the Claude Code environment disables the guard for scripted owner-driven
|
|
16
|
+
# runs, such as a template update. This is a guard against unreviewed changes, not a security
|
|
17
|
+
# boundary: a command that hides the target path (for example inside a script file, behind a
|
|
18
|
+
# variable, or in a directory changed by a script) is not detected.
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
[ "${DOCS_GUARD:-on}" = "off" ] && exit 0
|
|
21
|
+
|
|
22
|
+
# Without Node.js the hook cannot read the tool call; block it rather than allow everything.
|
|
23
|
+
command -v node >/dev/null || { echo "protect-docs: Node.js is required (README.md, Checks)" >&2; exit 2; }
|
|
24
|
+
json="$(cd "$(dirname "$0")/../.." && pwd)/scripts/lib/json.mjs"
|
|
25
|
+
{ IFS= read -r -d '' tool; IFS= read -r -d '' cwd; IFS= read -r -d '' file_path; IFS= read -r -d '' cmd
|
|
26
|
+
IFS= read -r -d '' secret; } < <(node "$json" fields tool_name cwd tool_input.file_path tool_input.command @secret)
|
|
27
|
+
[ -n "$cwd" ] && [ -d "$cwd" ] || cwd=$PWD
|
|
28
|
+
project="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
|
|
29
|
+
[ -n "$project" ] && project=$(cd "$project" 2>/dev/null && pwd -P) || project=
|
|
30
|
+
|
|
31
|
+
# True when $1 matches a line of the list file $2 (shell glob patterns; '#' starts a comment).
|
|
32
|
+
listed() {
|
|
33
|
+
local pat
|
|
34
|
+
[ -f "$2" ] || return 1
|
|
35
|
+
while IFS= read -r pat; do
|
|
36
|
+
case "$pat" in ''|'#'*) continue ;; esac
|
|
37
|
+
# shellcheck disable=SC2254
|
|
38
|
+
case "$1" in $pat) return 0 ;; esac
|
|
39
|
+
done < "$2"
|
|
40
|
+
return 1
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Prints why $1 is protected, or nothing when it is not.
|
|
44
|
+
protected_reason() {
|
|
45
|
+
local p="$1" abs rel dir rest seg norm climbed=1 root
|
|
46
|
+
case "$p" in /*) abs="$p" ;; *) abs="$cwd/$p" ;; esac
|
|
47
|
+
# Resolve "..", "//", and symbolic links in the part of the path that exists. In the part that
|
|
48
|
+
# does not exist yet, fold "." and "x/.."; a ".." left over climbs from the existing directory,
|
|
49
|
+
# and then the folded path is resolved once more, as it may enter another existing directory.
|
|
50
|
+
while [ -n "$climbed" ]; do
|
|
51
|
+
dir=$(dirname "$abs") rest=$(basename "$abs") norm= climbed=
|
|
52
|
+
while [ ! -d "$dir" ]; do rest="$(basename "$dir")/$rest"; dir=$(dirname "$dir"); done
|
|
53
|
+
dir=$(cd "$dir" && pwd -P) || return 0
|
|
54
|
+
while [ -n "$rest" ]; do
|
|
55
|
+
seg=${rest%%/*}
|
|
56
|
+
if [ "$seg" = "$rest" ]; then rest=; else rest=${rest#*/}; fi
|
|
57
|
+
case "$seg" in
|
|
58
|
+
''|.) ;;
|
|
59
|
+
..) if [ -z "$norm" ]; then dir=$(dirname "$dir") climbed=1
|
|
60
|
+
elif [ "${norm%/*}" = "$norm" ]; then norm=
|
|
61
|
+
else norm=${norm%/*}; fi ;;
|
|
62
|
+
*) norm=${norm:+$norm/}$seg ;;
|
|
63
|
+
esac
|
|
64
|
+
done
|
|
65
|
+
abs="$dir/$norm"
|
|
66
|
+
done
|
|
67
|
+
# The generated project that holds the path: its git repository when that has
|
|
68
|
+
# .template/owned.txt (a sibling worktree, another generated project), otherwise the session's
|
|
69
|
+
# project when the path is inside it (no git yet, a nested repository, a missing manifest).
|
|
70
|
+
root=$(git -C "$dir" rev-parse --show-toplevel 2>/dev/null) && root=$(cd "$root" && pwd -P) &&
|
|
71
|
+
[ -f "$root/.template/owned.txt" ] || root=$project
|
|
72
|
+
[ -n "$root" ] || return 0
|
|
73
|
+
rel="${abs#"$root"/}"
|
|
74
|
+
[ "$rel" != "$abs" ] || return 0
|
|
75
|
+
if [ "$rel" = AGENTS.md ]; then
|
|
76
|
+
echo "template-owned outside its project block; only the project block may change here"
|
|
77
|
+
return
|
|
78
|
+
fi
|
|
79
|
+
if approval_listed "$rel" "$root/.claude/path-classes"; then
|
|
80
|
+
echo "class approval in .claude/path-classes"
|
|
81
|
+
return
|
|
82
|
+
fi
|
|
83
|
+
if listed "$rel" "$root/.template/owned.txt"; then
|
|
84
|
+
echo "template-owned; change it in the template repository and update (docs/contributing.md, section 5)"
|
|
85
|
+
return
|
|
86
|
+
fi
|
|
87
|
+
case "$rel" in
|
|
88
|
+
.template/owned.sha256|.afe-answers.json|.copier-answers.yml)
|
|
89
|
+
echo "generated by a template update only" ;;
|
|
90
|
+
.claude/settings.json)
|
|
91
|
+
echo "generated by scripts/gen-settings.sh; change .claude/settings.project.json instead, with the owner" ;;
|
|
92
|
+
.claude/path-classes|.claude/protected-paths)
|
|
93
|
+
echo "the classes of paths, class approval" ;;
|
|
94
|
+
.claude/settings.project.json|.mcp.json)
|
|
95
|
+
echo "permissions and MCP servers, class approval" ;;
|
|
96
|
+
docs/README.md)
|
|
97
|
+
echo "documentation map; structure changes need the owner" ;;
|
|
98
|
+
docs/decisions/README.md)
|
|
99
|
+
;; # generated by scripts/gen-decisions-index.sh; scripts/check-decisions.sh checks it
|
|
100
|
+
docs/decisions/*.md)
|
|
101
|
+
if git -C "$root" cat-file -e "origin/main:$rel" 2>/dev/null ||
|
|
102
|
+
{ ! git -C "$root" rev-parse -q --verify origin/main >/dev/null && git -C "$root" cat-file -e "main:$rel" 2>/dev/null; }; then
|
|
103
|
+
echo "ADR that exists on main; only its status line changes (documentation-rules.md, section 5), otherwise supersede it with a new ADR"
|
|
104
|
+
elif [ -f "$abs" ] && ! head -n 8 "$abs" | grep -q -E '^status:[[:space:]]*proposed'; then
|
|
105
|
+
echo "ADR that is no longer proposed; immutable, supersede it with a new ADR"
|
|
106
|
+
fi ;;
|
|
107
|
+
*)
|
|
108
|
+
listed "$rel" "$root/.claude/protected-paths" && echo "listed in .claude/protected-paths" ;;
|
|
109
|
+
esac
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# True when $1 matches an `approval <glob>` line of .claude/path-classes, file $2.
|
|
113
|
+
approval_listed() {
|
|
114
|
+
local cls pat
|
|
115
|
+
[ -f "$2" ] || return 1
|
|
116
|
+
while read -r cls pat || [ -n "$cls" ]; do
|
|
117
|
+
[ "$cls" = approval ] && [ -n "$pat" ] || continue
|
|
118
|
+
# shellcheck disable=SC2254
|
|
119
|
+
case "$1" in $pat) return 0 ;; esac
|
|
120
|
+
done < "$2"
|
|
121
|
+
return 1
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Prints the paths a shell command can write, one per line: redirect targets in every segment, the
|
|
125
|
+
# destination of cp, mv, install, and ln, the files of tee, rm, truncate, and touch, dd's of=, the
|
|
126
|
+
# files of sed -i, perl -i, and git checkout, restore, rm, mv, and apply, and file-like tokens of an
|
|
127
|
+
# interpreter (python, node, ruby, perl, bash, sh), also in the heredoc a python, node, ruby, or
|
|
128
|
+
# perl reads; a heredoc on a line where a shell runs (`bash <<EOF`, `cat <<EOF | sh`,
|
|
129
|
+
# `source /dev/stdin <<EOF`) is parsed as commands. A relative target is joined to
|
|
130
|
+
# the directory of an earlier `cd DIR` with a literal DIR, and a git path argument to a literal
|
|
131
|
+
# `git -C DIR`. The body of any other heredoc is text, not commands; `<<<`, `$((1 << n))`, and
|
|
132
|
+
# `<<` inside quotes start no heredoc. Input files (<) and sources are not targets.
|
|
133
|
+
write_targets() {
|
|
134
|
+
printf '%s\n' "$1" | awk '
|
|
135
|
+
function out(t) { if (t == "") return; if (t !~ /^\// && pre != "") t = pre "/" t; print t }
|
|
136
|
+
function emit_all(from, x, na, A) { na = split(args, A, " "); for (x = from; x <= na; x++) out(A[x]) }
|
|
137
|
+
function files(text, tmp) {
|
|
138
|
+
tmp = text
|
|
139
|
+
while (match(tmp, /[A-Za-z0-9_.\/-]*(\.(md|json|jsonc|toml|yml|yaml|sh|txt|sha256|jinja|conf|pem|key)|\.claude\/(protected-paths|path-classes)|\.env[A-Za-z0-9_.]*)/)) {
|
|
140
|
+
out(substr(tmp, RSTART, RLENGTH)); tmp = substr(tmp, RSTART + RLENGTH)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
BEGIN { dir = "" }
|
|
144
|
+
hd != "" { l = $0; gsub(/^[ \t]+|[ \t]+$/, "", l); if (l == hd) { hd = ""; next }; if (hdint) { pre = dir; files($0) }; next }
|
|
145
|
+
{
|
|
146
|
+
line = $0; delim = ""
|
|
147
|
+
if (match(line, /<<-?[ \t]*["\047]?[A-Za-z_][A-Za-z0-9_]*/)) {
|
|
148
|
+
before = substr(line, 1, RSTART - 1); cand = substr(line, RSTART, RLENGTH)
|
|
149
|
+
dq = gsub(/"/, "\"", before); sq = gsub(/\047/, "\047", before)
|
|
150
|
+
if (before !~ /<$/ && before !~ /\$\(\([^)]*$/ && dq % 2 == 0 && sq % 2 == 0) {
|
|
151
|
+
delim = cand; sub(/^<<-?[ \t]*["\047]?/, "", delim)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
n = split(line, segs, /[|;&]+/)
|
|
155
|
+
for (i = 1; i <= n; i++) {
|
|
156
|
+
s = segs[i]; tmp = s; pre = dir
|
|
157
|
+
m = split(s, w, /[ \t]+/); k = 1
|
|
158
|
+
while (k <= m && (w[k] == "" || w[k] ~ /^[A-Za-z_][A-Za-z0-9_]*=/ || w[k] == "sudo" || w[k] == "command")) k++
|
|
159
|
+
cmd = w[k]; sub(/.*\//, "", cmd)
|
|
160
|
+
if (cmd == "cd") {
|
|
161
|
+
d = w[k + 1]; gsub(/^["\047]+|["\047]+$/, "", d)
|
|
162
|
+
if (d ~ /[$`~*?]/ || d == "" || d == "-") { }
|
|
163
|
+
else if (d ~ /^\//) dir = d; else dir = (dir == "" ? d : dir "/" d)
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
while (match(tmp, />>?[ \t]*["\047]?[A-Za-z0-9_.\/-]+/)) {
|
|
167
|
+
t = substr(tmp, RSTART, RLENGTH); sub(/^>>?[ \t]*["\047]?/, "", t); out(t)
|
|
168
|
+
tmp = substr(tmp, RSTART + RLENGTH)
|
|
169
|
+
}
|
|
170
|
+
if (cmd == "git" && w[k + 1] == "-C") {
|
|
171
|
+
d = w[k + 2]; gsub(/^["\047]+|["\047]+$/, "", d)
|
|
172
|
+
if (d !~ /[$`~*?]/) pre = (d ~ /^\// || dir == "") ? d : dir "/" d
|
|
173
|
+
for (x = k + 1; x + 2 <= m; x++) w[x] = w[x + 2]; m -= 2
|
|
174
|
+
}
|
|
175
|
+
gsub(/[0-9]*>>?[ \t]*["\047]?[^ \t]*/, "", s); gsub(/<<?-?[ \t]*[^ \t]*/, "", s)
|
|
176
|
+
if (cmd != "git") { m = split(s, w, /[ \t]+/); k = 1
|
|
177
|
+
while (k <= m && (w[k] == "" || w[k] ~ /^[A-Za-z_][A-Za-z0-9_]*=/ || w[k] == "sudo" || w[k] == "command")) k++ }
|
|
178
|
+
args = ""
|
|
179
|
+
for (j = k + 1; j <= m; j++) { a = w[j]; gsub(/^["\047]+|["\047]+$/, "", a); if (a !~ /^-/) args = args " " a }
|
|
180
|
+
if (cmd ~ /^(cp|mv|install|ln)$/) { na = split(args, A, " "); if (na) out(A[na]) }
|
|
181
|
+
else if (cmd ~ /^(tee|rm|truncate|touch)$/) emit_all(1)
|
|
182
|
+
else if (cmd == "dd") { na = split(args, A, " "); for (x = 1; x <= na; x++) if (sub(/^of=/, "", A[x])) out(A[x]) }
|
|
183
|
+
else if (cmd ~ /^(sed|perl)$/ && s ~ /[ \t]-[a-zA-Z]*i/) emit_all(1)
|
|
184
|
+
else if (cmd == "git" && w[k + 1] ~ /^(checkout|restore|rm|mv|apply)$/) emit_all(2)
|
|
185
|
+
if (cmd ~ /^(python3?|node|ruby|perl|bash|sh|zsh)$/) files(segs[i])
|
|
186
|
+
if (delim != "" && segs[i] ~ /<</) hdint = (cmd ~ /^(python3?|node|ruby|perl)$/)
|
|
187
|
+
if (cmd ~ /^(bash|sh|zsh|source|\.)$/) hdsh = 1
|
|
188
|
+
}
|
|
189
|
+
if (delim != "" && !hdsh) hd = delim
|
|
190
|
+
hdsh = 0
|
|
191
|
+
}'
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# Prints the first irreversible action in a shell command, or nothing: in any segment, `git push`
|
|
195
|
+
# (after git's global options such as -C DIR) with --force, --force-with-lease,
|
|
196
|
+
# --force-if-includes, --delete, --mirror, --prune, or --tags, a short option that contains f or
|
|
197
|
+
# d (-f, -uf, -d), a refspec that starts with + or :, or a tag (refs/tags/..., or v followed by a
|
|
198
|
+
# digit, the form of a release); `gh pr merge`; `gh release create` or `gh release delete`;
|
|
199
|
+
# `gh repo delete`; `gh workflow run`; `gh api` with the method DELETE; and publishing a package
|
|
200
|
+
# or an image: `npm`, `pnpm`, `yarn`, `bun`, `cargo`, `poetry`, or `uv publish`, `twine upload`,
|
|
201
|
+
# `gem push`, `dotnet nuget push`, `docker` or `podman push`.
|
|
202
|
+
# With "push" as $2 it prints instead the words of the first `git push` that are not options:
|
|
203
|
+
# the remote and the refspecs, one per line, or "-" alone when there are none.
|
|
204
|
+
irreversible() {
|
|
205
|
+
printf '%s\n' "$1" | awk -v mode="${2:-}" '{
|
|
206
|
+
n = split($0, segs, /[|;&]+/)
|
|
207
|
+
for (i = 1; i <= n; i++) {
|
|
208
|
+
m = split(segs[i], w, /[ \t]+/); k = 1
|
|
209
|
+
while (k <= m && (w[k] == "" || w[k] ~ /^[A-Za-z_][A-Za-z0-9_]*=/ || w[k] == "sudo" || w[k] == "command")) k++
|
|
210
|
+
cmd = w[k]; sub(/.*\//, "", cmd)
|
|
211
|
+
s = k + 1
|
|
212
|
+
if (cmd == "git") while (s <= m && w[s] ~ /^-/) s += (w[s] == "-C" || w[s] == "-c") ? 2 : 1
|
|
213
|
+
sub1 = w[s]; sub2 = w[s + 1]
|
|
214
|
+
if (cmd == "git" && sub1 == "push") {
|
|
215
|
+
if (mode == "push") {
|
|
216
|
+
out = ""
|
|
217
|
+
for (j = s + 1; j <= m; j++) { a = w[j]; gsub(/^["\047]+|["\047]+$/, "", a); if (a != "" && a !~ /^-/) out = out a "\n" }
|
|
218
|
+
printf "%s", (out == "" ? "-\n" : out); exit
|
|
219
|
+
}
|
|
220
|
+
for (j = s + 1; j <= m; j++) {
|
|
221
|
+
a = w[j]; gsub(/^["\047]+|["\047]+$/, "", a)
|
|
222
|
+
if (a ~ /^--(force|force-with-lease|force-if-includes|delete|mirror|prune|tags)(=.*)?$/ || a ~ /^-[A-Za-z]*[fd][A-Za-z]*$/ || a ~ /^[+:]/ || a ~ /^(refs\/tags\/|v[0-9])/) {
|
|
223
|
+
print "git push " a; exit
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (mode == "push") continue
|
|
228
|
+
if (cmd == "gh" && ((sub1 == "pr" && sub2 == "merge") || (sub1 == "release" && sub2 ~ /^(create|delete)$/) || (sub1 == "repo" && sub2 == "delete") || (sub1 == "workflow" && sub2 == "run"))) {
|
|
229
|
+
print "gh " sub1 " " sub2; exit
|
|
230
|
+
}
|
|
231
|
+
if (cmd == "gh" && sub1 == "api") {
|
|
232
|
+
for (j = s + 1; j <= m; j++) if ((w[j] ~ /^(-X|--method)$/ && toupper(w[j + 1]) == "DELETE") || toupper(w[j]) ~ /^(-XDELETE|--METHOD=DELETE)$/) { print "gh api DELETE"; exit }
|
|
233
|
+
}
|
|
234
|
+
if ((cmd ~ /^(npm|pnpm|yarn|bun|cargo|poetry|uv)$/ && sub1 == "publish") || (cmd == "twine" && sub1 == "upload") || (cmd == "gem" && sub1 == "push") ||
|
|
235
|
+
(cmd ~ /^(docker|podman)$/ && sub1 == "push") || (cmd == "dotnet" && sub1 == "nuget" && sub2 == "push")) {
|
|
236
|
+
print cmd " " sub1 (cmd == "dotnet" ? " " sub2 : ""); exit
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}'
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
# Prints the default branch when the shell command pushes to it, or nothing. Without a refspec,
|
|
243
|
+
# `git push` pushes the current branch. A default branch that the remote does not have yet (the
|
|
244
|
+
# first push of a new repository) is left to the owner's prompt, not denied.
|
|
245
|
+
pushes_default() {
|
|
246
|
+
local words remote refs r dst def cur
|
|
247
|
+
words=$(irreversible "$1" push)
|
|
248
|
+
[ -n "$words" ] || return 0
|
|
249
|
+
def=$(git -C "$cwd" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null) && def=${def#origin/} || def=main
|
|
250
|
+
if [ "$words" = - ]; then refs=; remote=origin; else remote=$(printf '%s\n' "$words" | head -n 1); refs=$(printf '%s\n' "$words" | tail -n +2); fi
|
|
251
|
+
if [ -z "$refs" ]; then
|
|
252
|
+
cur=$(git -C "$cwd" symbolic-ref --quiet --short HEAD 2>/dev/null) || return 0
|
|
253
|
+
refs=$cur
|
|
254
|
+
fi
|
|
255
|
+
while IFS= read -r r; do
|
|
256
|
+
dst=${r#+}; dst=${dst##*:}; dst=${dst#refs/heads/}
|
|
257
|
+
[ "$dst" = HEAD ] && dst=$(git -C "$cwd" symbolic-ref --quiet --short HEAD 2>/dev/null)
|
|
258
|
+
if [ "$dst" = "$def" ]; then
|
|
259
|
+
git -C "$cwd" ls-remote --exit-code --heads "$remote" "$def" > /dev/null 2>&1 && echo "$def"
|
|
260
|
+
return 0
|
|
261
|
+
fi
|
|
262
|
+
done <<< "$refs"
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
ask_owner() {
|
|
266
|
+
node "$json" ask "Protected file: $1 ($2). Changing it needs an explicit owner instruction (AGENTS.md, section 4). Approve only if you asked for this change; otherwise the agent must report it as an open question."
|
|
267
|
+
exit 0
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
deny_push() {
|
|
271
|
+
node "$json" deny "Pushing to the default branch $1 is not allowed: changes reach it only through a pull request (AGENTS.md, section 4; docs/contributing.md, section 1). Push a branch and open a pull request instead."
|
|
272
|
+
exit 0
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
ask_action() {
|
|
276
|
+
node "$json" ask "Irreversible action: $1. It needs an explicit owner instruction (AGENTS.md, section 4; docs/documentation-rules.md, section 8). Approve only if you asked for it."
|
|
277
|
+
exit 0
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# A push to the default branch is denied before anything asks: an approved prompt must never let
|
|
281
|
+
# it through.
|
|
282
|
+
if [ "$tool" = Bash ] && [ -n "$cmd" ]; then
|
|
283
|
+
branch=$(pushes_default "$cmd")
|
|
284
|
+
[ -n "$branch" ] && deny_push "$branch"
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
# A known token format in what the call writes: secrets never go into the repository.
|
|
288
|
+
if [ -n "$secret" ] && [[ "$tool" =~ ^(Edit|Write|MultiEdit|Bash)$ ]]; then
|
|
289
|
+
node "$json" ask "Possible secret ($secret) in this change. Secrets never go into the repository or a command line; use an environment variable or the CI secret store. Approve only if it is a placeholder or a test fixture."
|
|
290
|
+
exit 0
|
|
291
|
+
fi
|
|
292
|
+
|
|
293
|
+
case "$tool" in
|
|
294
|
+
Edit|Write|MultiEdit)
|
|
295
|
+
path=$file_path
|
|
296
|
+
[ -n "$path" ] || exit 0
|
|
297
|
+
reason=$(protected_reason "$path")
|
|
298
|
+
[ -n "$reason" ] && ask_owner "$path" "$reason"
|
|
299
|
+
;;
|
|
300
|
+
Bash)
|
|
301
|
+
[ -n "$cmd" ] || exit 0
|
|
302
|
+
# Rewriting the manifest is how a local edit of a template-owned file would be hidden.
|
|
303
|
+
if printf '%s' "$cmd" | grep -q -E 'check-template\.sh[^|;&]*--write'; then
|
|
304
|
+
ask_owner ".template/owned.sha256" "generated by a template update only, via Bash"
|
|
305
|
+
fi
|
|
306
|
+
action=$(irreversible "$cmd")
|
|
307
|
+
[ -n "$action" ] && ask_action "$action"
|
|
308
|
+
targets=$(write_targets "$cmd")
|
|
309
|
+
for tok in $targets; do
|
|
310
|
+
reason=$(protected_reason "$tok")
|
|
311
|
+
[ -n "$reason" ] && ask_owner "$tok" "$reason, via Bash"
|
|
312
|
+
done
|
|
313
|
+
;;
|
|
314
|
+
esac
|
|
315
|
+
exit 0
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SessionStart hook: prints the local state of the work in progress, so that a new, resumed, or
|
|
3
|
+
# compacted session starts from facts (docs/documentation-rules.md, section 7): the branch, the
|
|
4
|
+
# base of the change (scripts/lib/git-base.sh) and the commits ahead of it, the count of
|
|
5
|
+
# uncommitted and untracked files, unresolved merge conflicts, and each plan in docs/plans/ with
|
|
6
|
+
# its review-by date, flagged when the date is past (section 5), past ones first. At most 10 lines
|
|
7
|
+
# and local facts only: no network. Outside a git repository only the plans are shown. What it
|
|
8
|
+
# cannot determine it leaves out; it always exits 0.
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
root="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
|
|
11
|
+
[ -n "$root" ] || exit 0
|
|
12
|
+
cd "$root" 2>/dev/null || exit 0
|
|
13
|
+
lines=()
|
|
14
|
+
|
|
15
|
+
if git rev-parse --git-dir > /dev/null 2>&1; then
|
|
16
|
+
if git rev-parse --verify -q HEAD > /dev/null; then
|
|
17
|
+
line="branch $(git symbolic-ref --short -q HEAD || printf 'none (detached at %s)' "$(git rev-parse --short HEAD)")"
|
|
18
|
+
# shellcheck source=scripts/lib/git-base.sh
|
|
19
|
+
if . scripts/lib/git-base.sh 2> /dev/null && base=$(resolve_base 2> /dev/null); then
|
|
20
|
+
name=
|
|
21
|
+
for ref in origin/HEAD origin/main main; do
|
|
22
|
+
git rev-parse --verify -q "$ref^{commit}" > /dev/null 2>&1 || continue
|
|
23
|
+
[ "$(git merge-base "$ref" HEAD 2> /dev/null)" = "$base" ] || continue
|
|
24
|
+
name=$(git rev-parse --abbrev-ref "$ref" 2> /dev/null || echo "$ref")
|
|
25
|
+
break
|
|
26
|
+
done
|
|
27
|
+
ahead=$(git rev-list --count "$base..HEAD" 2> /dev/null)
|
|
28
|
+
if [ -n "$name" ] && [ -n "$ahead" ]; then
|
|
29
|
+
line="$line; base $name at $(git rev-parse --short "$base"), $ahead commits ahead"
|
|
30
|
+
elif [ -z "$name" ]; then
|
|
31
|
+
line="$line; no base branch (origin/HEAD, origin/main, main): only uncommitted changes count"
|
|
32
|
+
fi
|
|
33
|
+
fi
|
|
34
|
+
lines+=("$line")
|
|
35
|
+
fi
|
|
36
|
+
if st=$(git status --porcelain --untracked-files=all 2> /dev/null); then
|
|
37
|
+
untracked=$(printf '%s\n' "$st" | grep -c '^??')
|
|
38
|
+
changed=$(printf '%s\n' "$st" | grep -v '^??' | grep -c .)
|
|
39
|
+
lines+=("uncommitted files: $changed; untracked files: $untracked")
|
|
40
|
+
fi
|
|
41
|
+
conflicts=$(git ls-files -u 2> /dev/null | cut -f2 | sort -u | grep -c .)
|
|
42
|
+
[ "${conflicts:-0}" -gt 0 ] &&
|
|
43
|
+
lines+=("unresolved merge conflicts in $conflicts files: resolve them first (docs/contributing.md, section 5)")
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
shopt -s nullglob
|
|
47
|
+
plans=()
|
|
48
|
+
for p in docs/plans/*.md; do
|
|
49
|
+
[ "$(basename "$p")" = README.md ] || plans+=("$p")
|
|
50
|
+
done
|
|
51
|
+
# SESSION_START_TODAY (YYYY-MM-DD) stands in for the clock, for the tests.
|
|
52
|
+
today=${SESSION_START_TODAY:-$(date +%Y-%m-%d)}
|
|
53
|
+
# Plans past their review-by come first, then those without one, then the rest, so the five shown
|
|
54
|
+
# never hide a past one; the line for the rest counts the past ones among them.
|
|
55
|
+
past=() undated=() ahead=()
|
|
56
|
+
for p in ${plans[@]+"${plans[@]}"}; do
|
|
57
|
+
rb=$(awk 'NR == 1 && $0 != "---" {exit} NR > 1 && $0 == "---" {exit}
|
|
58
|
+
/^review-by:/ {sub(/^review-by:[[:space:]]*/, ""); gsub(/["'\''[:space:]]/, ""); print; exit}' "$p" 2> /dev/null)
|
|
59
|
+
if ! printf '%s' "$rb" | grep -q -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'; then
|
|
60
|
+
undated+=("plan $p: no review-by date")
|
|
61
|
+
elif [[ "$rb" < "$today" ]]; then
|
|
62
|
+
past+=("plan $p: review-by $rb is past; update or delete it (docs/documentation-rules.md, section 5)")
|
|
63
|
+
else
|
|
64
|
+
ahead+=("plan $p: review-by $rb")
|
|
65
|
+
fi
|
|
66
|
+
done
|
|
67
|
+
ordered=(${past[@]+"${past[@]}"} ${undated[@]+"${undated[@]}"} ${ahead[@]+"${ahead[@]}"})
|
|
68
|
+
[ "${#ordered[@]}" -eq 0 ] || for i in "${!ordered[@]}"; do
|
|
69
|
+
if [ "$i" -eq 5 ]; then
|
|
70
|
+
rest="and $(( ${#ordered[@]} - 5 )) more plans in docs/plans/"
|
|
71
|
+
[ "${#past[@]}" -le 5 ] || rest="$rest, $(( ${#past[@]} - 5 )) of them past their review-by"
|
|
72
|
+
lines+=("$rest")
|
|
73
|
+
break
|
|
74
|
+
fi
|
|
75
|
+
lines+=("${ordered[$i]}")
|
|
76
|
+
done
|
|
77
|
+
|
|
78
|
+
[ "${#lines[@]}" -gt 0 ] || exit 0
|
|
79
|
+
echo "Local state at session start (.claude/hooks/session-start.sh):"
|
|
80
|
+
printf -- '- %s\n' "${lines[@]}"
|
|
81
|
+
exit 0
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Stop hook: Claude Code may not finish a turn while the decision-record integrity checks of
|
|
3
|
+
# scripts/check-decisions.sh fail (docs/documentation-rules.md, sections 5 and 7). Merge readiness
|
|
4
|
+
# is not checked here: an agent legitimately stops with a `proposed` record to ask the owner. Exit 2
|
|
5
|
+
# sends the agent back with the check output. DOCS_GUARD=off disables it, as for the protect hook
|
|
6
|
+
# (docs/documentation-rules.md, section 8).
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
[ "${DOCS_GUARD:-on}" = "off" ] && exit 0
|
|
9
|
+
root="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
|
|
10
|
+
[ -n "$root" ] || exit 0
|
|
11
|
+
cd "$root" || exit 0
|
|
12
|
+
# During an unresolved merge conflict (a template update) the checks cannot pass yet; let the
|
|
13
|
+
# agent stop and ask instead of looping.
|
|
14
|
+
if [ -n "$(git ls-files -u 2>/dev/null)" ]; then
|
|
15
|
+
echo "Unresolved merge conflicts: checks resume once they are resolved (docs/contributing.md, section 5)." >&2
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
RALPH_SESSION=$(node scripts/lib/json.mjs get session_id none 2>/dev/null || echo none)
|
|
19
|
+
# shellcheck source=scripts/lib/harness-conf.sh
|
|
20
|
+
. scripts/lib/harness-conf.sh 2>/dev/null || { ralph_block() { printf '%s\n' "$2" >&2; exit 2; }; ralph_pass() { :; }; }
|
|
21
|
+
if out=$(scripts/check-decisions.sh 2>&1); then
|
|
22
|
+
ralph_pass decisions
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
ralph_block decisions "$(printf 'Decision record checks failed; fix them before finishing (docs/documentation-rules.md, section 5):\n%s' "$out")"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Stop hook: Claude Code may not finish a turn while the documentation checks fail. Runs
|
|
3
|
+
# scripts/check-docs.sh only when a Markdown file differs from the base of the change
|
|
4
|
+
# (scripts/lib/git-base.sh), committed or not, so unrelated turns pay nothing. Exit 2 sends the
|
|
5
|
+
# agent back with the check output; a missing tool is reported without blocking.
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
root="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
|
|
8
|
+
[ -n "$root" ] || exit 0
|
|
9
|
+
cd "$root" || exit 0
|
|
10
|
+
# During an unresolved merge conflict (a template update) the checks cannot pass yet; let the
|
|
11
|
+
# agent stop and ask instead of looping.
|
|
12
|
+
if [ -n "$(git ls-files -u 2>/dev/null)" ]; then
|
|
13
|
+
echo "Unresolved merge conflicts: checks resume once they are resolved (docs/contributing.md, section 5)." >&2
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
# shellcheck source=scripts/lib/git-base.sh
|
|
17
|
+
. scripts/lib/git-base.sh
|
|
18
|
+
base=$(resolve_base 2>/dev/null) || exit 0
|
|
19
|
+
changed=$( { git diff --name-only "$base" -- '*.md'; git ls-files --others --exclude-standard -- '*.md'; } 2>/dev/null | head -n 1)
|
|
20
|
+
[ -n "$changed" ] || exit 0
|
|
21
|
+
RALPH_SESSION=$(node scripts/lib/json.mjs get session_id none 2>/dev/null || echo none)
|
|
22
|
+
# shellcheck source=scripts/lib/harness-conf.sh
|
|
23
|
+
. scripts/lib/harness-conf.sh 2>/dev/null || { ralph_block() { printf '%s\n' "$2" >&2; exit 2; }; ralph_pass() { :; }; }
|
|
24
|
+
out=$(scripts/check-docs.sh 2>&1)
|
|
25
|
+
case $? in
|
|
26
|
+
0) ralph_pass docs; exit 0 ;;
|
|
27
|
+
3) printf 'Documentation checks could not run; install the missing tool:\n%s\n' "$out" >&2; exit 1 ;;
|
|
28
|
+
esac
|
|
29
|
+
ralph_block docs "$(printf 'Documentation checks failed; fix them before finishing (AGENTS.md, section 6):\n%s' "$out")"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Stop hook: Claude Code may not finish a turn while the project's fast check fails
|
|
3
|
+
# (docs/documentation-rules.md, section 7). Runs `scripts/env.sh fast`, the CHECK_FAST command of
|
|
4
|
+
# .claude/harness.conf, only when a file other than Markdown differs from the base of the change
|
|
5
|
+
# (scripts/lib/git-base.sh), committed or not, untracked included. It skips when CHECK_FAST is
|
|
6
|
+
# empty or is the default scripts/check-docs.sh, which stop-check-docs.sh already runs for
|
|
7
|
+
# Markdown. Exit 2 sends the agent back with the command's output, up to the Ralph cap.
|
|
8
|
+
set -uo pipefail
|
|
9
|
+
root="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
|
|
10
|
+
[ -n "$root" ] || exit 0
|
|
11
|
+
cd "$root" || exit 0
|
|
12
|
+
# During an unresolved merge conflict (a template update) the checks cannot pass yet; let the
|
|
13
|
+
# agent stop and ask instead of looping.
|
|
14
|
+
if [ -n "$(git ls-files -u 2>/dev/null)" ]; then
|
|
15
|
+
echo "Unresolved merge conflicts: checks resume once they are resolved (docs/contributing.md, section 5)." >&2
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
# shellcheck source=scripts/lib/harness-conf.sh
|
|
19
|
+
. scripts/lib/harness-conf.sh 2>/dev/null || exit 0
|
|
20
|
+
case "$(conf_get CHECK_FAST)" in
|
|
21
|
+
''|scripts/check-docs.sh) exit 0 ;;
|
|
22
|
+
esac
|
|
23
|
+
# shellcheck source=scripts/lib/git-base.sh
|
|
24
|
+
. scripts/lib/git-base.sh
|
|
25
|
+
base=$(resolve_base 2>/dev/null) || exit 0
|
|
26
|
+
changed=$( { git diff --name-only "$base" -- . ':(exclude)*.md'; git ls-files --others --exclude-standard -- . ':(exclude)*.md'; } 2>/dev/null | head -n 1)
|
|
27
|
+
[ -n "$changed" ] || exit 0
|
|
28
|
+
RALPH_SESSION=$(node scripts/lib/json.mjs get session_id none 2>/dev/null || echo none)
|
|
29
|
+
if out=$(scripts/env.sh fast 2>&1); then
|
|
30
|
+
ralph_pass fast
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
ralph_block fast "$(printf 'The fast check (CHECK_FAST in .claude/harness.conf) failed; fix it before finishing (AGENTS.md, section 6):\n%s' "$(printf '%s\n' "$out" | tail -n 60)")"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Stop hook: Claude Code may not finish a turn while a template-owned file differs from the template
|
|
3
|
+
# or AGENTS.md lost its project block (docs/documentation-rules.md, section 9). Exit 2 sends the
|
|
4
|
+
# agent back with the check output. DOCS_GUARD=off disables it, as for the protect hook.
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
[ "${DOCS_GUARD:-on}" = "off" ] && exit 0
|
|
7
|
+
root="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
|
|
8
|
+
[ -n "$root" ] || exit 0
|
|
9
|
+
cd "$root" || exit 0
|
|
10
|
+
# During an unresolved merge conflict (a template update) the checks cannot pass yet; let the
|
|
11
|
+
# agent stop and ask instead of looping.
|
|
12
|
+
if [ -n "$(git ls-files -u 2>/dev/null)" ]; then
|
|
13
|
+
echo "Unresolved merge conflicts: checks resume once they are resolved (docs/contributing.md, section 5)." >&2
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
RALPH_SESSION=$(node scripts/lib/json.mjs get session_id none 2>/dev/null || echo none)
|
|
17
|
+
# shellcheck source=scripts/lib/harness-conf.sh
|
|
18
|
+
. scripts/lib/harness-conf.sh 2>/dev/null || { ralph_block() { printf '%s\n' "$2" >&2; exit 2; }; ralph_pass() { :; }; }
|
|
19
|
+
if out=$(scripts/check-template.sh 2>&1); then
|
|
20
|
+
ralph_pass template
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
ralph_block template "$(printf 'Template checks failed; restore the template-owned files before finishing (docs/documentation-rules.md, section 9):\n%s' "$out")"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Stop hook: runs the project's own Stop hooks, .claude/hooks/project/stop/*.sh in name order, with
|
|
3
|
+
# the same input (docs/documentation-rules.md, section 9). A project hook blocks the agent by
|
|
4
|
+
# exiting with 2; its output goes to the agent. A hook that is not executable blocks too, because
|
|
5
|
+
# a silent gate is worse than a loud one. Other exit codes are reported without blocking.
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
root="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
|
|
8
|
+
[ -n "$root" ] || exit 0
|
|
9
|
+
cd "$root" || exit 0
|
|
10
|
+
shopt -s nullglob
|
|
11
|
+
input=$(cat)
|
|
12
|
+
RALPH_SESSION=$(printf '%s' "$input" | node scripts/lib/json.mjs get session_id none 2>/dev/null || echo none)
|
|
13
|
+
# shellcheck source=scripts/lib/harness-conf.sh
|
|
14
|
+
. scripts/lib/harness-conf.sh 2>/dev/null || { ralph_block() { printf '%s\n' "$2" >&2; exit 2; }; ralph_pass() { :; }; }
|
|
15
|
+
for h in "$root"/.claude/hooks/project/stop/*.sh; do
|
|
16
|
+
if [ ! -x "$h" ]; then
|
|
17
|
+
printf 'Project Stop hook %s is not executable; run chmod +x and commit it.\n' "$h" >&2
|
|
18
|
+
exit 2
|
|
19
|
+
fi
|
|
20
|
+
out=$(printf '%s' "$input" | "$h" 2>&1)
|
|
21
|
+
code=$?
|
|
22
|
+
[ "$code" -eq 2 ] && ralph_block "project-$(basename "$h" .sh)" "$out"
|
|
23
|
+
[ -z "$out" ] || printf '%s\n' "$out" >&2
|
|
24
|
+
ralph_pass "project-$(basename "$h" .sh)"
|
|
25
|
+
[ "$code" -eq 0 ] || printf 'Project Stop hook %s exited with %s\n' "$h" "$code" >&2
|
|
26
|
+
done
|
|
27
|
+
exit 0
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Path classes of this project (docs/documentation-rules.md, sections 8 and 9): one
|
|
2
|
+
# "<class> <glob>" per line, a shell glob relative to the repository root, in which * also matches
|
|
3
|
+
# /. A path takes the highest class among the lines that match it and the template's built-in
|
|
4
|
+
# ones (approval for the rules, the map, ADRs on main, and the generated manifest, answers file,
|
|
5
|
+
# and settings). A path that nothing
|
|
6
|
+
# matches is low when it is Markdown, otherwise normal. This file belongs to the project; a
|
|
7
|
+
# change to it is class approval.
|
|
8
|
+
# approval the owner approves the edit first: the protect hook asks, a headless run is denied
|
|
9
|
+
# high with the dual_review module, the Santa loop before the pull request
|
|
10
|
+
# gate listed under "Gate files" in the pull request, reviewed by the owner after the merge
|
|
11
|
+
# normal with the dual_review module, one reviewer before the pull request
|
|
12
|
+
# low no reviewer before the pull request
|
|
13
|
+
|
|
14
|
+
# Secrets and tools with irreversible effects.
|
|
15
|
+
approval .env
|
|
16
|
+
approval .env.*
|
|
17
|
+
approval *.pem
|
|
18
|
+
approval *.key
|
|
19
|
+
|
|
20
|
+
# Security, access, data migrations, money, public contracts. Add the project's own, such as
|
|
21
|
+
# policy or limits.
|
|
22
|
+
high *auth/*
|
|
23
|
+
high *security/*
|
|
24
|
+
high *secrets/*
|
|
25
|
+
high *migrations/*
|
|
26
|
+
high *billing/*
|
|
27
|
+
high *payments/*
|
|
28
|
+
high contracts/*
|
|
29
|
+
|
|
30
|
+
# Gates: workflows, the project's checks and Stop hooks, and the harness settings.
|
|
31
|
+
gate .github/workflows/*
|
|
32
|
+
gate scripts/checks.d/*
|
|
33
|
+
gate .claude/hooks/project/*
|
|
34
|
+
gate .claude/harness.conf
|
|
35
|
+
|
|
36
|
+
# Gates of any stack: dependency updates, pre-commit hooks, task runners, and tool versions. The
|
|
37
|
+
# `*/` lines gate the files of Make, just, and Task in subdirectories too, and `*.mk` every file
|
|
38
|
+
# with that extension; tool versions and mise are gated at the root only.
|
|
39
|
+
gate .github/dependabot.y*ml
|
|
40
|
+
gate renovate.json*
|
|
41
|
+
gate .github/renovate.json*
|
|
42
|
+
gate .renovaterc*
|
|
43
|
+
gate .pre-commit-config.yaml
|
|
44
|
+
gate Makefile
|
|
45
|
+
gate */Makefile
|
|
46
|
+
gate makefile
|
|
47
|
+
gate */makefile
|
|
48
|
+
gate GNUmakefile
|
|
49
|
+
gate */GNUmakefile
|
|
50
|
+
gate *.mk
|
|
51
|
+
gate justfile
|
|
52
|
+
gate */justfile
|
|
53
|
+
gate Justfile
|
|
54
|
+
gate */Justfile
|
|
55
|
+
gate .justfile
|
|
56
|
+
gate */.justfile
|
|
57
|
+
gate Taskfile.y*ml
|
|
58
|
+
gate */Taskfile.y*ml
|
|
59
|
+
gate Taskfile.dist.y*ml
|
|
60
|
+
gate */Taskfile.dist.y*ml
|
|
61
|
+
gate taskfile.y*ml
|
|
62
|
+
gate */taskfile.y*ml
|
|
63
|
+
gate taskfile.dist.y*ml
|
|
64
|
+
gate */taskfile.dist.y*ml
|
|
65
|
+
gate .tool-versions
|
|
66
|
+
gate mise.toml
|
|
67
|
+
gate mise.*.toml
|
|
68
|
+
gate .mise.toml
|
|
69
|
+
gate .mise.*.toml
|
|
70
|
+
gate mise/config.toml
|
|
71
|
+
gate .mise/config.toml
|
|
72
|
+
gate .config/mise.toml
|
|
73
|
+
gate .config/mise/*
|
|
74
|
+
|
|
75
|
+
# Dependency manifests and lock files, and the configuration of linters, test runners, coverage,
|
|
76
|
+
# and mutation testing.
|
|
77
|
+
gate package.json
|
|
78
|
+
gate */package.json
|
|
79
|
+
gate package-lock.json
|
|
80
|
+
gate pnpm-lock.yaml
|
|
81
|
+
gate yarn.lock
|
|
82
|
+
gate pyproject.toml
|
|
83
|
+
gate uv.lock
|
|
84
|
+
gate poetry.lock
|
|
85
|
+
gate requirements*.txt
|
|
86
|
+
gate go.mod
|
|
87
|
+
gate go.sum
|
|
88
|
+
gate Cargo.toml
|
|
89
|
+
gate Cargo.lock
|
|
90
|
+
gate .eslintrc*
|
|
91
|
+
gate eslint.config.*
|
|
92
|
+
gate .prettierrc*
|
|
93
|
+
gate ruff.toml
|
|
94
|
+
gate .ruff.toml
|
|
95
|
+
gate pytest.ini
|
|
96
|
+
gate tox.ini
|
|
97
|
+
gate .coveragerc
|
|
98
|
+
gate jest.config.*
|
|
99
|
+
gate vitest.config.*
|
|
100
|
+
gate tsconfig*.json
|
|
101
|
+
gate stryker.conf.*
|
|
102
|
+
gate .golangci.yml
|