@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.
Files changed (100) hide show
  1. package/dist/__tests__/claude-md-compose.test.d.ts +16 -0
  2. package/dist/__tests__/claude-md-compose.test.d.ts.map +1 -0
  3. package/dist/__tests__/claude-md-compose.test.js +116 -0
  4. package/dist/__tests__/claude-md-compose.test.js.map +1 -0
  5. package/dist/__tests__/claude-sync.test.js +15 -0
  6. package/dist/__tests__/claude-sync.test.js.map +1 -1
  7. package/dist/__tests__/config-generation.test.d.ts +7 -0
  8. package/dist/__tests__/config-generation.test.d.ts.map +1 -0
  9. package/dist/__tests__/config-generation.test.js +141 -0
  10. package/dist/__tests__/config-generation.test.js.map +1 -0
  11. package/dist/__tests__/config-sync.test.d.ts +10 -0
  12. package/dist/__tests__/config-sync.test.d.ts.map +1 -0
  13. package/dist/__tests__/config-sync.test.js +180 -0
  14. package/dist/__tests__/config-sync.test.js.map +1 -0
  15. package/dist/__tests__/project-config.test.js +1 -0
  16. package/dist/__tests__/project-config.test.js.map +1 -1
  17. package/dist/agent-config/.mx-config-generation.json +4 -0
  18. package/dist/agent-config/CLAUDE.md.template +140 -0
  19. package/dist/agent-config/OWNERS.md +51 -0
  20. package/dist/agent-config/agents/bar-raiser.md +205 -0
  21. package/dist/agent-config/agents/dogfood-auditor.md +243 -0
  22. package/dist/agent-config/agents/git-expert.md +380 -0
  23. package/dist/agent-config/agents/implementation-engineer.md +387 -0
  24. package/dist/agent-config/agents/memory-creator.md +330 -0
  25. package/dist/agent-config/agents/prd-writer.md +478 -0
  26. package/dist/agent-config/agents/prfaq-writer.md +344 -0
  27. package/dist/agent-config/agents/prior-art-researcher.md +264 -0
  28. package/dist/agent-config/agents/qa-tester.md +406 -0
  29. package/dist/agent-config/agents/red-team.md +330 -0
  30. package/dist/agent-config/agents/security-reviewer.md +300 -0
  31. package/dist/agent-config/agents/status-reporter.md +297 -0
  32. package/dist/agent-config/claude-md/CLAUDE.core.md +105 -0
  33. package/dist/agent-config/claude-md/CLAUDE.overlay.memnexus.md +68 -0
  34. package/dist/agent-config/claude-md/compose.mjs +179 -0
  35. package/dist/agent-config/claude-md/vars.memnexus.json +4 -0
  36. package/dist/agent-config/hooks/auto-capture-precompact.sh +176 -0
  37. package/dist/agent-config/hooks/auto-capture-session-end.sh +178 -0
  38. package/dist/agent-config/hooks/auto-checkpoint.sh +102 -0
  39. package/dist/agent-config/hooks/ci-validate-hook.sh +499 -0
  40. package/dist/agent-config/hooks/ci-validate-routes.conf +16 -0
  41. package/dist/agent-config/hooks/delegation-audit.sh +61 -0
  42. package/dist/agent-config/hooks/deploy-verification-reminder.sh +179 -0
  43. package/dist/agent-config/hooks/git-mutation-guard.sh +219 -0
  44. package/dist/agent-config/hooks/iteration-log-check.sh +212 -0
  45. package/dist/agent-config/hooks/memory-checkpoint-reminder.sh +103 -0
  46. package/dist/agent-config/hooks/mx-guard-hook.sh +77 -0
  47. package/dist/agent-config/hooks/named-memory-write-guard.sh +237 -0
  48. package/dist/agent-config/hooks/reload-checkpoint.sh +68 -0
  49. package/dist/agent-config/hooks/set-terminal-appearance.sh +34 -0
  50. package/dist/agent-config/hooks/workflow-dispatch-guard.sh +184 -0
  51. package/dist/agent-config/hooks/worktree-guard.sh +258 -0
  52. package/dist/agent-config/rules/eval-harness.md +33 -0
  53. package/dist/agent-config/rules/frontend.md +17 -0
  54. package/dist/agent-config/rules/mcp-protocol.md +15 -0
  55. package/dist/agent-config/rules/retrieval.md +17 -0
  56. package/dist/agent-config/settings.json +196 -0
  57. package/dist/agent-config/shared/eval-tiered-rigor.md +15 -0
  58. package/dist/agent-config/shared/team-operating-rules.md +37 -0
  59. package/dist/agent-config/skills/eval-claim-impact/SKILL.md +20 -0
  60. package/dist/agent-config/skills/eval-harness-review/SKILL.md +21 -0
  61. package/dist/agent-config/skills/mx-checkpoint/SKILL.md +111 -0
  62. package/dist/agent-config/skills/mx-fix-pr/SKILL.md +90 -0
  63. package/dist/agent-config/skills/mx-gap-assessment/SKILL.md +90 -0
  64. package/dist/agent-config/skills/mx-iteration-close/SKILL.md +81 -0
  65. package/dist/agent-config/skills/mx-post-merge-sync/SKILL.md +83 -0
  66. package/dist/agent-config/skills/mx-pre-pr-check/SKILL.md +85 -0
  67. package/dist/agent-config/skills/mx-save/SKILL.md +102 -0
  68. package/dist/agent-config/skills/mx-session-start/SKILL.md +85 -0
  69. package/dist/agent-config/skills/team-gate-checklist/SKILL.md +19 -0
  70. package/dist/commands/config-sync.d.ts +39 -0
  71. package/dist/commands/config-sync.d.ts.map +1 -0
  72. package/dist/commands/config-sync.js +212 -0
  73. package/dist/commands/config-sync.js.map +1 -0
  74. package/dist/commands/create.d.ts.map +1 -1
  75. package/dist/commands/create.js +3 -0
  76. package/dist/commands/create.js.map +1 -1
  77. package/dist/commands/init.d.ts.map +1 -1
  78. package/dist/commands/init.js +12 -4
  79. package/dist/commands/init.js.map +1 -1
  80. package/dist/commands/start.d.ts.map +1 -1
  81. package/dist/commands/start.js +6 -0
  82. package/dist/commands/start.js.map +1 -1
  83. package/dist/index.js +23 -0
  84. package/dist/index.js.map +1 -1
  85. package/dist/lib/bundled-config.d.ts +44 -0
  86. package/dist/lib/bundled-config.d.ts.map +1 -0
  87. package/dist/lib/bundled-config.js +164 -0
  88. package/dist/lib/bundled-config.js.map +1 -0
  89. package/dist/lib/claude.d.ts.map +1 -1
  90. package/dist/lib/claude.js +9 -3
  91. package/dist/lib/claude.js.map +1 -1
  92. package/dist/lib/config-generation.d.ts +36 -0
  93. package/dist/lib/config-generation.d.ts.map +1 -0
  94. package/dist/lib/config-generation.js +73 -0
  95. package/dist/lib/config-generation.js.map +1 -0
  96. package/dist/lib/project-config.d.ts +7 -0
  97. package/dist/lib/project-config.d.ts.map +1 -1
  98. package/dist/lib/project-config.js +4 -0
  99. package/dist/lib/project-config.js.map +1 -1
  100. 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