@erclx/aitk 3.52.0 → 3.53.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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/claude-pr-review/SKILL.md +8 -0
- package/docs/agents/audits.md +1 -1
- package/docs/agents/commands.md +3 -0
- package/docs/agents/gate.md +84 -0
- package/docs/agents/index.md +2 -0
- package/docs/agents/key-changes.md +103 -0
- package/docs/agents/markdown-audit.md +3 -3
- package/docs/agents/state-scoped-risk.md +1 -1
- package/docs/operating-model.md +8 -0
- package/package.json +3 -3
- package/scripts/core/list-seed-roots.sh +18 -0
- package/scripts/core/repair-bare-flag.sh +19 -0
- package/scripts/core/update.sh +3 -3
- package/src/audits/catalog.ts +2 -2
- package/src/audits/run.ts +2 -1
- package/src/cli.ts +7 -0
- package/src/commands/claude.ts +2 -2
- package/src/commands/context.ts +2 -2
- package/src/commands/gate.ts +189 -0
- package/src/commands/pr.ts +411 -0
- package/src/gate/measures.ts +682 -0
- package/src/gate/sequencer.ts +386 -0
- package/src/gate/stages.ts +412 -0
- package/src/pr/bijection.ts +145 -0
- package/src/pr/paths.ts +335 -0
- package/scripts/core/verify.sh +0 -694
package/scripts/core/verify.sh
DELETED
|
@@ -1,694 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -e
|
|
3
|
-
set -o pipefail
|
|
4
|
-
|
|
5
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
-
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
7
|
-
|
|
8
|
-
source "$PROJECT_ROOT/scripts/lib/ui.sh"
|
|
9
|
-
source "$PROJECT_ROOT/scripts/lib/worktree.sh"
|
|
10
|
-
source "$PROJECT_ROOT/scripts/lib/tooling.sh"
|
|
11
|
-
|
|
12
|
-
NESTED="${VERIFY_NESTED:-false}"
|
|
13
|
-
WRITE="${VERIFY_WRITE:-true}"
|
|
14
|
-
SCOPED=true
|
|
15
|
-
CHANGED_FILES=""
|
|
16
|
-
|
|
17
|
-
# Scenarios declaring no expectation, taken from `aitk sandbox coverage` against a
|
|
18
|
-
# clean tree. Raising it is a deliberate edit that says which scenario shipped
|
|
19
|
-
# unarmed and why.
|
|
20
|
-
SANDBOX_UNDECLARED_CEILING=47
|
|
21
|
-
|
|
22
|
-
# Rules no stack reaches, space separated and sorted the way `aitk gov list`
|
|
23
|
-
# emits them. `260-shadcn` and `320-tanstack-query` are opt-in libraries a
|
|
24
|
-
# project may not want. `505-at-references` used to sit here too, shipping
|
|
25
|
-
# with no stack on purpose since a rule under `claude/` would reach every
|
|
26
|
-
# base consumer through the folder-whole entry there. Its own install
|
|
27
|
-
# channel, `aitk snippets install`, retired with nothing left to deliver it,
|
|
28
|
-
# so `base` now carries `snippets` as a folder-whole entry of its own and the
|
|
29
|
-
# rule reaches every base consumer through that instead. Both are recorded
|
|
30
|
-
# here rather than in a config file: the list is what a reader compares a
|
|
31
|
-
# new arrival against, and a config file would absorb the arrival silently.
|
|
32
|
-
GOV_EXPECTED_UNREFERENCED="260-shadcn 320-tanstack-query"
|
|
33
|
-
|
|
34
|
-
# The retained counts the audit stage compares each run against. Spelled here
|
|
35
|
-
# rather than derived, because this script only ever names the file in a remedy
|
|
36
|
-
# a reader has to be able to open, and `aitk audits run` owns writing it.
|
|
37
|
-
AUDITS_BASELINE=".claude/audits/baseline.json"
|
|
38
|
-
|
|
39
|
-
# The corpora a `src/` test asserts over from outside `src/`, censused in
|
|
40
|
-
# `.claude/context/development/verification.md`. This array and that list are two
|
|
41
|
-
# copies of one set with nothing comparing them, so a corpus joining the census
|
|
42
|
-
# joins this array in the same change. The first four are directory prefixes
|
|
43
|
-
# because their tests walk the tree whole, which is what reaches a rule or a
|
|
44
|
-
# skill a branch adds rather than edits.
|
|
45
|
-
TEST_CORPORA_PATTERNS=(
|
|
46
|
-
'^claude/skills/'
|
|
47
|
-
'^governance/rules/'
|
|
48
|
-
'^\.claude/hooks/'
|
|
49
|
-
'^tooling/claude/seeds/\.claude/hooks/'
|
|
50
|
-
'^standards/markdown\.md$'
|
|
51
|
-
'^tooling/base/reference\.md$'
|
|
52
|
-
'^tooling/web/configs/scripts/worktree-port\.sh$'
|
|
53
|
-
'^\.cspell/banned-spellings\.txt$'
|
|
54
|
-
'^scripts/lib/worktree\.sh$'
|
|
55
|
-
'^scripts/core/check-ignore-parity\.sh$'
|
|
56
|
-
)
|
|
57
|
-
TEST_CORPORA=$(
|
|
58
|
-
IFS='|'
|
|
59
|
-
printf '%s' "${TEST_CORPORA_PATTERNS[*]}"
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
check_dependencies() {
|
|
63
|
-
command -v bun >/dev/null 2>&1 || log_error "bun is not installed"
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
print_usage() {
|
|
67
|
-
echo "Usage: bun run check [--all]"
|
|
68
|
-
echo " --all Run every stage instead of scoping shell, types, and tests to changed files"
|
|
69
|
-
echo " --help Print this message"
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
parse_args() {
|
|
73
|
-
local arg
|
|
74
|
-
for arg in "$@"; do
|
|
75
|
-
case "$arg" in
|
|
76
|
-
--all) SCOPED=false ;;
|
|
77
|
-
-h | --help)
|
|
78
|
-
print_usage
|
|
79
|
-
exit 0
|
|
80
|
-
;;
|
|
81
|
-
*) log_error "Unknown argument: $arg" ;;
|
|
82
|
-
esac
|
|
83
|
-
done
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
# Union of the branch's committed diff, the working tree, and untracked files.
|
|
87
|
-
# A wider set only means running more stages, so every fallback widens.
|
|
88
|
-
collect_changed_files() {
|
|
89
|
-
[ "$SCOPED" = true ] || return 0
|
|
90
|
-
|
|
91
|
-
local base head local_baseline=false
|
|
92
|
-
# origin/main, not local main. On main itself the local ref is HEAD, so a commit
|
|
93
|
-
# not yet pushed would drop out of the changed set and skip the scoped stages.
|
|
94
|
-
base=$(git -C "$PROJECT_ROOT" merge-base HEAD origin/main 2>/dev/null) || base=""
|
|
95
|
-
if [ -z "$base" ]; then
|
|
96
|
-
local_baseline=true
|
|
97
|
-
base=$(git -C "$PROJECT_ROOT" merge-base HEAD main 2>/dev/null) || base=""
|
|
98
|
-
fi
|
|
99
|
-
if [ -z "$base" ]; then
|
|
100
|
-
SCOPED=false
|
|
101
|
-
log_warn "No merge base with main. Running every stage."
|
|
102
|
-
return 0
|
|
103
|
-
fi
|
|
104
|
-
|
|
105
|
-
# Without a remote baseline, a merge base equal to HEAD hides committed work.
|
|
106
|
-
if [ "$local_baseline" = true ]; then
|
|
107
|
-
head=$(git -C "$PROJECT_ROOT" rev-parse HEAD 2>/dev/null) || head=""
|
|
108
|
-
if [ -z "$head" ] || [ "$base" = "$head" ]; then
|
|
109
|
-
SCOPED=false
|
|
110
|
-
log_warn "No pushed baseline to compare against. Running every stage."
|
|
111
|
-
return 0
|
|
112
|
-
fi
|
|
113
|
-
fi
|
|
114
|
-
|
|
115
|
-
CHANGED_FILES=$({
|
|
116
|
-
git -C "$PROJECT_ROOT" diff --name-only "$base" HEAD
|
|
117
|
-
git -C "$PROJECT_ROOT" diff --name-only HEAD
|
|
118
|
-
git -C "$PROJECT_ROOT" ls-files --others --exclude-standard
|
|
119
|
-
} | sort -u)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
has_changed() {
|
|
123
|
-
[ "$SCOPED" = true ] || return 0
|
|
124
|
-
printf '%s\n' "$CHANGED_FILES" | grep -qE "$1"
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
run_check() {
|
|
128
|
-
local cmd=$1
|
|
129
|
-
local err_msg=$2
|
|
130
|
-
local output
|
|
131
|
-
if ! output=$(eval "$cmd" 2>&1); then
|
|
132
|
-
echo "$output" | pipe_output
|
|
133
|
-
log_error "$err_msg"
|
|
134
|
-
fi
|
|
135
|
-
echo "$output" | pipe_output
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
# Whatever plugin and marketplace manifests the repo currently carries, so the
|
|
139
|
-
# stage picks up a new one without an edit here. Both listings honor .gitignore,
|
|
140
|
-
# which keeps linked worktrees and dependency copies out.
|
|
141
|
-
collect_plugin_manifests() {
|
|
142
|
-
{
|
|
143
|
-
git -C "$PROJECT_ROOT" ls-files -- '*.claude-plugin/plugin.json' '*.claude-plugin/marketplace.json'
|
|
144
|
-
git -C "$PROJECT_ROOT" ls-files --others --exclude-standard -- '*.claude-plugin/plugin.json' '*.claude-plugin/marketplace.json'
|
|
145
|
-
} | sort -u
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
# sha256 of a file under the coreutils name and the macOS one. `aitk capture`
|
|
149
|
-
# writes the same digest into the stamp through node's crypto, so the two sides
|
|
150
|
-
# agree on an algorithm rather than on a tool being installed.
|
|
151
|
-
#
|
|
152
|
-
# Neither tool present returns 1 and says so. Falling through to an empty digest
|
|
153
|
-
# reports a mismatch against a blank value, which tells the reader the image is
|
|
154
|
-
# wrong when the truth is that the checker never ran.
|
|
155
|
-
#
|
|
156
|
-
# The refusal goes to stderr because every caller reads this through `$(...)`,
|
|
157
|
-
# which captures stdout into the digest variable and would swallow the message.
|
|
158
|
-
# `run_check` folds stderr into what it pipes, so the reader still sees it.
|
|
159
|
-
file_sha256() {
|
|
160
|
-
local digest
|
|
161
|
-
if command -v sha256sum >/dev/null 2>&1; then
|
|
162
|
-
digest=$(sha256sum "$1")
|
|
163
|
-
elif command -v shasum >/dev/null 2>&1; then
|
|
164
|
-
digest=$(shasum -a 256 "$1")
|
|
165
|
-
else
|
|
166
|
-
echo "Neither sha256sum nor shasum is installed, so $1 cannot be hashed." >&2
|
|
167
|
-
return 1
|
|
168
|
-
fi
|
|
169
|
-
printf '%s\n' "${digest%% *}"
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
# One digest the stamp recorded against the file it was taken over. An absent
|
|
173
|
-
# field reports itself rather than comparing against an empty string, so a stamp
|
|
174
|
-
# predating the current format is distinguishable from a file that moved.
|
|
175
|
-
#
|
|
176
|
-
# Both names in the message come off the paths rather than from arguments. The
|
|
177
|
-
# caller passes absolute paths and the reader wants repository-relative ones, and
|
|
178
|
-
# deriving them here is what keeps a name from disagreeing with the file it
|
|
179
|
-
# labels once a second capture source calls this.
|
|
180
|
-
assert_stamp_field() {
|
|
181
|
-
local stamp=$1 field=$2 file=$3
|
|
182
|
-
local stamp_label=${stamp#"$PROJECT_ROOT"/}
|
|
183
|
-
local file_label=${file#"$PROJECT_ROOT"/}
|
|
184
|
-
local recorded actual
|
|
185
|
-
recorded=$(awk -v key="$field:" '$1 == key { print $2; exit }' "$stamp")
|
|
186
|
-
if [ -z "$recorded" ]; then
|
|
187
|
-
echo "$stamp_label carries no $field line, so it predates the capture that writes one."
|
|
188
|
-
return 1
|
|
189
|
-
fi
|
|
190
|
-
actual=$(file_sha256 "$file") || return 1
|
|
191
|
-
if [ "$recorded" != "$actual" ]; then
|
|
192
|
-
echo "$stamp_label records $field $recorded"
|
|
193
|
-
echo "$file_label hashes to $actual"
|
|
194
|
-
return 1
|
|
195
|
-
fi
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
# The drift assert covers the HTML because the PNG is a chromium render whose
|
|
199
|
-
# bytes move with the browser. That leaves the artifact a visitor actually sees
|
|
200
|
-
# asserted nowhere, so a branch that regenerates the HTML and never runs the
|
|
201
|
-
# capture passes every stage while shipping an image with the old counts.
|
|
202
|
-
#
|
|
203
|
-
# `aitk capture` records a digest of the markup it rendered and one of the image
|
|
204
|
-
# it wrote, so this reads provenance rather than timing. Comparing the commit
|
|
205
|
-
# that last touched each file passes any pair that moved together whatever the
|
|
206
|
-
# two files hold, which is what a binary conflict resolved by taking either side
|
|
207
|
-
# produces. All three absent passes, which is correct for a tree that carries
|
|
208
|
-
# none of them.
|
|
209
|
-
#
|
|
210
|
-
# Both digests are checked because either file can move alone. The markup side
|
|
211
|
-
# catches an edit committed with no capture, and the image side catches a PNG
|
|
212
|
-
# replaced under markup that never changed, which is the case the timing read
|
|
213
|
-
# caught by accident and a markup-only digest would drop.
|
|
214
|
-
assert_hero_stamp() {
|
|
215
|
-
local html="$PROJECT_ROOT/assets/hero.html"
|
|
216
|
-
local png="$PROJECT_ROOT/assets/hero.png"
|
|
217
|
-
local stamp="$PROJECT_ROOT/assets/hero.stamp"
|
|
218
|
-
|
|
219
|
-
if [ ! -f "$html" ] && [ ! -f "$png" ] && [ ! -f "$stamp" ]; then return 0; fi
|
|
220
|
-
|
|
221
|
-
local missing=""
|
|
222
|
-
[ -f "$html" ] || missing="$missing assets/hero.html"
|
|
223
|
-
[ -f "$png" ] || missing="$missing assets/hero.png"
|
|
224
|
-
[ -f "$stamp" ] || missing="$missing assets/hero.stamp"
|
|
225
|
-
if [ -n "$missing" ]; then
|
|
226
|
-
echo "Missing from the hero set:$missing"
|
|
227
|
-
return 1
|
|
228
|
-
fi
|
|
229
|
-
|
|
230
|
-
assert_stamp_field "$stamp" source-sha256 "$html" || return 1
|
|
231
|
-
assert_stamp_field "$stamp" image-sha256 "$png" || return 1
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
# Entries the audit actually measured, summed across the folders it resolved.
|
|
235
|
-
# `--json` carries one `"entries":<n>` per folder object, and the top-level key
|
|
236
|
-
# of that name holds an array, so the numeric match reaches folders alone.
|
|
237
|
-
#
|
|
238
|
-
# A root can resolve a folder and measure nothing in it, which is a passing gate
|
|
239
|
-
# over an empty set. The stage prints this per root rather than reporting one
|
|
240
|
-
# verdict for every root, or a tree nobody measured reads as a tree that passed.
|
|
241
|
-
seed_entry_count() {
|
|
242
|
-
printf '%s' "$1" | grep -o '"entries":[0-9]\+' | grep -o '[0-9]\+' |
|
|
243
|
-
awk '{ total += $1 } END { print total + 0 }'
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
# One numeric summary key out of a command's JSON record. Every caller passes a
|
|
247
|
-
# name the nested objects in that record do not carry, so the match reaches the
|
|
248
|
-
# top level alone and the caller does not depend on the order the keys are
|
|
249
|
-
# emitted in.
|
|
250
|
-
json_summary_field() {
|
|
251
|
-
printf '%s' "$2" | grep -o "\"$1\":[0-9]\+" | grep -o '[0-9]\+'
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
assert_no_drift() {
|
|
255
|
-
local paths=$1
|
|
256
|
-
local err_msg=$2
|
|
257
|
-
run_check "cd $PROJECT_ROOT && git diff --exit-code --quiet -- $paths" "$err_msg"
|
|
258
|
-
run_check "cd $PROJECT_ROOT && [ -z \"\$(git ls-files --others --exclude-standard -- $paths)\" ]" "$err_msg"
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
main() {
|
|
262
|
-
check_dependencies
|
|
263
|
-
parse_args "$@"
|
|
264
|
-
|
|
265
|
-
if [ "$NESTED" = false ]; then echo -e "${GREY}┌${NC}"; fi
|
|
266
|
-
|
|
267
|
-
repair_bare_flag
|
|
268
|
-
collect_changed_files
|
|
269
|
-
|
|
270
|
-
if [ "$WRITE" = true ]; then
|
|
271
|
-
echo -e "${GREY}├${NC} ${WHITE}Formatting${NC}"
|
|
272
|
-
run_check "bun run format" "Format failed"
|
|
273
|
-
log_info "Format applied"
|
|
274
|
-
else
|
|
275
|
-
echo -e "${GREY}├${NC} ${WHITE}Format check${NC}"
|
|
276
|
-
run_check "bun run check:format" "Format check failed"
|
|
277
|
-
log_info "Format check passed"
|
|
278
|
-
fi
|
|
279
|
-
|
|
280
|
-
log_step "Indexes"
|
|
281
|
-
run_check "bash $PROJECT_ROOT/scripts/core/regen-indexes.sh" "Index regen failed"
|
|
282
|
-
assert_no_drift "'*index.md'" "Indexes drifted. Run bun run check and commit the updated index files."
|
|
283
|
-
log_info "Indexes clean"
|
|
284
|
-
|
|
285
|
-
log_step "Consumed copies"
|
|
286
|
-
run_check "bash $PROJECT_ROOT/scripts/core/regen-claude-copies.sh" "Consumed-copy regen failed"
|
|
287
|
-
assert_no_drift ".claude/rules" "Consumed copies drifted. Run bun run check and commit .claude/rules."
|
|
288
|
-
log_info "Consumed copies clean"
|
|
289
|
-
|
|
290
|
-
# Only the HTML is asserted. The PNG beside it is a chromium render whose bytes
|
|
291
|
-
# move with the browser version, so a drift check over it would fail on a
|
|
292
|
-
# machine whose chromium differs rather than on a stale count.
|
|
293
|
-
log_step "Hero"
|
|
294
|
-
run_check "bash $PROJECT_ROOT/scripts/core/regen-hero.sh" "Hero regen failed"
|
|
295
|
-
assert_no_drift "assets/hero.html" "Hero counts drifted. Run bun run check, then aitk capture assets/hero.html, and commit assets/hero.html with assets/hero.png and assets/hero.stamp."
|
|
296
|
-
run_check "assert_hero_stamp" "The hero set disagrees with the stamp written when the image was captured. Run aitk capture assets/hero.html and commit all three files together."
|
|
297
|
-
log_info "Hero clean"
|
|
298
|
-
|
|
299
|
-
log_step "Tooling paths"
|
|
300
|
-
run_check "bash $PROJECT_ROOT/scripts/core/regen-tooling-paths.sh" "Tooling-path regen failed"
|
|
301
|
-
assert_no_drift "claude/skills/aitk-cli/SKILL.md" "The overwrite contract drifted from what the stacks hold. Run bun run check and commit claude/skills/aitk-cli/SKILL.md."
|
|
302
|
-
log_info "Tooling paths clean"
|
|
303
|
-
|
|
304
|
-
# The claude manifest is the only route a target's ignore set travels, and it
|
|
305
|
-
# is hand-maintained beside this repository's own `.gitignore` with nothing
|
|
306
|
-
# comparing the two. A drift between them reaches every target on the next
|
|
307
|
-
# `aitk tooling sync` and surfaces to nobody, which is why this gates rather
|
|
308
|
-
# than reports. It is not an `assert_no_drift`: no generator produces either
|
|
309
|
-
# list, so there is nothing to regenerate and diff.
|
|
310
|
-
log_step "Ignore parity"
|
|
311
|
-
run_check "bash $PROJECT_ROOT/scripts/core/check-ignore-parity.sh" "The ignore set a target receives disagrees with this repository's own."
|
|
312
|
-
log_info "Ignore parity clean"
|
|
313
|
-
|
|
314
|
-
log_step "Skill paths"
|
|
315
|
-
run_check "bash $PROJECT_ROOT/scripts/core/check-skill-paths.sh" "Shipped skills reference a repo-local path."
|
|
316
|
-
log_info "Skill paths clean"
|
|
317
|
-
|
|
318
|
-
log_step "Plugin boundary"
|
|
319
|
-
run_check "bash $PROJECT_ROOT/scripts/core/check-plugin-boundary.sh" "Plugin ships toolkit-internal content."
|
|
320
|
-
log_info "Plugin boundary clean"
|
|
321
|
-
|
|
322
|
-
# Seed prose is installed into every scaffolded project and read there as
|
|
323
|
-
# instruction about that project, so a line naming this repository's CLI hands
|
|
324
|
-
# a target a verb it may not be able to run. This gates for the reason the
|
|
325
|
-
# Seed standards stage below gates: a defect authored once propagates into
|
|
326
|
-
# every project scaffolded after it.
|
|
327
|
-
log_step "Seed independence"
|
|
328
|
-
run_check "bash $PROJECT_ROOT/scripts/core/check-seed-independence.sh" "Seed prose cites the toolkit CLI."
|
|
329
|
-
log_info "Seed prose cites no toolkit CLI"
|
|
330
|
-
|
|
331
|
-
# A stack entry naming a rule folder takes every rule in it, which is what
|
|
332
|
-
# stops a new rule from needing a second edit to reach a target. The failure
|
|
333
|
-
# it leaves open is a rule authored into a folder no stack names, which
|
|
334
|
-
# `aitk gov install` never reaches on its own, whether or not another domain
|
|
335
|
-
# installs the file by a different route.
|
|
336
|
-
#
|
|
337
|
-
# This reports and never fails. All three standing findings ship this way on
|
|
338
|
-
# purpose, so gating would fail every push over the deliberate case and teach
|
|
339
|
-
# a reader to route around the stage. Revisit if the set keeps growing and
|
|
340
|
-
# the pattern turns out to be an accident rather than a design.
|
|
341
|
-
log_step "Unreferenced rules"
|
|
342
|
-
local gov_json gov_status=0 unreferenced
|
|
343
|
-
gov_json=$(cd "$PROJECT_ROOT" && bun src/cli.ts gov list --json 2>/dev/null) || gov_status=$?
|
|
344
|
-
if [ "$gov_status" -ne 0 ] || [ -z "$gov_json" ]; then
|
|
345
|
-
log_warn "Skipped, the governance catalog did not report"
|
|
346
|
-
else
|
|
347
|
-
# `bun --eval` rather than a grep, because the key holds an array of names
|
|
348
|
-
# and the numeric matches the stages above use reach a scalar alone.
|
|
349
|
-
#
|
|
350
|
-
# The `ok:` sentinel carries success rather than the exit code, because
|
|
351
|
-
# `bun --eval` reading piped stdin exits 0 even when the script throws.
|
|
352
|
-
# Measured on Bun 1.3.14: the same throw exits 1 with no pipe attached. A
|
|
353
|
-
# payload that parses as text but not as JSON would therefore print nothing
|
|
354
|
-
# and exit clean, and empty already means every rule is reached, so reading
|
|
355
|
-
# the exit code would report a broken catalog as a clean sweep. A missing
|
|
356
|
-
# or non-array key takes the same branch, since a stage that cannot measure
|
|
357
|
-
# should say so rather than claim the sweep found nothing.
|
|
358
|
-
local reported
|
|
359
|
-
reported=$(printf '%s' "$gov_json" | bun --eval '
|
|
360
|
-
try {
|
|
361
|
-
const data = JSON.parse(require("node:fs").readFileSync(0, "utf8"))
|
|
362
|
-
if (!Array.isArray(data.unreferenced)) throw new Error("no field")
|
|
363
|
-
console.log("ok:" + data.unreferenced.join(" "))
|
|
364
|
-
} catch {
|
|
365
|
-
console.log("unreadable:")
|
|
366
|
-
}
|
|
367
|
-
')
|
|
368
|
-
unreferenced="${reported#ok:}"
|
|
369
|
-
if [ "${reported%%:*}" != "ok" ]; then
|
|
370
|
-
log_warn "Skipped, the governance catalog carried no readable unreferenced list"
|
|
371
|
-
elif [ -z "$unreferenced" ]; then
|
|
372
|
-
log_info "Every rule is reached by a stack"
|
|
373
|
-
elif [ "$unreferenced" = "$GOV_EXPECTED_UNREFERENCED" ]; then
|
|
374
|
-
log_info "Reached by no stack: $unreferenced (each recorded above with why)"
|
|
375
|
-
else
|
|
376
|
-
log_warn "Reached by no stack: $unreferenced"
|
|
377
|
-
log_warn "Expected: $GOV_EXPECTED_UNREFERENCED. Name the new rule in a stack, or update GOV_EXPECTED_UNREFERENCED in this script and say why it reaches no stack."
|
|
378
|
-
fi
|
|
379
|
-
fi
|
|
380
|
-
|
|
381
|
-
# Only the citation half of the audit gates. Length, depth, table, and index
|
|
382
|
-
# findings are judgment thresholds, and failing a push on one would make the
|
|
383
|
-
# stage something to route around. `bun src/cli.ts` rather than `aitk`, since a
|
|
384
|
-
# globally installed binary resolves to the main checkout no matter which
|
|
385
|
-
# worktree is running.
|
|
386
|
-
log_step "Context citations"
|
|
387
|
-
run_check "cd $PROJECT_ROOT && bun src/cli.ts context audit --citations-only" "A cited context path does not resolve. Run bun src/cli.ts context audit."
|
|
388
|
-
log_info "Context citations resolve"
|
|
389
|
-
|
|
390
|
-
# A rule citing a file that moved fails silently. The consumed-copy drift
|
|
391
|
-
# stage passes an authored rule and its copy that are wrong together, and
|
|
392
|
-
# nothing else resolves the path until a session opens it, which is how
|
|
393
|
-
# `561-teach.md` shipped a `references/glossary.md` that had never existed. A
|
|
394
|
-
# rule whose frontmatter glob names a directory that moved fails the same way,
|
|
395
|
-
# by never firing again.
|
|
396
|
-
#
|
|
397
|
-
# This gates for the reason the stage above gates: a path resolving to
|
|
398
|
-
# nothing carries no judgment. The classes where absence is correct, a path
|
|
399
|
-
# the rule declares in its own frontmatter and one git ignores, are separated
|
|
400
|
-
# inside the verb rather than left as a threshold here. Globs are read under
|
|
401
|
-
# `internal/rules/` alone, since a shipped rule's glob names a target's shape.
|
|
402
|
-
log_step "Rule citations"
|
|
403
|
-
run_check "cd $PROJECT_ROOT && bun src/cli.ts gov citations" "A path a rule cites, or an internal frontmatter glob, does not resolve. Run bun src/cli.ts gov citations."
|
|
404
|
-
log_info "Rule citations resolve"
|
|
405
|
-
|
|
406
|
-
# A banned character, word, or spelling is a fact rather than a threshold, so
|
|
407
|
-
# it fails the push while bullet, paragraph, and depth weight stay advisory
|
|
408
|
-
# for the reason the stage above leaves its own thresholds so.
|
|
409
|
-
#
|
|
410
|
-
# The whole corpus is measured rather than the changed files, because a
|
|
411
|
-
# `Do not use` bullet added to a standard bans a token retroactively and no
|
|
412
|
-
# file in the push that adds it was edited.
|
|
413
|
-
#
|
|
414
|
-
# `--json` sends the record to stdout and the frame to stderr, so a passing
|
|
415
|
-
# run stays silent and a failing one is re-run for its frame rather than
|
|
416
|
-
# parsed out of a stream this script would have to strip. `bun src/cli.ts`
|
|
417
|
-
# rather than `aitk` for the reason the stage above uses it.
|
|
418
|
-
log_step "Markdown bans"
|
|
419
|
-
local ban_status=0 ban_frame
|
|
420
|
-
(cd "$PROJECT_ROOT" && bun src/cli.ts markdown audit --json >/dev/null 2>&1) || ban_status=$?
|
|
421
|
-
case $ban_status in
|
|
422
|
-
0)
|
|
423
|
-
log_info "No banned character, word, or spelling"
|
|
424
|
-
;;
|
|
425
|
-
1)
|
|
426
|
-
log_warn "Skipped, the markdown audit refused and measured nothing"
|
|
427
|
-
;;
|
|
428
|
-
3)
|
|
429
|
-
log_error "The markdown audit shipped an empty ban set, so the corpus was walked and nothing was looked for. Check src/markdown/bans.ts."
|
|
430
|
-
;;
|
|
431
|
-
2)
|
|
432
|
-
# `|| true` because the re-run exits non-zero by construction, and `set -e`
|
|
433
|
-
# would take the script down before log_error names the remedy.
|
|
434
|
-
ban_frame=$(cd "$PROJECT_ROOT" && bun src/cli.ts markdown audit 2>&1 || true)
|
|
435
|
-
echo "$ban_frame" | pipe_output
|
|
436
|
-
log_error "Markdown prose carries a banned character, word, or spelling. Rewrite the sentence, and reach for a code span only where the token is genuinely an identifier under discussion."
|
|
437
|
-
;;
|
|
438
|
-
*)
|
|
439
|
-
log_error "The markdown audit exited $ban_status, which is neither a pass nor a finding."
|
|
440
|
-
;;
|
|
441
|
-
esac
|
|
442
|
-
|
|
443
|
-
# The stage above audits this repository. Its seed tree ships into every
|
|
444
|
-
# scaffolded project, so a seed breaking the standard it seeds propagates
|
|
445
|
-
# instead of sitting still, and no rule path reaches the tree to report it.
|
|
446
|
-
# `--gate` fails on the two findings beside citations that are facts, a
|
|
447
|
-
# missing required section and index drift, and leaves the thresholds
|
|
448
|
-
# advisory for the reason the stage above leaves them so. A passing run stays
|
|
449
|
-
# silent because the audit prints a frame that would nest inside this one.
|
|
450
|
-
log_step "Seed standards"
|
|
451
|
-
local seed_roots seed_root seed_output seed_frame seed_entries seed_measured seed_status
|
|
452
|
-
seed_roots=$(collect_seed_roots)
|
|
453
|
-
if [ -z "$seed_roots" ]; then
|
|
454
|
-
log_info "Skipped, no seed root carries .claude/"
|
|
455
|
-
else
|
|
456
|
-
seed_measured=0
|
|
457
|
-
while IFS= read -r seed_root; do
|
|
458
|
-
# `--json` puts the record on stdout and the frame on stderr, so the
|
|
459
|
-
# passing run stays silent and the failing one is re-run for its frame
|
|
460
|
-
# rather than parsed out of a stream this script would have to strip.
|
|
461
|
-
seed_status=0
|
|
462
|
-
seed_output=$(cd "$PROJECT_ROOT" && bun src/cli.ts context audit "$seed_root" --gate --json 2>/dev/null) || seed_status=$?
|
|
463
|
-
|
|
464
|
-
# The audit separates 1 from 2 and they mean opposite things. 2 is a seed
|
|
465
|
-
# breaking the standard it seeds. 1 is the audit refusing, which a seed
|
|
466
|
-
# root carrying no audited folder produces, and reporting that as a
|
|
467
|
-
# violation sends a reader hunting one that does not exist. Discovery is
|
|
468
|
-
# what puts this in reach, since a new stack seeding `.claude/` alone
|
|
469
|
-
# arrives here with no edit to this script.
|
|
470
|
-
case $seed_status in
|
|
471
|
-
0) ;;
|
|
472
|
-
1)
|
|
473
|
-
log_warn "$seed_root: no audited folder under .claude/, nothing measured"
|
|
474
|
-
continue
|
|
475
|
-
;;
|
|
476
|
-
*)
|
|
477
|
-
# `|| true` because the re-run exits non-zero by construction, and
|
|
478
|
-
# `set -e` would take the script down before log_error names the root.
|
|
479
|
-
seed_frame=$(cd "$PROJECT_ROOT" && bun src/cli.ts context audit "$seed_root" --gate 2>&1 || true)
|
|
480
|
-
echo "$seed_frame" | pipe_output
|
|
481
|
-
if [ "$seed_status" -eq 2 ]; then
|
|
482
|
-
log_error "A seed breaks the standard governing the folder it seeds: $seed_root"
|
|
483
|
-
else
|
|
484
|
-
log_error "The seed audit exited $seed_status against $seed_root, which is neither a pass nor a finding."
|
|
485
|
-
fi
|
|
486
|
-
;;
|
|
487
|
-
esac
|
|
488
|
-
|
|
489
|
-
seed_entries=$(seed_entry_count "$seed_output")
|
|
490
|
-
seed_measured=$((seed_measured + seed_entries))
|
|
491
|
-
|
|
492
|
-
if [ "$seed_entries" -eq 0 ]; then
|
|
493
|
-
log_warn "$seed_root: no entry under an audited folder, nothing measured"
|
|
494
|
-
else
|
|
495
|
-
log_info "$seed_root: $seed_entries entries measured"
|
|
496
|
-
fi
|
|
497
|
-
done <<<"$seed_roots"
|
|
498
|
-
|
|
499
|
-
if [ "$seed_measured" -eq 0 ]; then
|
|
500
|
-
log_warn "No seed entry was measured. The stage covered nothing."
|
|
501
|
-
fi
|
|
502
|
-
fi
|
|
503
|
-
|
|
504
|
-
# Presence of a required file is a fact, so it gates. The name, description,
|
|
505
|
-
# folder, and requirement-section measures beside it report and are read from a
|
|
506
|
-
# bare run. `bun src/cli.ts` for the reason the stage above uses it, and the
|
|
507
|
-
# command reads the cwd, so this measures the worktree being pushed.
|
|
508
|
-
log_step "Skill requirements"
|
|
509
|
-
run_check "cd $PROJECT_ROOT && bun src/cli.ts claude skills audit --requirements-only" "A skill folder carries no REQUIREMENT.md. Run bun src/cli.ts claude skills audit."
|
|
510
|
-
log_info "Skill requirements present"
|
|
511
|
-
|
|
512
|
-
# Scoped to arrival rather than the corpus, since standards/standard.md
|
|
513
|
-
# forbids writing a criterion into an existing standard outside the change
|
|
514
|
-
# that exercises it. Gating the 26 known gaps would fail every push until
|
|
515
|
-
# someone closed them all, which is the sweep that rule exists to prevent.
|
|
516
|
-
log_step "Standard success criteria"
|
|
517
|
-
local standards_output standards_status=0
|
|
518
|
-
standards_output=$(cd "$PROJECT_ROOT" && bun src/cli.ts standards audit --arrivals-only 2>&1) || standards_status=$?
|
|
519
|
-
if [ "$standards_status" -eq 0 ]; then
|
|
520
|
-
log_info "Arriving standards carry a success criterion"
|
|
521
|
-
elif [ "$standards_status" -eq 2 ]; then
|
|
522
|
-
echo "$standards_output" | pipe_output
|
|
523
|
-
log_error "A standard new to this branch carries no ## Success criterion section. Run bun src/cli.ts standards audit."
|
|
524
|
-
else
|
|
525
|
-
echo "$standards_output" | pipe_output
|
|
526
|
-
log_error "aitk standards audit could not read which standards arrived on this branch. Run bun src/cli.ts standards audit --json to see why."
|
|
527
|
-
fi
|
|
528
|
-
|
|
529
|
-
# `aitk sandbox coverage` moves only when a person runs it, so a scenario added
|
|
530
|
-
# with no expectation ships unnoticed. The gate is an absolute count of
|
|
531
|
-
# undeclared scenarios rather than a ratio or a floor under the declared count.
|
|
532
|
-
# A floor under the declared count passes the case this exists to catch, since
|
|
533
|
-
# adding an unarmed scenario leaves that number where it was. A ratio moves
|
|
534
|
-
# when a scenario is legitimately deleted, and this ceiling does not: deleting
|
|
535
|
-
# an unarmed scenario lowers it and deleting an armed one leaves it alone.
|
|
536
|
-
log_step "Sandbox coverage"
|
|
537
|
-
local coverage_output coverage_status=0 total armed undeclared
|
|
538
|
-
coverage_output=$(cd "$PROJECT_ROOT" && bun src/cli.ts sandbox coverage --json 2>/dev/null) || coverage_status=$?
|
|
539
|
-
if [ "$coverage_status" -ne 0 ]; then
|
|
540
|
-
if [ "${CI:-false}" = true ]; then
|
|
541
|
-
log_error "bun src/cli.ts sandbox coverage --json exited $coverage_status. The scenario tree ships in the checkout, so a run that does not report is a broken command rather than an absent tree, and skipping would report the pass this stage exists to withhold."
|
|
542
|
-
fi
|
|
543
|
-
log_warn "Skipped, the scenario tree did not report"
|
|
544
|
-
else
|
|
545
|
-
# `|| x=""` on both, because a grep that matches nothing exits non-zero and
|
|
546
|
-
# errexit would take the script down at the assignment, before the guard
|
|
547
|
-
# below could name what went missing.
|
|
548
|
-
total=$(json_summary_field totalScenarios "$coverage_output") || total=""
|
|
549
|
-
armed=$(json_summary_field armedScenarios "$coverage_output") || armed=""
|
|
550
|
-
if [ -z "$total" ] || [ -z "$armed" ]; then
|
|
551
|
-
log_error "The coverage report carried no scenario totals, so the stage measured nothing. Run bun src/cli.ts sandbox coverage --json."
|
|
552
|
-
fi
|
|
553
|
-
undeclared=$((total - armed))
|
|
554
|
-
if [ "$undeclared" -gt "$SANDBOX_UNDECLARED_CEILING" ]; then
|
|
555
|
-
log_error "$undeclared of $total scenarios declare no expectation, over the ceiling of $SANDBOX_UNDECLARED_CEILING. Declare expectations on the new scenario, or raise SANDBOX_UNDECLARED_CEILING in this script and say which scenario shipped unarmed."
|
|
556
|
-
fi
|
|
557
|
-
log_info "$armed of $total scenarios declare expectations, $undeclared undeclared against a ceiling of $SANDBOX_UNDECLARED_CEILING"
|
|
558
|
-
fi
|
|
559
|
-
|
|
560
|
-
# The three stages above gate on the three findings here that are facts, and
|
|
561
|
-
# this stage reports the rest. It runs the whole set anyway rather than only
|
|
562
|
-
# what those stages skip, because the aggregate's own value is one verdict
|
|
563
|
-
# over every audit, and a stage measuring a subset would report a health this
|
|
564
|
-
# repository never took.
|
|
565
|
-
#
|
|
566
|
-
# The duplicate walk costs 0.8s wall against roughly 4.4s of processor,
|
|
567
|
-
# measured on the authoring machine at 12 verbs run together. That is under
|
|
568
|
-
# every other stage in this script, which is what settles the open question
|
|
569
|
-
# about whether the pipeline can afford it.
|
|
570
|
-
#
|
|
571
|
-
# This reports and never fails. Growth in a judgment count is the thing the
|
|
572
|
-
# baseline exists to make visible, and failing a push on one would teach a
|
|
573
|
-
# contributor to route around the stage, which is the split every audit stage
|
|
574
|
-
# here already keeps. A fact still fails the push, at the specific stage above
|
|
575
|
-
# that names its own remedy.
|
|
576
|
-
log_step "Audit set"
|
|
577
|
-
local audits_output audits_status=0 audits_grown audits_shrunk audits_facts audits_unmeasured audits_absent audits_unrecorded
|
|
578
|
-
audits_output=$(cd "$PROJECT_ROOT" && bun src/cli.ts audits run --json 2>/dev/null) || audits_status=$?
|
|
579
|
-
if [ -z "$audits_output" ]; then
|
|
580
|
-
log_warn "Skipped, the audit set did not report (exit $audits_status)"
|
|
581
|
-
else
|
|
582
|
-
audits_grown=$(json_summary_field grown "$audits_output") || audits_grown=""
|
|
583
|
-
audits_shrunk=$(json_summary_field shrunk "$audits_output") || audits_shrunk=""
|
|
584
|
-
audits_facts=$(json_summary_field facts "$audits_output") || audits_facts=""
|
|
585
|
-
audits_unmeasured=$(json_summary_field unmeasured "$audits_output") || audits_unmeasured=""
|
|
586
|
-
audits_absent=$(json_summary_field absent "$audits_output") || audits_absent=""
|
|
587
|
-
audits_unrecorded=$(json_summary_field unrecorded "$audits_output") || audits_unrecorded=""
|
|
588
|
-
|
|
589
|
-
# An absent field is a record this stage cannot read, which is not the same
|
|
590
|
-
# as a run with nothing to report. Reading it as zero would print a clean
|
|
591
|
-
# line over a summary nobody parsed.
|
|
592
|
-
if [ -z "$audits_grown" ] || [ -z "$audits_facts" ] || [ -z "$audits_unmeasured" ]; then
|
|
593
|
-
log_warn "The audit record carried no summary, so this stage measured nothing. Run bun src/cli.ts audits run."
|
|
594
|
-
else
|
|
595
|
-
# An absent per-machine folder is the ordinary state here rather than a
|
|
596
|
-
# finding, since every one of them is gitignored and CI carries none. It
|
|
597
|
-
# is still stated, because a stage naming only what it measured claims a
|
|
598
|
-
# coverage it does not have.
|
|
599
|
-
if [ -n "$audits_absent" ] && [ "$audits_absent" -gt 0 ]; then
|
|
600
|
-
log_info "$audits_absent per-machine corpus/corpora absent, so unmeasured here by design"
|
|
601
|
-
fi
|
|
602
|
-
if [ "$audits_unmeasured" -gt 0 ]; then
|
|
603
|
-
log_warn "$audits_unmeasured audit(s) did not report, so the set is incomplete. Run bun src/cli.ts audits run."
|
|
604
|
-
fi
|
|
605
|
-
if [ "$audits_facts" -gt 0 ]; then
|
|
606
|
-
log_warn "$audits_facts audit(s) carry a finding that is a fact. The stage above names the remedy."
|
|
607
|
-
fi
|
|
608
|
-
if [ -n "$audits_unrecorded" ] && [ "$audits_unrecorded" -gt 0 ]; then
|
|
609
|
-
log_warn "$audits_unrecorded tracked audit(s) have no recorded floor. Take one with bun src/cli.ts audits run --record."
|
|
610
|
-
fi
|
|
611
|
-
if [ "$audits_grown" -gt 0 ]; then
|
|
612
|
-
log_warn "$audits_grown measure(s) grew against $AUDITS_BASELINE. Run bun src/cli.ts audits run to see which, then fix them or re-record and say why."
|
|
613
|
-
else
|
|
614
|
-
log_info "No measure grew against $AUDITS_BASELINE"
|
|
615
|
-
fi
|
|
616
|
-
if [ -n "$audits_shrunk" ] && [ "$audits_shrunk" -gt 0 ]; then
|
|
617
|
-
log_info "$audits_shrunk measure(s) fell against $AUDITS_BASELINE"
|
|
618
|
-
fi
|
|
619
|
-
fi
|
|
620
|
-
fi
|
|
621
|
-
|
|
622
|
-
# The plugin is the second delivery path and this is the only stage gating it,
|
|
623
|
-
# so the skip below is for a contributor's machine rather than for the merge
|
|
624
|
-
# gate. A runner installs the CLI as a workflow step, which makes an absent
|
|
625
|
-
# binary there a broken workflow, and skipping would report a pass for every
|
|
626
|
-
# manifest on the way to a marketplace install. A global install can also land
|
|
627
|
-
# the wrapper and no platform-native binary, which resolves on PATH and cannot
|
|
628
|
-
# run, so the guard tests both and CI refuses on either.
|
|
629
|
-
log_step "Plugin manifests"
|
|
630
|
-
local plugin_cli_state=ready
|
|
631
|
-
if ! command -v claude >/dev/null 2>&1; then
|
|
632
|
-
plugin_cli_state=absent
|
|
633
|
-
elif ! claude --version >/dev/null 2>&1; then
|
|
634
|
-
plugin_cli_state=broken
|
|
635
|
-
fi
|
|
636
|
-
if [ "$plugin_cli_state" = absent ]; then
|
|
637
|
-
if [ "${CI:-false}" = true ]; then
|
|
638
|
-
log_error "claude is not installed. CI installs it before this stage, so read the Install Plugin CLI step in .github/workflows/verify.yml."
|
|
639
|
-
fi
|
|
640
|
-
log_info "Skipped, claude is not installed"
|
|
641
|
-
elif [ "$plugin_cli_state" = broken ]; then
|
|
642
|
-
if [ "${CI:-false}" = true ]; then
|
|
643
|
-
log_error "claude is on PATH and claude --version fails, so the install brought down no platform-native binary and no manifest was read. Raise or lower the pinned version at the Install Plugin CLI step in .github/workflows/verify.yml, and record the move in .claude/context/ci.md."
|
|
644
|
-
fi
|
|
645
|
-
log_info "Skipped, claude is installed but cannot run"
|
|
646
|
-
else
|
|
647
|
-
local manifests manifest
|
|
648
|
-
manifests=$(collect_plugin_manifests)
|
|
649
|
-
if [ -z "$manifests" ]; then
|
|
650
|
-
log_info "Skipped, no manifests present"
|
|
651
|
-
else
|
|
652
|
-
while IFS= read -r manifest; do
|
|
653
|
-
run_check "cd $PROJECT_ROOT && claude plugin validate --strict '$manifest'" "Manifest validation failed: $manifest"
|
|
654
|
-
done <<<"$manifests"
|
|
655
|
-
log_info "Manifests valid"
|
|
656
|
-
fi
|
|
657
|
-
fi
|
|
658
|
-
|
|
659
|
-
log_step "Spelling"
|
|
660
|
-
run_check "bun run check:spell" "Spell check failed"
|
|
661
|
-
log_info "Spell check passed"
|
|
662
|
-
|
|
663
|
-
log_step "Shell"
|
|
664
|
-
if has_changed '\.sh$|^package\.json$'; then
|
|
665
|
-
run_check "bun run check:shell" "Shell check failed"
|
|
666
|
-
run_check "bash $PROJECT_ROOT/scripts/core/check-color-source.sh" "A color escape is defined outside scripts/lib/ui.sh."
|
|
667
|
-
log_info "Shell check passed"
|
|
668
|
-
else
|
|
669
|
-
log_info "Skipped, no shell changes"
|
|
670
|
-
fi
|
|
671
|
-
|
|
672
|
-
log_step "Types"
|
|
673
|
-
if has_changed '^src/|^tsconfig\.json$|^package\.json$'; then
|
|
674
|
-
run_check "bun run check:types" "Typecheck failed"
|
|
675
|
-
log_info "Typecheck passed"
|
|
676
|
-
else
|
|
677
|
-
log_info "Skipped, no TypeScript changes"
|
|
678
|
-
fi
|
|
679
|
-
|
|
680
|
-
log_step "Tests"
|
|
681
|
-
if has_changed "^src/|^vitest\.config\.ts\$|^tsconfig\.json\$|^package\.json\$|$TEST_CORPORA"; then
|
|
682
|
-
run_check "bun run test" "Tests failed"
|
|
683
|
-
log_info "Tests passed"
|
|
684
|
-
else
|
|
685
|
-
log_info "Skipped, no TypeScript or asserted-corpus changes"
|
|
686
|
-
fi
|
|
687
|
-
|
|
688
|
-
if [ "$NESTED" = false ]; then
|
|
689
|
-
echo -e "${GREY}└${NC}\n"
|
|
690
|
-
echo -e "${GREEN}✓ Verification passed${NC}"
|
|
691
|
-
fi
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
main "$@"
|