@memnexus-ai/mx-agent-cli 0.1.165 → 0.1.167
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/dist/__tests__/claude-md-compose.test.d.ts +16 -0
- package/dist/__tests__/claude-md-compose.test.d.ts.map +1 -0
- package/dist/__tests__/claude-md-compose.test.js +116 -0
- package/dist/__tests__/claude-md-compose.test.js.map +1 -0
- package/dist/__tests__/claude-sync.test.js +15 -0
- package/dist/__tests__/claude-sync.test.js.map +1 -1
- package/dist/__tests__/config-generation.test.d.ts +7 -0
- package/dist/__tests__/config-generation.test.d.ts.map +1 -0
- package/dist/__tests__/config-generation.test.js +141 -0
- package/dist/__tests__/config-generation.test.js.map +1 -0
- package/dist/__tests__/config-sync.test.d.ts +10 -0
- package/dist/__tests__/config-sync.test.d.ts.map +1 -0
- package/dist/__tests__/config-sync.test.js +180 -0
- package/dist/__tests__/config-sync.test.js.map +1 -0
- package/dist/__tests__/project-config.test.js +1 -0
- package/dist/__tests__/project-config.test.js.map +1 -1
- package/dist/agent-config/.mx-config-generation.json +4 -0
- package/dist/agent-config/CLAUDE.md.template +140 -0
- package/dist/agent-config/OWNERS.md +51 -0
- package/dist/agent-config/agents/bar-raiser.md +205 -0
- package/dist/agent-config/agents/dogfood-auditor.md +243 -0
- package/dist/agent-config/agents/git-expert.md +380 -0
- package/dist/agent-config/agents/implementation-engineer.md +387 -0
- package/dist/agent-config/agents/memory-creator.md +330 -0
- package/dist/agent-config/agents/prd-writer.md +478 -0
- package/dist/agent-config/agents/prfaq-writer.md +344 -0
- package/dist/agent-config/agents/prior-art-researcher.md +264 -0
- package/dist/agent-config/agents/qa-tester.md +406 -0
- package/dist/agent-config/agents/red-team.md +330 -0
- package/dist/agent-config/agents/security-reviewer.md +300 -0
- package/dist/agent-config/agents/status-reporter.md +297 -0
- package/dist/agent-config/claude-md/CLAUDE.core.md +105 -0
- package/dist/agent-config/claude-md/CLAUDE.overlay.memnexus.md +68 -0
- package/dist/agent-config/claude-md/compose.mjs +179 -0
- package/dist/agent-config/claude-md/vars.memnexus.json +4 -0
- package/dist/agent-config/hooks/auto-capture-precompact.sh +176 -0
- package/dist/agent-config/hooks/auto-capture-session-end.sh +178 -0
- package/dist/agent-config/hooks/auto-checkpoint.sh +102 -0
- package/dist/agent-config/hooks/ci-validate-hook.sh +499 -0
- package/dist/agent-config/hooks/ci-validate-routes.conf +16 -0
- package/dist/agent-config/hooks/delegation-audit.sh +61 -0
- package/dist/agent-config/hooks/deploy-verification-reminder.sh +179 -0
- package/dist/agent-config/hooks/git-mutation-guard.sh +219 -0
- package/dist/agent-config/hooks/iteration-log-check.sh +212 -0
- package/dist/agent-config/hooks/memory-checkpoint-reminder.sh +103 -0
- package/dist/agent-config/hooks/mx-guard-hook.sh +77 -0
- package/dist/agent-config/hooks/named-memory-write-guard.sh +237 -0
- package/dist/agent-config/hooks/reload-checkpoint.sh +68 -0
- package/dist/agent-config/hooks/set-terminal-appearance.sh +34 -0
- package/dist/agent-config/hooks/workflow-dispatch-guard.sh +184 -0
- package/dist/agent-config/hooks/worktree-guard.sh +258 -0
- package/dist/agent-config/rules/eval-harness.md +33 -0
- package/dist/agent-config/rules/frontend.md +17 -0
- package/dist/agent-config/rules/mcp-protocol.md +15 -0
- package/dist/agent-config/rules/retrieval.md +17 -0
- package/dist/agent-config/settings.json +196 -0
- package/dist/agent-config/shared/eval-tiered-rigor.md +15 -0
- package/dist/agent-config/shared/team-operating-rules.md +37 -0
- package/dist/agent-config/skills/eval-claim-impact/SKILL.md +20 -0
- package/dist/agent-config/skills/eval-harness-review/SKILL.md +21 -0
- package/dist/agent-config/skills/mx-checkpoint/SKILL.md +111 -0
- package/dist/agent-config/skills/mx-fix-pr/SKILL.md +90 -0
- package/dist/agent-config/skills/mx-gap-assessment/SKILL.md +90 -0
- package/dist/agent-config/skills/mx-iteration-close/SKILL.md +81 -0
- package/dist/agent-config/skills/mx-post-merge-sync/SKILL.md +83 -0
- package/dist/agent-config/skills/mx-pre-pr-check/SKILL.md +85 -0
- package/dist/agent-config/skills/mx-save/SKILL.md +102 -0
- package/dist/agent-config/skills/mx-session-start/SKILL.md +85 -0
- package/dist/agent-config/skills/team-gate-checklist/SKILL.md +19 -0
- package/dist/commands/config-sync.d.ts +39 -0
- package/dist/commands/config-sync.d.ts.map +1 -0
- package/dist/commands/config-sync.js +212 -0
- package/dist/commands/config-sync.js.map +1 -0
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +3 -0
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +12 -4
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +6 -0
- package/dist/commands/start.js.map +1 -1
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/bundled-config.d.ts +44 -0
- package/dist/lib/bundled-config.d.ts.map +1 -0
- package/dist/lib/bundled-config.js +164 -0
- package/dist/lib/bundled-config.js.map +1 -0
- package/dist/lib/claude.d.ts.map +1 -1
- package/dist/lib/claude.js +9 -3
- package/dist/lib/claude.js.map +1 -1
- package/dist/lib/config-generation.d.ts +36 -0
- package/dist/lib/config-generation.d.ts.map +1 -0
- package/dist/lib/config-generation.js +73 -0
- package/dist/lib/config-generation.js.map +1 -0
- package/dist/lib/project-config.d.ts +7 -0
- package/dist/lib/project-config.d.ts.map +1 -1
- package/dist/lib/project-config.js +4 -0
- package/dist/lib/project-config.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ci-validate-hook.sh — PreToolUse hook (matcher: Bash)
|
|
3
|
+
# Runs package-level validation scripts before git push to catch locally
|
|
4
|
+
# catchable CI failures (lint, build, typecheck, test) before they hit CI.
|
|
5
|
+
#
|
|
6
|
+
# Deployed by syncClaudeConfig() via mx-agent-system/agent-config/hooks/.
|
|
7
|
+
# Runs alongside mx-guard-hook.sh — different layers:
|
|
8
|
+
# mx-guard-hook.sh — infra-level (workflows, Helm, Docker, shell lint)
|
|
9
|
+
# ci-validate-hook.sh — code-level (package lint/build/test)
|
|
10
|
+
#
|
|
11
|
+
# Platform v2.31 (#3951) — under multi-session load the full jest suite flakes on
|
|
12
|
+
# wall-clock-sensitive tests and blocks green pushes. Two mitigations live here:
|
|
13
|
+
# 1. Machine-wide serialization: one flock around the whole validate block so
|
|
14
|
+
# concurrent agent sessions QUEUE instead of contending for CPU (the root
|
|
15
|
+
# cause of the flakes). Paired with a per-run jest --maxWorkers=50% cap in
|
|
16
|
+
# the validate-*.sh scripts so a single run can't saturate the box either.
|
|
17
|
+
# 2. Isolate-and-retry-once de-flake: when a validate run fails ONLY at the
|
|
18
|
+
# test step with a bounded set of failed test files (≤ MAX_ISOLATE_FILES),
|
|
19
|
+
# re-run the FULL suite once, SERIALIZED (--runInBand), still under the
|
|
20
|
+
# flock. Full-suite-green-in-isolation ⇒ machine noise ⇒ gate passes with a
|
|
21
|
+
# NOTE (and the flake is logged). Still-failing ⇒ real regression (incl.
|
|
22
|
+
# order-dependent / shared-state coupling bugs a per-file retry would mask)
|
|
23
|
+
# ⇒ gate blocks. See SF-2 note on run_validate.
|
|
24
|
+
#
|
|
25
|
+
# Exit codes:
|
|
26
|
+
# 0 = allow (not a push, or all validations pass, or a load-flake was absorbed)
|
|
27
|
+
# 2 = block (a real validation failure, or the serialization lock timed out)
|
|
28
|
+
|
|
29
|
+
# Fail-open: any unexpected error → let the push through. NOTE: the two
|
|
30
|
+
# deliberately fail-CLOSED paths (lock timeout, confirmed real regression) use
|
|
31
|
+
# explicit `exit 2` inside conditionals, so the ERR trap never downgrades them.
|
|
32
|
+
trap 'exit 0' ERR
|
|
33
|
+
set -u # Catch undefined variable references (typos silently expand to empty otherwise)
|
|
34
|
+
|
|
35
|
+
# ── State directory (SF-3: no predictable /tmp paths) ─────────────────────────
|
|
36
|
+
# The lock and flake log must not live at fixed, world-writable /tmp paths — a
|
|
37
|
+
# predictable name invites a symlink/clobber attack on a shared host (an attacker
|
|
38
|
+
# pre-creates the path as a symlink to a victim file; our `exec 9>` would then
|
|
39
|
+
# truncate it). Put them in a per-UID directory we own at mode 700. Preferred:
|
|
40
|
+
# the systemd per-user runtime dir (XDG_RUNTIME_DIR / /run/user/$UID) — a 0700
|
|
41
|
+
# tmpfs owned by the user and STABLE across sessions, so the machine-wide lock is
|
|
42
|
+
# still shared. Fallbacks preserve the ownership guarantee.
|
|
43
|
+
mx_validate_dir() {
|
|
44
|
+
local d uid owner
|
|
45
|
+
uid="$(id -u 2>/dev/null || echo 0)"
|
|
46
|
+
|
|
47
|
+
# 1. Per-user runtime dir (stable ⇒ shared lock; already 0700, user-owned).
|
|
48
|
+
local base="${XDG_RUNTIME_DIR:-/run/user/$uid}"
|
|
49
|
+
d="$base/mx-validate"
|
|
50
|
+
if mkdir -p "$d" 2>/dev/null && chmod 700 "$d" 2>/dev/null; then
|
|
51
|
+
printf '%s' "$d"; return 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# 2. Stable per-UID dir under TMPDIR — shared across sessions too, but only
|
|
55
|
+
# trusted if it is a real directory we own and NOT a pre-planted symlink.
|
|
56
|
+
d="${TMPDIR:-/tmp}/mx-validate-$uid"
|
|
57
|
+
mkdir "$d" 2>/dev/null # atomic create; ignore "already exists"
|
|
58
|
+
if [[ -d "$d" && ! -L "$d" ]]; then
|
|
59
|
+
owner="$(stat -c '%u' "$d" 2>/dev/null || echo -1)"
|
|
60
|
+
if [[ "$owner" == "$uid" ]]; then
|
|
61
|
+
chmod 700 "$d" 2>/dev/null || true
|
|
62
|
+
printf '%s' "$d"; return 0
|
|
63
|
+
fi
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# 3. Last resort: an unpredictable per-process dir. Safe from clobber, but not
|
|
67
|
+
# shared across sessions — serialization degrades to per-process. Rare.
|
|
68
|
+
mktemp -d "${TMPDIR:-/tmp}/mx-validate.XXXXXX"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# ── Tunables (env-overridable; defaults are production values) ────────────────
|
|
72
|
+
# Overridable so the co-located test suite can drive short timeouts / temp files.
|
|
73
|
+
MX_VALIDATE_DIR="$(mx_validate_dir)"
|
|
74
|
+
LOCK_FILE="${MX_VALIDATE_LOCK_FILE:-$MX_VALIDATE_DIR/validate.lock}"
|
|
75
|
+
LOCK_TIMEOUT="${MX_VALIDATE_LOCK_TIMEOUT:-900}" # seconds to wait in queue
|
|
76
|
+
FLAKE_LOG="${MX_VALIDATE_FLAKE_LOG:-$MX_VALIDATE_DIR/flakes.log}"
|
|
77
|
+
MAX_ISOLATE_FILES="${MX_VALIDATE_MAX_ISOLATE_FILES:-10}" # >this many failed files ⇒ real breakage
|
|
78
|
+
|
|
79
|
+
FAILED=0
|
|
80
|
+
|
|
81
|
+
# ── Parse failed test files from a runner's captured output ───────────────────
|
|
82
|
+
# jest and vitest both print "FAIL <path>" for each failing test file. Strip ANSI
|
|
83
|
+
# colour, take the path token, keep only things that look like a test/spec file,
|
|
84
|
+
# and dedupe. A suite-level crash (jest-worker OOM, config/compile error) prints
|
|
85
|
+
# NO per-file FAIL lines, so this returns empty — which the caller treats as
|
|
86
|
+
# "not isolable ⇒ block" (a wholly-failed suite must never enter retry).
|
|
87
|
+
parse_failed_test_files() {
|
|
88
|
+
local out="$1"
|
|
89
|
+
# SF-3: strip ANSI colour AND every other control character from the path
|
|
90
|
+
# token before it is trusted / logged (a filename with embedded control chars
|
|
91
|
+
# could corrupt the flake log or inject terminal escapes). The per-line `sed`
|
|
92
|
+
# operates on the token only — the inter-token newlines are the line
|
|
93
|
+
# separators, so they are preserved.
|
|
94
|
+
sed -E 's/\x1b\[[0-9;]*m//g' "$out" 2>/dev/null \
|
|
95
|
+
| grep -E '^[[:space:]]*FAIL[[:space:]]' 2>/dev/null \
|
|
96
|
+
| sed -E 's/^[[:space:]]*FAIL[[:space:]]+//' \
|
|
97
|
+
| awk '{print $1}' \
|
|
98
|
+
| sed -E 's/[[:cntrl:]]//g' \
|
|
99
|
+
| grep -E '\.(test|spec)\.[cm]?[jt]sx?$' 2>/dev/null \
|
|
100
|
+
| sort -u \
|
|
101
|
+
|| true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# ── Append a load-flake record to the gitignored local log ────────────────────
|
|
105
|
+
# Accumulates flake data (timestamp, load avg, package, files) for the follow-up
|
|
106
|
+
# wall-clock-guard → operation-count conversion work. Best-effort; never blocks.
|
|
107
|
+
record_flakes() {
|
|
108
|
+
local label="$1"; shift
|
|
109
|
+
local files_line
|
|
110
|
+
# SF-3: newlines/tabs → spaces, then drop any remaining control chars so the
|
|
111
|
+
# appended log line can't be corrupted by a hostile filename token.
|
|
112
|
+
files_line="$(printf '%s ' "$@" | tr '\n\t\r' ' ' | tr -d '[:cntrl:]')"
|
|
113
|
+
local ts load
|
|
114
|
+
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo 'unknown')"
|
|
115
|
+
load="$(cut -d' ' -f1 /proc/loadavg 2>/dev/null || echo '?')"
|
|
116
|
+
printf '%s\tload1m=%s\t%s\t%s\n' "$ts" "$load" "$label" "$files_line" \
|
|
117
|
+
>>"$FLAKE_LOG" 2>/dev/null || true
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# ── Acquire the machine-wide validation lock (deliberate placement) ───────────
|
|
121
|
+
# ONE lock in the hook around the whole validate block. Rationale (#3951):
|
|
122
|
+
# * Simplest correct design: it serializes even two same-package validates from
|
|
123
|
+
# two different sessions (the case the escalation specifically calls out).
|
|
124
|
+
# * Tradeoff, documented: different-package validates from two sessions ALSO
|
|
125
|
+
# serialize (they can't overlap). Accepted — cross-session CPU contention is
|
|
126
|
+
# exactly what we're removing, and the per-run --maxWorkers=50% cap keeps any
|
|
127
|
+
# single run from saturating the box, so total queue time stays bounded.
|
|
128
|
+
# * flock releases automatically on process death (fd close), so there is no
|
|
129
|
+
# stale-lock risk even if a session is killed mid-validate.
|
|
130
|
+
acquire_lock() {
|
|
131
|
+
# Open the lock file on fd 9. If we can't even open it (unexpected infra
|
|
132
|
+
# error), fail-OPEN: this is not the gate's job and matches the hook's
|
|
133
|
+
# overall fail-open posture for unexpected conditions. NOTE: no `2>/dev/null`
|
|
134
|
+
# on this exec — `exec` with only redirections applies them to the CURRENT
|
|
135
|
+
# shell permanently, so a trailing `2>/dev/null` would silence the hook's own
|
|
136
|
+
# stderr for the whole run. The `|| { ... }` guard handles a failed open.
|
|
137
|
+
exec 9>"$LOCK_FILE" || { echo "ci-validate: WARN cannot open lock file $LOCK_FILE — proceeding without serialization." >&2; return 0; }
|
|
138
|
+
|
|
139
|
+
if flock -n 9; then
|
|
140
|
+
return 0 # uncontended — go straight through
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
echo "ci-validate: another session is validating — queued (waiting up to ${LOCK_TIMEOUT}s)..." >&2
|
|
144
|
+
if flock -w "$LOCK_TIMEOUT" 9; then
|
|
145
|
+
echo "ci-validate: lock acquired — proceeding." >&2
|
|
146
|
+
return 0
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
# Timeout → FAIL CLOSED. This is a CI gate: failing open here would let an
|
|
150
|
+
# UNVALIDATED push through under exactly the sustained-high-load conditions
|
|
151
|
+
# where a regression is most likely to slip in. So we block and tell the user
|
|
152
|
+
# to retry (queue will drain) rather than wave the push through.
|
|
153
|
+
local holder=""
|
|
154
|
+
if command -v fuser >/dev/null 2>&1; then
|
|
155
|
+
holder="$(fuser "$LOCK_FILE" 2>/dev/null | tr -s ' ' || true)"
|
|
156
|
+
fi
|
|
157
|
+
{
|
|
158
|
+
echo "ci-validate: TIMED OUT after ${LOCK_TIMEOUT}s waiting for the validation lock ($LOCK_FILE)."
|
|
159
|
+
[[ -n "$holder" ]] && echo " Lock currently held by PID(s):$holder"
|
|
160
|
+
echo " Push BLOCKED (fail-closed — the validation gate must not be skipped under load)."
|
|
161
|
+
echo " The queue will drain; retry the push shortly."
|
|
162
|
+
} >&2
|
|
163
|
+
exit 2
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# ── Run one validation script, with isolate-and-retry-once de-flake ───────────
|
|
167
|
+
# All hook-authored output goes to stderr — the harness surfaces a blocking
|
|
168
|
+
# hook's stderr, not its stdout (KI-HOOKS-STDERR-SYSTEMIC). Sub-validation output
|
|
169
|
+
# is routed to stderr too so failures are visible on the block.
|
|
170
|
+
run_validate() {
|
|
171
|
+
local script="$1"
|
|
172
|
+
local label="$2"
|
|
173
|
+
local script_path
|
|
174
|
+
case "$script" in
|
|
175
|
+
/*) script_path="$script" ;; # absolute (test seam)
|
|
176
|
+
*) script_path="$REPO_ROOT/$script" ;; # repo-relative (production)
|
|
177
|
+
esac
|
|
178
|
+
if [[ ! -f "$script_path" ]]; then
|
|
179
|
+
echo "⚠ $label: $script not found — skipping" >&2
|
|
180
|
+
return
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
echo "" >&2
|
|
184
|
+
echo "Running $label validation..." >&2
|
|
185
|
+
|
|
186
|
+
local out; out="$(mktemp)"
|
|
187
|
+
local rc=0
|
|
188
|
+
# SF-1a: scrub the isolation-mode signals from the FIRST-pass environment.
|
|
189
|
+
# Otherwise an ambient MX_VALIDATE_SERIAL (or the legacy MX_VALIDATE_ISOLATE_
|
|
190
|
+
# FILES) would make the validate script exec straight into a serial/partial
|
|
191
|
+
# run and skip lint/build/the full parallel suite — silently turning the gate
|
|
192
|
+
# into a no-op. The deliberate retry below sets MX_VALIDATE_SERIAL explicitly,
|
|
193
|
+
# so nothing legitimate breaks.
|
|
194
|
+
env -u MX_VALIDATE_SERIAL -u MX_VALIDATE_ISOLATE_FILES bash "$script_path" >"$out" 2>&1 || rc=$?
|
|
195
|
+
cat "$out" >&2
|
|
196
|
+
|
|
197
|
+
if [[ $rc -eq 0 ]]; then
|
|
198
|
+
echo "✓ $label passed" >&2
|
|
199
|
+
rm -f "$out"
|
|
200
|
+
return
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
# ── Failure: decide whether it is an isolable load flake or a real break ────
|
|
204
|
+
|
|
205
|
+
# A NON-test step failed (lint / typecheck / build / openapi / gate). The
|
|
206
|
+
# validate-*.sh scripts emit a structured `[FAIL] <step> ...` marker per failed
|
|
207
|
+
# step; any such marker that is not the test step means a real breakage that an
|
|
208
|
+
# isolated test re-run must NOT mask. Block immediately, no retry.
|
|
209
|
+
if grep -E '^\[FAIL\]' "$out" 2>/dev/null | grep -qivE 'test' ; then
|
|
210
|
+
echo "✗ $label FAILED — a non-test step failed (lint/typecheck/build/openapi/gate)." >&2
|
|
211
|
+
echo " Not a load flake; blocking. Fix the errors above." >&2
|
|
212
|
+
FAILED=1; rm -f "$out"; return
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
local failed_files nfiles
|
|
216
|
+
failed_files="$(parse_failed_test_files "$out")"
|
|
217
|
+
if [[ -z "$failed_files" ]]; then
|
|
218
|
+
nfiles=0
|
|
219
|
+
else
|
|
220
|
+
nfiles="$(printf '%s\n' "$failed_files" | wc -l | tr -d ' ')"
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
# No isolable failed test files parsed ⇒ suite-level failure (compile error,
|
|
224
|
+
# jest-worker crash/OOM, config error). A wholly-failed suite must never enter
|
|
225
|
+
# retry — block.
|
|
226
|
+
if [[ "$nfiles" -eq 0 ]]; then
|
|
227
|
+
echo "✗ $label FAILED — no isolable failed test files found (suite-level failure/crash)." >&2
|
|
228
|
+
echo " Not eligible for de-flake retry; blocking." >&2
|
|
229
|
+
FAILED=1; rm -f "$out"; return
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
# Too many failed files ⇒ this is real breakage, not a couple of wall-clock
|
|
233
|
+
# flakes. Block immediately (bound the retry to ≤ MAX_ISOLATE_FILES).
|
|
234
|
+
if [[ "$nfiles" -gt "$MAX_ISOLATE_FILES" ]]; then
|
|
235
|
+
echo "✗ $label FAILED — $nfiles failed test files (> $MAX_ISOLATE_FILES cap)." >&2
|
|
236
|
+
echo " Too broad to be load flake; blocking immediately." >&2
|
|
237
|
+
FAILED=1; rm -f "$out"; return
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
# Eligible: ONE isolation pass. SF-2 — re-running ONLY the failed files would
|
|
241
|
+
# LAUNDER an order-dependent / shared-state failure: a test that fails solely
|
|
242
|
+
# because an earlier test in the suite mutated a shared singleton / DB / port /
|
|
243
|
+
# mock passes when run alone, and would be waved through as a "flake". So
|
|
244
|
+
# re-run the FULL suite once, SERIALIZED (jest --runInBand / maxWorkers=1),
|
|
245
|
+
# still under the machine-wide flock. Under the flock the retry runs on a quiet
|
|
246
|
+
# box, so genuine wall-clock load flakes (retrieval's case) pass; but because
|
|
247
|
+
# the WHOLE suite still runs, in order, a deterministic coupling bug still
|
|
248
|
+
# fails and blocks. Require GREEN to absorb.
|
|
249
|
+
# shellcheck disable=SC2086
|
|
250
|
+
echo "⟳ $label: $nfiles test file(s) failed under load — re-running the FULL suite ONCE in isolation (serial)..." >&2
|
|
251
|
+
# shellcheck disable=SC2086
|
|
252
|
+
printf ' under-load failure: %s\n' $failed_files >&2
|
|
253
|
+
|
|
254
|
+
local iso_out; iso_out="$(mktemp)"
|
|
255
|
+
local iso_rc=0
|
|
256
|
+
MX_VALIDATE_SERIAL=1 bash "$script_path" >"$iso_out" 2>&1 || iso_rc=$?
|
|
257
|
+
cat "$iso_out" >&2
|
|
258
|
+
|
|
259
|
+
local still_failing
|
|
260
|
+
still_failing="$(parse_failed_test_files "$iso_out")"
|
|
261
|
+
|
|
262
|
+
if [[ $iso_rc -eq 0 && -z "$still_failing" ]]; then
|
|
263
|
+
# Absorbed — the full suite is GREEN when run serially/uncontended, so the
|
|
264
|
+
# under-load failures were machine noise, not a real (or coupling) bug.
|
|
265
|
+
{
|
|
266
|
+
echo ""
|
|
267
|
+
echo " ┌───────────────────────────────────────────────────────────────"
|
|
268
|
+
echo " │ NOTE: $label flaked under load; FULL suite passed in isolation."
|
|
269
|
+
echo " │ Treating as machine noise — gate PASSES for $label."
|
|
270
|
+
echo " │ Under-load flaked test file(s):"
|
|
271
|
+
# shellcheck disable=SC2086
|
|
272
|
+
printf ' │ %s\n' $failed_files
|
|
273
|
+
echo " │ Recorded to $FLAKE_LOG for follow-up de-flake work."
|
|
274
|
+
echo " └───────────────────────────────────────────────────────────────"
|
|
275
|
+
} >&2
|
|
276
|
+
# shellcheck disable=SC2086
|
|
277
|
+
record_flakes "$label" $failed_files
|
|
278
|
+
rm -f "$out" "$iso_out"
|
|
279
|
+
return
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
# The full serial suite still failed ⇒ a REAL regression (including an
|
|
283
|
+
# order-dependent / shared-state coupling bug the single-file retry would have
|
|
284
|
+
# masked). Report what still fails and block.
|
|
285
|
+
echo "✗ $label FAILED — full serial suite still failed in isolation; blocking." >&2
|
|
286
|
+
if [[ -n "$still_failing" ]]; then
|
|
287
|
+
echo " REAL failures (fail even in a serial full-suite run):" >&2
|
|
288
|
+
# shellcheck disable=SC2086
|
|
289
|
+
printf ' %s\n' $still_failing >&2
|
|
290
|
+
local flakes
|
|
291
|
+
# under-load-only files that did NOT recur serially = load-flakes (informational)
|
|
292
|
+
# shellcheck disable=SC2086
|
|
293
|
+
flakes="$(comm -23 <(printf '%s\n' $failed_files | sort -u) <(printf '%s\n' $still_failing | sort -u) || true)"
|
|
294
|
+
if [[ -n "$flakes" ]]; then
|
|
295
|
+
echo " Load-flakes (passed serially, not the blocker):" >&2
|
|
296
|
+
# shellcheck disable=SC2086
|
|
297
|
+
printf ' %s\n' $flakes >&2
|
|
298
|
+
fi
|
|
299
|
+
else
|
|
300
|
+
echo " Serial re-run failed without parseable per-file FAIL lines (crash/compile" >&2
|
|
301
|
+
echo " error under isolation) — see output above. Under-load failure(s) were:" >&2
|
|
302
|
+
# shellcheck disable=SC2086
|
|
303
|
+
printf ' %s\n' $failed_files >&2
|
|
304
|
+
fi
|
|
305
|
+
FAILED=1
|
|
306
|
+
rm -f "$out" "$iso_out"
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# ── Test seam (SF-1b: must be impossible to engage silently on a real push) ───
|
|
311
|
+
# Drives the lock + de-flake machinery against explicit fixture scripts without
|
|
312
|
+
# a real repo / git diff, so ci-validate-hook.test.sh can exercise queueing,
|
|
313
|
+
# timeout, and every de-flake branch deterministically. MX_VALIDATE_SELFTEST is a
|
|
314
|
+
# space-separated list of "script::label" specs (script may be absolute).
|
|
315
|
+
#
|
|
316
|
+
# Two guards prevent the seam from shadowing real validation:
|
|
317
|
+
# 1. It is only honoured when MX_VALIDATE_SELFTEST_OK=1 is ALSO set (a stray
|
|
318
|
+
# MX_VALIDATE_SELFTEST alone is ignored — the gate runs normally).
|
|
319
|
+
# 2. Even with both set, if a real tool command is present on stdin (i.e. the
|
|
320
|
+
# harness invoked us for an actual push), we REFUSE with a loud banner and
|
|
321
|
+
# fail closed. Fixtures must never run in place of real validation.
|
|
322
|
+
if [[ -n "${MX_VALIDATE_SELFTEST:-}" ]]; then
|
|
323
|
+
if [[ "${MX_VALIDATE_SELFTEST_OK:-}" != "1" ]]; then
|
|
324
|
+
echo "ci-validate: MX_VALIDATE_SELFTEST is set but MX_VALIDATE_SELFTEST_OK=1 is not — ignoring the test seam and running normal validation." >&2
|
|
325
|
+
# fall through to the real push path below
|
|
326
|
+
else
|
|
327
|
+
# `timeout` guards against a hang if run interactively with no stdin; the
|
|
328
|
+
# test harness redirects </dev/null so this returns instantly.
|
|
329
|
+
SELFTEST_STDIN="$(timeout 5 cat 2>/dev/null || true)"
|
|
330
|
+
SELFTEST_CMD="$(printf '%s' "$SELFTEST_STDIN" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
|
|
331
|
+
if [[ -n "$SELFTEST_CMD" ]]; then
|
|
332
|
+
{
|
|
333
|
+
echo "╔══════════════════════════════════════════════════════════════════╗"
|
|
334
|
+
echo "║ SELFTEST MODE — REAL VALIDATION SKIPPED ║"
|
|
335
|
+
echo "╠══════════════════════════════════════════════════════════════════╣"
|
|
336
|
+
echo "║ MX_VALIDATE_SELFTEST + MX_VALIDATE_SELFTEST_OK were set while a"
|
|
337
|
+
echo "║ real tool command was present on stdin:"
|
|
338
|
+
echo "║ ${SELFTEST_CMD:0:120}"
|
|
339
|
+
echo "║ Refusing to run test fixtures in place of real validation."
|
|
340
|
+
echo "║ Push BLOCKED (fail-closed). Unset MX_VALIDATE_SELFTEST to push."
|
|
341
|
+
echo "╚══════════════════════════════════════════════════════════════════╝"
|
|
342
|
+
} >&2
|
|
343
|
+
exit 2
|
|
344
|
+
fi
|
|
345
|
+
REPO_ROOT="${MX_VALIDATE_SELFTEST_ROOT:-$PWD}"
|
|
346
|
+
acquire_lock
|
|
347
|
+
for spec in $MX_VALIDATE_SELFTEST; do
|
|
348
|
+
st_script="${spec%%::*}"
|
|
349
|
+
st_label="${spec##*::}"
|
|
350
|
+
run_validate "$st_script" "$st_label"
|
|
351
|
+
done
|
|
352
|
+
if [[ "$FAILED" -ne 0 ]]; then
|
|
353
|
+
echo "ci-validate(selftest): FAILED" >&2
|
|
354
|
+
exit 2
|
|
355
|
+
fi
|
|
356
|
+
echo "ci-validate(selftest): all checks passed." >&2
|
|
357
|
+
exit 0
|
|
358
|
+
fi
|
|
359
|
+
fi
|
|
360
|
+
|
|
361
|
+
INPUT=$(cat)
|
|
362
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
363
|
+
|
|
364
|
+
# Cheap literal pre-filter: bail before the regex on any command that can't
|
|
365
|
+
# contain "push". Keeps the pattern match off the hot path for the vast majority
|
|
366
|
+
# of commands (and off long non-push commands).
|
|
367
|
+
case "$COMMAND" in
|
|
368
|
+
*push*) ;;
|
|
369
|
+
*) exit 0 ;;
|
|
370
|
+
esac
|
|
371
|
+
|
|
372
|
+
# Only intercept git push commands. Match "git push" when it begins a command
|
|
373
|
+
# segment. A start-anchored ^git\ push matcher silently let the repo's own
|
|
374
|
+
# sanctioned compound pattern (cd $CLAUDE_WORKTREE_PATH/<pkg> && git push) and
|
|
375
|
+
# other routine forms bypass validation. Segment starts recognised:
|
|
376
|
+
# - start of command (^)
|
|
377
|
+
# - a separator: ; & | { ( or a control char (newline/tab — [:cntrl:] matches
|
|
378
|
+
# newline but NOT space, so newline-joined commands are segmented without
|
|
379
|
+
# turning every space into a boundary). SF-3 added `(` for the subshell
|
|
380
|
+
# form ( git push ).
|
|
381
|
+
# - a shell keyword: then / do / else (e.g. `if ...; then git push; fi`)
|
|
382
|
+
# Also tolerated before "git": leading redirections (`2>/dev/null `), env-var
|
|
383
|
+
# assignments (VAR=1 git push), and launcher/wrapper words (sudo/env/nice/nohup/
|
|
384
|
+
# time/timeout/ionice/stdbuf/command/xargs/setsid, optional numeric arg) so
|
|
385
|
+
# wrapper-prefixed pushes aren't skipped. GIT GLOBAL OPTIONS between `git` and
|
|
386
|
+
# `push` are consumed (High, #3934): `-C <path>`, `-c <k>=<v>`,
|
|
387
|
+
# `--git-dir[=| ]<p>`, `--work-tree[=| ]<p>`, `--namespace[=| ]<p>`,
|
|
388
|
+
# `--no-pager`, and generic single/double-dash forms with or without values.
|
|
389
|
+
# Pre-#3934 only `-C <path>` was consumed, so common benign forms like
|
|
390
|
+
# `git --no-pager push` and `git -c core.sshCommand=x push` skipped validation
|
|
391
|
+
# entirely. The trailing group accepts `)` so the tight subshell form (git push)
|
|
392
|
+
# closes correctly (SF-3). Kept consistent with git-mutation-guard GITSEG.
|
|
393
|
+
PUSH_RE='(^|[;&|{([:cntrl:]]|[[:space:]](then|do|else)[[:space:]])[[:space:]]*([0-9]*[<>][^[:space:]]*[[:space:]]+|[A-Za-z_][A-Za-z0-9_]*=[^[:space:];&|]*[[:space:]]+|(sudo|env|nice|nohup|time|timeout|ionice|stdbuf|command|xargs|setsid)[[:space:]]+([0-9]+[[:space:]]+)?)*git([[:space:]]+(-C[[:space:]]+[^[:space:];&|]+|-c[[:space:]]+[^[:space:];&|]+|--(git-dir|work-tree|namespace|super-prefix)(=[^[:space:];&|]*|[[:space:]]+[^[:space:];&|]+)|--[A-Za-z][A-Za-z0-9-]*=[^[:space:];&|]*|--[A-Za-z][A-Za-z0-9-]*|-[A-Za-z]))*[[:space:]]+push([[:space:]]|[;&|)]|$)'
|
|
394
|
+
if [[ ! "$COMMAND" =~ $PUSH_RE ]]; then
|
|
395
|
+
exit 0
|
|
396
|
+
fi
|
|
397
|
+
|
|
398
|
+
# ── Resolve repo root ────────────────────────────────────────────────────────
|
|
399
|
+
# The hook runs from the worktree's .claude/hooks/ dir; scripts/ is at the
|
|
400
|
+
# repo root. Find it by walking up from cwd.
|
|
401
|
+
REPO_ROOT=""
|
|
402
|
+
DIR="$(pwd)"
|
|
403
|
+
for _ in $(seq 1 8); do
|
|
404
|
+
if [[ -f "$DIR/package.json" && -f "$DIR/pnpm-workspace.yaml" ]]; then
|
|
405
|
+
REPO_ROOT="$DIR"
|
|
406
|
+
break
|
|
407
|
+
fi
|
|
408
|
+
DIR="$(dirname "$DIR")"
|
|
409
|
+
done
|
|
410
|
+
|
|
411
|
+
if [[ -z "$REPO_ROOT" ]]; then
|
|
412
|
+
exit 0 # Can't find repo root — fail-open
|
|
413
|
+
fi
|
|
414
|
+
|
|
415
|
+
# ── Detect changed packages ──────────────────────────────────────────────────
|
|
416
|
+
# Use three-dot diff against origin/main to find files changed on this branch.
|
|
417
|
+
# Falls back to HEAD (single-commit diff) if origin/main isn't available.
|
|
418
|
+
CHANGED_FILES=$(git -C "$REPO_ROOT" diff --name-only "origin/main...HEAD" 2>/dev/null \
|
|
419
|
+
|| git -C "$REPO_ROOT" diff --name-only HEAD 2>/dev/null \
|
|
420
|
+
|| true)
|
|
421
|
+
|
|
422
|
+
if [[ -z "$CHANGED_FILES" ]]; then
|
|
423
|
+
exit 0 # No changed files detected — nothing to validate
|
|
424
|
+
fi
|
|
425
|
+
|
|
426
|
+
# ── Load the package→validate-script routing table ───────────────────────────
|
|
427
|
+
# The map lives in ci-validate-routes.conf alongside this hook (portable projects
|
|
428
|
+
# ship their own). Format: <path-prefix-glob>|<validate-script>|<label>, one rule
|
|
429
|
+
# per line, '#' comments and blanks ignored. If the file is absent the hook logs
|
|
430
|
+
# a note and skips validation (fail-open) — never blocks a push on a missing map.
|
|
431
|
+
# MX_CI_VALIDATE_ROUTES overrides the path (test seam / portable override).
|
|
432
|
+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || echo "")"
|
|
433
|
+
ROUTES_FILE="${MX_CI_VALIDATE_ROUTES:-$HOOK_DIR/ci-validate-routes.conf}"
|
|
434
|
+
if [[ ! -f "$ROUTES_FILE" ]]; then
|
|
435
|
+
echo "ci-validate: routing table not found ($ROUTES_FILE) — skipping validation (fail-open, no packages routed)." >&2
|
|
436
|
+
exit 0
|
|
437
|
+
fi
|
|
438
|
+
|
|
439
|
+
R_PREFIX=(); R_SCRIPT=(); R_LABEL=()
|
|
440
|
+
while IFS='|' read -r prefix script label _rest; do
|
|
441
|
+
# Trim surrounding whitespace on each field.
|
|
442
|
+
prefix="${prefix#"${prefix%%[![:space:]]*}"}"; prefix="${prefix%"${prefix##*[![:space:]]}"}"
|
|
443
|
+
script="${script#"${script%%[![:space:]]*}"}"; script="${script%"${script##*[![:space:]]}"}"
|
|
444
|
+
label="${label#"${label%%[![:space:]]*}"}"; label="${label%"${label##*[![:space:]]}"}"
|
|
445
|
+
# Skip comments and incomplete rows.
|
|
446
|
+
[[ -z "$prefix" || "${prefix:0:1}" == "#" ]] && continue
|
|
447
|
+
[[ -z "$script" || -z "$label" ]] && continue
|
|
448
|
+
R_PREFIX+=("$prefix"); R_SCRIPT+=("$script"); R_LABEL+=("$label")
|
|
449
|
+
done < "$ROUTES_FILE"
|
|
450
|
+
|
|
451
|
+
if [[ ${#R_PREFIX[@]} -eq 0 ]]; then
|
|
452
|
+
echo "ci-validate: routing table $ROUTES_FILE has no usable rules — skipping validation (fail-open)." >&2
|
|
453
|
+
exit 0
|
|
454
|
+
fi
|
|
455
|
+
|
|
456
|
+
# ── Match changed files against the routes ───────────────────────────────────
|
|
457
|
+
MATCHED_IDX=()
|
|
458
|
+
for ((ri=0; ri<${#R_PREFIX[@]}; ri++)); do
|
|
459
|
+
while IFS= read -r f; do
|
|
460
|
+
[[ -z "$f" ]] && continue
|
|
461
|
+
# shellcheck disable=SC2053 # intentional glob match against the route prefix
|
|
462
|
+
if [[ "$f" == ${R_PREFIX[$ri]} ]]; then
|
|
463
|
+
MATCHED_IDX+=("$ri")
|
|
464
|
+
break
|
|
465
|
+
fi
|
|
466
|
+
done <<< "$CHANGED_FILES"
|
|
467
|
+
done
|
|
468
|
+
|
|
469
|
+
# Nothing relevant changed
|
|
470
|
+
if [[ ${#MATCHED_IDX[@]} -eq 0 ]]; then
|
|
471
|
+
exit 0
|
|
472
|
+
fi
|
|
473
|
+
|
|
474
|
+
# ── Run validations (serialized machine-wide) ────────────────────────────────
|
|
475
|
+
echo "ci-validate: checking changed packages before push..." >&2
|
|
476
|
+
|
|
477
|
+
# Serialize the whole validate block across all sessions. Released on exit.
|
|
478
|
+
acquire_lock
|
|
479
|
+
|
|
480
|
+
for ri in "${MATCHED_IDX[@]}"; do
|
|
481
|
+
run_validate "${R_SCRIPT[$ri]}" "${R_LABEL[$ri]}"
|
|
482
|
+
done
|
|
483
|
+
|
|
484
|
+
# ── Result ───────────────────────────────────────────────────────────────────
|
|
485
|
+
if [[ $FAILED -ne 0 ]]; then
|
|
486
|
+
{
|
|
487
|
+
echo ""
|
|
488
|
+
echo "ci-validate: push blocked — fix the errors above before pushing."
|
|
489
|
+
echo " Run manually:"
|
|
490
|
+
for ((ri=0; ri<${#R_SCRIPT[@]}; ri++)); do
|
|
491
|
+
echo " bash ${R_SCRIPT[$ri]}"
|
|
492
|
+
done
|
|
493
|
+
} >&2
|
|
494
|
+
exit 2
|
|
495
|
+
fi
|
|
496
|
+
|
|
497
|
+
echo "" >&2
|
|
498
|
+
echo "ci-validate: all checks passed." >&2
|
|
499
|
+
exit 0
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# ci-validate routing table — <path-prefix-glob>|<validate-script>|<label>
|
|
2
|
+
#
|
|
3
|
+
# One rule per line. ci-validate-hook.sh matches each changed file (repo-root
|
|
4
|
+
# relative) against every <path-prefix-glob> (shell case-glob). On a match the
|
|
5
|
+
# corresponding <validate-script> (repo-root relative) runs, tagged <label>.
|
|
6
|
+
# Lines beginning with '#' and blank lines are ignored. Whitespace around the
|
|
7
|
+
# '|' separators is trimmed.
|
|
8
|
+
#
|
|
9
|
+
# This file is the checked-in MemNexus default. If it is absent the hook logs a
|
|
10
|
+
# note and skips validation (fail-open) — a portable project ships its own map.
|
|
11
|
+
cli/*|scripts/validate-cli.sh|cli
|
|
12
|
+
mcp-server/*|scripts/validate-mcp-server.sh|mcp-server
|
|
13
|
+
customer-portal/*|scripts/validate-portal.sh|customer-portal
|
|
14
|
+
core-api/*|scripts/validate-core-api.sh|core-api
|
|
15
|
+
releases/sdk-generator/*|scripts/validate-sdk-generator.sh|sdk-generator
|
|
16
|
+
benchmarks/*|scripts/validate-benchmarks.sh|benchmarks
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# delegation-audit.sh — Delegation audit StopHook
|
|
3
|
+
#
|
|
4
|
+
# Emits an advisory when a session had many tool calls but zero sub-agent
|
|
5
|
+
# delegation (no Agent or Task tool calls). This is non-blocking and advisory
|
|
6
|
+
# only — it never prevents the session from ending.
|
|
7
|
+
#
|
|
8
|
+
# Fires on: Stop
|
|
9
|
+
# Input: JSON on stdin with transcript_path, session_id, stop_hook_active
|
|
10
|
+
# Output: advisory message to stdout (when threshold met)
|
|
11
|
+
# Exit: always 0 (never blocks)
|
|
12
|
+
|
|
13
|
+
# Fail-open: any error -> allow session to end normally
|
|
14
|
+
trap 'exit 0' ERR
|
|
15
|
+
|
|
16
|
+
# ── 1. Parse hook input ──────────────────────────────────────────────────
|
|
17
|
+
INPUT=$(cat)
|
|
18
|
+
|
|
19
|
+
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)
|
|
20
|
+
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null)
|
|
21
|
+
|
|
22
|
+
# ── 2. Anti-loop guard ───────────────────────────────────────────────────
|
|
23
|
+
if [[ "$STOP_HOOK_ACTIVE" == "true" ]]; then
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# ── 3. Fail-open guards ──────────────────────────────────────────────────
|
|
28
|
+
if [[ -z "$TRANSCRIPT_PATH" || ! -f "$TRANSCRIPT_PATH" ]]; then
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
32
|
+
|
|
33
|
+
# ── 4. Count total tool calls ────────────────────────────────────────────
|
|
34
|
+
TOOL_CALL_COUNT=$(jq -r '
|
|
35
|
+
select(.type == "assistant") |
|
|
36
|
+
.message.content[]? |
|
|
37
|
+
select(.type == "tool_use") |
|
|
38
|
+
.name
|
|
39
|
+
' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')
|
|
40
|
+
|
|
41
|
+
# ── 5. Count delegation calls (Agent or Task) ────────────────────────────
|
|
42
|
+
DELEGATION_COUNT=$(jq -r '
|
|
43
|
+
select(.type == "assistant") |
|
|
44
|
+
.message.content[]? |
|
|
45
|
+
select(.type == "tool_use") |
|
|
46
|
+
select(.name == "Agent" or .name == "Task") |
|
|
47
|
+
.name
|
|
48
|
+
' "$TRANSCRIPT_PATH" 2>/dev/null | wc -l | tr -d ' ')
|
|
49
|
+
|
|
50
|
+
# ── 6. Emit advisory if threshold met ────────────────────────────────────
|
|
51
|
+
if [[ "$TOOL_CALL_COUNT" -ge 30 && "$DELEGATION_COUNT" -eq 0 ]]; then
|
|
52
|
+
# Route to stderr for consistency with the hook-output convention
|
|
53
|
+
# (KI-HOOKS-STDERR-SYSTEMIC) — user-facing advisories go to stderr.
|
|
54
|
+
{
|
|
55
|
+
echo "⚠️ Delegation audit: This session had ${TOOL_CALL_COUNT} tool calls but 0 sub-agent spawns."
|
|
56
|
+
echo "Consider delegating implementation, code review, and adversarial testing to sub-agents per your roleguide."
|
|
57
|
+
echo "Run 'mx memories recall --query \"delegation\"' to review delegation patterns."
|
|
58
|
+
} >&2
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
exit 0
|