trackfw 6.10.0 → 7.1.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 (38) hide show
  1. package/README.md +0 -3
  2. package/bin/trackfw +38 -1
  3. package/package.json +1 -1
  4. package/src/branch/prune.js +450 -0
  5. package/src/branch/runner.js +33 -20
  6. package/src/changelog/index.js +114 -0
  7. package/src/commands/branch.js +59 -9
  8. package/src/commands/changelog.js +29 -0
  9. package/src/commands/commit.js +60 -0
  10. package/src/commands/discover.js +75 -0
  11. package/src/commands/doctor.js +32 -0
  12. package/src/commands/index.js +23 -4
  13. package/src/commands/integrations.js +23 -1
  14. package/src/commands/serve.js +52 -4
  15. package/src/commands/ship.js +4 -3
  16. package/src/commands/thirdparty.js +384 -0
  17. package/src/commands/update-harness.js +320 -12
  18. package/src/commands/update.js +3 -1
  19. package/src/commit/runner.js +352 -0
  20. package/src/config/index.js +27 -0
  21. package/src/generators/hooks.js +925 -6
  22. package/src/generators/init.js +28 -6
  23. package/src/i18n/locales/en-US.json +0 -21
  24. package/src/i18n/locales/es-ES.json +0 -21
  25. package/src/i18n/locales/pt-BR.json +0 -21
  26. package/src/integrations/doctor.js +0 -0
  27. package/src/integrations/index.js +19 -2
  28. package/src/integrations/manager.js +133 -19
  29. package/src/lib/fatal-error.js +64 -0
  30. package/src/lib/unknown-command.js +82 -0
  31. package/src/ship/runner.js +259 -95
  32. package/src/thirdparty/fetch.js +140 -0
  33. package/src/thirdparty/markers.js +207 -0
  34. package/src/thirdparty/provenance.js +114 -0
  35. package/src/thirdparty/quarantine.js +113 -0
  36. package/src/thirdparty/references.js +194 -0
  37. package/src/validator/index.js +1060 -27
  38. package/src/commands/plugins.js +0 -184
@@ -1,10 +1,13 @@
1
1
  'use strict'
2
2
 
3
3
  const fs = require('fs')
4
+ const os = require('os')
4
5
  const path = require('path')
6
+ const crypto = require('crypto')
5
7
  const { gitOutput } = require('./git-exec')
6
8
  const config = require('../config')
7
9
  const { checkTraceIds } = require('./traceid')
10
+ const { loadProvenance } = require('../thirdparty/provenance')
8
11
 
9
12
  const STALE_WIP_DAYS = 7
10
13
  let staleWipNowMs = () => Date.now()
