trackfw 7.0.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.
- package/bin/trackfw +38 -1
- package/package.json +1 -1
- package/src/branch/prune.js +450 -0
- package/src/commands/branch.js +47 -0
- package/src/commands/discover.js +6 -0
- package/src/commands/doctor.js +32 -0
- package/src/commands/index.js +1 -0
- package/src/commands/integrations.js +16 -1
- package/src/commands/serve.js +52 -4
- package/src/commands/update-harness.js +317 -11
- package/src/generators/hooks.js +478 -36
- package/src/i18n/locales/en-US.json +0 -3
- package/src/i18n/locales/es-ES.json +0 -3
- package/src/i18n/locales/pt-BR.json +0 -3
- package/src/integrations/doctor.js +0 -0
- package/src/integrations/manager.js +118 -17
- package/src/lib/fatal-error.js +64 -0
- package/src/ship/runner.js +52 -75
- package/src/validator/index.js +426 -92
package/src/validator/index.js
CHANGED
|
@@ -1167,13 +1167,20 @@ const GIT_BRANCH_GUARD_SCRIPT_MARKER = 'trackfw-git-branch-guard.sh'
|
|
|
1167
1167
|
// -fail-open-do-credential-guard, ML-1A). Hooks de escopo GLOBAL (~/.trackfw/..., trackfw update
|
|
1168
1168
|
// harness) ficam fora — caso distinto, fora do repositório do usuário, e a checagem de dedup
|
|
1169
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.
|
|
1170
1177
|
const CREDENTIAL_GUARD_HOOK_FILES = [
|
|
1171
|
-
{ relPath: '.claude/settings.json', cli: 'Claude Code' },
|
|
1172
|
-
{ relPath: '.codex/hooks.json', cli: 'Codex CLI' },
|
|
1173
|
-
{ relPath: '.gemini/settings.json', cli: 'Gemini CLI' },
|
|
1174
|
-
{ relPath: '.cursor/hooks.json', cli: 'Cursor' },
|
|
1175
|
-
{ relPath: '.github/hooks/trackfw-attention.json', cli: 'GitHub Copilot CLI' },
|
|
1176
|
-
{ 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 },
|
|
1177
1184
|
]
|
|
1178
1185
|
|
|
1179
1186
|
// resolveCredentialGuardHookPath resolve o valor bruto de um comando de hook (string extraída do
|
|
@@ -1214,11 +1221,18 @@ function resolveCredentialGuardHookPath(raw, root) {
|
|
|
1214
1221
|
}
|
|
1215
1222
|
|
|
1216
1223
|
// 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
|
|
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}.
|
|
1218
1229
|
//
|
|
1219
1230
|
// Os 6 formatos de hook usam campos diferentes para o comando: "command" (Claude/Codex/
|
|
1220
1231
|
// Gemini/Cursor), "bash" (GitHub Copilot CLI), "action.command" (Kiro). Varrer por VALOR em vez
|
|
1221
|
-
// de por caminho de chave evita acoplar esta regra à forma exata de cada schema.
|
|
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.
|
|
1222
1236
|
//
|
|
1223
1237
|
// Generalizado (ROADMAP-2026-08-15-trackfw-validate-deve-detectar-scripts-de-hook-ausentes-ou-
|
|
1224
1238
|
// desatualizados, ML-2A, port de collectCommandsWithMarker em
|
|
@@ -1227,7 +1241,10 @@ function resolveCredentialGuardHookPath(raw, root) {
|
|
|
1227
1241
|
// também para GIT_BRANCH_GUARD_SCRIPT_MARKER, sem duplicar a travessia recursiva.
|
|
1228
1242
|
function collectCommandsWithMarker(value, marker, out) {
|
|
1229
1243
|
if (typeof value === 'string') {
|
|
1230
|
-
|
|
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 })
|
|
1231
1248
|
return
|
|
1232
1249
|
}
|
|
1233
1250
|
if (Array.isArray(value)) {
|
|
@@ -1235,7 +1252,15 @@ function collectCommandsWithMarker(value, marker, out) {
|
|
|
1235
1252
|
return
|
|
1236
1253
|
}
|
|
1237
1254
|
if (value && typeof value === 'object') {
|
|
1238
|
-
|
|
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
|
+
}
|
|
1239
1264
|
}
|
|
1240
1265
|
}
|
|
1241
1266
|
|
|
@@ -1293,13 +1318,24 @@ function validateGuardHookResolvable(scriptMarker, cwd) {
|
|
|
1293
1318
|
collectCommandsWithMarker(parsed, scriptMarker, commands)
|
|
1294
1319
|
|
|
1295
1320
|
const seen = new Set()
|
|
1296
|
-
for (const
|
|
1297
|
-
|
|
1298
|
-
seen.
|
|
1321
|
+
for (const m of commands) {
|
|
1322
|
+
const seenKey = `${m.raw}${m.typeIsCommand}`
|
|
1323
|
+
if (seen.has(seenKey)) continue
|
|
1324
|
+
seen.add(seenKey)
|
|
1299
1325
|
|
|
1300
|
-
const resolved = resolveCredentialGuardHookPath(raw, root)
|
|
1326
|
+
const resolved = resolveCredentialGuardHookPath(m.raw, root)
|
|
1301
1327
|
if (resolved === null) continue
|
|
1302
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
|
+
|
|
1303
1339
|
let stat = null
|
|
1304
1340
|
try {
|
|
1305
1341
|
stat = fs.statSync(resolved)
|
|
@@ -1568,15 +1604,63 @@ const GBG_REF_BACKTICK = '\\`'
|
|
|
1568
1604
|
// (npm/tests/git_branch_guard.test.js): it regenerates the script via generateGitBranchGuardScript
|
|
1569
1605
|
// into a temp dir and asserts byte-equality against this constant.
|
|
1570
1606
|
const GIT_BRANCH_GUARD_SCRIPT_REFERENCE = `#!/usr/bin/env bash
|
|
1571
|
-
# trackfw git branch guard — bloqueia git commit/push/checkout -b
|
|
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.
|
|
1572
1623
|
set -euo pipefail
|
|
1573
1624
|
set -f
|
|
1574
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
|
+
|
|
1575
1659
|
# --- 1. Obter o comando git bruto ------------------------------------------------------------
|
|
1576
1660
|
if [ "$#" -gt 0 ]; then
|
|
1577
1661
|
CMD_RAW="$*"
|
|
1578
1662
|
else
|
|
1579
|
-
INPUT
|
|
1663
|
+
INPUT="$_TRACKFW_STDIN"
|
|
1580
1664
|
TRIMMED=$(printf '%s' "$INPUT" | sed -e 's/^[[:space:]]*//')
|
|
1581
1665
|
case "$TRIMMED" in
|
|
1582
1666
|
\\{*)
|
|
@@ -1609,15 +1693,143 @@ fi
|
|
|
1609
1693
|
|
|
1610
1694
|
[ -n "$CMD_RAW" ] || exit 0
|
|
1611
1695
|
|
|
1612
|
-
# --- 2.
|
|
1613
|
-
#
|
|
1614
|
-
#
|
|
1615
|
-
#
|
|
1616
|
-
#
|
|
1617
|
-
#
|
|
1618
|
-
# "git commit
|
|
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.
|
|
1619
1830
|
match_subcommand() {
|
|
1620
|
-
normalized=$(
|
|
1831
|
+
normalized=$(strip_heredoc_bodies "$1")
|
|
1832
|
+
normalized=$(quote_aware_split "$normalized")
|
|
1621
1833
|
while IFS= read -r seg; do
|
|
1622
1834
|
seg_trimmed=$(printf '%s' "$seg" | sed -e 's/^[[:space:]]*//')
|
|
1623
1835
|
[ -n "$seg_trimmed" ] || continue
|
|
@@ -1625,6 +1837,29 @@ match_subcommand() {
|
|
|
1625
1837
|
set -- $seg_trimmed
|
|
1626
1838
|
first="$1"
|
|
1627
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
|
|
1628
1863
|
[ "$base" = "git" ] || continue
|
|
1629
1864
|
shift
|
|
1630
1865
|
|
|
@@ -1658,9 +1893,80 @@ match_subcommand() {
|
|
|
1658
1893
|
return 0
|
|
1659
1894
|
;;
|
|
1660
1895
|
checkout)
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
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
|
|
1664
1970
|
fi
|
|
1665
1971
|
;;
|
|
1666
1972
|
esac
|
|
@@ -1676,6 +1982,15 @@ case "$SUBCOMMAND" in
|
|
|
1676
1982
|
checkout-b)
|
|
1677
1983
|
REASON="trackfw: git checkout -b bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_REF_BACKTICK + `. Ver CLAUDE.md §1."
|
|
1678
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
|
+
;;
|
|
1679
1994
|
commit)
|
|
1680
1995
|
REASON="trackfw: git commit bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw commit -m '<mensagem>'` + GBG_REF_BACKTICK + `. Ver CLAUDE.md §1."
|
|
1681
1996
|
;;
|
|
@@ -1882,15 +2197,34 @@ exit 0
|
|
|
1882
2197
|
// (internal/validator/validator_git_branch_guard.go) — note this list DIFFERS from
|
|
1883
2198
|
// CREDENTIAL_GUARD_HOOK_FILES for Copilot and Kiro (global scope uses .copilot/settings.json and
|
|
1884
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.
|
|
1885
2202
|
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' },
|
|
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 },
|
|
1892
2209
|
]
|
|
1893
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
|
+
|
|
1894
2228
|
// validateGuardGlobalHookResolvable is the GLOBAL-scope counterpart of
|
|
1895
2229
|
// validateGuardHookResolvable: for each of the 6 GLOBAL_GUARD_CONFIG_FILES that exists AND
|
|
1896
2230
|
// references scriptMarker, verifies the referenced script exists and is executable. Port of Go's
|
|
@@ -1911,7 +2245,8 @@ function validateGuardGlobalHookResolvable(scriptMarker) {
|
|
|
1911
2245
|
|
|
1912
2246
|
const msgs = []
|
|
1913
2247
|
for (const gf of GLOBAL_GUARD_CONFIG_FILES) {
|
|
1914
|
-
const
|
|
2248
|
+
const relPath = globalGuardConfigPath(gf, scriptMarker)
|
|
2249
|
+
const fullPath = path.join(home, relPath)
|
|
1915
2250
|
let content
|
|
1916
2251
|
try {
|
|
1917
2252
|
content = fs.readFileSync(fullPath, 'utf8')
|
|
@@ -1931,23 +2266,34 @@ function validateGuardGlobalHookResolvable(scriptMarker) {
|
|
|
1931
2266
|
collectCommandsWithMarker(parsed, scriptMarker, commands)
|
|
1932
2267
|
|
|
1933
2268
|
const seen = new Set()
|
|
1934
|
-
for (const
|
|
1935
|
-
|
|
1936
|
-
seen.
|
|
1937
|
-
|
|
1938
|
-
|
|
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
|
+
}
|
|
1939
2285
|
|
|
1940
2286
|
let stat = null
|
|
1941
2287
|
try {
|
|
1942
|
-
stat = fs.statSync(raw)
|
|
2288
|
+
stat = fs.statSync(m.raw)
|
|
1943
2289
|
} catch (_) {
|
|
1944
2290
|
stat = null
|
|
1945
2291
|
}
|
|
1946
2292
|
|
|
1947
2293
|
if (!stat) {
|
|
1948
|
-
msgs.push(`~/${
|
|
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`)
|
|
1949
2295
|
} else if ((stat.mode & 0o111) === 0) {
|
|
1950
|
-
msgs.push(`~/${
|
|
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`)
|
|
1951
2297
|
}
|
|
1952
2298
|
}
|
|
1953
2299
|
}
|
|
@@ -1956,61 +2302,45 @@ function validateGuardGlobalHookResolvable(scriptMarker) {
|
|
|
1956
2302
|
}
|
|
1957
2303
|
|
|
1958
2304
|
// validateGuardGlobalScriptIntegrity is the GLOBAL-scope counterpart of
|
|
1959
|
-
// validateCredentialGuardScriptIntegrity/validateGitBranchGuardScriptIntegrity:
|
|
1960
|
-
//
|
|
1961
|
-
//
|
|
1962
|
-
// (internal/validator/validator_git_branch_guard.go)
|
|
1963
|
-
//
|
|
1964
|
-
//
|
|
1965
|
-
//
|
|
1966
|
-
|
|
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) {
|
|
1967
2328
|
const home = os.homedir()
|
|
1968
2329
|
if (!home) return []
|
|
1969
2330
|
|
|
1970
|
-
const
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
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
|
-
}
|
|
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 []
|
|
2011
2339
|
}
|
|
2012
2340
|
|
|
2013
|
-
return
|
|
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`]
|
|
2014
2344
|
}
|
|
2015
2345
|
|
|
2016
2346
|
// validateCredentialGuardGlobalHookResolvable / validateCredentialGuardGlobalScriptIntegrity /
|
|
@@ -2036,6 +2366,10 @@ function validateGitBranchGuardGlobalHookResolvable() {
|
|
|
2036
2366
|
function validateGitBranchGuardGlobalScriptIntegrity() {
|
|
2037
2367
|
return validateGuardGlobalScriptIntegrity(GIT_BRANCH_GUARD_SCRIPT_MARKER, GIT_BRANCH_GUARD_SCRIPT_REFERENCE)
|
|
2038
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).
|
|
2039
2373
|
|
|
2040
2374
|
// CREDENTIAL_GUARD_MODE_LOOKBEHIND_LINES mirrors the shell script's own resolution of
|
|
2041
2375
|
// credential_guard.mode (`grep -A 5 '^credential_guard:'`): the mode key is found on the matched
|