javi-forge 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +45 -1
  2. package/ci-local/README.md +56 -11
  3. package/ci-local/ci-local.ps1 +445 -0
  4. package/ci-local/ci-local.sh +52 -9
  5. package/ci-local/docker/python.Dockerfile +7 -0
  6. package/ci-local/hooks/commit-msg +120 -44
  7. package/ci-local/hooks/commit-msg.test.sh +286 -0
  8. package/ci-local/hooks/pre-commit +9 -1
  9. package/ci-local/hooks/pre-push +13 -2
  10. package/ci-local/install.ps1 +367 -0
  11. package/ci-local/install.sh +151 -5
  12. package/ci-local/uninstall.ps1 +85 -0
  13. package/ci-local/uninstall.sh +95 -0
  14. package/dist/cli/dispatch/ci.d.ts +12 -0
  15. package/dist/cli/dispatch/ci.js +38 -0
  16. package/dist/cli/dispatch/security.d.ts +11 -0
  17. package/dist/cli/dispatch/security.js +68 -0
  18. package/dist/cli/dispatch/simple-renderers.d.ts +16 -0
  19. package/dist/cli/dispatch/simple-renderers.js +65 -0
  20. package/dist/cli/dispatch/skill-publish.d.ts +15 -0
  21. package/dist/cli/dispatch/skill-publish.js +44 -0
  22. package/dist/cli/dispatch/skills-cmd.d.ts +13 -0
  23. package/dist/cli/dispatch/skills-cmd.js +93 -0
  24. package/dist/cli/dispatch/tdd.d.ts +11 -0
  25. package/dist/cli/dispatch/tdd.js +45 -0
  26. package/dist/cli/dispatch/types.d.ts +29 -0
  27. package/dist/cli/dispatch/types.js +2 -0
  28. package/dist/cli/dispatch/workflow.d.ts +11 -0
  29. package/dist/cli/dispatch/workflow.js +67 -0
  30. package/dist/cli/help.d.ts +127 -0
  31. package/dist/cli/help.js +138 -0
  32. package/dist/cli/runtime.d.ts +16 -0
  33. package/dist/cli/runtime.js +38 -0
  34. package/dist/cli/validators.d.ts +17 -0
  35. package/dist/cli/validators.js +35 -0
  36. package/dist/commands/analyze.js +1 -3
  37. package/dist/commands/ci.d.ts +47 -0
  38. package/dist/commands/ci.js +380 -63
  39. package/dist/commands/doctor.js +2 -4
  40. package/dist/commands/init/report.d.ts +13 -0
  41. package/dist/commands/init/report.js +13 -0
  42. package/dist/commands/init/steps/agent-skills.d.ts +13 -0
  43. package/dist/commands/init/steps/agent-skills.js +45 -0
  44. package/dist/commands/init/steps/ai-sync.d.ts +15 -0
  45. package/dist/commands/init/steps/ai-sync.js +62 -0
  46. package/dist/commands/init/steps/ci.d.ts +24 -0
  47. package/dist/commands/init/steps/ci.js +74 -0
  48. package/dist/commands/init/steps/claude-md.d.ts +14 -0
  49. package/dist/commands/init/steps/claude-md.js +54 -0
  50. package/dist/commands/init/steps/code-graph.d.ts +16 -0
  51. package/dist/commands/init/steps/code-graph.js +68 -0
  52. package/dist/commands/init/steps/context-dir.d.ts +13 -0
  53. package/dist/commands/init/steps/context-dir.js +47 -0
  54. package/dist/commands/init/steps/docker-deploy.d.ts +16 -0
  55. package/dist/commands/init/steps/docker-deploy.js +60 -0
  56. package/dist/commands/init/steps/ghagga.d.ts +15 -0
  57. package/dist/commands/init/steps/ghagga.js +58 -0
  58. package/dist/commands/init/steps/git.d.ts +25 -0
  59. package/dist/commands/init/steps/git.js +81 -0
  60. package/dist/commands/init/steps/gitignore.d.ts +13 -0
  61. package/dist/commands/init/steps/gitignore.js +39 -0
  62. package/dist/commands/init/steps/local-ai.d.ts +14 -0
  63. package/dist/commands/init/steps/local-ai.js +53 -0
  64. package/dist/commands/init/steps/manifest.d.ts +13 -0
  65. package/dist/commands/init/steps/manifest.js +58 -0
  66. package/dist/commands/init/steps/memory.d.ts +15 -0
  67. package/dist/commands/init/steps/memory.js +60 -0
  68. package/dist/commands/init/steps/mock.d.ts +13 -0
  69. package/dist/commands/init/steps/mock.js +64 -0
  70. package/dist/commands/init/steps/sdd.d.ts +12 -0
  71. package/dist/commands/init/steps/sdd.js +40 -0
  72. package/dist/commands/init/steps/security.d.ts +28 -0
  73. package/dist/commands/init/steps/security.js +103 -0
  74. package/dist/commands/init/types.d.ts +30 -0
  75. package/dist/commands/init/types.js +2 -0
  76. package/dist/commands/init.d.ts +5 -4
  77. package/dist/commands/init.js +42 -630
  78. package/dist/commands/llmstxt.js +1 -1
  79. package/dist/commands/plugin.js +1 -1
  80. package/dist/commands/security.js +23 -4
  81. package/dist/commands/skills/analysis.d.ts +19 -0
  82. package/dist/commands/skills/analysis.js +211 -0
  83. package/dist/commands/skills/benchmark.d.ts +6 -0
  84. package/dist/commands/skills/benchmark.js +82 -0
  85. package/dist/commands/skills/constants.d.ts +10 -0
  86. package/dist/commands/skills/constants.js +27 -0
  87. package/dist/commands/skills/directives.d.ts +21 -0
  88. package/dist/commands/skills/directives.js +101 -0
  89. package/dist/commands/skills/parsing.d.ts +16 -0
  90. package/dist/commands/skills/parsing.js +108 -0
  91. package/dist/commands/skills/rules.d.ts +5 -0
  92. package/dist/commands/skills/rules.js +32 -0
  93. package/dist/commands/skills/scoring.d.ts +47 -0
  94. package/dist/commands/skills/scoring.js +306 -0
  95. package/dist/commands/skills.d.ts +7 -125
  96. package/dist/commands/skills.js +8 -874
  97. package/dist/commands/tdd-pipeline.js +1 -1
  98. package/dist/commands/tdd.js +1 -1
  99. package/dist/commands/workflow.js +2 -0
  100. package/dist/constants.js +2 -2
  101. package/dist/index.js +26 -473
  102. package/dist/lib/agent-skills.js +1 -1
  103. package/dist/{commands → lib}/atlassian-mcp.js +2 -2
  104. package/dist/lib/auto-skill-install.js +2 -3
  105. package/dist/lib/auto-wire.js +7 -7
  106. package/dist/lib/ci-config.d.ts +65 -0
  107. package/dist/lib/ci-config.js +297 -0
  108. package/dist/lib/claudemd.js +1 -1
  109. package/dist/lib/codex-export.js +4 -8
  110. package/dist/lib/common.js +2 -2
  111. package/dist/lib/context.js +2 -2
  112. package/dist/{commands → lib}/crash-recovery.js +4 -4
  113. package/dist/lib/docker.d.ts +22 -1
  114. package/dist/lib/docker.js +57 -49
  115. package/dist/lib/exec.d.ts +10 -0
  116. package/dist/lib/exec.js +11 -0
  117. package/dist/lib/frontmatter.js +2 -2
  118. package/dist/{commands → lib}/harness-audit.js +1 -1
  119. package/dist/{commands → lib}/parallel-batch.js +1 -3
  120. package/dist/lib/plugin.js +2 -4
  121. package/dist/{commands → lib}/secret-scanner.js +3 -1
  122. package/dist/{commands → lib}/security-analysis.js +4 -2
  123. package/dist/lib/skill-publish.js +23 -5
  124. package/dist/{commands → lib}/skill-scanner.d.ts +1 -1
  125. package/dist/{commands → lib}/skill-scanner.js +3 -3
  126. package/dist/lib/stack-detector.js +3 -3
  127. package/dist/{commands → lib}/supply-chain.js +1 -1
  128. package/dist/lib/template.js +2 -2
  129. package/dist/lib/workflow/discovery.d.ts +8 -0
  130. package/dist/lib/workflow/discovery.js +18 -1
  131. package/dist/lib/workflow/parser.js +15 -15
  132. package/dist/lib/workflow/validator.js +4 -4
  133. package/dist/tasks/index.d.ts +3 -3
  134. package/dist/tasks/index.js +3 -3
  135. package/dist/tasks/scaffold-tasks.d.ts +1 -1
  136. package/dist/tasks/scaffold-tasks.js +10 -10
  137. package/dist/tasks/task-id.d.ts +1 -1
  138. package/dist/tasks/task-id.js +9 -5
  139. package/dist/tasks/task-tracker.d.ts +1 -1
  140. package/dist/tasks/task-tracker.js +7 -7
  141. package/dist/ui/App.js +1 -1
  142. package/dist/ui/CIContext.js +1 -2
  143. package/dist/ui/Header.js +4 -4
  144. package/dist/ui/NameInput.js +1 -1
  145. package/dist/ui/Progress.d.ts +1 -1
  146. package/dist/ui/Progress.js +1 -1
  147. package/dist/ui/StackSelector.js +1 -1
  148. package/lib/common.psm1 +167 -0
  149. package/lib/common.sh +30 -0
  150. package/package.json +38 -19
  151. /package/dist/{commands → lib}/atlassian-mcp.d.ts +0 -0
  152. /package/dist/{commands → lib}/crash-recovery.d.ts +0 -0
  153. /package/dist/{commands → lib}/harness-audit.d.ts +0 -0
  154. /package/dist/{commands → lib}/parallel-batch.d.ts +0 -0
  155. /package/dist/{commands → lib}/secret-scanner.d.ts +0 -0
  156. /package/dist/{commands → lib}/security-analysis.d.ts +0 -0
  157. /package/dist/{commands → lib}/supply-chain.d.ts +0 -0
  158. /package/dist/{commands → lib}/team-presets.d.ts +0 -0
  159. /package/dist/{commands → lib}/team-presets.js +0 -0
