trackfw 6.10.0 → 7.0.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.
@@ -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,6 +1155,13 @@ 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
@@ -1203,30 +1213,49 @@ function resolveCredentialGuardHookPath(raw, root) {
1203
1213
  return null
1204
1214
  }
1205
1215
 
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.
1216
+ // collectCommandsWithMarker percorre recursivamente um valor JSON já decodificado e coleta todo
1217
+ // valor-string que contém marker, independentemente do nome do campo que o contém.
1209
1218
  //
1210
1219
  // Os 6 formatos de hook usam campos diferentes para o comando: "command" (Claude/Codex/
1211
1220
  // Gemini/Cursor), "bash" (GitHub Copilot CLI), "action.command" (Kiro). Varrer por VALOR em vez
1212
1221
  // de por caminho de chave evita acoplar esta regra à forma exata de cada schema.
1213
- function collectCredentialGuardCommands(value, out) {
1222
+ //
1223
+ // Generalizado (ROADMAP-2026-08-15-trackfw-validate-deve-detectar-scripts-de-hook-ausentes-ou-
1224
+ // desatualizados, ML-2A, port de collectCommandsWithMarker em
1225
+ // internal/validator/validator_git_branch_guard.go) para aceitar qualquer marker — originalmente
1226
+ // collectCredentialGuardCommands, hardcoded para CREDENTIAL_GUARD_SCRIPT_MARKER; reusado agora
1227
+ // também para GIT_BRANCH_GUARD_SCRIPT_MARKER, sem duplicar a travessia recursiva.
1228
+ function collectCommandsWithMarker(value, marker, out) {
1214
1229
  if (typeof value === 'string') {
1215
- if (value.includes(CREDENTIAL_GUARD_SCRIPT_MARKER)) out.push(value)
1230
+ if (value.includes(marker)) out.push(value)
1216
1231
  return
1217
1232
  }
1218
1233
  if (Array.isArray(value)) {
1219
- for (const item of value) collectCredentialGuardCommands(item, out)
1234
+ for (const item of value) collectCommandsWithMarker(item, marker, out)
1220
1235
  return
1221
1236
  }
1222
1237
  if (value && typeof value === 'object') {
1223
- for (const key of Object.keys(value)) collectCredentialGuardCommands(value[key], out)
1238
+ for (const key of Object.keys(value)) collectCommandsWithMarker(value[key], marker, out)
1224
1239
  }
1225
1240
  }
1226
1241
 
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.
1242
+ // collectCredentialGuardCommands é o wrapper específico de credential-guard sobre
1243
+ // collectCommandsWithMarker — preservado para compatibilidade de assinatura (exportada
1244
+ // historicamente com 2 argumentos).
1245
+ function collectCredentialGuardCommands(value, out) {
1246
+ collectCommandsWithMarker(value, CREDENTIAL_GUARD_SCRIPT_MARKER, out)
1247
+ }
1248
+
1249
+ // validateGuardHookResolvable é a implementação genérica compartilhada pelas regras
1250
+ // "credential_guard_hook_resolvable" e "git_branch_guard_hook_resolvable": para cada arquivo de
1251
+ // hook de PROJETO que existir, extrai os comandos que referenciam scriptMarker, resolve o caminho
1252
+ // e verifica que o script existe e é executável.
1253
+ //
1254
+ // Generalizado a partir da antiga validateCredentialGuardHookResolvable (ROADMAP-2026-08-15-
1255
+ // trackfw-validate-deve-detectar-scripts-de-hook-ausentes-ou-desatualizados, ML-2A, port de
1256
+ // validateGuardHookResolvable em internal/validator/validator_credential_guard.go) — a lógica de
1257
+ // resolução de caminho por CLI é idêntica para os 2 scripts, só o marker e o texto da mensagem
1258
+ // mudam.
1230
1259
  //
1231
1260
  // Riscos de regressão mapeados no roadmap:
1232
1261
  // - A regra só avalia entradas que EXISTEM. Ausência de entrada de guard é estado legítimo
@@ -1234,7 +1263,12 @@ function collectCredentialGuardCommands(value, out) {
1234
1263
  // - Arquivo de hook ausente é pulado em silêncio.
1235
1264
  // - Arquivo de hook presente mas com JSON inválido é pulado em silêncio — validar a forma do
1236
1265
  // JSON não é escopo desta regra.
1237
- function validateCredentialGuardHookResolvable(cwd) {
1266
+ //
1267
+ // process.cwd() no Node já retorna o caminho FÍSICO (resolve symlinks via getcwd(3) diretamente),
1268
+ // diferente de os.Getwd() do Go — ver o comentário sobre EvalSymlinks em
1269
+ // internal/validator/validator_git_branch_guard.go's validateGuardHookResolvable equivalente
1270
+ // (validator_credential_guard.go). Nenhuma resolução extra de symlink é necessária aqui.
1271
+ function validateGuardHookResolvable(scriptMarker, cwd) {
1238
1272
  const root = cwd || process.cwd()
1239
1273
  const msgs = []
1240
1274
 
@@ -1256,7 +1290,7 @@ function validateCredentialGuardHookResolvable(cwd) {
1256
1290
  }
1257
1291
 
1258
1292
  const commands = []
1259
- collectCredentialGuardCommands(parsed, commands)
1293
+ collectCommandsWithMarker(parsed, scriptMarker, commands)
1260
1294
 
1261
1295
  const seen = new Set()
1262
1296
  for (const raw of commands) {
@@ -1274,9 +1308,9 @@ function validateCredentialGuardHookResolvable(cwd) {
1274
1308
  }
1275
1309
 
1276
1310
  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`)
1311
+ msgs.push(`${hf.relPath} (${hf.cli}) references ${scriptMarker} resolved to "${resolved}", but the script does not exist — run \`trackfw update\` to regenerate it`)
1278
1312
  } 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`)
1313
+ msgs.push(`${hf.relPath} (${hf.cli}) references ${scriptMarker} resolved to "${resolved}", but the script is not executable — run \`trackfw update\` to regenerate it`)
1280
1314
  }
1281
1315
  }