@@ -1152,18 +1155,32 @@ function validateNoteOrphan(cwd) {
1152
1155
  // procura dentro dos comandos de hook de projeto.
1153
1156
  const CREDENTIAL_GUARD_SCRIPT_MARKER = 'trackfw-credential-guard.sh'
1154
1157
 
1158
+ // GIT_BRANCH_GUARD_SCRIPT_MARKER é o nome do script que a regra git_branch_guard_hook_resolvable
1159
+ // procura dentro dos comandos de hook de projeto (ROADMAP-2026-08-15-trackfw-validate-deve-
1160
+ // detectar-scripts-de-hook-ausentes-ou-desatualizados, ML-2A — port de
1161
+ // internal/validator/validator_git_branch_guard.go's gitBranchGuardScriptMarker). Mesmo padrão de
1162
+ // CREDENTIAL_GUARD_SCRIPT_MARKER — só o nome do arquivo muda.
1163
+ const GIT_BRANCH_GUARD_SCRIPT_MARKER = 'trackfw-git-branch-guard.sh'
1164
+
1155
1165
  // CREDENTIAL_GUARD_HOOK_FILES é a lista fechada dos arquivos de hook de PROJETO que o trackfw
1156
1166
  // gera hoje e que podem conter uma entrada de credential-guard (ROADMAP-2026-08-12-mitigacao-do
1157
1167
  // -fail-open-do-credential-guard, ML-1A). Hooks de escopo GLOBAL (~/.trackfw/..., trackfw update
1158
1168
  // harness) ficam fora — caso distinto, fora do repositório do usuário, e a checagem de dedup
1159
1169
  // globalCredentialGuardInstalled*() já os pula de propósito nas entradas de projeto.
1170
+ //
1171
+ // requiresCommandType (ROADMAP-2026-08-17 ML-4B, port of Go's credentialGuardHookFile.
1172
+ // requiresCommandType): true for every CLI whose writer always emits a sibling "type":"command"
1173
+ // field (Claude/Codex/Gemini, GitHub Copilot CLI, Kiro) — a command match found WITHOUT that
1174
+ // sibling is a structurally malformed entry the CLI silently never executes (hades-tf ML-4A
1175
+ // barrier finding), not merely "absent". false only for Cursor, whose schema
1176
+ // ({"command":...}) never carries a "type" field at all.
1160
1177
  const CREDENTIAL_GUARD_HOOK_FILES = [
1161
- { relPath: '.claude/settings.json', cli: 'Claude Code' },
1162
- { relPath: '.codex/hooks.json', cli: 'Codex CLI' },
1163
- { relPath: '.gemini/settings.json', cli: 'Gemini CLI' },
1164
- { relPath: '.cursor/hooks.json', cli: 'Cursor' },
1165
- { relPath: '.github/hooks/trackfw-attention.json', cli: 'GitHub Copilot CLI' },
1166
- { relPath: '.kiro/hooks/trackfw-attention.json', cli: 'Kiro' },
1178
+ { relPath: '.claude/settings.json', cli: 'Claude Code', requiresCommandType: true },
1179
+ { relPath: '.codex/hooks.json', cli: 'Codex CLI', requiresCommandType: true },
1180
+ { relPath: '.gemini/settings.json', cli: 'Gemini CLI', requiresCommandType: true },
1181
+ { relPath: '.cursor/hooks.json', cli: 'Cursor', requiresCommandType: false },
1182
+ { relPath: '.github/hooks/trackfw-attention.json', cli: 'GitHub Copilot CLI', requiresCommandType: true },
1183
+ { relPath: '.kiro/hooks/trackfw-attention.json', cli: 'Kiro', requiresCommandType: true },
1167
1184
  ]
1168
1185
 
1169
1186
  // resolveCredentialGuardHookPath resolve o valor bruto de um comando de hook (string extraída do
@@ -1203,30 +1220,67 @@ function resolveCredentialGuardHookPath(raw, root) {
1203
1220
  return null
1204
1221
  }
1205
1222
 
1206
- // collectCredentialGuardCommands percorre recursivamente um valor JSON já decodificado e coleta
1207
- // todo valor-string que referencia trackfw-credential-guard.sh, independentemente do nome do
1208
- // campo que o contém.
1223
+ // collectCommandsWithMarker percorre recursivamente um valor JSON já decodificado e coleta todo
1224
+ // valor-string que contém marker, independentemente do nome do campo que o contém — junto com um
1225
+ // sinal estrutural (typeIsCommand) de que o objeto imediato que o contém também tem
1226
+ // "type":"command" como campo irmão (ROADMAP-2026-08-17 ML-4B, port de guardCommandMatch em
1227
+ // internal/validator/validator_credential_guard.go). Cada entrada de `out` é
1228
+ // {raw, typeIsCommand}.
1209
1229
  //
1210
1230
  // Os 6 formatos de hook usam campos diferentes para o comando: "command" (Claude/Codex/
1211
1231
  // Gemini/Cursor), "bash" (GitHub Copilot CLI), "action.command" (Kiro). Varrer por VALOR em vez
1212
- // de por caminho de chave evita acoplar esta regra à forma exata de cada schema.
1213
- function collectCredentialGuardCommands(value, out) {
1232
+ // de por caminho de chave evita acoplar esta regra à forma exata de cada schema. Todo schema aqui
1233
+ // coloca "type" como IRMÃO do campo do comando dentro do MESMO objeto — nunca aninhado mais fundo
1234
+ // — então basta ler "type" do objeto que collectCommandsWithMarker já está visitando; não é
1235
+ // necessária uma travessia schema-aware separada.
1236
+ //
1237
+ // Generalizado (ROADMAP-2026-08-15-trackfw-validate-deve-detectar-scripts-de-hook-ausentes-ou-
1238
+ // desatualizados, ML-2A, port de collectCommandsWithMarker em
1239
+ // internal/validator/validator_git_branch_guard.go) para aceitar qualquer marker — originalmente
1240
+ // collectCredentialGuardCommands, hardcoded para CREDENTIAL_GUARD_SCRIPT_MARKER; reusado agora
1241
+ // também para GIT_BRANCH_GUARD_SCRIPT_MARKER, sem duplicar a travessia recursiva.
1242
+ function collectCommandsWithMarker(value, marker, out) {
1214
1243
  if (typeof value === 'string') {
1215
- if (value.includes(CREDENTIAL_GUARD_SCRIPT_MARKER)) out.push(value)
1244
+ // Top-level/loose string match, outside any enclosing object — defensive fallback, not
1245
+ // expected to fire since every hook file read here is rooted at a JSON object. No enclosing
1246
+ // object means no "type" sibling to read.
1247
+ if (value.includes(marker)) out.push({ raw: value, typeIsCommand: false })
1216
1248
  return
1217
1249
  }
1218
1250
  if (Array.isArray(value)) {
1219
- for (const item of value) collectCredentialGuardCommands(item, out)
1251
+ for (const item of value) collectCommandsWithMarker(item, marker, out)
1220
1252
  return
1221
1253
  }
1222
1254
  if (value && typeof value === 'object') {
1223
- for (const key of Object.keys(value)) collectCredentialGuardCommands(value[key], out)
1255
+ const typeIsCommand = value.type === 'command'
1256
+ for (const key of Object.keys(value)) {
1257
+ const val = value[key]
1258
+ if (typeof val === 'string') {
1259
+ if (val.includes(marker)) out.push({ raw: val, typeIsCommand })
1260
+ continue
1261
+ }
1262
+ collectCommandsWithMarker(val, marker, out)
1263
+ }
1224
1264
  }
1225
1265
  }
1226
1266
 
1227
- // validateCredentialGuardHookResolvable é a regra "credential_guard_hook_resolvable": para cada
1228
- // arquivo de hook de PROJETO que existir, extrai os comandos que referenciam
1229
- // trackfw-credential-guard.sh, resolve o caminho e verifica que o script existe e é executável.
1267
+ // collectCredentialGuardCommands é o wrapper específico de credential-guard sobre
1268
+ // collectCommandsWithMarker — preservado para compatibilidade de assinatura (exportada
1269
+ // historicamente com 2 argumentos).
1270
+ function collectCredentialGuardCommands(value, out) {
1271
+ collectCommandsWithMarker(value, CREDENTIAL_GUARD_SCRIPT_MARKER, out)
1272
+ }
1273
+
1274
+ // validateGuardHookResolvable é a implementação genérica compartilhada pelas regras
1275
+ // "credential_guard_hook_resolvable" e "git_branch_guard_hook_resolvable": para cada arquivo de
1276
+ // hook de PROJETO que existir, extrai os comandos que referenciam scriptMarker, resolve o caminho
1277
+ // e verifica que o script existe e é executável.
1278
+ //
1279
+ // Generalizado a partir da antiga validateCredentialGuardHookResolvable (ROADMAP-2026-08-15-
1280
+ // trackfw-validate-deve-detectar-scripts-de-hook-ausentes-ou-desatualizados, ML-2A, port de
1281
+ // validateGuardHookResolvable em internal/validator/validator_credential_guard.go) — a lógica de
1282
+ // resolução de caminho por CLI é idêntica para os 2 scripts, só o marker e o texto da mensagem
1283
+ // mudam.
1230
1284
  //
1231
1285
  // Riscos de regressão mapeados no roadmap:
1232
1286
  // - A regra só avalia entradas que EXISTEM. Ausência de entrada de guard é estado legítimo
@@ -1234,7 +1288,12 @@ function collectCredentialGuardCommands(value, out) {
1234
1288
  // - Arquivo de hook ausente é pulado em silêncio.
1235
1289
  // - Arquivo de hook presente mas com JSON inválido é pulado em silêncio — validar a forma do
1236
1290
  // JSON não é escopo desta regra.
1237
- function validateCredentialGuardHookResolvable(cwd) {
1291
+ //
1292
+ // process.cwd() no Node já retorna o caminho FÍSICO (resolve symlinks via getcwd(3) diretamente),
1293
+ // diferente de os.Getwd() do Go — ver o comentário sobre EvalSymlinks em
1294
+ // internal/validator/validator_git_branch_guard.go's validateGuardHookResolvable equivalente
1295
+ // (validator_credential_guard.go). Nenhuma resolução extra de symlink é necessária aqui.
1296
+ function validateGuardHookResolvable(scriptMarker, cwd) {
1238
1297
  const root = cwd || process.cwd()
1239
1298
  const msgs = []
1240
1299
 
@@ -1256,16 +1315,27 @@ function validateCredentialGuardHookResolvable(cwd) {
1256
1315
  }
1257
1316
 
1258
1317
  const commands = []
1259
- collectCredentialGuardCommands(parsed, commands)
1318
+ collectCommandsWithMarker(parsed, scriptMarker, commands)
1260
1319
 
1261
1320
  const seen = new Set()
1262
- for (const raw of commands) {
1263
- if (seen.has(raw)) continue
1264
- seen.add(raw)
1321
+ for (const m of commands) {
1322
+ const seenKey = `${m.raw}${m.typeIsCommand}`
1323
+ if (seen.has(seenKey)) continue
1324
+ seen.add(seenKey)
1265
1325
 
1266
- const resolved = resolveCredentialGuardHookPath(raw, root)
1326
+ const resolved = resolveCredentialGuardHookPath(m.raw, root)
1267
1327
  if (resolved === null) continue
1268
1328
 
1329
+ // ROADMAP-2026-08-17 ML-4B: a command that resolves to a real path but sits inside a
1330
+ // structurally malformed entry (missing/wrong "type" where this CLI's schema requires it —
1331
+ // hades-tf ML-4A barrier finding) will NEVER be executed by the CLI, regardless of whether
1332
+ // the script itself exists and is executable. Reported instead of the exists/executable
1333
+ // checks below, which assume a structurally valid entry.
1334
+ if (hf.requiresCommandType && !m.typeIsCommand) {
1335
+ msgs.push(`${hf.relPath} (${hf.cli}) references ${scriptMarker} resolved to "${resolved}", but the hook entry is missing "type":"command" (or has an invalid type) — ${hf.cli} will silently never execute it; run \`trackfw update\` to regenerate it`)
1336
+ continue
1337
+ }
1338
+
1269
1339
  let stat = null
1270
1340
  try {
1271
1341
  stat = fs.statSync(resolved)
@@ -1274,9 +1344,9 @@ function validateCredentialGuardHookResolvable(cwd) {
1274
1344
  }
1275
1345
 
1276
1346
  if (!stat) {
1277
- msgs.push(`${hf.relPath} (${hf.cli}) references trackfw-credential-guard.sh resolved to "${resolved}", but the script does not exist — run \`trackfw update\` to regenerate it`)
1347
+ msgs.push(`${hf.relPath} (${hf.cli}) references ${scriptMarker} resolved to "${resolved}", but the script does not exist — run \`trackfw update\` to regenerate it`)
1278
1348
  } else if ((stat.mode & 0o111) === 0) {
1279
- msgs.push(`${hf.relPath} (${hf.cli}) references trackfw-credential-guard.sh resolved to "${resolved}", but the script is not executable — run \`trackfw update\` to regenerate it`)
1349
+ msgs.push(`${hf.relPath} (${hf.cli}) references ${scriptMarker} resolved to "${resolved}", but the script is not executable — run \`trackfw update\` to regenerate it`)
1280
1350
  }
1281
1351
  }
1282
1352
  }
@@ -1284,6 +1354,18 @@ function validateCredentialGuardHookResolvable(cwd) {
1284
1354
  return msgs
1285
1355
  }
1286
1356
 
1357
+ // validateCredentialGuardHookResolvable é a regra "credential_guard_hook_resolvable" — ver
1358
+ // validateGuardHookResolvable para a implementação compartilhada.
1359
+ function validateCredentialGuardHookResolvable(cwd) {
1360
+ return validateGuardHookResolvable(CREDENTIAL_GUARD_SCRIPT_MARKER, cwd)
1361
+ }
1362
+
1363
+ // validateGitBranchGuardHookResolvable é a regra "git_branch_guard_hook_resolvable" — ver
1364
+ // validateGuardHookResolvable para a implementação compartilhada.
1365
+ function validateGitBranchGuardHookResolvable(cwd) {
1366
+ return validateGuardHookResolvable(GIT_BRANCH_GUARD_SCRIPT_MARKER, cwd)
1367
+ }
1368
+
1287
1369
  function isGitWorktree(dir) {
1288
1370
  try {
1289
1371
  const out = gitOutput(dir, ['rev-parse', '--is-inside-work-tree'])
@@ -1486,6 +1568,809 @@ function validateCredentialGuardScriptIntegrity(cwd) {
1486
1568
  ]
1487
1569
  }
1488
1570
 
1571
+ // ROADMAP-2026-08-15-trackfw-validate-deve-detectar-scripts-de-hook-ausentes-ou-desatualizados,
1572
+ // ML-2A: port of internal/validator/validator_git_branch_guard.go — adds git-branch-guard
1573
+ // coverage to the two existing credential-guard checks (existence/executability via
1574
+ // validateGuardHookResolvable above, and content-drift integrity via
1575
+ // validateGitBranchGuardScriptIntegrity below), plus the GLOBAL-scope check that was missing for
1576
+ // BOTH guards before this ML (same gap the Go implementation closed first).
1577
+
1578
+ // GBG_REF_BACKTICK — validator-local copy of the same escaped-backtick helper hooks.js's
1579
+ // GBG_BACKTICK uses (npm/src/generators/hooks.js), to embed REASON's `` `cmd` `` inside the
1580
+ // script's double-quoted shell string without breaking this JS template literal.
1581
+ const GBG_REF_BACKTICK = '\\`'
1582
+
1583
+ // GIT_BRANCH_GUARD_SCRIPT_REFERENCE is a validator-local copy of the
1584
+ // scripts/trackfw-git-branch-guard.sh template composed in npm/src/generators/hooks.js
1585
+ // (GIT_BRANCH_GUARD_SCRIPT const). Not required directly from generators/hooks.js: like
1586
+ // CREDENTIAL_GUARD_SCRIPT_REFERENCE above, generateGitBranchGuardScript() writes to disk AND
1587
+ // console.log()s a success line on every call — calling it from inside `trackfw validate` would
1588
+ // leak a stray " ✓ scripts/trackfw-git-branch-guard.sh" line into validate's output. There is no
1589
+ // import-cycle constraint in Node (unlike Go's internal/validator ↔ internal/generators cycle —
1590
+ // see gitBranchGuardScriptReference's doc comment in
1591
+ // internal/validator/validator_git_branch_guard_reference.go) but the console.log side effect is
1592
+ // reason enough on its own to keep a local copy here, matching the choice already made for
1593
+ // credential-guard in this same file.
1594
+ //
1595
+ // Unlike CREDENTIAL_GUARD_SCRIPT_REFERENCE, GIT_BRANCH_GUARD_SCRIPT_REFERENCE is used VERBATIM for
1596
+ // both the project scope and the global scope (generateGitBranchGuardScript /
1597
+ // generateGlobalGitBranchGuardScript write the exact same GIT_BRANCH_GUARD_SCRIPT constant) — so
1598
+ // this single reference constant covers both git_branch_guard_script_integrity (project,
1599
+ // scripts/trackfw-git-branch-guard.sh) and the global integrity check
1600
+ // (~/.trackfw/scripts/trackfw-git-branch-guard.sh), no second reference constant needed.
1601
+ //
1602
+ // Drift between this copy and the real generator is caught by
1603
+ // "GIT_BRANCH_GUARD_SCRIPT_REFERENCE é byte-idêntico ao que generateGitBranchGuardScript emite"
1604
+ // (npm/tests/git_branch_guard.test.js): it regenerates the script via generateGitBranchGuardScript
1605
+ // into a temp dir and asserts byte-equality against this constant.
1606
+ const GIT_BRANCH_GUARD_SCRIPT_REFERENCE = `#!/usr/bin/env bash
1607
+ # trackfw git branch guard — bloqueia git commit/push/checkout -b/branch/worktree add -b
1608
+ # brutos por subagente
1609
+ #
1610
+ # TRIPWIRE, NÃO FRONTEIRA DE SEGURANÇA: detecta o caso óbvio — comando git literal, sem
1611
+ # indireção de shell — não é defesa contra um agente adversário competente. Evasões que
1612
+ # exigem tokenizar como o bash (ex.: git\${IFS}push, {git,push}, g""it push) permanecem
1613
+ # abertas por decisão: ver docs/adr/ADR-2026-08-12-nao-ha-prevencao-contra-agente-induzido-
1614
+ # com-escrita-irrestrita-a-resposta-e-deteccao-ancorada-no-git.md. O stripping de
1615
+ # env/command abaixo reconhece as formas SEM argumentos antes de git (env git ...,
1616
+ # command git ...) e o env seguido de uma sequência de atribuições CHAVE=valor
1617
+ # (env FOO=bar git ..., env FOO=bar BAZ=qux git ...) — env com FLAGS (env -i git ...,
1618
+ # env --ignore-environment git ...) e command com flags (command -p git ...) continuam
1619
+ # evadindo; declarado, não fechado (ver AC5 do ML que adicionou esse stripping). A
1620
+ # segmentação abaixo
1621
+ # (quote_aware_split) evita falso-positivo em texto citado — não deve ser lida como imune a
1622
+ # evasão por citação/tokenização do shell.
1623
+ set -euo pipefail
1624
+ set -f
1625
+
1626
+ # --- 0. Drena o stdin ANTES de qualquer saída antecipada (ML-1B, ROADMAP-2026-08-17-guard-
1627
+ # global-cabeado-com-no-op-fora-de-projeto-e-integridade-independente-de-fiacao.md): sem isso,
1628
+ # quem escreve o payload JSON no pipe recebe EPIPE quando o no-op abaixo sai com 0 antes de ler
1629
+ # — reprodutível em 100% das chamadas fora de projeto trackfw, não é corrida de timing. Só drena
1630
+ # se stdin não for um terminal interativo (-t 0): em invocação manual sem pipe, "cat" bloquearia
1631
+ # esperando EOF (Ctrl-D). O valor lido é reaproveitado no passo 1 abaixo — nunca há uma segunda
1632
+ # leitura.
1633
+ _TRACKFW_STDIN=""
1634
+ [ -t 0 ] || _TRACKFW_STDIN=$(cat 2>/dev/null || true)
1635
+
1636
+ # --- 0b. No-op fora de projeto trackfw (ADR-2026-08-17-guard-global-cabeado-com-no-op-fora-de-
1637
+ # projeto-trackfw.md): sobe diretórios a partir do cwd FÍSICO (pwd -P, resolve symlink) até
1638
+ # achar trackfw.yaml na raiz do projeto. Sem trackfw.yaml em nenhum ancestral, o guard não se
1639
+ # aplica — fora de projeto trackfw não há trackfw ship como alternativa, e bloquear ali é custo
1640
+ # sem contrapartida. Custo medido: só parameter expansion e test -f por nível, nenhum fork de
1641
+ # processo; limitado pela profundidade do caminho.
1642
+ _TRACKFW_ROOT_DIR=$(pwd -P)
1643
+ _TRACKFW_FOUND=0
1644
+ while :; do
1645
+ if [ -f "$_TRACKFW_ROOT_DIR/trackfw.yaml" ]; then
1646
+ _TRACKFW_FOUND=1
1647
+ break
1648
+ fi
1649
+ if [ "$_TRACKFW_ROOT_DIR" = "/" ]; then
1650
+ break
1651
+ fi
1652
+ _TRACKFW_ROOT_DIR="\${_TRACKFW_ROOT_DIR%/*}"
1653
+ if [ -z "$_TRACKFW_ROOT_DIR" ]; then
1654
+ _TRACKFW_ROOT_DIR="/"
1655
+ fi
1656
+ done
1657
+ [ "$_TRACKFW_FOUND" -eq 1 ] || exit 0
1658
+
1659
+ # --- 1. Obter o comando git bruto ------------------------------------------------------------
1660
+ if [ "$#" -gt 0 ]; then
1661
+ CMD_RAW="$*"
1662
+ else
1663
+ INPUT="$_TRACKFW_STDIN"
1664
+ TRIMMED=$(printf '%s' "$INPUT" | sed -e 's/^[[:space:]]*//')
1665
+ case "$TRIMMED" in
1666
+ \\{*)
1667
+ CMD_RAW=""
1668
+ if command -v jq >/dev/null 2>&1; then
1669
+ CMD_RAW=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // .command // .tool_info.command_line // .hook_input.command // empty' 2>/dev/null || true)
1670
+ fi
1671
+ if [ -z "$CMD_RAW" ] || [ "$CMD_RAW" = "null" ]; then
1672
+ CMD_RAW=$(printf '%s' "$INPUT" | sed -n 's/.*"tool_input"[[:space:]]*:[[:space:]]*{[^}]*"command"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | head -1)
1673
+ fi
1674
+ if [ -z "$CMD_RAW" ]; then
1675
+ CMD_RAW=$(printf '%s' "$INPUT" | sed -n 's/.*"tool_info"[[:space:]]*:[[:space:]]*{[^}]*"command_line"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | head -1)
1676
+ fi
1677
+ if [ -z "$CMD_RAW" ]; then
1678
+ CMD_RAW=$(printf '%s' "$INPUT" | sed -n 's/.*"hook_input"[[:space:]]*:[[:space:]]*{[^}]*"command"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | head -1)
1679
+ fi
1680
+ if [ -z "$CMD_RAW" ]; then
1681
+ CMD_RAW=$(printf '%s' "$INPUT" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | head -1)
1682
+ fi
1683
+ ;;
1684
+ *)
1685
+ CMD_RAW="$INPUT"
1686
+ ;;
1687
+ esac
1688
+ fi
1689
+
1690
+ if [ -z "$CMD_RAW" ]; then
1691
+ CMD_RAW="\${TRACKFW_GIT_COMMAND:-}"
1692
+ fi
1693
+
1694
+ [ -n "$CMD_RAW" ] || exit 0
1695
+
1696
+ # --- 2. Pré-processamento anti-falso-positivo: neutraliza separadores reais (';', '&&',
1697
+ # '||', '|', quebra de linha) que estão DENTRO de aspas ou de corpo de heredoc, para que
1698
+ # conteúdo de mensagem (ex.: ` + "`" + `-m "linha 1\\nlinha 2"` + "`" + `) nunca seja fatiado em pseudo-segmentos
1699
+ # e lido como comando -------------------------------------------------------------------
1700
+ #
1701
+ # strip_heredoc_bodies: remove o CORPO de blocos heredoc (<<DELIM ... DELIM), preservando a
1702
+ # linha de abertura e a linha terminadora — cobre o padrão ` + "`" + `git commit -F- <<'EOF' ... EOF` + "`" + `
1703
+ # (heredoc não citado, fora do escopo de quote_aware_split abaixo). Heurística por linha, não
1704
+ # sintaxe completa de shell: só remove o corpo quando encontra a linha terminadora
1705
+ # correspondente. Se o heredoc nunca fecha (terminador ausente ou não localizado), devolve o
1706
+ # texto ORIGINAL sem qualquer alteração — lado seguro: mais restritivo é preferível a esconder
1707
+ # um comando real atrás de um heredoc mal-formado.
1708
+ strip_heredoc_bodies() {
1709
+ printf '%s' "$1" | awk '
1710
+ BEGIN {
1711
+ dq = sprintf("%c", 34)
1712
+ sq = sprintf("%c", 39)
1713
+ in_heredoc = 0
1714
+ delim = ""
1715
+ ok = 1
1716
+ }
1717
+ {
1718
+ raw = raw $0 "\\n"
1719
+ if (in_heredoc) {
1720
+ trimmed = $0
1721
+ sub(/^[ \\t]+/, "", trimmed)
1722
+ sub(/[ \\t]+$/, "", trimmed)
1723
+ if (trimmed == delim) {
1724
+ in_heredoc = 0
1725
+ out = out $0 "\\n"
1726
+ }
1727
+ next
1728
+ }
1729
+ if (match($0, /<<-?[ \\t]*[^ \\t]+/)) {
1730
+ d = substr($0, RSTART, RLENGTH)
1731
+ sub(/^<<-?[ \\t]*/, "", d)
1732
+ gsub(dq, "", d)
1733
+ gsub(sq, "", d)
1734
+ if (d != "") {
1735
+ delim = d
1736
+ in_heredoc = 1
1737
+ }
1738
+ }
1739
+ out = out $0 "\\n"
1740
+ }
1741
+ END {
1742
+ if (in_heredoc) ok = 0
1743
+ if (ok) { printf "%s", out } else { printf "%s", raw }
1744
+ }
1745
+ '
1746
+ }
1747
+
1748
+ # quote_aware_split: emite o texto com ';' isolado, '&&', '||' e '|' isolado convertidos em
1749
+ # quebra de linha — EXCETO quando ocorrem dentro de uma string entre aspas simples ou duplas,
1750
+ # caso em que são preservados como texto e uma quebra de linha real dentro das aspas vira
1751
+ # espaço (nunca gera um novo pseudo-segmento). Substitui o antigo ` + "`" + `sed` + "`" + ` cego, que não
1752
+ # distinguia texto citado de sintaxe de comando — a causa raiz do falso-positivo de linha de
1753
+ # mensagem de commit iniciada por "git ...". Aspas não fechadas até o fim da entrada
1754
+ # permanecem "abertas" até o fim — mesma semântica do shell real: uma aspa não fechada nunca
1755
+ # deixa o texto seguinte executar como comando novo, só torna o restante parte da mesma
1756
+ # string.
1757
+ quote_aware_split() {
1758
+ printf '%s' "$1" | awk '
1759
+ BEGIN {
1760
+ dq = sprintf("%c", 34)
1761
+ sq = sprintf("%c", 39)
1762
+ bs = sprintf("%c", 92)
1763
+ nl = sprintf("%c", 10)
1764
+ }
1765
+ { s = (NR == 1) ? $0 : s nl $0 }
1766
+ END {
1767
+ n = length(s)
1768
+ q = ""
1769
+ out = ""
1770
+ i = 1
1771
+ while (i <= n) {
1772
+ c = substr(s, i, 1)
1773
+ if (q != "") {
1774
+ if (q == dq && c == bs && i < n) {
1775
+ nx = substr(s, i + 1, 1)
1776
+ out = out c (nx == nl ? " " : nx)
1777
+ i += 2
1778
+ continue
1779
+ }
1780
+ if (c == q) {
1781
+ q = ""
1782
+ out = out c
1783
+ i++
1784
+ continue
1785
+ }
1786
+ out = out (c == nl ? " " : c)
1787
+ i++
1788
+ continue
1789
+ }
1790
+ if (c == dq || c == sq) {
1791
+ q = c
1792
+ out = out c
1793
+ i++
1794
+ continue
1795
+ }
1796
+ if (substr(s, i, 2) == "&&" || substr(s, i, 2) == "||") {
1797
+ out = out nl
1798
+ i += 2
1799
+ continue
1800
+ }
1801
+ if (c == ";" || c == "|") {
1802
+ out = out nl
1803
+ i++
1804
+ continue
1805
+ }
1806
+ out = out c
1807
+ i++
1808
+ }
1809
+ printf "%s", out
1810
+ }
1811
+ '
1812
+ }
1813
+
1814
+ # match_subcommand — casa contra "git (commit|push|checkout -b|switch -c)", segmento por
1815
+ # segmento. Cada segmento é um comando real, obtido depois do pré-processamento acima
1816
+ # (strip_heredoc_bodies + quote_aware_split), que converte ';', '&&', '||', '|' fora de aspas
1817
+ # em quebra de linha e neutraliza os mesmos separadores quando aparecem dentro de
1818
+ # aspas/heredoc. "git" só conta se for o PRIMEIRO token do segmento (por basename, então
1819
+ # /usr/bin/git também casa) — nunca uma ocorrência solta em qualquer posição da string
1820
+ # inteira. Isso evita: (a) o segundo comando de uma cadeia escapar da checagem, (b) um path
1821
+ # absoluto para o git escapar por comparação de igualdade exata, e (c) texto de prosa —
1822
+ # inclusive linha de mensagem de commit que COMEÇA com "git <sub>" (ex.: uma tabela
1823
+ # documentando comandos bloqueados) — ser tratado como comando, porque esse texto agora nunca
1824
+ # produz um novo segmento. ` + "`" + `git switch -c/-C/--create` + "`" + ` (forma alternativa a ` + "`" + `checkout -b` + "`" + `
1825
+ # para criar branch) é reconhecido varrendo TODOS os tokens após o subcomando, não só o
1826
+ # primeiro — cobre ` + "`" + `git switch --track -c feat/x` + "`" + ` (flag antes de -c).
1827
+ # checkout -b é reconhecido do mesmo jeito: varre TODOS os tokens até achar -b/-B/--orphan,
1828
+ # não só o primeiro. Prefixos env e command antes de git são descartados antes da checagem do
1829
+ # basename — cobre env git push/command git push sem exigir tokenizar como o bash.
1830
+ match_subcommand() {
1831
+ normalized=$(strip_heredoc_bodies "$1")
1832
+ normalized=$(quote_aware_split "$normalized")
1833
+ while IFS= read -r seg; do
1834
+ seg_trimmed=$(printf '%s' "$seg" | sed -e 's/^[[:space:]]*//')
1835
+ [ -n "$seg_trimmed" ] || continue
1836
+
1837
+ set -- $seg_trimmed
1838
+ first="$1"
1839
+ base="\${first##*/}"
1840
+ while [ "$base" = "env" ] || [ "$base" = "command" ]; do
1841
+ is_env="$base"
1842
+ shift
1843
+ [ "$#" -gt 0 ] || break
1844
+ if [ "$is_env" = "env" ]; then
1845
+ while [ "$#" -gt 0 ]; do
1846
+ case "$1" in
1847
+ -*)
1848
+ break
1849
+ ;;
1850
+ *=*)
1851
+ shift
1852
+ ;;
1853
+ *)
1854
+ break
1855
+ ;;
1856
+ esac
1857
+ done
1858
+ [ "$#" -gt 0 ] || break
1859
+ fi
1860
+ first="$1"
1861
+ base="\${first##*/}"
1862
+ done
1863
+ [ "$base" = "git" ] || continue
1864
+ shift
1865
+
1866
+ sub=""
1867
+ while [ "$#" -gt 0 ]; do
1868
+ tok="$1"
1869
+ case "$tok" in
1870
+ -C|-c|--work-tree|--git-dir|--namespace)
1871
+ if [ "$#" -ge 2 ]; then shift 2; else shift; fi
1872
+ continue
1873
+ ;;
1874
+ -*)
1875
+ shift
1876
+ continue
1877
+ ;;
1878
+ *)
1879
+ sub="$tok"
1880
+ shift
1881
+ break
1882
+ ;;
1883
+ esac
1884
+ done
1885
+
1886
+ case "$sub" in
1887
+ commit)
1888
+ echo "commit"
1889
+ return 0
1890
+ ;;
1891
+ push)
1892
+ echo "push"
1893
+ return 0
1894
+ ;;
1895
+ checkout)
1896
+ for tok2 in "$@"; do
1897
+ case "$tok2" in
1898
+ -b|-B|--orphan|--orphan=*)
1899
+ echo "checkout-b"
1900
+ return 0
1901
+ ;;
1902
+ esac
1903
+ done
1904
+ ;;
1905
+ switch)
1906
+ for tok2 in "$@"; do
1907
+ case "$tok2" in
1908
+ -c|-C|--create|--create=*|--force-create|--force-create=*)
1909
+ echo "switch-c"
1910
+ return 0
1911
+ ;;
1912
+ esac
1913
+ done
1914
+ ;;
1915
+ branch)
1916
+ # git branch é majoritariamente leitura (sem args, -a, -r, -l, --list, -v/-vv,
1917
+ # --show-current, --contains, --no-contains, --merged, --no-merged, --sort=,
1918
+ # --format=, --points-at, -d/-D/--delete) — bloquear leitura seria pior que a
1919
+ # brecha. Só bloqueia: (a) -c/-C/-m/-M/--copy/--move (cria/renomeia branch,
1920
+ # qualquer posição de token) ou (b) um argumento posicional puro (nome da branch a
1921
+ # criar), a menos que -d/-D/--delete também esteja presente (delete tem
1922
+ # posicional legítimo — o nome a apagar). Flags de valor conhecidas (--contains,
1923
+ # --no-contains, --sort, --format, --points-at, --merged, --no-merged) têm seu
1924
+ # valor seguinte pulado quando vem em token separado, para não ser lido como
1925
+ # posicional de criação.
1926
+ branch_action=0
1927
+ has_delete=0
1928
+ saw_positional=0
1929
+ skip_next=0
1930
+ for tok2 in "$@"; do
1931
+ if [ "$skip_next" = "1" ]; then
1932
+ skip_next=0
1933
+ continue
1934
+ fi
1935
+ case "$tok2" in
1936
+ -c|-C|-m|-M|--copy|--copy=*|--move|--move=*)
1937
+ branch_action=1
1938
+ ;;
1939
+ -d|-D|--delete|--delete=*)
1940
+ has_delete=1
1941
+ ;;
1942
+ --contains|--no-contains|--sort|--format|--points-at|--merged|--no-merged)
1943
+ skip_next=1
1944
+ ;;
1945
+ -*)
1946
+ ;;
1947
+ *)
1948
+ saw_positional=1
1949
+ ;;
1950
+ esac
1951
+ done
1952
+ if [ "$has_delete" != "1" ]; then
1953
+ if [ "$branch_action" = "1" ] || [ "$saw_positional" = "1" ]; then
1954
+ echo "branch-create"
1955
+ return 0
1956
+ fi
1957
+ fi
1958
+ ;;
1959
+ worktree)
1960
+ if [ "\${1:-}" = "add" ]; then
1961
+ shift
1962
+ for tok2 in "$@"; do
1963
+ case "$tok2" in
1964
+ -b|-B)
1965
+ echo "worktree-add-b"
1966
+ return 0
1967
+ ;;
1968
+ esac
1969
+ done
1970
+ fi
1971
+ ;;
1972
+ esac
1973
+ done <<EOF
1974
+ $normalized
1975
+ EOF
1976
+ return 1
1977
+ }
1978
+
1979
+ SUBCOMMAND=$(match_subcommand "$CMD_RAW") || exit 0
1980
+
1981
+ case "$SUBCOMMAND" in
1982
+ checkout-b)
1983
+ REASON="trackfw: git checkout -b bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_REF_BACKTICK + `. Ver CLAUDE.md §1."
1984
+ ;;
1985
+ switch-c)
1986
+ REASON="trackfw: git switch -c bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_REF_BACKTICK + `. Ver CLAUDE.md §1."
1987
+ ;;
1988
+ branch-create)
1989
+ REASON="trackfw: git branch bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_REF_BACKTICK + `. Ver CLAUDE.md §1."
1990
+ ;;
1991
+ worktree-add-b)
1992
+ REASON="trackfw: git worktree add -b bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_REF_BACKTICK + `. Ver CLAUDE.md §1."
1993
+ ;;
1994
+ commit)
1995
+ REASON="trackfw: git commit bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw commit -m '<mensagem>'` + GBG_REF_BACKTICK + `. Ver CLAUDE.md §1."
1996
+ ;;
1997
+ push)
1998
+ REASON="trackfw: git push bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw ship` + GBG_REF_BACKTICK + `. Ver CLAUDE.md §1."
1999
+ ;;
2000
+ *)
2001
+ exit 0
2002
+ ;;
2003
+ esac
2004
+
2005
+ printf '{"decision":"block","reason":"%s"}\\n' "$REASON"
2006
+ echo "$REASON" >&2
2007
+ exit 2
2008
+ `
2009
+
2010
+ // validateGitBranchGuardScriptIntegrity é a regra "git_branch_guard_script_integrity": compara
2011
+ // scripts/trackfw-git-branch-guard.sh em disco contra o template que esta versão do trackfw
2012
+ // geraria. Espelha validateCredentialGuardScriptIntegrity exatamente — mesmo contrato de silêncio
2013
+ // na ausência (existência é responsabilidade de git_branch_guard_hook_resolvable, não desta regra).
2014
+ function validateGitBranchGuardScriptIntegrity(cwd) {
2015
+ const root = cwd || process.cwd()
2016
+ const relPath = 'scripts/trackfw-git-branch-guard.sh'
2017
+ let content
2018
+ try {
2019
+ content = fs.readFileSync(path.join(root, relPath), 'utf8')
2020
+ } catch (e) {
2021
+ if (e.code === 'ENOENT') return []
2022
+ return [inspectionDiagnostic('git_branch_guard_script_integrity', relPath, e)]
2023
+ }
2024
+
2025
+ if (content === GIT_BRANCH_GUARD_SCRIPT_REFERENCE) return []
2026
+
2027
+ return [
2028
+ `${relPath} content diverges from the template this version of trackfw generates — ` +
2029
+ 'if you did not edit this file by hand, run `trackfw update` to regenerate it',
2030
+ ]
2031
+ }
2032
+
2033
+ // CREDENTIAL_GUARD_GLOBAL_SCRIPT_REFERENCE is a validator-local copy of the GLOBAL-scope
2034
+ // ~/.trackfw/scripts/trackfw-credential-guard.sh template composed in npm/src/generators/hooks.js
2035
+ // (GLOBAL_CREDENTIAL_GUARD_SCRIPT = CG_HEADER + CG_DETECTION_CORE + CG_GLOBAL_TAIL). This is a
2036
+ // DIFFERENT template than CREDENTIAL_GUARD_SCRIPT_REFERENCE (the project-scope variant): the
2037
+ // global variant omits the project-guard block ("no-op outside a trackfw.yaml project") and
2038
+ // defaults credential_guard.mode to "block" instead of "warn" — mirrors Go's
2039
+ // credentialGuardGlobalScriptReference
2040
+ // (internal/validator/validator_credential_guard_global_reference.go). Comparing the global
2041
+ // on-disk script against CREDENTIAL_GUARD_SCRIPT_REFERENCE (the project template) would be a
2042
+ // guaranteed false positive for every user with the global harness installed.
2043
+ const CREDENTIAL_GUARD_GLOBAL_SCRIPT_REFERENCE = `#!/usr/bin/env bash
2044
+ # trackfw credential guard — PreToolUse/PostToolUse hook
2045
+ set -euo pipefail
2046
+
2047
+ INPUT=$(cat)
2048
+
2049
+ JWT_PATTERN='eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+'
2050
+ AWS_KEY_PATTERN='AKIA[0-9A-Z]{16}'
2051
+
2052
+ MATCH=""
2053
+ if printf '%s' "$INPUT" | grep -qE "$JWT_PATTERN"; then
2054
+ MATCH="JWT"
2055
+ elif printf '%s' "$INPUT" | grep -qE "$AWS_KEY_PATTERN"; then
2056
+ MATCH="AWS access key"
2057
+ fi
2058
+
2059
+ # The raw payload is JSON: any double quote inside the underlying tool_input.command is
2060
+ # escaped as \\" -- unescape those before scanning for redirect targets, or a quoted target
2061
+ # like "$TMPFILE" is seen as starting with a literal backslash instead of a variable
2062
+ # reference.
2063
+ RAW=$(printf '%s' "$INPUT" | sed 's/\\\\"/"/g')
2064
+
2065
+ # Ignore matches that are only ever written to an ephemeral destination
2066
+ # (mktemp-derived path or /dev/null). A match with no redirect at all
2067
+ # (printed to stdout, e.g.) or redirected to a plain file path still
2068
+ # alerts -- that is the incident this hook guards against.
2069
+ is_ephemeral_target() {
2070
+ local target
2071
+ target=$(printf '%s' "$1" | tr -d "\\"'" | sed -E 's/[},]+$//')
2072
+ case "$target" in
2073
+ /dev/null) return 0 ;;
2074
+ *mktemp*) return 0 ;;
2075
+ esac
2076
+ if printf '%s' "$target" | grep -qE '^\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?$'; then
2077
+ local varname pattern
2078
+ varname=$(printf '%s' "$target" | sed -E 's/^\\$\\{?([A-Za-z_][A-Za-z0-9_]*)\\}?$/\\1/')
2079
+ pattern="*\${varname}="'$(mktemp'"*"
2080
+ case "$RAW" in
2081
+ $pattern) return 0 ;;
2082
+ esac
2083
+ fi
2084
+ return 1
2085
+ }
2086
+
2087
+ REDIRECTS=$(printf '%s' "$RAW" | grep -oE '[0-9]?>>?[[:space:]]*[^[:space:]|&;,:]+' || true)
2088
+
2089
+ # Second detection layer: only runs when the payload scan above found nothing -- keeps the common
2090
+ # case (match already found) cheap and avoids reading files unnecessarily. Files above the size cap
2091
+ # are skipped silently.
2092
+ scan_file_for_pattern() {
2093
+ local path size
2094
+ path=$(printf '%s' "$1" | tr -d "\\"'" | sed -E 's/[},]+$//')
2095
+ [ -n "$path" ] && [ -f "$path" ] || return 1
2096
+ size=$(wc -c < "$path" 2>/dev/null | tr -d '[:space:]')
2097
+ size=\${size:-0}
2098
+ [ "$size" -lt 1048576 ] || return 1
2099
+ if grep -qE "$JWT_PATTERN" "$path" 2>/dev/null; then
2100
+ MATCH="JWT"
2101
+ return 0
2102
+ fi
2103
+ if grep -qE "$AWS_KEY_PATTERN" "$path" 2>/dev/null; then
2104
+ MATCH="AWS access key"
2105
+ return 0
2106
+ fi
2107
+ return 1
2108
+ }
2109
+
2110
+ if [ -z "$MATCH" ] && [ -n "$REDIRECTS" ]; then
2111
+ while IFS= read -r line; do
2112
+ if [ -z "$line" ]; then
2113
+ continue
2114
+ fi
2115
+ target=$(printf '%s' "$line" | sed -E 's/^[0-9]?>>?[[:space:]]*//')
2116
+ if ! is_ephemeral_target "$target"; then
2117
+ scan_file_for_pattern "$target" && break
2118
+ fi
2119
+ done <<< "$REDIRECTS"
2120
+ fi
2121
+
2122
+ if [ -z "$MATCH" ]; then
2123
+ CMD_LINE=$(printf '%s' "$RAW" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')
2124
+ if [ -n "$CMD_LINE" ]; then
2125
+ set -- $CMD_LINE
2126
+ cmd_name="\${1:-}"
2127
+ case "$cmd_name" in
2128
+ cat|head|tail|jq|grep)
2129
+ shift
2130
+ for token in "$@"; do
2131
+ scan_file_for_pattern "$token" && break
2132
+ done
2133
+ ;;
2134
+ esac
2135
+ fi
2136
+ fi
2137
+
2138
+ [ -n "$MATCH" ] || exit 0
2139
+
2140
+ HAS_REDIRECT=0
2141
+ EXEMPT=1
2142
+ if [ -n "$REDIRECTS" ]; then
2143
+ while IFS= read -r line; do
2144
+ if [ -z "$line" ]; then
2145
+ continue
2146
+ fi
2147
+ HAS_REDIRECT=1
2148
+ target=$(printf '%s' "$line" | sed -E 's/^[0-9]?>>?[[:space:]]*//')
2149
+ if ! is_ephemeral_target "$target"; then
2150
+ EXEMPT=0
2151
+ fi
2152
+ done <<< "$REDIRECTS"
2153
+ fi
2154
+
2155
+ if [ "$HAS_REDIRECT" -eq 1 ] && [ "$EXEMPT" -eq 1 ]; then
2156
+ exit 0
2157
+ fi
2158
+
2159
+ DEFAULT_MODE="block"
2160
+ MODE=$(grep -A 5 '^credential_guard:' trackfw.yaml 2>/dev/null | grep 'mode:' | head -1 | sed -E 's/^[[:space:]]*mode:[[:space:]]*//; s/[[:space:]]*#.*$//' | tr -d "\\"'" || true)
2161
+ case "$MODE" in
2162
+ warn|block) ;;
2163
+ *) MODE="$DEFAULT_MODE" ;;
2164
+ esac
2165
+
2166
+ if [ "$MODE" = "block" ]; then
2167
+ echo "trackfw-credential-guard: blocked - possible $MATCH detected in tool payload." >&2
2168
+ exit 2
2169
+ fi
2170
+
2171
+ echo "trackfw-credential-guard: warning - possible $MATCH detected in tool payload." >&2
2172
+
2173
+ ROADMAP_DIR="docs/roadmaps"
2174
+ if [ ! -d "$ROADMAP_DIR" ]; then
2175
+ exit 0
2176
+ fi
2177
+
2178
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
2179
+ MSG="Possible $MATCH detected in tool payload - review before materializing credentials in plain text."
2180
+ MSG_ESC=$(echo "$MSG" | tr -d '\\000-\\037' | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
2181
+
2182
+ mkdir -p "$ROADMAP_DIR"
2183
+ printf '{"tool":"credential-guard","message":"%s","level":"action_required","timestamp":"%s"}\\n' \\
2184
+ "$MSG_ESC" \\
2185
+ "$TIMESTAMP" > "$ROADMAP_DIR/.trackfw-credential-guard.json"
2186
+
2187
+ exit 0
2188
+ `
2189
+
2190
+ // globalGuardConfigFile associates a GLOBAL (per-CLI, $HOME-rooted) hook/settings config file with
2191
+ // the CLI that consumes it, for the global-scope guard checks below. Distinct from
2192
+ // CREDENTIAL_GUARD_HOOK_FILES, whose relPath is rooted at the PROJECT root, not $HOME.
2193
+ //
2194
+ // GLOBAL_GUARD_CONFIG_FILES is the closed list of GLOBAL hook/settings files `trackfw update
2195
+ // harness` can write a guard entry into — the global-scope counterpart of
2196
+ // CREDENTIAL_GUARD_HOOK_FILES. Paths and CLI labels ported from Go's globalGuardConfigFiles
2197
+ // (internal/validator/validator_git_branch_guard.go) — note this list DIFFERS from
2198
+ // CREDENTIAL_GUARD_HOOK_FILES for Copilot and Kiro (global scope uses .copilot/settings.json and
2199
+ // .kiro/hooks/trackfw-credential-guard.json, not the project-scope attention-hook paths).
2200
+ // requiresCommandType (ROADMAP-2026-08-17 ML-4B) mirrors CREDENTIAL_GUARD_HOOK_FILES'
2201
+ // requiresCommandType above — true for every CLI except Cursor.
2202
+ const GLOBAL_GUARD_CONFIG_FILES = [
2203
+ { relPath: '.claude/settings.json', cli: 'Claude Code', requiresCommandType: true },
2204
+ { relPath: '.codex/hooks.json', cli: 'Codex CLI', requiresCommandType: true },
2205
+ { relPath: '.gemini/settings.json', cli: 'Gemini CLI', requiresCommandType: true },
2206
+ { relPath: '.cursor/hooks.json', cli: 'Cursor', requiresCommandType: false },
2207
+ { relPath: '.copilot/settings.json', cli: 'GitHub Copilot CLI', requiresCommandType: true },
2208
+ { relPath: '.kiro/hooks/trackfw-credential-guard.json', cli: 'Kiro', requiresCommandType: true },
2209
+ ]
2210
+
2211
+ // globalGuardConfigPath resolves the actual on-disk path (relative to $HOME) that
2212
+ // validateGuardGlobalHookResolvable must read for a given (GLOBAL_GUARD_CONFIG_FILES entry,
2213
+ // scriptMarker) pair. Port of Go's globalGuardConfigPath (internal/validator/
2214
+ // validator_git_branch_guard.go) — see that function's doc comment for the full rationale
2215
+ // (5 CLIs share one merge-based file across both guards; Kiro is the sole exception, with a
2216
+ // dedicated file per guard because its writer rewrites the whole document wholesale).
2217
+ //
2218
+ // ROADMAP-2026-08-17 ML-3B: before this function existed, GLOBAL_GUARD_CONFIG_FILES only ever
2219
+ // pointed Kiro at trackfw-credential-guard.json for BOTH guards, so git_branch_guard_hook_resolvable
2220
+ // never inspected ~/.kiro/hooks/trackfw-git-branch-guard.json at all.
2221
+ function globalGuardConfigPath(gf, scriptMarker) {
2222
+ if (gf.cli === 'Kiro' && scriptMarker === GIT_BRANCH_GUARD_SCRIPT_MARKER) {
2223
+ return '.kiro/hooks/trackfw-git-branch-guard.json'
2224
+ }
2225
+ return gf.relPath
2226
+ }
2227
+
2228
+ // validateGuardGlobalHookResolvable is the GLOBAL-scope counterpart of
2229
+ // validateGuardHookResolvable: for each of the 6 GLOBAL_GUARD_CONFIG_FILES that exists AND
2230
+ // references scriptMarker, verifies the referenced script exists and is executable. Port of Go's
2231
+ // validateGuardGlobalHookResolvable (internal/validator/validator_git_branch_guard.go) — see that
2232
+ // function's doc comment for the full trigger-condition and fail-open rationale.
2233
+ //
2234
+ // Global entries are written by npm/src/generators (harnessCredentialGuardTarget*-equivalent code
2235
+ // in agentfiles-equivalent generators) as fully resolved absolute paths, never a placeholder like
2236
+ // $CLAUDE_PROJECT_DIR — so, unlike the project-scope resolveCredentialGuardHookPath, no
2237
+ // prefix-stripping is needed here: any matched command that is NOT already an absolute path is
2238
+ // skipped (never treated as a violation).
2239
+ //
2240
+ // Fail-open: unresolvable $HOME, unreadable file, or invalid JSON all skip that file in silence —
2241
+ // same contract validateGuardHookResolvable already has for project-scope files.
2242
+ function validateGuardGlobalHookResolvable(scriptMarker) {
2243
+ const home = os.homedir()
2244
+ if (!home) return []
2245
+
2246
+ const msgs = []
2247
+ for (const gf of GLOBAL_GUARD_CONFIG_FILES) {
2248
+ const relPath = globalGuardConfigPath(gf, scriptMarker)
2249
+ const fullPath = path.join(home, relPath)
2250
+ let content
2251
+ try {
2252
+ content = fs.readFileSync(fullPath, 'utf8')
2253
+ } catch (e) {
2254
+ if (e.code === 'ENOENT') continue
2255
+ continue
2256
+ }
2257
+
2258
+ let parsed
2259
+ try {
2260
+ parsed = JSON.parse(content)
2261
+ } catch (_) {
2262
+ continue
2263
+ }
2264
+
2265
+ const commands = []
2266
+ collectCommandsWithMarker(parsed, scriptMarker, commands)
2267
+
2268
+ const seen = new Set()
2269
+ for (const m of commands) {
2270
+ const seenKey = `${m.raw} ${m.typeIsCommand}`
2271
+ if (seen.has(seenKey)) continue
2272
+ seen.add(seenKey)
2273
+
2274
+ if (!path.isAbsolute(m.raw)) continue
2275
+
2276
+ // ROADMAP-2026-08-17 ML-4B: reproduced by hades-tf (ML-4A barrier) — a global config entry
2277
+ // with the correct absolute command but missing/wrong "type" (hand-edited, an older trackfw
2278
+ // version, another tool's merge) is silently never executed by the CLI, even though the
2279
+ // script itself exists and is fine. Reported instead of the exists/executable checks below,
2280
+ // which assume the entry is structurally valid.
2281
+ if (gf.requiresCommandType && !m.typeIsCommand) {
2282
+ msgs.push(`~/${relPath} (${gf.cli}, global scope) references ${scriptMarker} resolved to "${m.raw}", but the hook entry is missing "type":"command" (or has an invalid type) — ${gf.cli} will silently never execute it; run \`trackfw update harness\` to regenerate it`)
2283
+ continue
2284
+ }
2285
+
2286
+ let stat = null
2287
+ try {
2288
+ stat = fs.statSync(m.raw)
2289
+ } catch (_) {
2290
+ stat = null
2291
+ }
2292
+
2293
+ if (!stat) {
2294
+ msgs.push(`~/${relPath} (${gf.cli}, global scope) references ${scriptMarker} resolved to "${m.raw}", but the script does not exist — run \`trackfw update harness\` to regenerate it`)
2295
+ } else if ((stat.mode & 0o111) === 0) {
2296
+ msgs.push(`~/${relPath} (${gf.cli}, global scope) references ${scriptMarker} resolved to "${m.raw}", but the script is not executable — run \`trackfw update harness\` to regenerate it`)
2297
+ }
2298
+ }
2299
+ }
2300
+
2301
+ return msgs
2302
+ }
2303
+
2304
+ // validateGuardGlobalScriptIntegrity is the GLOBAL-scope counterpart of
2305
+ // validateCredentialGuardScriptIntegrity/validateGitBranchGuardScriptIntegrity: verifies the
2306
+ // content of ~/.trackfw/scripts/<scriptFileName> (the fixed location the global generators write
2307
+ // to) against referenceContent byte-for-byte. Port of Go's validateGuardGlobalScriptIntegrity
2308
+ // (internal/validator/validator_git_branch_guard.go).
2309
+ //
2310
+ // ROADMAP-2026-08-17 (guard global cabeado com no-op / integridade independente de fiação), ML-3A:
2311
+ // deliberately triggers on ARTIFACT EXISTENCE, not on any GLOBAL_GUARD_CONFIG_FILES entry
2312
+ // referencing scriptMarker — the old per-config trigger meant a script trackfw itself wrote (via
2313
+ // `trackfw update harness`) but that no config yet pointed at could rot indefinitely with
2314
+ // `validate` green. "If trackfw wrote the script, trackfw verifies the script" — existence is the
2315
+ // only precondition; wiring is irrelevant to whether the artifact itself has drifted.
2316
+ //
2317
+ // Fail-open on absence: a script trackfw never wrote (user never ran `update harness`) is not an
2318
+ // error.
2319
+ //
2320
+ // Single evaluation per script (not per referencing config): caps the message count at 1
2321
+ // regardless of how many (or how few — including zero) configs reference the same on-disk path —
2322
+ // this is what prevents double-reporting now that git-branch-guard has both global wiring (Wave 2)
2323
+ // and a global artifact.
2324
+ //
2325
+ // scriptMarker doubles as the script's own filename (trackfw-credential-guard.sh /
2326
+ // trackfw-git-branch-guard.sh) in both call sites below — same equivalence the Go port relies on.
2327
+ function validateGuardGlobalScriptIntegrity(scriptFileName, referenceContent) {
2328
+ const home = os.homedir()
2329
+ if (!home) return []
2330
+
2331
+ const scriptPath = path.join(home, '.trackfw', 'scripts', scriptFileName)
2332
+ let content
2333
+ try {
2334
+ content = fs.readFileSync(scriptPath, 'utf8')
2335
+ } catch (_) {
2336
+ // Not installed is not a violation — same contract as every other *_script_integrity check
2337
+ // (project-scope and global) in this file.
2338
+ return []
2339
+ }
2340
+
2341
+ if (content === referenceContent) return []
2342
+
2343
+ return [`${scriptPath} (global scope) content diverges from the template this version of trackfw generates — if you did not edit this file by hand, run \`trackfw update harness\` to regenerate it`]
2344
+ }
2345
+
2346
+ // validateCredentialGuardGlobalHookResolvable / validateCredentialGuardGlobalScriptIntegrity /
2347
+ // validateGitBranchGuardGlobalHookResolvable / validateGitBranchGuardGlobalScriptIntegrity are the
2348
+ // 4 thin wrappers wired in validateUnfiltered below — each folds its messages into the SAME rule
2349
+ // name as its project-scope counterpart (credential_guard_hook_resolvable,
2350
+ // credential_guard_script_integrity, git_branch_guard_hook_resolvable,
2351
+ // git_branch_guard_script_integrity respectively), so no new rules: entries in trackfw.yaml are
2352
+ // needed. Port of the 4 equivalent wrappers in
2353
+ // internal/validator/validator_git_branch_guard.go.
2354
+ function validateCredentialGuardGlobalHookResolvable() {
2355
+ return validateGuardGlobalHookResolvable(CREDENTIAL_GUARD_SCRIPT_MARKER)
2356
+ }
2357
+
2358
+ function validateCredentialGuardGlobalScriptIntegrity() {
2359
+ return validateGuardGlobalScriptIntegrity(CREDENTIAL_GUARD_SCRIPT_MARKER, CREDENTIAL_GUARD_GLOBAL_SCRIPT_REFERENCE)
2360
+ }
2361
+
2362
+ function validateGitBranchGuardGlobalHookResolvable() {
2363
+ return validateGuardGlobalHookResolvable(GIT_BRANCH_GUARD_SCRIPT_MARKER)
2364
+ }
2365
+
2366
+ function validateGitBranchGuardGlobalScriptIntegrity() {
2367
+ return validateGuardGlobalScriptIntegrity(GIT_BRANCH_GUARD_SCRIPT_MARKER, GIT_BRANCH_GUARD_SCRIPT_REFERENCE)
2368
+ }
2369
+ // NOTE: CREDENTIAL_GUARD_SCRIPT_MARKER === 'trackfw-credential-guard.sh' and
2370
+ // GIT_BRANCH_GUARD_SCRIPT_MARKER === 'trackfw-git-branch-guard.sh' — both markers are already the
2371
+ // literal script filenames, so they double as scriptFileName above with no extra constant needed
2372
+ // (same reuse the Go port relies on).
2373
+
1489
2374
  // CREDENTIAL_GUARD_MODE_LOOKBEHIND_LINES mirrors the shell script's own resolution of
1490
2375
  // credential_guard.mode (`grep -A 5 '^credential_guard:'`): the mode key is found on the matched
1491
2376
  // line or within the 5 lines following it.
@@ -1600,6 +2485,12 @@ const RULE_DEFAULTS = {
1600
2485
  // credential_guard_mode_downgrade is deliberately absent: it falls through to ruleSeverity's
1601
2486
  // 'error' default.
1602
2487
  credential_guard_script_integrity: 'warning',
2488
+ // ROADMAP-2026-08-15-trackfw-validate-deve-detectar-scripts-de-hook-ausentes-ou-desatualizados,
2489
+ // ML-2A: same rationale as credential_guard_script_integrity above — the script carries no
2490
+ // version marker, so this rule cannot tell legitimate drift from tampering.
2491
+ // git_branch_guard_hook_resolvable is deliberately absent from this map (falls through to
2492
+ // 'error'), mirroring credential_guard_hook_resolvable.
2493
+ git_branch_guard_script_integrity: 'warning',
1603
2494
  }
1604
2495
 
1605
2496
  // ROADMAP-2026-08-12-ancorar-rules-no-head-para-as-regras-de-credential-guard, ML-1A.
@@ -1719,6 +2610,122 @@ function saveBaseline(violations, warnings) {
1719
2610
  fs.writeFileSync(BASELINE_FILE, JSON.stringify(bf, null, 2), 'utf8')
1720
2611
  }
1721
2612
 
2613
+ function sha256Hex(buf) {
2614
+ return crypto.createHash('sha256').update(buf).digest('hex')
2615
+ }
2616
+
2617
+ // THIRDPARTY_ORIGIN é o valor de Claim.origin que marca um artefato do manifest como instalado
2618
+ // por `third-party install` (ADR-2026-08-15 D11).
2619
+ const THIRDPARTY_ORIGIN = 'thirdparty'
2620
+
2621
+ // validateThirdPartyArtifactHasProvenance implementa a regra "thirdparty_artifact_has_provenance"
2622
+ // (ADR-2026-08-15 D2) — a detecção real, ancorada em git, por trás do guardrail
2623
+ // TRACKFW_ORCHESTRATOR_SESSION. NUNCA faz fetch de rede (D6): lê só
2624
+ // .trackfw/integrations-manifest.json, .trackfw/thirdparty-provenance.json e
2625
+ // .trackfw/thirdparty-quarantine/<checksum>.json, todos já em disco (e, por convenção deste
2626
+ // projeto, versionados no repositório).
2627
+ //
2628
+ // Duas ramificações, ambas fatais (error — a regra está deliberadamente ausente de RULE_DEFAULTS):
2629
+ // 1. um artifact do manifest carrega um claim com origin === "thirdparty" mas
2630
+ // thirdparty-provenance.json não tem entrada chaveada por aquele destino;
2631
+ // 2. existe entrada de proveniência, mas seu installed_sha256 não pode ser reconciliado com o que
2632
+ // de fato está em disco no destino declarado.
2633
+ //
2634
+ // Sobre a ramificação 2 (ADR-2026-08-15 D2-bis, ML-3B) — checksum_sha256 é sha256 dos bytes BRUTOS
2635
+ // (D6), mas o arquivo instalado é sempre NormalizeThirdPartyContent(raw) — não é a função
2636
+ // identidade em geral, então comparar checksum_sha256 direto contra sha256(arquivo instalado)
2637
+ // produz falso-positivo em toda instalação legítima cujo conteúdo bruto não fosse já exatamente
2638
+ // TrimSpace+newline único. A resolução ML-3A usava o registro de quarentena como ponte entre os
2639
+ // dois domínios — correta, mas tornava um artefato de ESTÁGIO (.trackfw/thirdparty-quarantine/,
2640
+ // destinado a ser podado) dependência obrigatória de um gate PERMANENTE. D2-bis resolve isso com
2641
+ // um segundo campo na entrada de proveniência, installed_sha256 = sha256(bytes NORMALIZADOS),
2642
+ // calculado no momento do install pelo mesmo código que grava o arquivo (npm/src/commands/thirdparty.js).
2643
+ // checksum_sha256 permanece intocado, é a âncora de aprovação D8c. A ramificação 2 agora compara
2644
+ // sha256(arquivo instalado) diretamente contra entry.installed_sha256 — dois domínios já
2645
+ // normalizados, sem ponte via quarentena. A ausência da quarentena deixou de ser erro desta regra.
2646
+ function validateThirdPartyArtifactHasProvenance(cwd) {
2647
+ const root = cwd || process.cwd()
2648
+
2649
+ const manifestPath = path.join(root, '.trackfw', 'integrations-manifest.json')
2650
+ let manifest
2651
+ try {
2652
+ const raw = fs.readFileSync(manifestPath, 'utf8')
2653
+ manifest = JSON.parse(raw)
2654
+ } catch (err) {
2655
+ if (err.code === 'ENOENT') return []
2656
+ throw new Error(`thirdparty_artifact_has_provenance: read ${manifestPath}: ${err.message}`)
2657
+ }
2658
+
2659
+ const destinations = []
2660
+ for (const [destination, artifact] of Object.entries(manifest.artifacts || {})) {
2661
+ const claims = artifact.claims || []
2662
+ if (claims.some(claim => claim.origin === THIRDPARTY_ORIGIN)) destinations.push(destination)
2663
+ }
2664
+ if (destinations.length === 0) return []
2665
+ destinations.sort()
2666
+
2667
+ let prov
2668
+ try {
2669
+ prov = loadProvenance(root)
2670
+ } catch (err) {
2671
+ throw new Error(`thirdparty_artifact_has_provenance: ${err.message}`)
2672
+ }
2673
+
2674
+ const msgs = []
2675
+ for (const destination of destinations) {
2676
+ // Provenance keys are NOT the manifest's absolute destination —
2677
+ // verified empirically against the real install command
2678
+ // (npm/src/commands/thirdparty.js): verifyApproval/upsertProvenanceEntry
2679
+ // are called with the project-root-relative (or "~/"-prefixed,
2680
+ // global-scope) destination string BEFORE IntegrationManager.resolve()
2681
+ // joins it against root to produce the absolute manifest key. Every
2682
+ // claim reached here came from the PROJECT manifest, so its scope is
2683
+ // always "project" (a global-scope claim lives in the home manifest
2684
+ // instead, which this rule intentionally never reads). path.relative
2685
+ // inverts resolve()'s path.resolve(root, relative) exactly. Mirrors
2686
+ // internal/validator/validator_thirdparty_provenance.go.
2687
+ const provenanceKey = path.relative(root, destination)
2688
+ const entry = (prov.entries || {})[provenanceKey]
2689
+ if (!entry) {
2690
+ msgs.push(
2691
+ `thirdparty_artifact_has_provenance: "${destination}" is claimed as a third-party artifact but has no ` +
2692
+ 'entry in .trackfw/thirdparty-provenance.json — obtain a favorable hades-tf review and record an ' +
2693
+ 'approved provenance entry for this destination before this can pass validate (D2 branch i)'
2694
+ )
2695
+ continue
2696
+ }
2697
+
2698
+ let installed
2699
+ try {
2700
+ installed = fs.readFileSync(destination)
2701
+ } catch (err) {
2702
+ msgs.push(
2703
+ `thirdparty_artifact_has_provenance: "${destination}" is claimed as a third-party artifact with an ` +
2704
+ `approved provenance entry, but the destination file could not be read (${err.message})`
2705
+ )
2706
+ continue
2707
+ }
2708
+
2709
+ // entry.installed_sha256 may be `undefined` (key absent — e.g. an
2710
+ // approver-authored entry from before `install` ever ran, or a partial
2711
+ // install that failed between manager.Install and the installed_sha256
2712
+ // upsert). Coerce to '' so the comparison and the message text match
2713
+ // Go (zero-value "") and Python (.get(..., "")) byte-for-byte — do NOT
2714
+ // interpolate entry.installed_sha256 directly, that renders the
2715
+ // JS-only literal "undefined" into the violation message.
2716
+ const installedSHA256 = entry.installed_sha256 || ''
2717
+ if (sha256Hex(installed) !== installedSHA256) {
2718
+ msgs.push(
2719
+ `thirdparty_artifact_has_provenance: "${destination}" — installed content does not match ` +
2720
+ `installed_sha256 ${installedSHA256} recorded in .trackfw/thirdparty-provenance.json — the ` +
2721
+ 'artifact was modified after approval or installed outside the fetch/install flow (D2 branch ii, D2-bis)'
2722
+ )
2723
+ }
2724
+ }
2725
+
2726
+ return msgs
2727
+ }
2728
+
1722
2729
  // validateUnfiltered executa todas as validações e retorna { violations, warnings } sem ratchet.
1723
2730
  async function validateUnfiltered() {
1724
2731
  resetMeta()
@@ -1740,9 +2747,18 @@ async function validateUnfiltered() {
1740
2747
  applyRule('blocked_by_draft_adr', validateREQsNotBlockedByDraftADRs(), violations, warnings)
1741
2748
  applyRule('adr_accepted_when_req_done', validateADRAcceptedWhenREQDone(), violations, warnings)
1742
2749
  applyRule('note_orphan', validateNoteOrphan(), violations, warnings)
1743
- applyRule('credential_guard_hook_resolvable', validateCredentialGuardHookResolvable(), violations, warnings)
1744
- applyRule('credential_guard_script_integrity', validateCredentialGuardScriptIntegrity(), violations, warnings)
2750
+ applyRule('credential_guard_hook_resolvable', validateCredentialGuardHookResolvable().concat(validateCredentialGuardGlobalHookResolvable()), violations, warnings)
2751
+ applyRule('credential_guard_script_integrity', validateCredentialGuardScriptIntegrity().concat(validateCredentialGuardGlobalScriptIntegrity()), violations, warnings)
1745
2752
  applyRule('credential_guard_mode_downgrade', validateCredentialGuardModeDowngrade(), violations, warnings)
2753
+ // ROADMAP-2026-08-15-trackfw-validate-deve-detectar-scripts-de-hook-ausentes-ou-desatualizados,
2754
+ // ML-2A — port de internal/validator/validator.go's wiring das 4 regras/wrappers em
2755
+ // validator_git_branch_guard.go.
2756
+ applyRule('git_branch_guard_hook_resolvable', validateGitBranchGuardHookResolvable().concat(validateGitBranchGuardGlobalHookResolvable()), violations, warnings)
2757
+ applyRule('git_branch_guard_script_integrity', validateGitBranchGuardScriptIntegrity().concat(validateGitBranchGuardGlobalScriptIntegrity()), violations, warnings)
2758
+
2759
+ // ADR-2026-08-15-gate-de-duas-fases-..., ML-3A (D2): detecção ancorada em git por trás do
2760
+ // guardrail TRACKFW_ORCHESTRATOR_SESSION.
2761
+ applyRule('thirdparty_artifact_has_provenance', validateThirdPartyArtifactHasProvenance(), violations, warnings)
1746
2762
 
1747
2763
  // Regras configuráveis via applyRule (popula _itemMeta automaticamente)
1748
2764
  applyRule('req_has_adr', validateREQsHaveADR(), violations, warnings)
@@ -2012,4 +3028,21 @@ module.exports = {
2012
3028
  validateCredentialGuardModeDowngrade,
2013
3029
  extractCredentialGuardMode,
2014
3030
  CREDENTIAL_GUARD_SCRIPT_REFERENCE,
3031
+ // ROADMAP-2026-08-15-trackfw-validate-deve-detectar-scripts-de-hook-ausentes-ou-desatualizados,
3032
+ // ML-2A
3033
+ collectCommandsWithMarker,
3034
+ validateGuardHookResolvable,
3035
+ validateGitBranchGuardHookResolvable,
3036
+ validateGitBranchGuardScriptIntegrity,
3037
+ GIT_BRANCH_GUARD_SCRIPT_REFERENCE,
3038
+ CREDENTIAL_GUARD_GLOBAL_SCRIPT_REFERENCE,
3039
+ GLOBAL_GUARD_CONFIG_FILES,
3040
+ validateGuardGlobalHookResolvable,
3041
+ validateGuardGlobalScriptIntegrity,
3042
+ // ROADMAP-2026-08-15-instalacao-de-skills-de-terceiro-via-url-para-agentes-especialistas, ML-3A
3043
+ validateThirdPartyArtifactHasProvenance,
3044
+ validateCredentialGuardGlobalHookResolvable,
3045
+ validateCredentialGuardGlobalScriptIntegrity,
3046
+ validateGitBranchGuardGlobalHookResolvable,
3047
+ validateGitBranchGuardGlobalScriptIntegrity,
2015
3048
  }