@@ -16,17 +16,45 @@ set -e
16
16
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17
17
  PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
18
18
 
19
- # Source shared library for colors and detect_stack
19
+ # Require realpath: load-bearing for the lib symlink check below.
20
+ if ! command -v realpath >/dev/null 2>&1; then
21
+ printf 'ERROR: realpath is required (install coreutils).\n' >&2
22
+ exit 1
23
+ fi
24
+
25
+ # Source shared library for colors and detect_stack.
20
26
  # Look inside ci-local/lib first (self-contained), fallback to project-level lib/
27
+ LIB_CANDIDATE=""
21
28
  if [ -f "$SCRIPT_DIR/lib/common.sh" ]; then
22
- source "$SCRIPT_DIR/lib/common.sh"
29
+ LIB_CANDIDATE="$SCRIPT_DIR/lib/common.sh"
23
30
  elif [ -f "$SCRIPT_DIR/../lib/common.sh" ]; then
24
- source "$SCRIPT_DIR/../lib/common.sh"
31
+ LIB_CANDIDATE="$SCRIPT_DIR/../lib/common.sh"
25
32
  else
26
33
  echo "ERROR: lib/common.sh not found"
27
34
  exit 1
28
35
  fi
29
36
 
37
+ # SECURITY: validate lib/common.sh before sourcing. A hostile symlink runs
38
+ # attacker code at user privilege as soon as `source` evaluates the file.
39
+ # Audit round 4 (2026-05-17) PoC verified the same hijack on install.sh.
40
+ _LIB_REAL=$(realpath "$LIB_CANDIDATE")
41
+ _PROJECT_REAL=$(realpath "$PROJECT_DIR")
42
+ case "$_LIB_REAL" in
43
+ "$_PROJECT_REAL"|"$_PROJECT_REAL"/*) ;;
44
+ *)
45
+ printf 'ERROR: lib/common.sh resolves outside the project root:\n' >&2
46
+ printf ' LIB resolved: %s\n' "$_LIB_REAL" >&2
47
+ printf ' PROJECT resolved: %s\n' "$_PROJECT_REAL" >&2
48
+ exit 1
49
+ ;;
50
+ esac
51
+
52
+ # Source the RESOLVED path to close the TOCTOU window between validation
53
+ # and source (round 5 finding). Preserve the variables under temp names
54
+ # in case any downstream caller relies on them.
55
+ source "$_LIB_REAL"
56
+ unset _LIB_REAL _PROJECT_REAL
57
+
30
58
  # =============================================================================
31
59
  # CI COMMAND SETUP (extends shared detect_stack with CI-specific commands)
32
60
  # =============================================================================
@@ -72,9 +100,13 @@ setup_ci_commands() {
72
100
  else
73
101
  COMPILE_CMD=""
74
102
  fi
75
- # Only set test if test script exists
103
+ # Only set test if test script exists. If a "test:hooks" script
104
+ # is present, chain it so the commit-msg hook stays under coverage.
76
105
  if grep -q '"test"' "$PROJECT_DIR/package.json" 2>/dev/null; then
77
106
  TEST_CMD="$BUILD_TOOL test"
107
+ if grep -q '"test:hooks"' "$PROJECT_DIR/package.json" 2>/dev/null; then
108
+ TEST_CMD="$TEST_CMD && $BUILD_TOOL run test:hooks"
109
+ fi
78
110
  else
79
111
  TEST_CMD=""
80
112
  fi
@@ -122,6 +154,17 @@ create_dockerfile() {
122
154
  local docker_dir="$SCRIPT_DIR/docker"
123
155
  mkdir -p "$docker_dir"
124
156
 
157
+ # Base image digests are pinned for deterministic CI. Refresh with
158
+ # `docker pull <img> && docker inspect <img>` and update both
159
+ # ci-local.sh and ci-local.ps1 in lockstep (the bytes must match for
160
+ # the dockerfile-hash cache to align across platforms).
161
+ #
162
+ # Java is left tag-only because JAVA_VERSION is a build-arg that lets
163
+ # the user pick a JDK version at runtime; pinning the digest would
164
+ # break any version other than 21. Default JAVA_VERSION=21 digest
165
+ # for reference (2026-05-18):
166
+ # eclipse-temurin:21-jdk-noble
167
+ # @sha256:1f5835564a8c5cee3e9e849a63e9b04ce5c7083070e4cbcaabbcdc36d9147450
125
168
  case "$STACK_TYPE" in
126
169
  java-gradle|java-maven)
127
170
  cat > "$docker_dir/$DOCKERFILE" << 'DOCKERFILE'
@@ -137,7 +180,7 @@ DOCKERFILE
137
180
  ;;
138
181
  node)
139
182
  cat > "$docker_dir/$DOCKERFILE" << 'DOCKERFILE'
140
- FROM node:22-slim
183
+ FROM node:22-slim@sha256:689c11043dad91472750cd824c97dd5e2318e9dd6f954e492fe7af0135d33ceb
141
184
  RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
142
185
  RUN npm install -g pnpm
143
186
  RUN useradd -m -s /bin/bash runner
@@ -148,7 +191,7 @@ DOCKERFILE
148
191
  ;;
149
192
  python)
150
193
  cat > "$docker_dir/$DOCKERFILE" << 'DOCKERFILE'
151
- FROM python:3.12-slim
194
+ FROM python:3.12-slim@sha256:401f6e1a67dad31a1bd78e9ad22d0ee0a3b52154e6bd30e90be696bb6a3d7461
152
195
  RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
153
196
  RUN pip install --no-cache-dir pytest ruff pylint poetry
154
197
  RUN useradd -m -s /bin/bash runner
@@ -159,7 +202,7 @@ DOCKERFILE
159
202
  ;;
160
203
  go)
161
204
  cat > "$docker_dir/$DOCKERFILE" << 'DOCKERFILE'
162
- FROM golang:1.23-bookworm
205
+ FROM golang:1.23-bookworm@sha256:167053a2bb901972bf2c1611f8f52c44d5fe7e762e5cab213708d82c421614db
163
206
  RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
164
207
  RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0 && \
165
208
  mv /root/go/bin/golangci-lint /usr/local/bin/
@@ -171,7 +214,7 @@ DOCKERFILE
171
214
  ;;
172
215
  rust)
173
216
  cat > "$docker_dir/$DOCKERFILE" << 'DOCKERFILE'
174
- FROM rust:1.83-slim
217
+ FROM rust:1.83-slim@sha256:540c902e99c384163b688bbd8b5b8520e94e7731b27f7bd0eaa56ae1960627ab
175
218
  RUN apt-get update && apt-get install -y git pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
176
219
  RUN rustup component add clippy rustfmt
177
220
  RUN useradd -m -s /bin/bash runner
@@ -182,7 +225,7 @@ DOCKERFILE
182
225
  ;;
183
226
  *)
184
227
  cat > "$docker_dir/$DOCKERFILE" << 'DOCKERFILE'
185
- FROM ubuntu:24.04
228
+ FROM ubuntu:24.04@sha256:c4a8d5503dfb2a3eb8ab5f807da5bc69a85730fb49b5cfca2330194ebcc41c7b
186
229
  RUN apt-get update && apt-get install -y git curl && rm -rf /var/lib/apt/lists/*
187
230
  RUN useradd -m -s /bin/bash runner
188
231
  USER runner
@@ -0,0 +1,7 @@
1
+ FROM python:3.12-slim
2
+ RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
3
+ RUN pip install --no-cache-dir pytest ruff pylint poetry
4
+ RUN useradd -m -s /bin/bash runner
5
+ USER runner
6
+ WORKDIR /home/runner/work
7
+ ENTRYPOINT ["/bin/bash", "-c"]
@@ -1,68 +1,145 @@
1
1
  #!/bin/bash
2
2
  set -e
3
3
  # =============================================================================
4
- # COMMIT-MSG: Validate commit message
4
+ # COMMIT-MSG: Block accidental AI-attribution in commit messages
5
5
  # =============================================================================
6
- # Bloquea mensajes con atribución a IA
6
+ # Best-effort, NOT an adversarial-grade security control.
7
+ #
8
+ # Diseño:
9
+ # 1. Normalizar el mensaje (Unicode invisibles, combining marks, markdown,
10
+ # puntuación, emoji, full-width latin via NFKC)
11
+ # 2. Match patrones case-insensitive contra la versión normalizada
12
+ # Y contra el mensaje crudo (por si normalize sobre-strip-ea)
13
+ #
14
+ # Alcance:
15
+ # ✅ Defiende contra: copy-paste accidental, whitespace tricks (doble
16
+ # espacio, NBSP, ZWSP), markdown formatting, full-width latin,
17
+ # combining diacritics, dashes/underscores en model names, separadores
18
+ # de puntuación / emoji.
19
+ # ❌ NO defiende contra: homoglifos cross-script (Cyrillic Сlaude, Greek
20
+ # Ϲlaude, Cherokee Ꮯlaude), tag characters, variation selectors,
21
+ # provider names standalone sin verbo o modelo, bypasses creativos
22
+ # de adversario motivado con tabla Unicode en mano.
23
+ #
24
+ # El test suite (commit-msg.test.sh) documenta qué bypasses están cubiertos
25
+ # y cuáles aceptamos como limitación. Si necesitás defensa adversarial,
26
+ # usá signed commits, no regex.
7
27
  # =============================================================================
8
28
 
9
29
  COMMIT_MSG_FILE="$1"
10
- COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
30
+ RAW_MSG=$(cat "$COMMIT_MSG_FILE")
11
31
 
12
32
  RED='\033[0;31m'
13
33
  GREEN='\033[0;32m'
14
34
  YELLOW='\033[1;33m'
15
35
  NC='\033[0m'
16
36
 
17
- # Patrones prohibidos en mensaje de commit (case insensitive)
37
+ # ─── Normalización ──────────────────────────────────────────────────
38
+ # Si perl falla (no instalado, módulo Unicode::Normalize ausente), el
39
+ # normalize retorna el mensaje crudo. El loop de patrones igual matchea
40
+ # contra RAW_MSG, así que perl roto degrada pero no abre todo.
41
+ normalize_msg() {
42
+ if ! command -v perl >/dev/null 2>&1; then
43
+ printf "%s" "$1"
44
+ return
45
+ fi
46
+ # 2>/dev/null suprime warnings de perl. Si el script falla por completo,
47
+ # || printf "%s" "$1" deja pasar el raw — el match contra RAW_MSG abajo
48
+ # sigue cubriendo los patrones literales.
49
+ printf "%s" "$1" | perl -CSAD -pe '
50
+ # 1. Reemplazar Unicode formatting/invisibles por ESPACIO (no borrarlos).
51
+ # Borrarlos junta "by" + "Claude" → "byClaude" y la regex pierde el match.
52
+ # ZWSP/ZWNJ/ZWJ/LRM/RLM, NBSP, bidi controls, BOM, word-joiner.
53
+ s/[\x{00A0}\x{200B}-\x{200F}\x{2028}-\x{202F}\x{2060}-\x{206F}\x{FEFF}]/ /g;
54
+ # 2. Strip combining diacritics. Esto folds "Ćlaude" (C + U+0301) a "Claude".
55
+ s/[\x{0300}-\x{036F}]//g;
56
+ # 3. NFKC normaliza compatibility forms (full-width latin → ascii)
57
+ use Unicode::Normalize qw(NFKC);
58
+ $_ = NFKC($_);
59
+ # 4. Markdown / puntuación / emoji → espacio
60
+ # Markdown: * _ ` ~
61
+ # Puntuación: . , : ; ! ? ( ) [ ] { } < > / \ | " '\''
62
+ # Emoji: Misc Symbols (2600-27BF), Pictographs (1F300-1FAFF)
63
+ s/[*_`~.,:;!?()\[\]{}<>\\\/|"'\''\x{2600}-\x{27BF}\x{1F300}-\x{1FAFF}]/ /g;
64
+ # 5. Colapsar whitespace (incluye \n, \t, espacios múltiples)
65
+ s/\s+/ /g;
66
+ ' 2>/dev/null || printf "%s" "$1"
67
+ }
68
+
69
+ NORMALIZED_MSG=$(normalize_msg "$RAW_MSG")
70
+
71
+ # ─── Patrones (case insensitive, grep -iE) ──────────────────────────
72
+ #
73
+ # Notas:
74
+ # \b = word boundary (GNU grep ext.)
75
+ # [[:space:]]+ = uno o más espacios (POSIX, portable)
76
+ # [-_ ]? = separador opcional entre proveedor y modelo
77
+ #
78
+ # Limitación conocida y aceptada: homoglifos Unicode (Cyrillic С en lugar
79
+ # de Latin C) requieren transliteración (Text::Unidecode) que es módulo
80
+ # no-core de perl. Documentado, no se intenta defender.
81
+ PROVIDER_PAT='(claude|gpt|chatgpt|openai|anthropic|gemini|copilot|llm|cursor|windsurf|codeium|cody|aider)'
82
+
18
83
  AI_PATTERNS=(
19
- "co-authored-by:.*claude"
20
- "co-authored-by:.*anthropic"
21
- "co-authored-by:.*gpt"
22
- "co-authored-by:.*openai"
23
- "co-authored-by:.*copilot"
24
- "co-authored-by:.*gemini"
25
- "co-authored-by:.*\bai\b"
26
- "made by claude"
27
- "made by gpt"
28
- "made by ai"
29
- "made with claude"
30
- "made with ai"
31
- "generated by claude"
32
- "generated by gpt"
33
- "generated by ai"
34
- "generated with claude"
35
- "generated with ai"
36
- "written by claude"
37
- "written by ai"
38
- "created by claude"
39
- "created by ai"
40
- "assisted by claude"
41
- "assisted by ai"
42
- "with help from claude"
43
- "claude code"
44
- "claude opus"
45
- "claude sonnet"
46
- "claude haiku"
47
- "gpt-4"
48
- "gpt-3"
49
- "chatgpt"
50
- "@anthropic.com"
51
- "@openai.com"
52
- "noreply@anthropic"
53
- "noreply@openai"
84
+ # ── Co-authored-by trailer ────────────────────────────────────────
85
+ # Captura cualquier proveedor en el mismo trailer.
86
+ "co-authored-by:.*\b${PROVIDER_PAT}\b"
87
+ # AI en trailer: AI alone OR AIAssistant / AIBot / AIAgent compounds
88
+ "co-authored-by:.*\bai(assistant|bot|agent|helper)?\b"
89
+ # Standalone Copilot/Codeium/Cursor en trailer (sin prefix vendor)
90
+ "co-authored-by:.*\b(copilot|cursor|codeium|cody|aider|chatgpt)\b"
91
+
92
+ # ── Verbos de atribución + by/with + proveedor ───────────────────
93
+ # Ej: "made by Claude", "generated with GPT", "built with OpenAI"
94
+ # Verbos expandidos en audit-2 para cubrir "I used X", "X helped me",
95
+ # "wrote by AI", "prompted to write", "asked Claude to".
96
+ "(made|generated|written|created|assisted|built|developed|authored|produced|coded|crafted|helped|used|wrote|prompted|asked|fixed|refactored)[[:space:]]+(by|with|using|via)[[:space:]]+${PROVIDER_PAT}"
97
+ "(made|generated|written|created|assisted|built|developed|authored|produced|coded|crafted|helped|used|wrote|prompted|asked|fixed|refactored)[[:space:]]+(by|with|using|via)[[:space:]]+\bai(assistant|bot|agent|helper)?\b"
98
+
99
+ # ── "with help from / thanks to / powered by X" ──────────────────
100
+ "(with[[:space:]]+help[[:space:]]+from|thanks[[:space:]]+to|powered[[:space:]]+by|courtesy[[:space:]]+of)[[:space:]]+${PROVIDER_PAT}"
101
+ "(with[[:space:]]+help[[:space:]]+from|thanks[[:space:]]+to|powered[[:space:]]+by|courtesy[[:space:]]+of)[[:space:]]+\bai(assistant|bot|agent|helper)?\b"
102
+
103
+ # ── "X-assisted" / "X assisted" ──────────────────────────────────
104
+ # Cubre: anthropic-assisted, openai assisted, ai_assisted, etc.
105
+ "${PROVIDER_PAT}[-_ ]?assisted"
106
+ "\bai[-_ ]?assisted"
107
+
108
+ # ── Standalone branding / tooling ────────────────────────────────
109
+ "claude[-_ ]?code"
110
+ "(github|microsoft)[-_ ]?copilot"
111
+ "\bchatgpt\b"
112
+ "\bcursor[-_ ]?(ai|ide)?\b"
113
+ "\bwindsurf\b"
114
+ "\bcodeium\b"
115
+ "\bcody\b"
116
+ "\baider\b"
117
+
118
+ # ── Claude + variante de modelo ──────────────────────────────────
119
+ # Cubre: "claude opus", "claude-sonnet", "Claude3", "claude4.5", "Claude 4.7"
120
+ "claude[-_ ]?(opus|sonnet|haiku|[0-9])"
121
+
122
+ # ── GPT + versión ────────────────────────────────────────────────
123
+ # Cubre: gpt-4, gpt 3, gpt_5, gpt4.5
124
+ "gpt[-_ ]?[0-9]"
125
+
126
+ # ── Email domains de los proveedores ─────────────────────────────
127
+ "@(anthropic|openai|cursor|codeium)\\.(com|sh|ai)"
128
+ "noreply@(anthropic|openai)"
54
129
  )
55
130
 
131
+ # Run pattern matching against the NORMALIZED message.
132
+ # Use both raw and normalized — raw catches anything normalization might over-strip.
56
133
  for pattern in "${AI_PATTERNS[@]}"; do
57
- if echo "$COMMIT_MSG" | grep -iqE "$pattern"; then
134
+ if printf "%s" "$NORMALIZED_MSG" | grep -iqE "$pattern" \
135
+ || printf "%s" "$RAW_MSG" | grep -iqE "$pattern"; then
58
136
  echo -e ""
59
137
  echo -e "${RED}COMMIT BLOCKED: AI Attribution Detected${NC}"
60
138
  echo -e ""
61
- echo -e "${RED}Found in commit message: ${NC}"
62
- echo -e "${YELLOW} Pattern: $pattern${NC}"
139
+ echo -e "${RED}Pattern matched:${NC} ${YELLOW}$pattern${NC}"
63
140
  echo -e ""
64
141
  echo -e "${RED}Your commit message:${NC}"
65
- echo -e "${YELLOW}$(cat "$COMMIT_MSG_FILE")${NC}"
142
+ echo -e "${YELLOW}$RAW_MSG${NC}"
66
143
  echo -e ""
67
144
  echo -e "${GREEN}Remove AI attribution. You are the sole author.${NC}"
68
145
  echo -e ""
@@ -70,5 +147,4 @@ for pattern in "${AI_PATTERNS[@]}"; do
70
147
  fi
71
148
  done
72
149
 
73
- # Mensaje OK
74
150
  exit 0
@@ -0,0 +1,286 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # COMMIT-MSG TEST SUITE
4
+ # =============================================================================
5
+ # Valida que el hook commit-msg bloquea AI attribution en TODAS las variantes
6
+ # conocidas, incluyendo bypasses identificados en auditoría 2026-05-17.
7
+ #
8
+ # Uso:
9
+ # ./commit-msg.test.sh
10
+ #
11
+ # Exit code 0 = todos los tests pasan
12
+ # Exit code 1 = al menos un test falla
13
+ # =============================================================================
14
+
15
+ set -u
16
+
17
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
18
+ HOOK="$SCRIPT_DIR/commit-msg"
19
+
20
+ if [ ! -x "$HOOK" ]; then
21
+ echo "ERROR: commit-msg hook not found or not executable: $HOOK"
22
+ exit 1
23
+ fi
24
+
25
+ PASS=0
26
+ FAIL=0
27
+ FAILED_TESTS=()
28
+
29
+ # Garantiza limpieza de tmpfiles aún si el hook revienta con set -u, etc.
30
+ TMP_FILES=()
31
+ cleanup() {
32
+ if [ ${#TMP_FILES[@]} -gt 0 ]; then
33
+ rm -f "${TMP_FILES[@]}"
34
+ fi
35
+ }
36
+ trap cleanup EXIT INT TERM
37
+
38
+ make_tmp() {
39
+ local f
40
+ f=$(mktemp)
41
+ TMP_FILES+=("$f")
42
+ printf "%s" "$1" > "$f"
43
+ printf "%s" "$f"
44
+ }
45
+
46
+ # expect_block <description> <message>
47
+ # El hook debe BLOQUEAR este mensaje (exit != 0)
48
+ expect_block() {
49
+ local desc="$1"
50
+ local msg="$2"
51
+ local tmpfile
52
+ tmpfile=$(make_tmp "$msg")
53
+ if "$HOOK" "$tmpfile" >/dev/null 2>&1; then
54
+ FAIL=$((FAIL + 1))
55
+ FAILED_TESTS+=("[NOT BLOCKED] $desc | msg: '$msg'")
56
+ echo " FAIL: $desc (should block but passed): '$msg'"
57
+ else
58
+ PASS=$((PASS + 1))
59
+ fi
60
+ }
61
+
62
+ # expect_pass <description> <message>
63
+ # El hook debe PERMITIR este mensaje (exit 0)
64
+ expect_pass() {
65
+ local desc="$1"
66
+ local msg="$2"
67
+ local tmpfile
68
+ tmpfile=$(make_tmp "$msg")
69
+ if "$HOOK" "$tmpfile" >/dev/null 2>&1; then
70
+ PASS=$((PASS + 1))
71
+ else
72
+ FAIL=$((FAIL + 1))
73
+ FAILED_TESTS+=("[FALSE POSITIVE] $desc | msg: '$msg'")
74
+ echo " FAIL: $desc (should pass but blocked): '$msg'"
75
+ fi
76
+ }
77
+
78
+ echo "=== Running commit-msg hook test suite ==="
79
+ echo ""
80
+
81
+ # ─── Cases que DEBEN bloquear ────────────────────────────────────────
82
+
83
+ # Co-authored-by con cada proveedor
84
+ expect_block "Co-authored-by Claude" "feat: add foo
85
+
86
+ Co-authored-by: Claude <claude@anthropic.com>"
87
+
88
+ expect_block "Co-authored-by claude lowercase" "feat: bar
89
+
90
+ co-authored-by: claude <c@a.com>"
91
+
92
+ expect_block "Co-authored-by double space" "feat: baz
93
+
94
+ Co-authored-by: Claude <c@a.com>"
95
+
96
+ expect_block "Co-authored-by GPT" "feat: x
97
+
98
+ Co-authored-by: GPT-4 <g@o.com>"
99
+
100
+ expect_block "Co-authored-by Copilot" "feat: y
101
+
102
+ Co-authored-by: GitHub Copilot <c@gh.com>"
103
+
104
+ expect_block "Co-authored-by ai standalone" "feat: z
105
+
106
+ Co-authored-by: AI Assistant <a@b.com>"
107
+
108
+ # Made/Generated/Written/Created by/with X — single space
109
+ expect_block "Made by Claude" "feat: add Made by Claude"
110
+ expect_block "Made with Claude" "feat: Made with Claude"
111
+ expect_block "Generated by GPT" "feat: Generated by GPT"
112
+ expect_block "Written with OpenAI" "fix: Written with OpenAI"
113
+ expect_block "Created by Anthropic" "feat: Created by Anthropic"
114
+ expect_block "Assisted by AI" "feat: Assisted by AI"
115
+ expect_block "Built with Gemini" "feat: Built with Gemini"
116
+ expect_block "Developed with Copilot" "feat: Developed with Copilot"
117
+ expect_block "Authored by LLM" "feat: Authored by LLM"
118
+
119
+ # Made/Generated/etc — DOUBLE space (bypass identificado en audit)
120
+ expect_block "Made by Claude (double space)" "feat: Made by Claude"
121
+ expect_block "Generated with GPT (double space)" "fix: Generated with GPT"
122
+ expect_block "Made by\tClaude (tab)" "$(printf 'feat: Made by\tClaude')"
123
+
124
+ # With help from X
125
+ expect_block "With help from Claude" "feat: x
126
+
127
+ With help from Claude"
128
+ expect_block "with help from GPT-4" "feat: with help from GPT-4"
129
+
130
+ # X-assisted variants (bypass identificado)
131
+ expect_block "anthropic-assisted" "feat: anthropic-assisted refactor"
132
+ expect_block "openai-assisted" "feat: openai-assisted change"
133
+ expect_block "ai-assisted" "feat: ai-assisted commit"
134
+ expect_block "Claude assisted (space)" "feat: Claude assisted work"
135
+ expect_block "LLM_assisted (underscore)" "feat: llm_assisted feature"
136
+
137
+ # Tool branding
138
+ expect_block "claude code" "feat: built with claude code"
139
+ expect_block "claude-code" "feat: via claude-code"
140
+ expect_block "claude_code" "feat: claude_code generated"
141
+ expect_block "GitHub Copilot" "feat: GitHub Copilot helped"
142
+ expect_block "Microsoft Copilot" "feat: Microsoft Copilot did this"
143
+
144
+ # Claude model variants (bypass identificado)
145
+ expect_block "claude opus (space)" "feat: claude opus tested"
146
+ expect_block "claude-sonnet (dash)" "feat: claude-sonnet used"
147
+ expect_block "claude_haiku (underscore)" "feat: claude_haiku approach"
148
+ expect_block "Claude3.5" "feat: Claude3.5 wrote this"
149
+ expect_block "claude4.7" "feat: claude4.7 contributed"
150
+ expect_block "Claude 4" "feat: Claude 4 logic"
151
+
152
+ # GPT model variants
153
+ expect_block "gpt-4" "feat: gpt-4 reviewed"
154
+ expect_block "gpt 5" "feat: gpt 5 wrote"
155
+ expect_block "gpt_3" "feat: gpt_3 helped"
156
+ expect_block "GPT4" "feat: GPT4 was used"
157
+
158
+ # ChatGPT
159
+ expect_block "ChatGPT" "feat: ChatGPT generated"
160
+ expect_block "chatgpt" "feat: powered by chatgpt"
161
+
162
+ # Emails
163
+ expect_block "@anthropic.com" "feat: x
164
+
165
+ Author: foo@anthropic.com"
166
+ expect_block "@openai.com" "feat: y
167
+
168
+ Co-authored-by: bot@openai.com"
169
+ expect_block "noreply@anthropic" "feat: noreply@anthropic.example"
170
+
171
+ # ─── Bypasses identificados en review 2026-05-17 (post-hardening) ─────
172
+
173
+ # Markdown formatting alrededor del proveedor
174
+ expect_block "Made by **Claude** (markdown bold)" "feat: Made by **Claude**"
175
+ expect_block "Made by __GPT__ (markdown italic)" "feat: Made by __GPT__"
176
+ expect_block "Made by \`OpenAI\` (markdown code)" "feat: Made by \`OpenAI\`"
177
+ expect_block "Made by ~~Gemini~~ (strikethrough)" "feat: Made by ~~Gemini~~"
178
+
179
+ # Puntuación entre verbo y proveedor (no whitespace)
180
+ expect_block "Made by.Claude" "feat: Made by.Claude"
181
+ expect_block "Made by,Claude" "feat: Made by,Claude"
182
+ expect_block "Made by:Claude" "feat: Made by:Claude"
183
+ expect_block "Made by;Claude" "feat: Made by;Claude"
184
+
185
+ # Newline entre verbo y proveedor
186
+ expect_block "Made by newline Claude" "$(printf 'feat: Made by\nClaude')"
187
+
188
+ # Emoji separator
189
+ expect_block "Made by emoji Claude" "feat: Made by 🤖 Claude"
190
+ expect_block "Generated by emoji GPT" "feat: Generated by ✨ GPT"
191
+
192
+ # Unicode invisible chars (ZWSP, RLM, NBSP)
193
+ expect_block "Made by ZWSP Claude" "$(printf 'feat: Made by​Claude')"
194
+ expect_block "Made by NBSP Claude" "$(printf 'feat: Made by Claude')"
195
+ expect_block "Made by RLM Claude" "$(printf 'feat: Made by‏Claude')"
196
+
197
+ # "thanks/powered by/courtesy of" alternativas
198
+ expect_block "thanks to Claude" "feat: thanks to Claude for help"
199
+ expect_block "powered by GPT" "feat: powered by GPT"
200
+ expect_block "courtesy of OpenAI" "feat: courtesy of OpenAI"
201
+
202
+ # AI IDEs / herramientas
203
+ expect_block "Cursor IDE" "feat: built with Cursor"
204
+ expect_block "Windsurf" "feat: written with Windsurf"
205
+ expect_block "Codeium" "feat: codeium suggested this"
206
+ expect_block "Cody" "feat: with Cody help"
207
+ expect_block "Aider" "feat: aider generated"
208
+
209
+ # Co-authored-by sin espacio antes de AI (bypass del review Sonnet)
210
+ expect_block "Co-authored-by AIAssistant" "feat: x
211
+
212
+ Co-authored-by: AIAssistant <a@b.com>"
213
+
214
+ # Co-authored-by Copilot standalone (sin GitHub/Microsoft prefix)
215
+ expect_block "Co-authored-by bare Copilot" "feat: x
216
+
217
+ Co-authored-by: Copilot <c@gh.com>"
218
+
219
+ # Verbos alternativos
220
+ expect_block "produced by Claude" "feat: produced by Claude"
221
+ expect_block "coded by GPT" "feat: coded by GPT"
222
+ expect_block "crafted with AI" "feat: crafted with AI"
223
+
224
+ # Using/via como alternativas a by/with
225
+ expect_block "made using Claude" "feat: made using Claude"
226
+ expect_block "generated via GPT" "feat: generated via GPT"
227
+
228
+ # Full-width characters (NFKC normalization)
229
+ expect_block "Made by Claude (fullwidth)" "feat: Made by Claude"
230
+
231
+ # ─── Bypasses adicionales identificados en audit-2 (2026-05-17 review B) ──
232
+
233
+ # Combining diacritics (U+0300-U+036F) — Ćlaude = C + U+0301
234
+ expect_block "Ćlaude (combining acute)" "$(printf 'feat: Made by Ćlaude')"
235
+ expect_block "Cláude (combining grave)" "$(printf 'feat: Made by Clàude')"
236
+
237
+ # Verbos expandidos — sólo aplican si hay (by|with|using|via) después del verbo.
238
+ # "asked Claude to ..." NO bloquea por diseño: el riesgo de false positive en
239
+ # nombres personales o conversaciones legítimas es alto. Si necesitás defensa
240
+ # adversarial usá signed commits.
241
+ expect_block "helped by Claude" "feat: helped by Claude"
242
+ expect_block "wrote with GPT" "feat: wrote with GPT-4"
243
+ expect_block "prompted via AI" "feat: prompted via AI"
244
+ expect_block "fixed by Copilot" "feat: fixed by Copilot"
245
+ expect_block "refactored with Cursor" "feat: refactored with Cursor"
246
+
247
+ # Cases que DOCUMENTADAMENTE NO bloqueamos (bare provider sin verb-prepositional):
248
+ # - "asked Claude to refactor" (FP risk: pidió a una persona llamada Claude)
249
+ # - "I used Claude to write" (FP risk: usé al usuario Claude para escribir)
250
+ # - "claude helped me debug" (FP risk: Claude la persona ayudó a debuggear)
251
+ # Estos son límites aceptados; ver header del hook.
252
+ expect_pass "asked Claude (person, not attribution)" "feat: asked Claude to review the PR"
253
+ expect_pass "user Claude in seed" "feat: configure user Claude in fixtures"
254
+
255
+ # ─── Cases que DEBEN PASAR (false positive checks) ────────────────────
256
+
257
+ expect_pass "Simple feat" "feat: add user profile endpoint"
258
+ expect_pass "Simple fix" "fix: handle empty array in parser"
259
+ expect_pass "Refactor" "refactor: extract auth into middleware"
260
+ expect_pass "Docs" "docs: clarify install instructions"
261
+ expect_pass "Multi-line valid commit" "feat: add search
262
+
263
+ Implements full-text search using PostgreSQL FTS.
264
+ Includes integration tests."
265
+ expect_pass "Author named Claude (not attribution)" "feat: add user named Claude in seed data"
266
+ expect_pass "AI as acronym in product name" "feat: integrate with PaiAi API gateway"
267
+ expect_pass "Word 'made' without AI context" "feat: changes I made last week to the parser"
268
+
269
+ # ─── Summary ──────────────────────────────────────────────────────────
270
+
271
+ echo ""
272
+ echo "=== Results ==="
273
+ echo " Passed: $PASS"
274
+ echo " Failed: $FAIL"
275
+ echo ""
276
+
277
+ if [ $FAIL -gt 0 ]; then
278
+ echo "Failed tests:"
279
+ for t in "${FAILED_TESTS[@]}"; do
280
+ echo " - $t"
281
+ done
282
+ exit 1
283
+ fi
284
+
285
+ echo "All tests passed."
286
+ exit 0
@@ -4,14 +4,22 @@
4
4
  # =============================================================================
5
5
  # Requires: npm install -g javi-forge
6
6
  # To skip: git commit --no-verify
7
+ # Target: ~30s. The hook prints elapsed time so drift can be tracked.
7
8
  # =============================================================================
8
9
 
9
10
  set -e
10
11
 
12
+ _start=$SECONDS
11
13
  echo "PRE-COMMIT: Running quick check..."
12
14
  javi-forge ci --quick --no-docker --no-security --no-ci-ghagga || {
15
+ _elapsed=$((SECONDS - _start))
13
16
  echo ""
14
- echo "Quick check FAILED — Fix the issues above before committing."
17
+ echo "Quick check FAILED after ${_elapsed}s — Fix the issues above before committing."
15
18
  echo "To skip: git commit --no-verify"
16
19
  exit 1
17
20
  }
21
+ _elapsed=$((SECONDS - _start))
22
+ echo "PRE-COMMIT: ${_elapsed}s (target: 30s)"
23
+ if [ "$_elapsed" -gt 60 ]; then
24
+ echo " WARNING: pre-commit took ${_elapsed}s, well over the 30s target."
25
+ fi