1282
1316
  }
@@ -1284,6 +1318,18 @@ function validateCredentialGuardHookResolvable(cwd) {
1284
1318
  return msgs
1285
1319
  }
1286
1320
 
1321
+ // validateCredentialGuardHookResolvable é a regra "credential_guard_hook_resolvable" — ver
1322
+ // validateGuardHookResolvable para a implementação compartilhada.
1323
+ function validateCredentialGuardHookResolvable(cwd) {
1324
+ return validateGuardHookResolvable(CREDENTIAL_GUARD_SCRIPT_MARKER, cwd)
1325
+ }
1326
+
1327
+ // validateGitBranchGuardHookResolvable é a regra "git_branch_guard_hook_resolvable" — ver
1328
+ // validateGuardHookResolvable para a implementação compartilhada.
1329
+ function validateGitBranchGuardHookResolvable(cwd) {
1330
+ return validateGuardHookResolvable(GIT_BRANCH_GUARD_SCRIPT_MARKER, cwd)
1331
+ }
1332
+
1287
1333
  function isGitWorktree(dir) {
1288
1334
  try {
1289
1335
  const out = gitOutput(dir, ['rev-parse', '--is-inside-work-tree'])
@@ -1486,6 +1532,511 @@ function validateCredentialGuardScriptIntegrity(cwd) {
1486
1532
  ]
1487
1533
  }
1488
1534
 
1535
+ // ROADMAP-2026-08-15-trackfw-validate-deve-detectar-scripts-de-hook-ausentes-ou-desatualizados,
1536
+ // ML-2A: port of internal/validator/validator_git_branch_guard.go — adds git-branch-guard
1537
+ // coverage to the two existing credential-guard checks (existence/executability via
1538
+ // validateGuardHookResolvable above, and content-drift integrity via
1539
+ // validateGitBranchGuardScriptIntegrity below), plus the GLOBAL-scope check that was missing for
1540
+ // BOTH guards before this ML (same gap the Go implementation closed first).
1541
+
1542
+ // GBG_REF_BACKTICK — validator-local copy of the same escaped-backtick helper hooks.js's
1543
+ // GBG_BACKTICK uses (npm/src/generators/hooks.js), to embed REASON's `` `cmd` `` inside the
1544
+ // script's double-quoted shell string without breaking this JS template literal.
1545
+ const GBG_REF_BACKTICK = '\\`'
1546
+
1547
+ // GIT_BRANCH_GUARD_SCRIPT_REFERENCE is a validator-local copy of the
1548
+ // scripts/trackfw-git-branch-guard.sh template composed in npm/src/generators/hooks.js
1549
+ // (GIT_BRANCH_GUARD_SCRIPT const). Not required directly from generators/hooks.js: like
1550
+ // CREDENTIAL_GUARD_SCRIPT_REFERENCE above, generateGitBranchGuardScript() writes to disk AND
1551
+ // console.log()s a success line on every call — calling it from inside `trackfw validate` would
1552
+ // leak a stray " ✓ scripts/trackfw-git-branch-guard.sh" line into validate's output. There is no
1553
+ // import-cycle constraint in Node (unlike Go's internal/validator ↔ internal/generators cycle —
1554
+ // see gitBranchGuardScriptReference's doc comment in
1555
+ // internal/validator/validator_git_branch_guard_reference.go) but the console.log side effect is
1556
+ // reason enough on its own to keep a local copy here, matching the choice already made for
1557
+ // credential-guard in this same file.
1558
+ //
1559
+ // Unlike CREDENTIAL_GUARD_SCRIPT_REFERENCE, GIT_BRANCH_GUARD_SCRIPT_REFERENCE is used VERBATIM for
1560
+ // both the project scope and the global scope (generateGitBranchGuardScript /
1561
+ // generateGlobalGitBranchGuardScript write the exact same GIT_BRANCH_GUARD_SCRIPT constant) — so
1562
+ // this single reference constant covers both git_branch_guard_script_integrity (project,
1563
+ // scripts/trackfw-git-branch-guard.sh) and the global integrity check
1564
+ // (~/.trackfw/scripts/trackfw-git-branch-guard.sh), no second reference constant needed.
1565
+ //
1566
+ // Drift between this copy and the real generator is caught by
1567
+ // "GIT_BRANCH_GUARD_SCRIPT_REFERENCE é byte-idêntico ao que generateGitBranchGuardScript emite"
1568
+ // (npm/tests/git_branch_guard.test.js): it regenerates the script via generateGitBranchGuardScript
1569
+ // into a temp dir and asserts byte-equality against this constant.
1570
+ const GIT_BRANCH_GUARD_SCRIPT_REFERENCE = `#!/usr/bin/env bash
1571
+ # trackfw git branch guard — bloqueia git commit/push/checkout -b brutos por subagente
1572
+ set -euo pipefail
1573
+ set -f
1574
+
1575
+ # --- 1. Obter o comando git bruto ------------------------------------------------------------
1576
+ if [ "$#" -gt 0 ]; then
1577
+ CMD_RAW="$*"
1578
+ else
1579
+ INPUT=$(cat 2>/dev/null || true)
1580
+ TRIMMED=$(printf '%s' "$INPUT" | sed -e 's/^[[:space:]]*//')
1581
+ case "$TRIMMED" in
1582
+ \\{*)
1583
+ CMD_RAW=""
1584
+ if command -v jq >/dev/null 2>&1; then
1585
+ CMD_RAW=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // .command // .tool_info.command_line // .hook_input.command // empty' 2>/dev/null || true)
1586
+ fi
1587
+ if [ -z "$CMD_RAW" ] || [ "$CMD_RAW" = "null" ]; then
1588
+ CMD_RAW=$(printf '%s' "$INPUT" | sed -n 's/.*"tool_input"[[:space:]]*:[[:space:]]*{[^}]*"command"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | head -1)
1589
+ fi
1590
+ if [ -z "$CMD_RAW" ]; then
1591
+ CMD_RAW=$(printf '%s' "$INPUT" | sed -n 's/.*"tool_info"[[:space:]]*:[[:space:]]*{[^}]*"command_line"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | head -1)
1592
+ fi
1593
+ if [ -z "$CMD_RAW" ]; then
1594
+ CMD_RAW=$(printf '%s' "$INPUT" | sed -n 's/.*"hook_input"[[:space:]]*:[[:space:]]*{[^}]*"command"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | head -1)
1595
+ fi
1596
+ if [ -z "$CMD_RAW" ]; then
1597
+ CMD_RAW=$(printf '%s' "$INPUT" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | head -1)
1598
+ fi
1599
+ ;;
1600
+ *)
1601
+ CMD_RAW="$INPUT"
1602
+ ;;
1603
+ esac
1604
+ fi
1605
+
1606
+ if [ -z "$CMD_RAW" ]; then
1607
+ CMD_RAW="\${TRACKFW_GIT_COMMAND:-}"
1608
+ fi
1609
+
1610
+ [ -n "$CMD_RAW" ] || exit 0
1611
+
1612
+ # --- 2. Casar contra "git (commit|push|checkout -b)", segmento por segmento -----------------
1613
+ # Cada segmento é um comando real (dividido por ; && || | e por quebra de linha). "git" só
1614
+ # conta se for o PRIMEIRO token do segmento (por basename, então /usr/bin/git também casa) —
1615
+ # nunca uma ocorrência solta em qualquer posição da string inteira. Isso evita: (a) o segundo
1616
+ # comando de uma cadeia escapar da checagem, (b) um path absoluto para o git escapar por
1617
+ # comparação de igualdade exata, e (c) texto de prosa (ex.: mensagem de commit mencionando
1618
+ # "git commit" no meio de uma frase) ser tratado como comando.
1619
+ match_subcommand() {
1620
+ normalized=$(printf '%s' "$1" | sed -e 's/&&/\\n/g' -e 's/||/\\n/g' -e 's/[;|]/\\n/g')
1621
+ while IFS= read -r seg; do
1622
+ seg_trimmed=$(printf '%s' "$seg" | sed -e 's/^[[:space:]]*//')
1623
+ [ -n "$seg_trimmed" ] || continue
1624
+
1625
+ set -- $seg_trimmed
1626
+ first="$1"
1627
+ base="\${first##*/}"
1628
+ [ "$base" = "git" ] || continue
1629
+ shift
1630
+
1631
+ sub=""
1632
+ while [ "$#" -gt 0 ]; do
1633
+ tok="$1"
1634
+ case "$tok" in
1635
+ -C|-c|--work-tree|--git-dir|--namespace)
1636
+ if [ "$#" -ge 2 ]; then shift 2; else shift; fi
1637
+ continue
1638
+ ;;
1639
+ -*)
1640
+ shift
1641
+ continue
1642
+ ;;
1643
+ *)
1644
+ sub="$tok"
1645
+ shift
1646
+ break
1647
+ ;;
1648
+ esac
1649
+ done
1650
+
1651
+ case "$sub" in
1652
+ commit)
1653
+ echo "commit"
1654
+ return 0
1655
+ ;;
1656
+ push)
1657
+ echo "push"
1658
+ return 0
1659
+ ;;
1660
+ checkout)
1661
+ if [ "\${1:-}" = "-b" ]; then
1662
+ echo "checkout-b"
1663
+ return 0
1664
+ fi
1665
+ ;;
1666
+ esac
1667
+ done <<EOF
1668
+ $normalized
1669
+ EOF
1670
+ return 1
1671
+ }
1672
+
1673
+ SUBCOMMAND=$(match_subcommand "$CMD_RAW") || exit 0
1674
+
1675
+ case "$SUBCOMMAND" in
1676
+ checkout-b)
1677
+ REASON="trackfw: git checkout -b bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_REF_BACKTICK + `. Ver CLAUDE.md §1."
1678
+ ;;
1679
+ commit)
1680
+ REASON="trackfw: git commit bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw commit -m '<mensagem>'` + GBG_REF_BACKTICK + `. Ver CLAUDE.md §1."
1681
+ ;;
1682
+ push)
1683
+ REASON="trackfw: git push bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw ship` + GBG_REF_BACKTICK + `. Ver CLAUDE.md §1."
1684
+ ;;
1685
+ *)
1686
+ exit 0
1687
+ ;;
1688
+ esac
1689
+
1690
+ printf '{"decision":"block","reason":"%s"}\\n' "$REASON"
1691
+ echo "$REASON" >&2
1692
+ exit 2
1693
+ `
1694
+
1695
+ // validateGitBranchGuardScriptIntegrity é a regra "git_branch_guard_script_integrity": compara
1696
+ // scripts/trackfw-git-branch-guard.sh em disco contra o template que esta versão do trackfw
1697
+ // geraria. Espelha validateCredentialGuardScriptIntegrity exatamente — mesmo contrato de silêncio
1698
+ // na ausência (existência é responsabilidade de git_branch_guard_hook_resolvable, não desta regra).
1699
+ function validateGitBranchGuardScriptIntegrity(cwd) {
1700
+ const root = cwd || process.cwd()
1701
+ const relPath = 'scripts/trackfw-git-branch-guard.sh'
1702
+ let content
1703
+ try {
1704
+ content = fs.readFileSync(path.join(root, relPath), 'utf8')
1705
+ } catch (e) {
1706
+ if (e.code === 'ENOENT') return []
1707
+ return [inspectionDiagnostic('git_branch_guard_script_integrity', relPath, e)]
1708
+ }
1709
+
1710
+ if (content === GIT_BRANCH_GUARD_SCRIPT_REFERENCE) return []
1711
+
1712
+ return [
1713
+ `${relPath} content diverges from the template this version of trackfw generates — ` +
1714
+ 'if you did not edit this file by hand, run `trackfw update` to regenerate it',
1715
+ ]
1716
+ }
1717
+
1718
+ // CREDENTIAL_GUARD_GLOBAL_SCRIPT_REFERENCE is a validator-local copy of the GLOBAL-scope
1719
+ // ~/.trackfw/scripts/trackfw-credential-guard.sh template composed in npm/src/generators/hooks.js
1720
+ // (GLOBAL_CREDENTIAL_GUARD_SCRIPT = CG_HEADER + CG_DETECTION_CORE + CG_GLOBAL_TAIL). This is a
1721
+ // DIFFERENT template than CREDENTIAL_GUARD_SCRIPT_REFERENCE (the project-scope variant): the
1722
+ // global variant omits the project-guard block ("no-op outside a trackfw.yaml project") and
1723
+ // defaults credential_guard.mode to "block" instead of "warn" — mirrors Go's
1724
+ // credentialGuardGlobalScriptReference
1725
+ // (internal/validator/validator_credential_guard_global_reference.go). Comparing the global
1726
+ // on-disk script against CREDENTIAL_GUARD_SCRIPT_REFERENCE (the project template) would be a
1727
+ // guaranteed false positive for every user with the global harness installed.
1728
+ const CREDENTIAL_GUARD_GLOBAL_SCRIPT_REFERENCE = `#!/usr/bin/env bash
1729
+ # trackfw credential guard — PreToolUse/PostToolUse hook
1730
+ set -euo pipefail
1731
+
1732
+ INPUT=$(cat)
1733
+
1734
+ JWT_PATTERN='eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+'
1735
+ AWS_KEY_PATTERN='AKIA[0-9A-Z]{16}'
1736
+
1737
+ MATCH=""
1738
+ if printf '%s' "$INPUT" | grep -qE "$JWT_PATTERN"; then
1739
+ MATCH="JWT"
1740
+ elif printf '%s' "$INPUT" | grep -qE "$AWS_KEY_PATTERN"; then
1741
+ MATCH="AWS access key"
1742
+ fi
1743
+
1744
+ # The raw payload is JSON: any double quote inside the underlying tool_input.command is
1745
+ # escaped as \\" -- unescape those before scanning for redirect targets, or a quoted target
1746
+ # like "$TMPFILE" is seen as starting with a literal backslash instead of a variable
1747
+ # reference.
1748
+ RAW=$(printf '%s' "$INPUT" | sed 's/\\\\"/"/g')
1749
+
1750
+ # Ignore matches that are only ever written to an ephemeral destination
1751
+ # (mktemp-derived path or /dev/null). A match with no redirect at all
1752
+ # (printed to stdout, e.g.) or redirected to a plain file path still
1753
+ # alerts -- that is the incident this hook guards against.
1754
+ is_ephemeral_target() {
1755
+ local target
1756
+ target=$(printf '%s' "$1" | tr -d "\\"'" | sed -E 's/[},]+$//')
1757
+ case "$target" in
1758
+ /dev/null) return 0 ;;
1759
+ *mktemp*) return 0 ;;
1760
+ esac
1761
+ if printf '%s' "$target" | grep -qE '^\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?$'; then
1762
+ local varname pattern
1763
+ varname=$(printf '%s' "$target" | sed -E 's/^\\$\\{?([A-Za-z_][A-Za-z0-9_]*)\\}?$/\\1/')
1764
+ pattern="*\${varname}="'$(mktemp'"*"
1765
+ case "$RAW" in
1766
+ $pattern) return 0 ;;
1767
+ esac
1768
+ fi
1769
+ return 1
1770
+ }
1771
+
1772
+ REDIRECTS=$(printf '%s' "$RAW" | grep -oE '[0-9]?>>?[[:space:]]*[^[:space:]|&;,:]+' || true)
1773
+
1774
+ # Second detection layer: only runs when the payload scan above found nothing -- keeps the common
1775
+ # case (match already found) cheap and avoids reading files unnecessarily. Files above the size cap
1776
+ # are skipped silently.
1777
+ scan_file_for_pattern() {
1778
+ local path size
1779
+ path=$(printf '%s' "$1" | tr -d "\\"'" | sed -E 's/[},]+$//')
1780
+ [ -n "$path" ] && [ -f "$path" ] || return 1
1781
+ size=$(wc -c < "$path" 2>/dev/null | tr -d '[:space:]')
1782
+ size=\${size:-0}
1783
+ [ "$size" -lt 1048576 ] || return 1
1784
+ if grep -qE "$JWT_PATTERN" "$path" 2>/dev/null; then
1785
+ MATCH="JWT"
1786
+ return 0
1787
+ fi
1788
+ if grep -qE "$AWS_KEY_PATTERN" "$path" 2>/dev/null; then
1789
+ MATCH="AWS access key"
1790
+ return 0
1791
+ fi
1792
+ return 1
1793
+ }
1794
+
1795
+ if [ -z "$MATCH" ] && [ -n "$REDIRECTS" ]; then
1796
+ while IFS= read -r line; do
1797
+ if [ -z "$line" ]; then
1798
+ continue
1799
+ fi
1800
+ target=$(printf '%s' "$line" | sed -E 's/^[0-9]?>>?[[:space:]]*//')
1801
+ if ! is_ephemeral_target "$target"; then
1802
+ scan_file_for_pattern "$target" && break
1803
+ fi
1804
+ done <<< "$REDIRECTS"
1805
+ fi
1806
+
1807
+ if [ -z "$MATCH" ]; then
1808
+ CMD_LINE=$(printf '%s' "$RAW" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')
1809
+ if [ -n "$CMD_LINE" ]; then
1810
+ set -- $CMD_LINE
1811
+ cmd_name="\${1:-}"
1812
+ case "$cmd_name" in
1813
+ cat|head|tail|jq|grep)
1814
+ shift
1815
+ for token in "$@"; do
1816
+ scan_file_for_pattern "$token" && break
1817
+ done
1818
+ ;;
1819
+ esac
1820
+ fi
1821
+ fi
1822
+
1823
+ [ -n "$MATCH" ] || exit 0
1824
+
1825
+ HAS_REDIRECT=0
1826
+ EXEMPT=1
1827
+ if [ -n "$REDIRECTS" ]; then
1828
+ while IFS= read -r line; do
1829
+ if [ -z "$line" ]; then
1830
+ continue
1831
+ fi
1832
+ HAS_REDIRECT=1
1833
+ target=$(printf '%s' "$line" | sed -E 's/^[0-9]?>>?[[:space:]]*//')
1834
+ if ! is_ephemeral_target "$target"; then
1835
+ EXEMPT=0
1836
+ fi
1837
+ done <<< "$REDIRECTS"
1838
+ fi
1839
+
1840
+ if [ "$HAS_REDIRECT" -eq 1 ] && [ "$EXEMPT" -eq 1 ]; then
1841
+ exit 0
1842
+ fi
1843
+
1844
+ DEFAULT_MODE="block"
1845
+ 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)
1846
+ case "$MODE" in
1847
+ warn|block) ;;
1848
+ *) MODE="$DEFAULT_MODE" ;;
1849
+ esac
1850
+
1851
+ if [ "$MODE" = "block" ]; then
1852
+ echo "trackfw-credential-guard: blocked - possible $MATCH detected in tool payload." >&2
1853
+ exit 2
1854
+ fi
1855
+
1856
+ echo "trackfw-credential-guard: warning - possible $MATCH detected in tool payload." >&2
1857
+
1858
+ ROADMAP_DIR="docs/roadmaps"
1859
+ if [ ! -d "$ROADMAP_DIR" ]; then
1860
+ exit 0
1861
+ fi
1862
+
1863
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1864
+ MSG="Possible $MATCH detected in tool payload - review before materializing credentials in plain text."
1865
+ MSG_ESC=$(echo "$MSG" | tr -d '\\000-\\037' | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
1866
+
1867
+ mkdir -p "$ROADMAP_DIR"
1868
+ printf '{"tool":"credential-guard","message":"%s","level":"action_required","timestamp":"%s"}\\n' \\
1869
+ "$MSG_ESC" \\
1870
+ "$TIMESTAMP" > "$ROADMAP_DIR/.trackfw-credential-guard.json"
1871
+
1872
+ exit 0
1873
+ `
1874
+
1875
+ // globalGuardConfigFile associates a GLOBAL (per-CLI, $HOME-rooted) hook/settings config file with
1876
+ // the CLI that consumes it, for the global-scope guard checks below. Distinct from
1877
+ // CREDENTIAL_GUARD_HOOK_FILES, whose relPath is rooted at the PROJECT root, not $HOME.
1878
+ //
1879
+ // GLOBAL_GUARD_CONFIG_FILES is the closed list of GLOBAL hook/settings files `trackfw update
1880
+ // harness` can write a guard entry into — the global-scope counterpart of
1881
+ // CREDENTIAL_GUARD_HOOK_FILES. Paths and CLI labels ported from Go's globalGuardConfigFiles
1882
+ // (internal/validator/validator_git_branch_guard.go) — note this list DIFFERS from
1883
+ // CREDENTIAL_GUARD_HOOK_FILES for Copilot and Kiro (global scope uses .copilot/settings.json and
1884
+ // .kiro/hooks/trackfw-credential-guard.json, not the project-scope attention-hook paths).
1885
+ const GLOBAL_GUARD_CONFIG_FILES = [
1886
+ { relPath: '.claude/settings.json', cli: 'Claude Code' },
1887
+ { relPath: '.codex/hooks.json', cli: 'Codex CLI' },
1888
+ { relPath: '.gemini/settings.json', cli: 'Gemini CLI' },
1889
+ { relPath: '.cursor/hooks.json', cli: 'Cursor' },
1890
+ { relPath: '.copilot/settings.json', cli: 'GitHub Copilot CLI' },
1891
+ { relPath: '.kiro/hooks/trackfw-credential-guard.json', cli: 'Kiro' },
1892
+ ]
1893
+
1894
+ // validateGuardGlobalHookResolvable is the GLOBAL-scope counterpart of
1895
+ // validateGuardHookResolvable: for each of the 6 GLOBAL_GUARD_CONFIG_FILES that exists AND
1896
+ // references scriptMarker, verifies the referenced script exists and is executable. Port of Go's
1897
+ // validateGuardGlobalHookResolvable (internal/validator/validator_git_branch_guard.go) — see that
1898
+ // function's doc comment for the full trigger-condition and fail-open rationale.
1899
+ //
1900
+ // Global entries are written by npm/src/generators (harnessCredentialGuardTarget*-equivalent code
1901
+ // in agentfiles-equivalent generators) as fully resolved absolute paths, never a placeholder like
1902
+ // $CLAUDE_PROJECT_DIR — so, unlike the project-scope resolveCredentialGuardHookPath, no
1903
+ // prefix-stripping is needed here: any matched command that is NOT already an absolute path is
1904
+ // skipped (never treated as a violation).
1905
+ //
1906
+ // Fail-open: unresolvable $HOME, unreadable file, or invalid JSON all skip that file in silence —
1907
+ // same contract validateGuardHookResolvable already has for project-scope files.
1908
+ function validateGuardGlobalHookResolvable(scriptMarker) {
1909
+ const home = os.homedir()
1910
+ if (!home) return []
1911
+
1912
+ const msgs = []
1913
+ for (const gf of GLOBAL_GUARD_CONFIG_FILES) {
1914
+ const fullPath = path.join(home, gf.relPath)
1915
+ let content
1916
+ try {
1917
+ content = fs.readFileSync(fullPath, 'utf8')
1918
+ } catch (e) {
1919
+ if (e.code === 'ENOENT') continue
1920
+ continue
1921
+ }
1922
+
1923
+ let parsed
1924
+ try {
1925
+ parsed = JSON.parse(content)
1926
+ } catch (_) {
1927
+ continue
1928
+ }
1929
+
1930
+ const commands = []
1931
+ collectCommandsWithMarker(parsed, scriptMarker, commands)
1932
+
1933
+ const seen = new Set()
1934
+ for (const raw of commands) {
1935
+ if (seen.has(raw)) continue
1936
+ seen.add(raw)
1937
+
1938
+ if (!path.isAbsolute(raw)) continue
1939
+
1940
+ let stat = null
1941
+ try {
1942
+ stat = fs.statSync(raw)
1943
+ } catch (_) {
1944
+ stat = null
1945
+ }
1946
+
1947
+ if (!stat) {
1948
+ msgs.push(`~/${gf.relPath} (${gf.cli}, global scope) references ${scriptMarker} resolved to "${raw}", but the script does not exist — run \`trackfw update harness\` to regenerate it`)
1949
+ } else if ((stat.mode & 0o111) === 0) {
1950
+ msgs.push(`~/${gf.relPath} (${gf.cli}, global scope) references ${scriptMarker} resolved to "${raw}", but the script is not executable — run \`trackfw update harness\` to regenerate it`)
1951
+ }
1952
+ }
1953
+ }
1954
+
1955
+ return msgs
1956
+ }
1957
+
1958
+ // validateGuardGlobalScriptIntegrity is the GLOBAL-scope counterpart of
1959
+ // validateCredentialGuardScriptIntegrity/validateGitBranchGuardScriptIntegrity: for each of the 6
1960
+ // GLOBAL_GUARD_CONFIG_FILES that references scriptMarker, verifies the referenced script's content
1961
+ // matches referenceContent byte-for-byte. Port of Go's validateGuardGlobalScriptIntegrity
1962
+ // (internal/validator/validator_git_branch_guard.go) — same trigger condition and fail-open
1963
+ // contract as validateGuardGlobalHookResolvable above; does not re-validate existence/
1964
+ // executability, only content, matching the project-scope split between *_hook_resolvable and
1965
+ // *_script_integrity.
1966
+ function validateGuardGlobalScriptIntegrity(scriptMarker, referenceContent) {
1967
+ const home = os.homedir()
1968
+ if (!home) return []
1969
+
1970
+ const msgs = []
1971
+ for (const gf of GLOBAL_GUARD_CONFIG_FILES) {
1972
+ const fullPath = path.join(home, gf.relPath)
1973
+ let content
1974
+ try {
1975
+ content = fs.readFileSync(fullPath, 'utf8')
1976
+ } catch (e) {
1977
+ if (e.code === 'ENOENT') continue
1978
+ continue
1979
+ }
1980
+
1981
+ let parsed
1982
+ try {
1983
+ parsed = JSON.parse(content)
1984
+ } catch (_) {
1985
+ continue
1986
+ }
1987
+
1988
+ const commands = []
1989
+ collectCommandsWithMarker(parsed, scriptMarker, commands)
1990
+
1991
+ const seen = new Set()
1992
+ for (const raw of commands) {
1993
+ if (seen.has(raw)) continue
1994
+ seen.add(raw)
1995
+
1996
+ if (!path.isAbsolute(raw)) continue
1997
+
1998
+ let scriptContent
1999
+ try {
2000
+ scriptContent = fs.readFileSync(raw, 'utf8')
2001
+ } catch (_) {
2002
+ // Existence/readability is validateGuardGlobalHookResolvable's job — do not double-report
2003
+ // the same underlying condition under two rule names.
2004
+ continue
2005
+ }
2006
+
2007
+ if (scriptContent === referenceContent) continue
2008
+
2009
+ msgs.push(`${raw} (global scope, referenced from ~/${gf.relPath}, ${gf.cli}) 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`)
2010
+ }
2011
+ }
2012
+
2013
+ return msgs
2014
+ }
2015
+
2016
+ // validateCredentialGuardGlobalHookResolvable / validateCredentialGuardGlobalScriptIntegrity /
2017
+ // validateGitBranchGuardGlobalHookResolvable / validateGitBranchGuardGlobalScriptIntegrity are the
2018
+ // 4 thin wrappers wired in validateUnfiltered below — each folds its messages into the SAME rule
2019
+ // name as its project-scope counterpart (credential_guard_hook_resolvable,
2020
+ // credential_guard_script_integrity, git_branch_guard_hook_resolvable,
2021
+ // git_branch_guard_script_integrity respectively), so no new rules: entries in trackfw.yaml are
2022
+ // needed. Port of the 4 equivalent wrappers in
2023
+ // internal/validator/validator_git_branch_guard.go.
2024
+ function validateCredentialGuardGlobalHookResolvable() {
2025
+ return validateGuardGlobalHookResolvable(CREDENTIAL_GUARD_SCRIPT_MARKER)
2026
+ }
2027
+
2028
+ function validateCredentialGuardGlobalScriptIntegrity() {
2029
+ return validateGuardGlobalScriptIntegrity(CREDENTIAL_GUARD_SCRIPT_MARKER, CREDENTIAL_GUARD_GLOBAL_SCRIPT_REFERENCE)
2030
+ }
2031
+
2032
+ function validateGitBranchGuardGlobalHookResolvable() {
2033
+ return validateGuardGlobalHookResolvable(GIT_BRANCH_GUARD_SCRIPT_MARKER)
2034
+ }
2035
+
2036
+ function validateGitBranchGuardGlobalScriptIntegrity() {
2037
+ return validateGuardGlobalScriptIntegrity(GIT_BRANCH_GUARD_SCRIPT_MARKER, GIT_BRANCH_GUARD_SCRIPT_REFERENCE)
2038
+ }
2039
+
1489
2040
  // CREDENTIAL_GUARD_MODE_LOOKBEHIND_LINES mirrors the shell script's own resolution of
1490
2041
  // credential_guard.mode (`grep -A 5 '^credential_guard:'`): the mode key is found on the matched
1491
2042
  // line or within the 5 lines following it.
@@ -1600,6 +2151,12 @@ const RULE_DEFAULTS = {
1600
2151
  // credential_guard_mode_downgrade is deliberately absent: it falls through to ruleSeverity's
1601
2152
  // 'error' default.
1602
2153
  credential_guard_script_integrity: 'warning',
2154
+ // ROADMAP-2026-08-15-trackfw-validate-deve-detectar-scripts-de-hook-ausentes-ou-desatualizados,
2155
+ // ML-2A: same rationale as credential_guard_script_integrity above — the script carries no
2156
+ // version marker, so this rule cannot tell legitimate drift from tampering.
2157
+ // git_branch_guard_hook_resolvable is deliberately absent from this map (falls through to
2158
+ // 'error'), mirroring credential_guard_hook_resolvable.
2159
+ git_branch_guard_script_integrity: 'warning',
1603
2160
  }
1604
2161
 
1605
2162
  // ROADMAP-2026-08-12-ancorar-rules-no-head-para-as-regras-de-credential-guard, ML-1A.
@@ -1719,6 +2276,122 @@ function saveBaseline(violations, warnings) {
1719
2276
  fs.writeFileSync(BASELINE_FILE, JSON.stringify(bf, null, 2), 'utf8')
1720
2277
  }
1721
2278
 
2279
+ function sha256Hex(buf) {
2280
+ return crypto.createHash('sha256').update(buf).digest('hex')
2281
+ }
2282
+
2283
+ // THIRDPARTY_ORIGIN é o valor de Claim.origin que marca um artefato do manifest como instalado
2284
+ // por `third-party install` (ADR-2026-08-15 D11).
2285
+ const THIRDPARTY_ORIGIN = 'thirdparty'
2286
+
2287
+ // validateThirdPartyArtifactHasProvenance implementa a regra "thirdparty_artifact_has_provenance"
2288
+ // (ADR-2026-08-15 D2) — a detecção real, ancorada em git, por trás do guardrail
2289
+ // TRACKFW_ORCHESTRATOR_SESSION. NUNCA faz fetch de rede (D6): lê só
2290
+ // .trackfw/integrations-manifest.json, .trackfw/thirdparty-provenance.json e
2291
+ // .trackfw/thirdparty-quarantine/<checksum>.json, todos já em disco (e, por convenção deste
2292
+ // projeto, versionados no repositório).
2293
+ //
2294
+ // Duas ramificações, ambas fatais (error — a regra está deliberadamente ausente de RULE_DEFAULTS):
2295
+ // 1. um artifact do manifest carrega um claim com origin === "thirdparty" mas
2296
+ // thirdparty-provenance.json não tem entrada chaveada por aquele destino;
2297
+ // 2. existe entrada de proveniência, mas seu installed_sha256 não pode ser reconciliado com o que
2298
+ // de fato está em disco no destino declarado.
2299
+ //
2300
+ // Sobre a ramificação 2 (ADR-2026-08-15 D2-bis, ML-3B) — checksum_sha256 é sha256 dos bytes BRUTOS
2301
+ // (D6), mas o arquivo instalado é sempre NormalizeThirdPartyContent(raw) — não é a função
2302
+ // identidade em geral, então comparar checksum_sha256 direto contra sha256(arquivo instalado)
2303
+ // produz falso-positivo em toda instalação legítima cujo conteúdo bruto não fosse já exatamente
2304
+ // TrimSpace+newline único. A resolução ML-3A usava o registro de quarentena como ponte entre os
2305
+ // dois domínios — correta, mas tornava um artefato de ESTÁGIO (.trackfw/thirdparty-quarantine/,
2306
+ // destinado a ser podado) dependência obrigatória de um gate PERMANENTE. D2-bis resolve isso com
2307
+ // um segundo campo na entrada de proveniência, installed_sha256 = sha256(bytes NORMALIZADOS),
2308
+ // calculado no momento do install pelo mesmo código que grava o arquivo (npm/src/commands/thirdparty.js).
2309
+ // checksum_sha256 permanece intocado, é a âncora de aprovação D8c. A ramificação 2 agora compara
2310
+ // sha256(arquivo instalado) diretamente contra entry.installed_sha256 — dois domínios já
2311
+ // normalizados, sem ponte via quarentena. A ausência da quarentena deixou de ser erro desta regra.
2312
+ function validateThirdPartyArtifactHasProvenance(cwd) {
2313
+ const root = cwd || process.cwd()
2314
+
2315
+ const manifestPath = path.join(root, '.trackfw', 'integrations-manifest.json')
2316
+ let manifest
2317
+ try {
2318
+ const raw = fs.readFileSync(manifestPath, 'utf8')
2319
+ manifest = JSON.parse(raw)
2320
+ } catch (err) {
2321
+ if (err.code === 'ENOENT') return []
2322
+ throw new Error(`thirdparty_artifact_has_provenance: read ${manifestPath}: ${err.message}`)
2323
+ }
2324
+
2325
+ const destinations = []
2326
+ for (const [destination, artifact] of Object.entries(manifest.artifacts || {})) {
2327
+ const claims = artifact.claims || []
2328
+ if (claims.some(claim => claim.origin === THIRDPARTY_ORIGIN)) destinations.push(destination)
2329
+ }
2330
+ if (destinations.length === 0) return []
2331
+ destinations.sort()
2332
+
2333
+ let prov
2334
+ try {
2335
+ prov = loadProvenance(root)
2336
+ } catch (err) {
2337
+ throw new Error(`thirdparty_artifact_has_provenance: ${err.message}`)
2338
+ }
2339
+
2340
+ const msgs = []
2341
+ for (const destination of destinations) {
2342
+ // Provenance keys are NOT the manifest's absolute destination —
2343
+ // verified empirically against the real install command
2344
+ // (npm/src/commands/thirdparty.js): verifyApproval/upsertProvenanceEntry
2345
+ // are called with the project-root-relative (or "~/"-prefixed,
2346
+ // global-scope) destination string BEFORE IntegrationManager.resolve()
2347
+ // joins it against root to produce the absolute manifest key. Every
2348
+ // claim reached here came from the PROJECT manifest, so its scope is
2349
+ // always "project" (a global-scope claim lives in the home manifest
2350
+ // instead, which this rule intentionally never reads). path.relative
2351
+ // inverts resolve()'s path.resolve(root, relative) exactly. Mirrors
2352
+ // internal/validator/validator_thirdparty_provenance.go.
2353
+ const provenanceKey = path.relative(root, destination)
2354
+ const entry = (prov.entries || {})[provenanceKey]
2355
+ if (!entry) {
2356
+ msgs.push(
2357
+ `thirdparty_artifact_has_provenance: "${destination}" is claimed as a third-party artifact but has no ` +
2358
+ 'entry in .trackfw/thirdparty-provenance.json — obtain a favorable hades-tf review and record an ' +
2359
+ 'approved provenance entry for this destination before this can pass validate (D2 branch i)'
2360
+ )
2361
+ continue
2362
+ }
2363
+
2364
+ let installed
2365
+ try {
2366
+ installed = fs.readFileSync(destination)
2367
+ } catch (err) {
2368
+ msgs.push(
2369
+ `thirdparty_artifact_has_provenance: "${destination}" is claimed as a third-party artifact with an ` +
2370
+ `approved provenance entry, but the destination file could not be read (${err.message})`
2371
+ )
2372
+ continue
2373
+ }
2374
+
2375
+ // entry.installed_sha256 may be `undefined` (key absent — e.g. an
2376
+ // approver-authored entry from before `install` ever ran, or a partial
2377
+ // install that failed between manager.Install and the installed_sha256
2378
+ // upsert). Coerce to '' so the comparison and the message text match
2379
+ // Go (zero-value "") and Python (.get(..., "")) byte-for-byte — do NOT
2380
+ // interpolate entry.installed_sha256 directly, that renders the
2381
+ // JS-only literal "undefined" into the violation message.
2382
+ const installedSHA256 = entry.installed_sha256 || ''
2383
+ if (sha256Hex(installed) !== installedSHA256) {
2384
+ msgs.push(
2385
+ `thirdparty_artifact_has_provenance: "${destination}" — installed content does not match ` +
2386
+ `installed_sha256 ${installedSHA256} recorded in .trackfw/thirdparty-provenance.json — the ` +
2387
+ 'artifact was modified after approval or installed outside the fetch/install flow (D2 branch ii, D2-bis)'
2388
+ )
2389
+ }
2390
+ }
2391
+
2392
+ return msgs
2393
+ }
2394
+
1722
2395
  // validateUnfiltered executa todas as validações e retorna { violations, warnings } sem ratchet.
1723
2396
  async function validateUnfiltered() {
1724
2397
  resetMeta()
@@ -1740,9 +2413,18 @@ async function validateUnfiltered() {
1740
2413
  applyRule('blocked_by_draft_adr', validateREQsNotBlockedByDraftADRs(), violations, warnings)
1741
2414
  applyRule('adr_accepted_when_req_done', validateADRAcceptedWhenREQDone(), violations, warnings)
1742
2415
  applyRule('note_orphan', validateNoteOrphan(), violations, warnings)
1743
- applyRule('credential_guard_hook_resolvable', validateCredentialGuardHookResolvable(), violations, warnings)
1744
- applyRule('credential_guard_script_integrity', validateCredentialGuardScriptIntegrity(), violations, warnings)
2416
+ applyRule('credential_guard_hook_resolvable', validateCredentialGuardHookResolvable().concat(validateCredentialGuardGlobalHookResolvable()), violations, warnings)
2417
+ applyRule('credential_guard_script_integrity', validateCredentialGuardScriptIntegrity().concat(validateCredentialGuardGlobalScriptIntegrity()), violations, warnings)
1745
2418
  applyRule('credential_guard_mode_downgrade', validateCredentialGuardModeDowngrade(), violations, warnings)
2419
+ // ROADMAP-2026-08-15-trackfw-validate-deve-detectar-scripts-de-hook-ausentes-ou-desatualizados,
2420
+ // ML-2A — port de internal/validator/validator.go's wiring das 4 regras/wrappers em
2421
+ // validator_git_branch_guard.go.
2422
+ applyRule('git_branch_guard_hook_resolvable', validateGitBranchGuardHookResolvable().concat(validateGitBranchGuardGlobalHookResolvable()), violations, warnings)
2423
+ applyRule('git_branch_guard_script_integrity', validateGitBranchGuardScriptIntegrity().concat(validateGitBranchGuardGlobalScriptIntegrity()), violations, warnings)
2424
+
2425
+ // ADR-2026-08-15-gate-de-duas-fases-..., ML-3A (D2): detecção ancorada em git por trás do
2426
+ // guardrail TRACKFW_ORCHESTRATOR_SESSION.
2427
+ applyRule('thirdparty_artifact_has_provenance', validateThirdPartyArtifactHasProvenance(), violations, warnings)
1746
2428
 
1747
2429
  // Regras configuráveis via applyRule (popula _itemMeta automaticamente)
1748
2430
  applyRule('req_has_adr', validateREQsHaveADR(), violations, warnings)
@@ -2012,4 +2694,21 @@ module.exports = {
2012
2694
  validateCredentialGuardModeDowngrade,
2013
2695
  extractCredentialGuardMode,
2014
2696
  CREDENTIAL_GUARD_SCRIPT_REFERENCE,
2697
+ // ROADMAP-2026-08-15-trackfw-validate-deve-detectar-scripts-de-hook-ausentes-ou-desatualizados,
2698
+ // ML-2A
2699
+ collectCommandsWithMarker,
2700
+ validateGuardHookResolvable,
2701
+ validateGitBranchGuardHookResolvable,
2702
+ validateGitBranchGuardScriptIntegrity,
2703
+ GIT_BRANCH_GUARD_SCRIPT_REFERENCE,
2704
+ CREDENTIAL_GUARD_GLOBAL_SCRIPT_REFERENCE,
2705
+ GLOBAL_GUARD_CONFIG_FILES,
2706
+ validateGuardGlobalHookResolvable,
2707
+ validateGuardGlobalScriptIntegrity,
2708
+ // ROADMAP-2026-08-15-instalacao-de-skills-de-terceiro-via-url-para-agentes-especialistas, ML-3A
2709
+ validateThirdPartyArtifactHasProvenance,
2710
+ validateCredentialGuardGlobalHookResolvable,
2711
+ validateCredentialGuardGlobalScriptIntegrity,
2712
+ validateGitBranchGuardGlobalHookResolvable,
2713
+ validateGitBranchGuardGlobalScriptIntegrity,
2015
2714
  }