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.
- package/README.md +0 -3
- package/bin/trackfw +38 -1
- package/package.json +1 -1
- package/src/branch/prune.js +450 -0
- package/src/branch/runner.js +33 -20
- package/src/changelog/index.js +114 -0
- package/src/commands/branch.js +59 -9
- package/src/commands/changelog.js +29 -0
- package/src/commands/commit.js +60 -0
- package/src/commands/discover.js +75 -0
- package/src/commands/doctor.js +32 -0
- package/src/commands/index.js +23 -4
- package/src/commands/integrations.js +23 -1
- package/src/commands/serve.js +52 -4
- package/src/commands/ship.js +4 -3
- package/src/commands/thirdparty.js +384 -0
- package/src/commands/update-harness.js +320 -12
- package/src/commands/update.js +3 -1
- package/src/commit/runner.js +352 -0
- package/src/config/index.js +27 -0
- package/src/generators/hooks.js +925 -6
- package/src/generators/init.js +28 -6
- package/src/i18n/locales/en-US.json +0 -21
- package/src/i18n/locales/es-ES.json +0 -21
- package/src/i18n/locales/pt-BR.json +0 -21
- package/src/integrations/doctor.js +0 -0
- package/src/integrations/index.js +19 -2
- package/src/integrations/manager.js +133 -19
- package/src/lib/fatal-error.js +64 -0
- package/src/lib/unknown-command.js +82 -0
- package/src/ship/runner.js +259 -95
- package/src/thirdparty/fetch.js +140 -0
- package/src/thirdparty/markers.js +207 -0
- package/src/thirdparty/provenance.js +114 -0
- package/src/thirdparty/quarantine.js +113 -0
- package/src/thirdparty/references.js +194 -0
- package/src/validator/index.js +1060 -27
- package/src/commands/plugins.js +0 -184
package/src/generators/hooks.js
CHANGED
|
@@ -43,6 +43,23 @@ function mergeSimpleCommandArray(existing, command) {
|
|
|
43
43
|
return arr
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Merge helper for Windsurf's `.windsurf/hooks.json` `hooks.<event>` arrays
|
|
48
|
+
* (e.g. `hooks.pre_run_command`): each entry is `{"command": "...",
|
|
49
|
+
* "show_output": bool}` -- one field richer than mergeSimpleCommandArray's
|
|
50
|
+
* plain `{"command"}` shape (Cursor), so it needs its own merge helper.
|
|
51
|
+
* Mirrors Go's mergeSimpleCommandArray call in InjectWindsurfHooks
|
|
52
|
+
* (internal/generators/agentfiles.go), which is generic over the entry
|
|
53
|
+
* shape via makeEntry/getCmd callbacks -- Node has no equivalent generic
|
|
54
|
+
* variant, so this is a dedicated function instead.
|
|
55
|
+
*/
|
|
56
|
+
function mergeWindsurfHookArray(existing, command, showOutput) {
|
|
57
|
+
const arr = Array.isArray(existing) ? existing.slice() : []
|
|
58
|
+
if (hasEntry(arr, 'command', command)) return arr
|
|
59
|
+
arr.push({ command, show_output: showOutput })
|
|
60
|
+
return arr
|
|
61
|
+
}
|
|
62
|
+
|
|
46
63
|
/**
|
|
47
64
|
* Merge helper para arrays de hooks tipo GitHub Copilot
|
|
48
65
|
* (hooks.preToolUse/postToolUse): cada entry é
|
|
@@ -404,6 +421,497 @@ const CREDENTIAL_GUARD_SCRIPT = CG_HEADER + CG_PROJECT_GUARD + CG_DETECTION_CORE
|
|
|
404
421
|
|
|
405
422
|
const GLOBAL_CREDENTIAL_GUARD_SCRIPT = CG_HEADER + CG_DETECTION_CORE + CG_GLOBAL_TAIL
|
|
406
423
|
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
// GIT_BRANCH_GUARD_SCRIPT — trackfw-git-branch-guard.sh (ML-1A, ROADMAP-2026-08-14)
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
// Byte-identical port of internal/generators/scaffold.go:gitBranchGuardScript
|
|
428
|
+
// (Go's canonical reference). Blocks raw `git commit`/`git push`/
|
|
429
|
+
// `git checkout -b` by a subagent, regardless of runtime contract: emits BOTH
|
|
430
|
+
// `{"decision":"block","reason":"..."}` on stdout (Claude/Gemini JSON-stdout
|
|
431
|
+
// style) AND `exit 2` (Codex/Windsurf/Cursor exit-code style) simultaneously,
|
|
432
|
+
// same simplification decision as the Go const's doc comment. Unlike
|
|
433
|
+
// CREDENTIAL_GUARD_SCRIPT, this script is IDENTICAL between project and
|
|
434
|
+
// global scope (no trackfw.yaml dependency, no mode/roadmap_dir resolution)
|
|
435
|
+
// — a single constant is written by both generateGitBranchGuardScript and
|
|
436
|
+
// generateGlobalGitBranchGuardScript below, mirroring Go's single
|
|
437
|
+
// `gitBranchGuardScript` const reused by both Generate*/GenerateGlobal*
|
|
438
|
+
// functions.
|
|
439
|
+
//
|
|
440
|
+
// KNOWN LIMITATION (fix for 3 real bugs found by ML-4A manual E2E testing, 2026-08-14 —
|
|
441
|
+
// chained command `git status; git push ...` escaping detection, absolute path
|
|
442
|
+
// `/usr/bin/git commit` escaping via exact-string compare, and prose mentioning "git commit"
|
|
443
|
+
// inside a quoted string being read as a real command): the script's `match_subcommand()`
|
|
444
|
+
// now splits the raw command into segments on `;`/`&&`/`||`/`|`/newline and only treats a
|
|
445
|
+
// segment as a real git invocation if `git` (by basename) is that segment's FIRST token —
|
|
446
|
+
// this fixes all 3 bugs, but the splitter is NOT shell-quote-aware. It splits on those
|
|
447
|
+
// delimiter characters wherever they occur in the raw string, even inside single/double
|
|
448
|
+
// quotes. Practical consequence proven live in this session: a line inside a multi-line
|
|
449
|
+
// heredoc body that itself STARTS with the token `git` still blocks (newline is treated as
|
|
450
|
+
// a command boundary regardless of quoting context), and testing this script by embedding a
|
|
451
|
+
// literal `; git push`/`; git commit` substring inside a quoted JSON payload passed as a raw
|
|
452
|
+
// Bash tool command can trip the SAME guard when it's wired as this session's own
|
|
453
|
+
// PreToolUse/Bash hook — write such payloads to a file and pipe `< file.json` instead of
|
|
454
|
+
// inlining them in the shell command text. This limitation applies equally to the generated
|
|
455
|
+
// shell script itself (Go-generated and Node-generated content is byte-identical), even
|
|
456
|
+
// though internal/generators/scaffold.go's gitBranchGuardScript doc comment does not yet
|
|
457
|
+
// call it out explicitly — flagging for trackfw_architect to consider adding there too.
|
|
458
|
+
// GBG_BACKTICK — literal 2-char sequence "\\`" (backslash + backtick), used to embed a
|
|
459
|
+
// shell-escaped backtick (so REASON's `` `cmd` `` doesn't trigger bash command substitution
|
|
460
|
+
// inside the surrounding double-quoted string) without breaking the enclosing JS template
|
|
461
|
+
// literal. Mirrors the exact same problem Go's `gitBranchGuardScript` const solves by
|
|
462
|
+
// breaking out of its raw string and concatenating `"`"` — see scaffold.go's REASON lines
|
|
463
|
+
// (`\` + "`" + `trackfw ...`). A plain, unescaped backtick inside a JS template literal
|
|
464
|
+
// would either be interpreted as JS template-literal syntax (illegal placement) or, if
|
|
465
|
+
// naively escaped as just `` \` ``, would defeat the same regex-based source extraction the
|
|
466
|
+
// credential-guard parity test already documents as unable to handle string concatenation
|
|
467
|
+
// (see the CG_PROJECT_TAIL/CG_GLOBAL_TAIL comment above) — an embedded, un-terminated
|
|
468
|
+
// `` \` `` inside a single backtick-delimited block would make a naive
|
|
469
|
+
// `` const NAME = \`...\` `` regex stop at the first inner backtick instead of the real
|
|
470
|
+
// closing one. Splitting at every backtick insertion point, exactly like Go's own
|
|
471
|
+
// workaround, sidesteps both problems.
|
|
472
|
+
const GBG_BACKTICK = '\\`'
|
|
473
|
+
|
|
474
|
+
const GIT_BRANCH_GUARD_SCRIPT = `#!/usr/bin/env bash
|
|
475
|
+
# trackfw git branch guard — bloqueia git commit/push/checkout -b/branch/worktree add -b
|
|
476
|
+
# brutos por subagente
|
|
477
|
+
#
|
|
478
|
+
# TRIPWIRE, NÃO FRONTEIRA DE SEGURANÇA: detecta o caso óbvio — comando git literal, sem
|
|
479
|
+
# indireção de shell — não é defesa contra um agente adversário competente. Evasões que
|
|
480
|
+
# exigem tokenizar como o bash (ex.: git\${IFS}push, {git,push}, g""it push) permanecem
|
|
481
|
+
# abertas por decisão: ver docs/adr/ADR-2026-08-12-nao-ha-prevencao-contra-agente-induzido-
|
|
482
|
+
# com-escrita-irrestrita-a-resposta-e-deteccao-ancorada-no-git.md. O stripping de
|
|
483
|
+
# env/command abaixo reconhece as formas SEM argumentos antes de git (env git ...,
|
|
484
|
+
# command git ...) e o env seguido de uma sequência de atribuições CHAVE=valor
|
|
485
|
+
# (env FOO=bar git ..., env FOO=bar BAZ=qux git ...) — env com FLAGS (env -i git ...,
|
|
486
|
+
# env --ignore-environment git ...) e command com flags (command -p git ...) continuam
|
|
487
|
+
# evadindo; declarado, não fechado (ver AC5 do ML que adicionou esse stripping). A
|
|
488
|
+
# segmentação abaixo
|
|
489
|
+
# (quote_aware_split) evita falso-positivo em texto citado — não deve ser lida como imune a
|
|
490
|
+
# evasão por citação/tokenização do shell.
|
|
491
|
+
set -euo pipefail
|
|
492
|
+
set -f
|
|
493
|
+
|
|
494
|
+
# --- 0. Drena o stdin ANTES de qualquer saída antecipada (ML-1B, ROADMAP-2026-08-17-guard-
|
|
495
|
+
# global-cabeado-com-no-op-fora-de-projeto-e-integridade-independente-de-fiacao.md): sem isso,
|
|
496
|
+
# quem escreve o payload JSON no pipe recebe EPIPE quando o no-op abaixo sai com 0 antes de ler
|
|
497
|
+
# — reprodutível em 100% das chamadas fora de projeto trackfw, não é corrida de timing. Só drena
|
|
498
|
+
# se stdin não for um terminal interativo (-t 0): em invocação manual sem pipe, "cat" bloquearia
|
|
499
|
+
# esperando EOF (Ctrl-D). O valor lido é reaproveitado no passo 1 abaixo — nunca há uma segunda
|
|
500
|
+
# leitura.
|
|
501
|
+
_TRACKFW_STDIN=""
|
|
502
|
+
[ -t 0 ] || _TRACKFW_STDIN=$(cat 2>/dev/null || true)
|
|
503
|
+
|
|
504
|
+
# --- 0b. No-op fora de projeto trackfw (ADR-2026-08-17-guard-global-cabeado-com-no-op-fora-de-
|
|
505
|
+
# projeto-trackfw.md): sobe diretórios a partir do cwd FÍSICO (pwd -P, resolve symlink) até
|
|
506
|
+
# achar trackfw.yaml na raiz do projeto. Sem trackfw.yaml em nenhum ancestral, o guard não se
|
|
507
|
+
# aplica — fora de projeto trackfw não há trackfw ship como alternativa, e bloquear ali é custo
|
|
508
|
+
# sem contrapartida. Custo medido: só parameter expansion e test -f por nível, nenhum fork de
|
|
509
|
+
# processo; limitado pela profundidade do caminho.
|
|
510
|
+
_TRACKFW_ROOT_DIR=$(pwd -P)
|
|
511
|
+
_TRACKFW_FOUND=0
|
|
512
|
+
while :; do
|
|
513
|
+
if [ -f "$_TRACKFW_ROOT_DIR/trackfw.yaml" ]; then
|
|
514
|
+
_TRACKFW_FOUND=1
|
|
515
|
+
break
|
|
516
|
+
fi
|
|
517
|
+
if [ "$_TRACKFW_ROOT_DIR" = "/" ]; then
|
|
518
|
+
break
|
|
519
|
+
fi
|
|
520
|
+
_TRACKFW_ROOT_DIR="\${_TRACKFW_ROOT_DIR%/*}"
|
|
521
|
+
if [ -z "$_TRACKFW_ROOT_DIR" ]; then
|
|
522
|
+
_TRACKFW_ROOT_DIR="/"
|
|
523
|
+
fi
|
|
524
|
+
done
|
|
525
|
+
[ "$_TRACKFW_FOUND" -eq 1 ] || exit 0
|
|
526
|
+
|
|
527
|
+
# --- 1. Obter o comando git bruto ------------------------------------------------------------
|
|
528
|
+
if [ "$#" -gt 0 ]; then
|
|
529
|
+
CMD_RAW="$*"
|
|
530
|
+
else
|
|
531
|
+
INPUT="$_TRACKFW_STDIN"
|
|
532
|
+
TRIMMED=$(printf '%s' "$INPUT" | sed -e 's/^[[:space:]]*//')
|
|
533
|
+
case "$TRIMMED" in
|
|
534
|
+
\\{*)
|
|
535
|
+
CMD_RAW=""
|
|
536
|
+
if command -v jq >/dev/null 2>&1; then
|
|
537
|
+
CMD_RAW=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // .command // .tool_info.command_line // .hook_input.command // empty' 2>/dev/null || true)
|
|
538
|
+
fi
|
|
539
|
+
if [ -z "$CMD_RAW" ] || [ "$CMD_RAW" = "null" ]; then
|
|
540
|
+
CMD_RAW=$(printf '%s' "$INPUT" | sed -n 's/.*"tool_input"[[:space:]]*:[[:space:]]*{[^}]*"command"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | head -1)
|
|
541
|
+
fi
|
|
542
|
+
if [ -z "$CMD_RAW" ]; then
|
|
543
|
+
CMD_RAW=$(printf '%s' "$INPUT" | sed -n 's/.*"tool_info"[[:space:]]*:[[:space:]]*{[^}]*"command_line"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | head -1)
|
|
544
|
+
fi
|
|
545
|
+
if [ -z "$CMD_RAW" ]; then
|
|
546
|
+
CMD_RAW=$(printf '%s' "$INPUT" | sed -n 's/.*"hook_input"[[:space:]]*:[[:space:]]*{[^}]*"command"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | head -1)
|
|
547
|
+
fi
|
|
548
|
+
if [ -z "$CMD_RAW" ]; then
|
|
549
|
+
CMD_RAW=$(printf '%s' "$INPUT" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | head -1)
|
|
550
|
+
fi
|
|
551
|
+
;;
|
|
552
|
+
*)
|
|
553
|
+
CMD_RAW="$INPUT"
|
|
554
|
+
;;
|
|
555
|
+
esac
|
|
556
|
+
fi
|
|
557
|
+
|
|
558
|
+
if [ -z "$CMD_RAW" ]; then
|
|
559
|
+
CMD_RAW="\${TRACKFW_GIT_COMMAND:-}"
|
|
560
|
+
fi
|
|
561
|
+
|
|
562
|
+
[ -n "$CMD_RAW" ] || exit 0
|
|
563
|
+
|
|
564
|
+
# --- 2. Pré-processamento anti-falso-positivo: neutraliza separadores reais (';', '&&',
|
|
565
|
+
# '||', '|', quebra de linha) que estão DENTRO de aspas ou de corpo de heredoc, para que
|
|
566
|
+
# conteúdo de mensagem (ex.: ` + "`" + `-m "linha 1\\nlinha 2"` + "`" + `) nunca seja fatiado em pseudo-segmentos
|
|
567
|
+
# e lido como comando -------------------------------------------------------------------
|
|
568
|
+
#
|
|
569
|
+
# strip_heredoc_bodies: remove o CORPO de blocos heredoc (<<DELIM ... DELIM), preservando a
|
|
570
|
+
# linha de abertura e a linha terminadora — cobre o padrão ` + "`" + `git commit -F- <<'EOF' ... EOF` + "`" + `
|
|
571
|
+
# (heredoc não citado, fora do escopo de quote_aware_split abaixo). Heurística por linha, não
|
|
572
|
+
# sintaxe completa de shell: só remove o corpo quando encontra a linha terminadora
|
|
573
|
+
# correspondente. Se o heredoc nunca fecha (terminador ausente ou não localizado), devolve o
|
|
574
|
+
# texto ORIGINAL sem qualquer alteração — lado seguro: mais restritivo é preferível a esconder
|
|
575
|
+
# um comando real atrás de um heredoc mal-formado.
|
|
576
|
+
strip_heredoc_bodies() {
|
|
577
|
+
printf '%s' "$1" | awk '
|
|
578
|
+
BEGIN {
|
|
579
|
+
dq = sprintf("%c", 34)
|
|
580
|
+
sq = sprintf("%c", 39)
|
|
581
|
+
in_heredoc = 0
|
|
582
|
+
delim = ""
|
|
583
|
+
ok = 1
|
|
584
|
+
}
|
|
585
|
+
{
|
|
586
|
+
raw = raw $0 "\\n"
|
|
587
|
+
if (in_heredoc) {
|
|
588
|
+
trimmed = $0
|
|
589
|
+
sub(/^[ \\t]+/, "", trimmed)
|
|
590
|
+
sub(/[ \\t]+$/, "", trimmed)
|
|
591
|
+
if (trimmed == delim) {
|
|
592
|
+
in_heredoc = 0
|
|
593
|
+
out = out $0 "\\n"
|
|
594
|
+
}
|
|
595
|
+
next
|
|
596
|
+
}
|
|
597
|
+
if (match($0, /<<-?[ \\t]*[^ \\t]+/)) {
|
|
598
|
+
d = substr($0, RSTART, RLENGTH)
|
|
599
|
+
sub(/^<<-?[ \\t]*/, "", d)
|
|
600
|
+
gsub(dq, "", d)
|
|
601
|
+
gsub(sq, "", d)
|
|
602
|
+
if (d != "") {
|
|
603
|
+
delim = d
|
|
604
|
+
in_heredoc = 1
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
out = out $0 "\\n"
|
|
608
|
+
}
|
|
609
|
+
END {
|
|
610
|
+
if (in_heredoc) ok = 0
|
|
611
|
+
if (ok) { printf "%s", out } else { printf "%s", raw }
|
|
612
|
+
}
|
|
613
|
+
'
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
# quote_aware_split: emite o texto com ';' isolado, '&&', '||' e '|' isolado convertidos em
|
|
617
|
+
# quebra de linha — EXCETO quando ocorrem dentro de uma string entre aspas simples ou duplas,
|
|
618
|
+
# caso em que são preservados como texto e uma quebra de linha real dentro das aspas vira
|
|
619
|
+
# espaço (nunca gera um novo pseudo-segmento). Substitui o antigo ` + "`" + `sed` + "`" + ` cego, que não
|
|
620
|
+
# distinguia texto citado de sintaxe de comando — a causa raiz do falso-positivo de linha de
|
|
621
|
+
# mensagem de commit iniciada por "git ...". Aspas não fechadas até o fim da entrada
|
|
622
|
+
# permanecem "abertas" até o fim — mesma semântica do shell real: uma aspa não fechada nunca
|
|
623
|
+
# deixa o texto seguinte executar como comando novo, só torna o restante parte da mesma
|
|
624
|
+
# string.
|
|
625
|
+
quote_aware_split() {
|
|
626
|
+
printf '%s' "$1" | awk '
|
|
627
|
+
BEGIN {
|
|
628
|
+
dq = sprintf("%c", 34)
|
|
629
|
+
sq = sprintf("%c", 39)
|
|
630
|
+
bs = sprintf("%c", 92)
|
|
631
|
+
nl = sprintf("%c", 10)
|
|
632
|
+
}
|
|
633
|
+
{ s = (NR == 1) ? $0 : s nl $0 }
|
|
634
|
+
END {
|
|
635
|
+
n = length(s)
|
|
636
|
+
q = ""
|
|
637
|
+
out = ""
|
|
638
|
+
i = 1
|
|
639
|
+
while (i <= n) {
|
|
640
|
+
c = substr(s, i, 1)
|
|
641
|
+
if (q != "") {
|
|
642
|
+
if (q == dq && c == bs && i < n) {
|
|
643
|
+
nx = substr(s, i + 1, 1)
|
|
644
|
+
out = out c (nx == nl ? " " : nx)
|
|
645
|
+
i += 2
|
|
646
|
+
continue
|
|
647
|
+
}
|
|
648
|
+
if (c == q) {
|
|
649
|
+
q = ""
|
|
650
|
+
out = out c
|
|
651
|
+
i++
|
|
652
|
+
continue
|
|
653
|
+
}
|
|
654
|
+
out = out (c == nl ? " " : c)
|
|
655
|
+
i++
|
|
656
|
+
continue
|
|
657
|
+
}
|
|
658
|
+
if (c == dq || c == sq) {
|
|
659
|
+
q = c
|
|
660
|
+
out = out c
|
|
661
|
+
i++
|
|
662
|
+
continue
|
|
663
|
+
}
|
|
664
|
+
if (substr(s, i, 2) == "&&" || substr(s, i, 2) == "||") {
|
|
665
|
+
out = out nl
|
|
666
|
+
i += 2
|
|
667
|
+
continue
|
|
668
|
+
}
|
|
669
|
+
if (c == ";" || c == "|") {
|
|
670
|
+
out = out nl
|
|
671
|
+
i++
|
|
672
|
+
continue
|
|
673
|
+
}
|
|
674
|
+
out = out c
|
|
675
|
+
i++
|
|
676
|
+
}
|
|
677
|
+
printf "%s", out
|
|
678
|
+
}
|
|
679
|
+
'
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
# match_subcommand — casa contra "git (commit|push|checkout -b|switch -c)", segmento por
|
|
683
|
+
# segmento. Cada segmento é um comando real, obtido depois do pré-processamento acima
|
|
684
|
+
# (strip_heredoc_bodies + quote_aware_split), que converte ';', '&&', '||', '|' fora de aspas
|
|
685
|
+
# em quebra de linha e neutraliza os mesmos separadores quando aparecem dentro de
|
|
686
|
+
# aspas/heredoc. "git" só conta se for o PRIMEIRO token do segmento (por basename, então
|
|
687
|
+
# /usr/bin/git também casa) — nunca uma ocorrência solta em qualquer posição da string
|
|
688
|
+
# inteira. Isso evita: (a) o segundo comando de uma cadeia escapar da checagem, (b) um path
|
|
689
|
+
# absoluto para o git escapar por comparação de igualdade exata, e (c) texto de prosa —
|
|
690
|
+
# inclusive linha de mensagem de commit que COMEÇA com "git <sub>" (ex.: uma tabela
|
|
691
|
+
# documentando comandos bloqueados) — ser tratado como comando, porque esse texto agora nunca
|
|
692
|
+
# produz um novo segmento. ` + "`" + `git switch -c/-C/--create` + "`" + ` (forma alternativa a ` + "`" + `checkout -b` + "`" + `
|
|
693
|
+
# para criar branch) é reconhecido varrendo TODOS os tokens após o subcomando, não só o
|
|
694
|
+
# primeiro — cobre ` + "`" + `git switch --track -c feat/x` + "`" + ` (flag antes de -c).
|
|
695
|
+
# checkout -b é reconhecido do mesmo jeito: varre TODOS os tokens até achar -b/-B/--orphan,
|
|
696
|
+
# não só o primeiro. Prefixos env e command antes de git são descartados antes da checagem do
|
|
697
|
+
# basename — cobre env git push/command git push sem exigir tokenizar como o bash.
|
|
698
|
+
match_subcommand() {
|
|
699
|
+
normalized=$(strip_heredoc_bodies "$1")
|
|
700
|
+
normalized=$(quote_aware_split "$normalized")
|
|
701
|
+
while IFS= read -r seg; do
|
|
702
|
+
seg_trimmed=$(printf '%s' "$seg" | sed -e 's/^[[:space:]]*//')
|
|
703
|
+
[ -n "$seg_trimmed" ] || continue
|
|
704
|
+
|
|
705
|
+
set -- $seg_trimmed
|
|
706
|
+
first="$1"
|
|
707
|
+
base="\${first##*/}"
|
|
708
|
+
while [ "$base" = "env" ] || [ "$base" = "command" ]; do
|
|
709
|
+
is_env="$base"
|
|
710
|
+
shift
|
|
711
|
+
[ "$#" -gt 0 ] || break
|
|
712
|
+
if [ "$is_env" = "env" ]; then
|
|
713
|
+
while [ "$#" -gt 0 ]; do
|
|
714
|
+
case "$1" in
|
|
715
|
+
-*)
|
|
716
|
+
break
|
|
717
|
+
;;
|
|
718
|
+
*=*)
|
|
719
|
+
shift
|
|
720
|
+
;;
|
|
721
|
+
*)
|
|
722
|
+
break
|
|
723
|
+
;;
|
|
724
|
+
esac
|
|
725
|
+
done
|
|
726
|
+
[ "$#" -gt 0 ] || break
|
|
727
|
+
fi
|
|
728
|
+
first="$1"
|
|
729
|
+
base="\${first##*/}"
|
|
730
|
+
done
|
|
731
|
+
[ "$base" = "git" ] || continue
|
|
732
|
+
shift
|
|
733
|
+
|
|
734
|
+
sub=""
|
|
735
|
+
while [ "$#" -gt 0 ]; do
|
|
736
|
+
tok="$1"
|
|
737
|
+
case "$tok" in
|
|
738
|
+
-C|-c|--work-tree|--git-dir|--namespace)
|
|
739
|
+
if [ "$#" -ge 2 ]; then shift 2; else shift; fi
|
|
740
|
+
continue
|
|
741
|
+
;;
|
|
742
|
+
-*)
|
|
743
|
+
shift
|
|
744
|
+
continue
|
|
745
|
+
;;
|
|
746
|
+
*)
|
|
747
|
+
sub="$tok"
|
|
748
|
+
shift
|
|
749
|
+
break
|
|
750
|
+
;;
|
|
751
|
+
esac
|
|
752
|
+
done
|
|
753
|
+
|
|
754
|
+
case "$sub" in
|
|
755
|
+
commit)
|
|
756
|
+
echo "commit"
|
|
757
|
+
return 0
|
|
758
|
+
;;
|
|
759
|
+
push)
|
|
760
|
+
echo "push"
|
|
761
|
+
return 0
|
|
762
|
+
;;
|
|
763
|
+
checkout)
|
|
764
|
+
for tok2 in "$@"; do
|
|
765
|
+
case "$tok2" in
|
|
766
|
+
-b|-B|--orphan|--orphan=*)
|
|
767
|
+
echo "checkout-b"
|
|
768
|
+
return 0
|
|
769
|
+
;;
|
|
770
|
+
esac
|
|
771
|
+
done
|
|
772
|
+
;;
|
|
773
|
+
switch)
|
|
774
|
+
for tok2 in "$@"; do
|
|
775
|
+
case "$tok2" in
|
|
776
|
+
-c|-C|--create|--create=*|--force-create|--force-create=*)
|
|
777
|
+
echo "switch-c"
|
|
778
|
+
return 0
|
|
779
|
+
;;
|
|
780
|
+
esac
|
|
781
|
+
done
|
|
782
|
+
;;
|
|
783
|
+
branch)
|
|
784
|
+
# git branch é majoritariamente leitura (sem args, -a, -r, -l, --list, -v/-vv,
|
|
785
|
+
# --show-current, --contains, --no-contains, --merged, --no-merged, --sort=,
|
|
786
|
+
# --format=, --points-at, -d/-D/--delete) — bloquear leitura seria pior que a
|
|
787
|
+
# brecha. Só bloqueia: (a) -c/-C/-m/-M/--copy/--move (cria/renomeia branch,
|
|
788
|
+
# qualquer posição de token) ou (b) um argumento posicional puro (nome da branch a
|
|
789
|
+
# criar), a menos que -d/-D/--delete também esteja presente (delete tem
|
|
790
|
+
# posicional legítimo — o nome a apagar). Flags de valor conhecidas (--contains,
|
|
791
|
+
# --no-contains, --sort, --format, --points-at, --merged, --no-merged) têm seu
|
|
792
|
+
# valor seguinte pulado quando vem em token separado, para não ser lido como
|
|
793
|
+
# posicional de criação.
|
|
794
|
+
branch_action=0
|
|
795
|
+
has_delete=0
|
|
796
|
+
saw_positional=0
|
|
797
|
+
skip_next=0
|
|
798
|
+
for tok2 in "$@"; do
|
|
799
|
+
if [ "$skip_next" = "1" ]; then
|
|
800
|
+
skip_next=0
|
|
801
|
+
continue
|
|
802
|
+
fi
|
|
803
|
+
case "$tok2" in
|
|
804
|
+
-c|-C|-m|-M|--copy|--copy=*|--move|--move=*)
|
|
805
|
+
branch_action=1
|
|
806
|
+
;;
|
|
807
|
+
-d|-D|--delete|--delete=*)
|
|
808
|
+
has_delete=1
|
|
809
|
+
;;
|
|
810
|
+
--contains|--no-contains|--sort|--format|--points-at|--merged|--no-merged)
|
|
811
|
+
skip_next=1
|
|
812
|
+
;;
|
|
813
|
+
-*)
|
|
814
|
+
;;
|
|
815
|
+
*)
|
|
816
|
+
saw_positional=1
|
|
817
|
+
;;
|
|
818
|
+
esac
|
|
819
|
+
done
|
|
820
|
+
if [ "$has_delete" != "1" ]; then
|
|
821
|
+
if [ "$branch_action" = "1" ] || [ "$saw_positional" = "1" ]; then
|
|
822
|
+
echo "branch-create"
|
|
823
|
+
return 0
|
|
824
|
+
fi
|
|
825
|
+
fi
|
|
826
|
+
;;
|
|
827
|
+
worktree)
|
|
828
|
+
if [ "\${1:-}" = "add" ]; then
|
|
829
|
+
shift
|
|
830
|
+
for tok2 in "$@"; do
|
|
831
|
+
case "$tok2" in
|
|
832
|
+
-b|-B)
|
|
833
|
+
echo "worktree-add-b"
|
|
834
|
+
return 0
|
|
835
|
+
;;
|
|
836
|
+
esac
|
|
837
|
+
done
|
|
838
|
+
fi
|
|
839
|
+
;;
|
|
840
|
+
esac
|
|
841
|
+
done <<EOF
|
|
842
|
+
$normalized
|
|
843
|
+
EOF
|
|
844
|
+
return 1
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
SUBCOMMAND=$(match_subcommand "$CMD_RAW") || exit 0
|
|
848
|
+
|
|
849
|
+
case "$SUBCOMMAND" in
|
|
850
|
+
checkout-b)
|
|
851
|
+
REASON="trackfw: git checkout -b bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_BACKTICK + `. Ver CLAUDE.md §1."
|
|
852
|
+
;;
|
|
853
|
+
switch-c)
|
|
854
|
+
REASON="trackfw: git switch -c bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_BACKTICK + `. Ver CLAUDE.md §1."
|
|
855
|
+
;;
|
|
856
|
+
branch-create)
|
|
857
|
+
REASON="trackfw: git branch bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_BACKTICK + `. Ver CLAUDE.md §1."
|
|
858
|
+
;;
|
|
859
|
+
worktree-add-b)
|
|
860
|
+
REASON="trackfw: git worktree add -b bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_BACKTICK + `. Ver CLAUDE.md §1."
|
|
861
|
+
;;
|
|
862
|
+
commit)
|
|
863
|
+
REASON="trackfw: git commit bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw commit -m '<mensagem>'` + GBG_BACKTICK + `. Ver CLAUDE.md §1."
|
|
864
|
+
;;
|
|
865
|
+
push)
|
|
866
|
+
REASON="trackfw: git push bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw ship` + GBG_BACKTICK + `. Ver CLAUDE.md §1."
|
|
867
|
+
;;
|
|
868
|
+
*)
|
|
869
|
+
exit 0
|
|
870
|
+
;;
|
|
871
|
+
esac
|
|
872
|
+
|
|
873
|
+
printf '{"decision":"block","reason":"%s"}\\n' "$REASON"
|
|
874
|
+
echo "$REASON" >&2
|
|
875
|
+
exit 2
|
|
876
|
+
`
|
|
877
|
+
|
|
878
|
+
// ---------------------------------------------------------------------------
|
|
879
|
+
// generateGitBranchGuardScript — writes scripts/trackfw-git-branch-guard.sh
|
|
880
|
+
// ---------------------------------------------------------------------------
|
|
881
|
+
// This ML only creates the script — it is NOT wired into any hooks.json/
|
|
882
|
+
// settings.json here (that is this roadmap's Wave 3 scope). Mirrors
|
|
883
|
+
// generateCredentialGuardScript exactly (mkdirSync + writeFileSync, mode 0o755).
|
|
884
|
+
function generateGitBranchGuardScript(cwd) {
|
|
885
|
+
const root = cwd || process.cwd()
|
|
886
|
+
const scriptsDir = path.join(root, 'scripts')
|
|
887
|
+
fs.mkdirSync(scriptsDir, { recursive: true })
|
|
888
|
+
|
|
889
|
+
const scriptPath = path.join(scriptsDir, 'trackfw-git-branch-guard.sh')
|
|
890
|
+
fs.writeFileSync(scriptPath, GIT_BRANCH_GUARD_SCRIPT, { encoding: 'utf8', mode: 0o755 })
|
|
891
|
+
|
|
892
|
+
console.log(' ✓ scripts/trackfw-git-branch-guard.sh')
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// ---------------------------------------------------------------------------
|
|
896
|
+
// generateGlobalGitBranchGuardScript — writes <home>/.trackfw/scripts/trackfw-git-branch-guard.sh
|
|
897
|
+
// ---------------------------------------------------------------------------
|
|
898
|
+
// Destinado a ser referenciado por hooks globais de CLI, instalados via
|
|
899
|
+
// `trackfw update harness` -- não é chamado por `trackfw init`/`trackfw
|
|
900
|
+
// update` (escopo de projeto), que continuam usando
|
|
901
|
+
// generateGitBranchGuardScript. Mirrors generateGlobalCredentialGuardScript.
|
|
902
|
+
function generateGlobalGitBranchGuardScript(home) {
|
|
903
|
+
if (!home) {
|
|
904
|
+
throw new Error('home directory vazio')
|
|
905
|
+
}
|
|
906
|
+
const scriptsDir = path.join(home, '.trackfw', 'scripts')
|
|
907
|
+
fs.mkdirSync(scriptsDir, { recursive: true })
|
|
908
|
+
|
|
909
|
+
const scriptPath = path.join(scriptsDir, 'trackfw-git-branch-guard.sh')
|
|
910
|
+
fs.writeFileSync(scriptPath, GIT_BRANCH_GUARD_SCRIPT, { encoding: 'utf8', mode: 0o755 })
|
|
911
|
+
|
|
912
|
+
console.log(' ✓ .trackfw/scripts/trackfw-git-branch-guard.sh')
|
|
913
|
+
}
|
|
914
|
+
|
|
407
915
|
// ---------------------------------------------------------------------------
|
|
408
916
|
// generateCredentialGuardScript — writes scripts/trackfw-credential-guard.sh
|
|
409
917
|
// ---------------------------------------------------------------------------
|
|
@@ -521,6 +1029,53 @@ const GUARD_CMD_CURSOR = 'scripts/trackfw-credential-guard.sh'
|
|
|
521
1029
|
// successfully in practice.
|
|
522
1030
|
const GUARD_CMD_CLAUDE = '$CLAUDE_PROJECT_DIR/scripts/trackfw-credential-guard.sh'
|
|
523
1031
|
|
|
1032
|
+
// ---------------------------------------------------------------------------
|
|
1033
|
+
// GBG_CMD_* — git-branch-guard command per CLI (ROADMAP-2026-08-14, ML-3B,
|
|
1034
|
+
// Wave 3). Same path-resolution mechanism per runtime already established
|
|
1035
|
+
// above for GUARD_CMD_*/SIGNAL_CMD_* (Claude's $CLAUDE_PROJECT_DIR pin,
|
|
1036
|
+
// Codex's `$(git rev-parse --show-toplevel)` substitution, Gemini's
|
|
1037
|
+
// $GEMINI_PROJECT_DIR, Copilot/Cursor's project-root-relative path) — the
|
|
1038
|
+
// git branch guard has no per-CLI variance of its own, it just needs the
|
|
1039
|
+
// same "always resolve to the project root regardless of cwd drift" fix
|
|
1040
|
+
// each of those constants already encodes for the credential guard.
|
|
1041
|
+
//
|
|
1042
|
+
// Wiring scope of this ML (Claude/Codex/Gemini/Cursor): registers the guard
|
|
1043
|
+
// against the SAME hook events/matchers already used for the credential
|
|
1044
|
+
// guard in each of these 4 CLIs' existing merge-based hooks.json/
|
|
1045
|
+
// settings.json contract — a `Bash`/`run_shell_command`/`beforeShellExecution`
|
|
1046
|
+
// PreToolUse-equivalent entry is sufficient to intercept `git commit`/`push`/
|
|
1047
|
+
// `checkout -b` before they execute; no PostToolUse entry is added (unlike
|
|
1048
|
+
// the credential guard) because blocking after the git command already ran
|
|
1049
|
+
// is too late to be useful.
|
|
1050
|
+
//
|
|
1051
|
+
// NOT implemented in this ML (documented gap, mirrors the Go side of this
|
|
1052
|
+
// same roadmap wave, which also has not built these yet): GitHub Copilot's
|
|
1053
|
+
// `--deny-tool='shell(git commit)'`-style entries in a dedicated
|
|
1054
|
+
// permissions-config.json/settings.json (a different mechanism than the
|
|
1055
|
+
// existing `.github/hooks/trackfw-attention.json` hooks file this module
|
|
1056
|
+
// already generates for Copilot); Windsurf's `pre_run_command` hook +
|
|
1057
|
+
// `windsurf.cascadeCommandsAllowList` deny entry (injectWindsurfHooks today
|
|
1058
|
+
// only injects textual rules, same as Go's InjectWindsurfHooks); and Amazon Q
|
|
1059
|
+
// Developer's `preToolUse`/`execute_bash` hook + `deniedCommands` regex +
|
|
1060
|
+
// restricted custom-agent toolset (no Amazon Q hook generator exists in this
|
|
1061
|
+
// module today — only the textual `.amazonq/developer/guidelines.md` rules
|
|
1062
|
+
// generator in generators/init.js — matching Go, which also has no
|
|
1063
|
+
// InjectAmazonQHooks yet). Each of these requires a NEW file-format decision
|
|
1064
|
+
// this roadmap wave has not yet settled on either stack; building one from
|
|
1065
|
+
// scratch here, ahead of that decision, risks diverging from whatever the Go
|
|
1066
|
+
// side lands on. Flagged for a follow-up ML once that decision is made.
|
|
1067
|
+
const GBG_CMD_CLAUDE = '$CLAUDE_PROJECT_DIR/scripts/trackfw-git-branch-guard.sh'
|
|
1068
|
+
const GBG_CMD_CODEX = CODEX_ROOT + '/scripts/trackfw-git-branch-guard.sh"'
|
|
1069
|
+
const GBG_CMD_GEMINI = '$GEMINI_PROJECT_DIR/scripts/trackfw-git-branch-guard.sh'
|
|
1070
|
+
const GBG_CMD_CURSOR = 'scripts/trackfw-git-branch-guard.sh'
|
|
1071
|
+
const GBG_CMD_COPILOT = 'scripts/trackfw-git-branch-guard.sh'
|
|
1072
|
+
const GBG_CMD_PLAIN = 'scripts/trackfw-git-branch-guard.sh'
|
|
1073
|
+
// GBG_CMD_WINDSURF -- Windsurf invokes hooks.pre_run_command entries via a shell (confirmed
|
|
1074
|
+
// against https://docs.devin.ai/desktop/cascade/hooks), so the guard script is wrapped in
|
|
1075
|
+
// `bash <path>` rather than invoked directly, unlike every other CLI above. Mirrors Go's
|
|
1076
|
+
// windsurfGitGuardCmd (internal/generators/agentfiles.go).
|
|
1077
|
+
const GBG_CMD_WINDSURF = 'bash scripts/trackfw-git-branch-guard.sh'
|
|
1078
|
+
|
|
524
1079
|
// ---------------------------------------------------------------------------
|
|
525
1080
|
// Global credential-guard dedup (ROADMAP-2026-08-06 Wave 3/ML-3A)
|
|
526
1081
|
// ---------------------------------------------------------------------------
|
|
@@ -556,17 +1111,95 @@ function readGlobalHookJSON(...relParts) {
|
|
|
556
1111
|
}
|
|
557
1112
|
}
|
|
558
1113
|
|
|
559
|
-
/**
|
|
1114
|
+
/**
|
|
1115
|
+
* Collapses runs of consecutive slashes ("//" -> "/", any position,
|
|
1116
|
+
* including leading) and strips a trailing slash, so two on-disk forms of
|
|
1117
|
+
* the SAME script path compare equal regardless of incidental formatting
|
|
1118
|
+
* (e.g. $HOME resolving with a trailing slash, as happens with macOS's
|
|
1119
|
+
* $TMPDIR, or a hand-edited config file). Does NOT resolve "." / ".."
|
|
1120
|
+
* segments or symlinks -- those transforms would let unrelated paths
|
|
1121
|
+
* compare equal (silently disarming the dedup, the more dangerous failure
|
|
1122
|
+
* mode here) and symlink resolution errors on a path that does not exist
|
|
1123
|
+
* yet, which every caller here must fail OPEN on. Hand-rolled instead of
|
|
1124
|
+
* path.normalize because it disagrees with Go's filepath.Clean and Python's
|
|
1125
|
+
* os.path.normpath on leading "//" and trailing "/" handling (measured) --
|
|
1126
|
+
* mirrored byte-for-byte in internal/generators/agentfiles.go
|
|
1127
|
+
* (normalizeGuardPath) and pypi/trackfw/generators/hooks.py
|
|
1128
|
+
* (_normalize_guard_path). Never call with anything other than a script
|
|
1129
|
+
* path -- it is not a general string normalizer.
|
|
1130
|
+
*/
|
|
1131
|
+
function normalizeGuardPath(p) {
|
|
1132
|
+
if (!p) return p
|
|
1133
|
+
let out = ''
|
|
1134
|
+
let prevSlash = false
|
|
1135
|
+
for (const ch of p) {
|
|
1136
|
+
if (ch === '/') {
|
|
1137
|
+
if (prevSlash) continue
|
|
1138
|
+
prevSlash = true
|
|
1139
|
+
} else {
|
|
1140
|
+
prevSlash = false
|
|
1141
|
+
}
|
|
1142
|
+
out += ch
|
|
1143
|
+
}
|
|
1144
|
+
if (out.length > 1 && out.endsWith('/')) {
|
|
1145
|
+
out = out.replace(/\/+$/, '') || '/'
|
|
1146
|
+
}
|
|
1147
|
+
return out
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/** Reports whether a and b denote the same script command path after normalizeGuardPath. */
|
|
1151
|
+
function samePathCommand(a, b) {
|
|
1152
|
+
return normalizeGuardPath(a) === normalizeGuardPath(b)
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Read-only counterpart of mergeClaudeHookArray. Compares command paths via
|
|
1157
|
+
* samePathCommand (normalized), not raw string equality.
|
|
1158
|
+
*
|
|
1159
|
+
* ROADMAP-2026-08-17 ML-4B: also requires the sibling `type` field to equal
|
|
1160
|
+
* "command" -- mergeClaudeHookArray always writes
|
|
1161
|
+
* {type:'command',command:...}, and Claude/Codex/Gemini all silently ignore
|
|
1162
|
+
* a hook entry missing "type":"command" (measured, hades-tf ML-4A barrier
|
|
1163
|
+
* finding: a global entry with the correct command but no `type` field
|
|
1164
|
+
* looked "installed" to the dedup, so the project-scope entry was skipped
|
|
1165
|
+
* in favor of a global entry that never actually executes -- leaving BOTH
|
|
1166
|
+
* scopes silently unprotected while `trackfw validate` stayed green).
|
|
1167
|
+
* Requiring "type":"command" here closes that gap: a malformed global entry
|
|
1168
|
+
* is now treated as "not installed", so the project-scope entry gets
|
|
1169
|
+
* re-wired instead of being skipped.
|
|
1170
|
+
*/
|
|
560
1171
|
function hookArrayHasCommand(existing, matcher, command) {
|
|
561
1172
|
const arr = Array.isArray(existing) ? existing : []
|
|
562
1173
|
for (const item of arr) {
|
|
563
1174
|
if (!item || item.matcher !== matcher) continue
|
|
564
1175
|
const inner = Array.isArray(item.hooks) ? item.hooks : []
|
|
565
|
-
if (inner.some(h => h && h.command === command)) return true
|
|
1176
|
+
if (inner.some(h => h && h.type === 'command' && typeof h.command === 'string' && samePathCommand(h.command, command))) return true
|
|
566
1177
|
}
|
|
567
1178
|
return false
|
|
568
1179
|
}
|
|
569
1180
|
|
|
1181
|
+
/**
|
|
1182
|
+
* Read-only, path-normalized counterpart of hasEntry -- used ONLY by the
|
|
1183
|
+
* global-dedup read paths (globalXInstalledCursor/Copilot below), never by
|
|
1184
|
+
* the write-side merge/idempotency helpers (mergeSimpleCommandArray,
|
|
1185
|
+
* injectCursorHooks), which must keep comparing raw strings so their
|
|
1186
|
+
* idempotency behavior does not drift from Go/Python. See samePathCommand's
|
|
1187
|
+
* doc comment for why the value must always be a script path.
|
|
1188
|
+
*
|
|
1189
|
+
* ROADMAP-2026-08-17 ML-4B: requireCommandType mirrors Go's
|
|
1190
|
+
* simpleArrayHasValue -- Copilot entries (mergeCredentialGuardCopilotHooks)
|
|
1191
|
+
* always carry "type":"command" and Copilot ignores an entry without it
|
|
1192
|
+
* (same hades-tf ML-4A finding as hookArrayHasCommand above), so Copilot
|
|
1193
|
+
* callers pass true. Cursor entries (mergeCredentialGuardCursorHooks,
|
|
1194
|
+
* {command:...}) never carry a `type` field -- not part of Cursor's schema
|
|
1195
|
+
* -- so requiring it there would make this always return false for a
|
|
1196
|
+
* perfectly valid, executing Cursor entry; Cursor callers pass false. Do
|
|
1197
|
+
* NOT uniformize this across CLIs.
|
|
1198
|
+
*/
|
|
1199
|
+
function hasEntryPath(arr, field, value, requireCommandType) {
|
|
1200
|
+
return Array.isArray(arr) && arr.some(e => e && (!requireCommandType || e.type === 'command') && typeof e[field] === 'string' && samePathCommand(e[field], value))
|
|
1201
|
+
}
|
|
1202
|
+
|
|
570
1203
|
function globalCredentialGuardInstalledClaude() {
|
|
571
1204
|
const scriptPath = globalCredentialGuardScriptPath()
|
|
572
1205
|
if (!scriptPath) return false
|
|
@@ -596,7 +1229,7 @@ function globalCredentialGuardInstalledCursor() {
|
|
|
596
1229
|
if (!scriptPath) return false
|
|
597
1230
|
const root = readGlobalHookJSON('.cursor', 'hooks.json')
|
|
598
1231
|
if (!root || !root.hooks) return false
|
|
599
|
-
return
|
|
1232
|
+
return hasEntryPath(root.hooks.beforeShellExecution, 'command', scriptPath, false)
|
|
600
1233
|
}
|
|
601
1234
|
|
|
602
1235
|
function globalCredentialGuardInstalledCopilot() {
|
|
@@ -604,7 +1237,7 @@ function globalCredentialGuardInstalledCopilot() {
|
|
|
604
1237
|
if (!scriptPath) return false
|
|
605
1238
|
const root = readGlobalHookJSON('.copilot', 'settings.json')
|
|
606
1239
|
if (!root || !root.hooks) return false
|
|
607
|
-
return
|
|
1240
|
+
return hasEntryPath(root.hooks.preToolUse, 'bash', scriptPath, true)
|
|
608
1241
|
}
|
|
609
1242
|
|
|
610
1243
|
/**
|
|
@@ -624,6 +1257,68 @@ function globalCredentialGuardInstalledKiro() {
|
|
|
624
1257
|
}
|
|
625
1258
|
}
|
|
626
1259
|
|
|
1260
|
+
// ---------------------------------------------------------------------------
|
|
1261
|
+
// git-branch-guard global-installed dedup (ROADMAP-2026-08-17 Wave 2/ML-2B)
|
|
1262
|
+
//
|
|
1263
|
+
// Mirrors the globalCredentialGuardInstalled<Tool> family above exactly,
|
|
1264
|
+
// pointed at ~/.trackfw/scripts/trackfw-git-branch-guard.sh instead of
|
|
1265
|
+
// trackfw-credential-guard.sh. Only 5 of the 6 credential-guard dedup
|
|
1266
|
+
// targets have a git-branch-guard counterpart: Kiro's project-scope
|
|
1267
|
+
// injector never wires git-branch-guard at all (see injectKiroHooks'
|
|
1268
|
+
// git-branch-guard doc comment), so there is no
|
|
1269
|
+
// globalGitBranchGuardInstalledKiro. Windsurf/AmazonQ wire git-branch-guard
|
|
1270
|
+
// at project scope but have no global-scope target (ML-2A only added
|
|
1271
|
+
// targets for the 6 CLIs credential-guard already covers) and no
|
|
1272
|
+
// credential-guard dedup precedent either -- consistent, not a gap.
|
|
1273
|
+
// ---------------------------------------------------------------------------
|
|
1274
|
+
|
|
1275
|
+
/** Mirrors Go's globalGitBranchGuardScriptPath. */
|
|
1276
|
+
function globalGitBranchGuardScriptPath() {
|
|
1277
|
+
const home = os.homedir()
|
|
1278
|
+
if (!home) return null
|
|
1279
|
+
return path.join(home, '.trackfw', 'scripts', 'trackfw-git-branch-guard.sh')
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function globalGitBranchGuardInstalledClaude() {
|
|
1283
|
+
const scriptPath = globalGitBranchGuardScriptPath()
|
|
1284
|
+
if (!scriptPath) return false
|
|
1285
|
+
const root = readGlobalHookJSON('.claude', 'settings.json')
|
|
1286
|
+
if (!root || !root.hooks) return false
|
|
1287
|
+
return hookArrayHasCommand(root.hooks.PreToolUse, 'Bash', scriptPath)
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function globalGitBranchGuardInstalledCodex() {
|
|
1291
|
+
const scriptPath = globalGitBranchGuardScriptPath()
|
|
1292
|
+
if (!scriptPath) return false
|
|
1293
|
+
const root = readGlobalHookJSON('.codex', 'hooks.json')
|
|
1294
|
+
if (!root || !root.hooks) return false
|
|
1295
|
+
return hookArrayHasCommand(root.hooks.PreToolUse, 'Bash', scriptPath)
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function globalGitBranchGuardInstalledGemini() {
|
|
1299
|
+
const scriptPath = globalGitBranchGuardScriptPath()
|
|
1300
|
+
if (!scriptPath) return false
|
|
1301
|
+
const root = readGlobalHookJSON('.gemini', 'settings.json')
|
|
1302
|
+
if (!root || !root.hooks) return false
|
|
1303
|
+
return hookArrayHasCommand(root.hooks.BeforeTool, 'run_shell_command', scriptPath)
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function globalGitBranchGuardInstalledCursor() {
|
|
1307
|
+
const scriptPath = globalGitBranchGuardScriptPath()
|
|
1308
|
+
if (!scriptPath) return false
|
|
1309
|
+
const root = readGlobalHookJSON('.cursor', 'hooks.json')
|
|
1310
|
+
if (!root || !root.hooks) return false
|
|
1311
|
+
return hasEntryPath(root.hooks.beforeShellExecution, 'command', scriptPath, false)
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function globalGitBranchGuardInstalledCopilot() {
|
|
1315
|
+
const scriptPath = globalGitBranchGuardScriptPath()
|
|
1316
|
+
if (!scriptPath) return false
|
|
1317
|
+
const root = readGlobalHookJSON('.copilot', 'settings.json')
|
|
1318
|
+
if (!root || !root.hooks) return false
|
|
1319
|
+
return hasEntryPath(root.hooks.preToolUse, 'bash', scriptPath, true)
|
|
1320
|
+
}
|
|
1321
|
+
|
|
627
1322
|
// ---------------------------------------------------------------------------
|
|
628
1323
|
// generateAttentionScripts — writes the two shell scripts to scripts/
|
|
629
1324
|
// ---------------------------------------------------------------------------
|
|
@@ -689,6 +1384,15 @@ function injectClaudeHooks(cwd) {
|
|
|
689
1384
|
data.hooks.PostToolUse = mergeClaudeHookArray(data.hooks.PostToolUse, 'Write|Edit', GUARD_CMD_CLAUDE)
|
|
690
1385
|
}
|
|
691
1386
|
|
|
1387
|
+
// git branch guard (ROADMAP-2026-08-14, ML-3B/Wave 3, step 1): PreToolUse-only, matcher
|
|
1388
|
+
// "Bash" — blocks raw `git commit`/`git push`/`git checkout -b` before they execute.
|
|
1389
|
+
// Dedup (ROADMAP-2026-08-17 Wave 2/ML-2B): skip the project-scope entry when the global
|
|
1390
|
+
// one is already installed (`trackfw update harness --targets claude-git-branch-guard`),
|
|
1391
|
+
// so the guard doesn't fire (and print its block message) twice per Bash call.
|
|
1392
|
+
if (!globalGitBranchGuardInstalledClaude()) {
|
|
1393
|
+
data.hooks.PreToolUse = mergeClaudeHookArray(data.hooks.PreToolUse, 'Bash', GBG_CMD_CLAUDE)
|
|
1394
|
+
}
|
|
1395
|
+
|
|
692
1396
|
writeJSON(filePath, data)
|
|
693
1397
|
}
|
|
694
1398
|
|
|
@@ -742,6 +1446,14 @@ function injectCodexHooks(cwd) {
|
|
|
742
1446
|
data.hooks.PostToolUse = mergeClaudeHookArray(data.hooks.PostToolUse, 'apply_patch', GUARD_CMD_CODEX)
|
|
743
1447
|
}
|
|
744
1448
|
|
|
1449
|
+
// git branch guard (ROADMAP-2026-08-14, ML-3B/Wave 3, step 2): PreToolUse-only, matcher
|
|
1450
|
+
// "Bash" -- git commands only ever run via the Bash tool, never apply_patch.
|
|
1451
|
+
// Dedup (ROADMAP-2026-08-17 Wave 2/ML-2B): skip project-scope when the global one is
|
|
1452
|
+
// already installed (`trackfw update harness --targets codex-git-branch-guard`).
|
|
1453
|
+
if (!globalGitBranchGuardInstalledCodex()) {
|
|
1454
|
+
data.hooks.PreToolUse = mergeClaudeHookArray(data.hooks.PreToolUse, 'Bash', GBG_CMD_CODEX)
|
|
1455
|
+
}
|
|
1456
|
+
|
|
745
1457
|
writeJSON(filePath, data)
|
|
746
1458
|
}
|
|
747
1459
|
|
|
@@ -808,6 +1520,14 @@ function injectGeminiHooks(cwd) {
|
|
|
808
1520
|
data.hooks.AfterTool = mergeClaudeHookArray(data.hooks.AfterTool, 'write_file|replace', GUARD_CMD_GEMINI)
|
|
809
1521
|
}
|
|
810
1522
|
|
|
1523
|
+
// git branch guard (ROADMAP-2026-08-14, ML-3B/Wave 3, step 3): BeforeTool-only, matcher
|
|
1524
|
+
// "run_shell_command" -- git commands only ever run via the shell tool.
|
|
1525
|
+
// Dedup (ROADMAP-2026-08-17 Wave 2/ML-2B): skip project-scope when the global one is
|
|
1526
|
+
// already installed (`trackfw update harness --targets gemini-git-branch-guard`).
|
|
1527
|
+
if (!globalGitBranchGuardInstalledGemini()) {
|
|
1528
|
+
data.hooks.BeforeTool = mergeClaudeHookArray(data.hooks.BeforeTool, 'run_shell_command', GBG_CMD_GEMINI)
|
|
1529
|
+
}
|
|
1530
|
+
|
|
811
1531
|
writeJSON(filePath, data)
|
|
812
1532
|
}
|
|
813
1533
|
|
|
@@ -958,6 +1678,23 @@ function injectCopilotHooks(cwd) {
|
|
|
958
1678
|
postToolUse.push({ type: 'command', matcher: 'create|edit', bash: GUARD_CMD_COPILOT, cwd: '.', timeoutSec: 10 })
|
|
959
1679
|
}
|
|
960
1680
|
|
|
1681
|
+
// git branch guard (ROADMAP-2026-08-14, ML-3B/Wave 3, step 4): the roadmap describes
|
|
1682
|
+
// `--deny-tool='shell(git commit)'`-style CLI flags in a permissions-config.json/
|
|
1683
|
+
// settings.json file, but no such file/flag mechanism exists anywhere else in this
|
|
1684
|
+
// module -- Copilot's only established deny-adjacent mechanism here is this same
|
|
1685
|
+
// preToolUse/postToolUse hooks file already used for credential-guard above. Mirrors
|
|
1686
|
+
// Go's InjectCopilotHooks (internal/generators/agentfiles.go), which made the same
|
|
1687
|
+
// choice for the same reason; the CLI-flag/permissions-config.json approach is a
|
|
1688
|
+
// documented, deliberate divergence from the roadmap's literal wording. This file is
|
|
1689
|
+
// overwritten wholesale every run, but that only means there is no migration concern
|
|
1690
|
+
// -- it does not exempt this entry from the dedup-against-global check
|
|
1691
|
+
// (ROADMAP-2026-08-17 Wave 2/ML-2B): skip project-scope when the global one is
|
|
1692
|
+
// already installed (`trackfw update harness --targets copilot-git-branch-guard`),
|
|
1693
|
+
// same reasoning as the credential-guard dedup above.
|
|
1694
|
+
if (!globalGitBranchGuardInstalledCopilot()) {
|
|
1695
|
+
preToolUse.push({ type: 'command', matcher: 'bash', bash: GBG_CMD_COPILOT, cwd: '.', timeoutSec: 10 })
|
|
1696
|
+
}
|
|
1697
|
+
|
|
961
1698
|
const data = {
|
|
962
1699
|
version: 1,
|
|
963
1700
|
hooks: { preToolUse, postToolUse },
|
|
@@ -1090,16 +1827,186 @@ function injectCursorHooks(cwd) {
|
|
|
1090
1827
|
}
|
|
1091
1828
|
}
|
|
1092
1829
|
|
|
1830
|
+
// git branch guard (ROADMAP-2026-08-14, ML-3B/Wave 3, step 5): beforeShellExecution-only --
|
|
1831
|
+
// Cursor's dedicated pre-execution Bash event; git commands only ever run there.
|
|
1832
|
+
// Dedup (ROADMAP-2026-08-17 Wave 2/ML-2B): skip project-scope when the global one is
|
|
1833
|
+
// already installed (`trackfw update harness --targets cursor-git-branch-guard`). The
|
|
1834
|
+
// key is intentionally only touched inside this conditional (never created outside it)
|
|
1835
|
+
// so that when BOTH credential-guard and git-branch-guard are deduped away, the key
|
|
1836
|
+
// stays absent from the emitted JSON rather than becoming a present-but-empty array --
|
|
1837
|
+
// matches Go's InjectCursorHooks, which check-agent-hooks-parity.sh's structural
|
|
1838
|
+
// comparator treats as significant (absent key vs empty array is drift, not noise).
|
|
1839
|
+
if (!globalGitBranchGuardInstalledCursor()) {
|
|
1840
|
+
if (!Array.isArray(data.hooks.beforeShellExecution)) data.hooks.beforeShellExecution = []
|
|
1841
|
+
if (!hasEntry(data.hooks.beforeShellExecution, 'command', GBG_CMD_CURSOR)) {
|
|
1842
|
+
data.hooks.beforeShellExecution.push({ command: GBG_CMD_CURSOR })
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1093
1846
|
writeJSON(filePath, data)
|
|
1094
1847
|
}
|
|
1095
1848
|
|
|
1096
1849
|
// ---------------------------------------------------------------------------
|
|
1097
|
-
// Windsurf — update .windsurfrules with attention instruction
|
|
1850
|
+
// Windsurf — update .windsurfrules with attention instruction, and register
|
|
1851
|
+
// the `pre_run_command` git-branch-guard hook in Windsurf's real hooks file.
|
|
1098
1852
|
// ---------------------------------------------------------------------------
|
|
1853
|
+
// Path/schema correction (apolo-tf, 2026-08-14, post-ML-3A audit): the
|
|
1854
|
+
// original ML-3A implementation wrote a dedicated, wholly-owned file at
|
|
1855
|
+
// `.windsurf/hooks/trackfw-git-branch-guard.json` with an invented payload
|
|
1856
|
+
// shape (`{"version":1,"hooks":[{"name":...,"trigger":"pre_run_command",
|
|
1857
|
+
// "action":{...}}]}`) that was flagged in its own doc comment as UNCONFIRMED
|
|
1858
|
+
// against official documentation. A verification pass against
|
|
1859
|
+
// https://docs.devin.ai/desktop/cascade/hooks confirmed both the path and
|
|
1860
|
+
// the shape were wrong:
|
|
1861
|
+
// - Windsurf reads hooks from a single fixed-name file, `.windsurf/hooks.json`
|
|
1862
|
+
// -- NOT a directory of per-hook files under `.windsurf/hooks/`.
|
|
1863
|
+
// - The schema is `{"hooks": {"<event>": [{"command": "...", "show_output":
|
|
1864
|
+
// bool}]}}` -- an object keyed by event name (e.g. "pre_run_command",
|
|
1865
|
+
// "post_run_command"), each mapping to an ARRAY of hook defs. There is no
|
|
1866
|
+
// "name"/"trigger"/"action" envelope.
|
|
1867
|
+
// - The hook script receives its context via stdin as JSON, including
|
|
1868
|
+
// `tool_info.command_line` -- GIT_BRANCH_GUARD_SCRIPT above now tries
|
|
1869
|
+
// this field explicitly (in addition to the generic `.command`/
|
|
1870
|
+
// `.tool_input.command`/`.hook_input.command` fields it already
|
|
1871
|
+
// handled). This is a byte-shared script across all 3 CLIs -- do not
|
|
1872
|
+
// edit it again without mirroring Go's gitBranchGuardScript.
|
|
1873
|
+
//
|
|
1874
|
+
// Merge is idempotent and shaped like every other multi-tool settings file in
|
|
1875
|
+
// this module: existing `pre_run_command` entries from other tools (or a
|
|
1876
|
+
// prior trackfw run) are preserved; only an entry with our exact command
|
|
1877
|
+
// string is deduped via mergeWindsurfHookArray. Other events already present
|
|
1878
|
+
// (e.g. a user- or third-party-authored `post_run_command`) are left
|
|
1879
|
+
// untouched.
|
|
1880
|
+
//
|
|
1881
|
+
// Migration: if the stale `.windsurf/hooks/trackfw-git-branch-guard.json`
|
|
1882
|
+
// file from the incorrect ML-3A version exists on disk, it is removed here
|
|
1883
|
+
// (never left orphaned), and the now-possibly-empty legacy `.windsurf/hooks`
|
|
1884
|
+
// directory is best-effort cleaned up too -- mirrors Go's InjectWindsurfHooks
|
|
1885
|
+
// migration step exactly (os.Remove + best-effort rmdir).
|
|
1886
|
+
//
|
|
1887
|
+
// `windsurf.cascadeCommandsAllowList` (an IDE *user settings* key, not a
|
|
1888
|
+
// project-local file) remains out of scope -- same reasoning as before this
|
|
1889
|
+
// fix: trackfw has no established, confirmed mechanism for rewriting IDE user
|
|
1890
|
+
// settings safely, and inventing one on a guess repeats the exact mistake
|
|
1891
|
+
// this fix corrects. Documented as an open gap in docs/cli-parity.md.
|
|
1892
|
+
const LEGACY_WINDSURF_HOOKS_FILE = 'trackfw-git-branch-guard.json'
|
|
1099
1893
|
|
|
1100
1894
|
function injectWindsurfHooks(cwd) {
|
|
1101
1895
|
const { injectRulesForTool } = require('./init')
|
|
1102
|
-
|
|
1896
|
+
injectRulesForTool('windsurf', cwd)
|
|
1897
|
+
|
|
1898
|
+
// Migration: remove the incorrect, previously-written dedicated hook file
|
|
1899
|
+
// from an older (buggy) trackfw run, so it doesn't linger as a dead,
|
|
1900
|
+
// never-consumed artifact once the correct .windsurf/hooks.json exists.
|
|
1901
|
+
const legacyDir = path.join(cwd, '.windsurf', 'hooks')
|
|
1902
|
+
const legacyPath = path.join(legacyDir, LEGACY_WINDSURF_HOOKS_FILE)
|
|
1903
|
+
try {
|
|
1904
|
+
fs.unlinkSync(legacyPath)
|
|
1905
|
+
} catch (e) {
|
|
1906
|
+
if (e.code !== 'ENOENT') throw e
|
|
1907
|
+
}
|
|
1908
|
+
// Best-effort cleanup of the now-possibly-empty legacy directory; ignore
|
|
1909
|
+
// failure (non-empty dir, e.g. holding unrelated user files, or already
|
|
1910
|
+
// gone) -- never fatal.
|
|
1911
|
+
try {
|
|
1912
|
+
fs.rmdirSync(legacyDir)
|
|
1913
|
+
} catch (_) {
|
|
1914
|
+
// ignore
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
const dir = path.join(cwd, '.windsurf')
|
|
1918
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
1919
|
+
const filePath = path.join(dir, 'hooks.json')
|
|
1920
|
+
const data = readJSON(filePath)
|
|
1921
|
+
|
|
1922
|
+
if (typeof data.hooks !== 'object' || data.hooks === null || Array.isArray(data.hooks)) {
|
|
1923
|
+
data.hooks = {}
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
data.hooks.pre_run_command = mergeWindsurfHookArray(data.hooks.pre_run_command, GBG_CMD_WINDSURF, true)
|
|
1927
|
+
|
|
1928
|
+
writeJSON(filePath, data)
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
// ---------------------------------------------------------------------------
|
|
1932
|
+
// Amazon Q Developer CLI — .amazonq/cli-agents/q_cli_default.json
|
|
1933
|
+
// (ROADMAP-2026-08-14, ML-3B/Wave 3, step 7)
|
|
1934
|
+
// ---------------------------------------------------------------------------
|
|
1935
|
+
// Path/schema correction (apolo-tf, 2026-08-14, post-ML-3A audit): the
|
|
1936
|
+
// original ML-3A implementation wrote to `.amazonq/settings.json`, which was
|
|
1937
|
+
// flagged in its own doc comment as NOT independently confirmed against
|
|
1938
|
+
// official Amazon Q documentation. Verification against
|
|
1939
|
+
// https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-custom-agents-configuration.html
|
|
1940
|
+
// and https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-agents-default-behavior.html
|
|
1941
|
+
// confirmed the real mechanism is a *custom agent* file, not a settings
|
|
1942
|
+
// file: `.amazonq/cli-agents/q_cli_default.json` -- `q_cli_default.json` is
|
|
1943
|
+
// the documented convention closest to activating automatically without
|
|
1944
|
+
// requiring a manual `--agent` flag, though AWS has an open bug where this
|
|
1945
|
+
// default-name override is not always honored
|
|
1946
|
+
// (github.com/aws/amazon-q-developer-cli#2922) -- a custom agent named
|
|
1947
|
+
// `q_cli_default.json` is not guaranteed to always be picked up
|
|
1948
|
+
// automatically depending on CLI version/config. Documented rather than
|
|
1949
|
+
// worked around.
|
|
1950
|
+
//
|
|
1951
|
+
// Two guard mechanisms are wired, unchanged from the original ML-3A
|
|
1952
|
+
// implementation -- only the target file (and the minimal-but-valid custom
|
|
1953
|
+
// agent envelope around it) moved:
|
|
1954
|
+
// - hooks.preToolUse[matcher:"execute_bash"] -> the guard script, same
|
|
1955
|
+
// matcher+hooks[].command shape already used by Claude/Codex/Gemini
|
|
1956
|
+
// above (reuses mergeClaudeHookArray for idempotent merge).
|
|
1957
|
+
// - toolsSettings.execute_bash.deniedCommands -> a regex denylist
|
|
1958
|
+
// evaluated before allow, independent of and in addition to the hook.
|
|
1959
|
+
//
|
|
1960
|
+
// Minimal-but-valid custom agent schema (per command-line-custom-agents-
|
|
1961
|
+
// configuration.html): only set defaults for fields not already present, so
|
|
1962
|
+
// re-running against a hand-edited or previously-generated file never
|
|
1963
|
+
// clobbers user customization -- same "preserve existing settings" contract
|
|
1964
|
+
// as every other merge-based injector in this module. `tools: ["*"]` is
|
|
1965
|
+
// written on first creation so the default agent keeps today's unrestricted
|
|
1966
|
+
// tool access (this fix does not narrow what any agent can do, only where
|
|
1967
|
+
// the deny wiring lives). Mirrors Go's InjectAmazonQHooks
|
|
1968
|
+
// (internal/generators/agentfiles.go) field-for-field.
|
|
1969
|
+
//
|
|
1970
|
+
// Native custom-agent toolset restriction (REQ acceptance criterion): still
|
|
1971
|
+
// NOT implemented here -- this ML only wires the guard/deny fields on the
|
|
1972
|
+
// one default agent file; per-specialist-agent toolset restriction is out of
|
|
1973
|
+
// scope, same limitation already accepted for Gemini above.
|
|
1974
|
+
const AMAZONQ_CLI_AGENTS_DIR = 'cli-agents'
|
|
1975
|
+
const AMAZONQ_DEFAULT_AGENT_FILE = 'q_cli_default.json'
|
|
1976
|
+
const GBG_DENIED_COMMANDS_PATTERN = '^git (commit|push|checkout -b)'
|
|
1977
|
+
|
|
1978
|
+
function injectAmazonQHooks(cwd) {
|
|
1979
|
+
const filePath = path.join(cwd, '.amazonq', AMAZONQ_CLI_AGENTS_DIR, AMAZONQ_DEFAULT_AGENT_FILE)
|
|
1980
|
+
const data = readJSON(filePath)
|
|
1981
|
+
|
|
1982
|
+
const defaults = {
|
|
1983
|
+
name: 'q_cli_default',
|
|
1984
|
+
description: 'trackfw-managed default agent — wires the git branch guard hook/denylist. See docs/cli-parity.md.',
|
|
1985
|
+
prompt: null,
|
|
1986
|
+
mcpServers: {},
|
|
1987
|
+
tools: ['*'],
|
|
1988
|
+
toolAliases: {},
|
|
1989
|
+
allowedTools: [],
|
|
1990
|
+
resources: [],
|
|
1991
|
+
useLegacyMcpJson: false,
|
|
1992
|
+
}
|
|
1993
|
+
for (const [k, v] of Object.entries(defaults)) {
|
|
1994
|
+
if (!Object.prototype.hasOwnProperty.call(data, k)) data[k] = v
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
if (!data.hooks) data.hooks = {}
|
|
1998
|
+
data.hooks.preToolUse = mergeClaudeHookArray(data.hooks.preToolUse, 'execute_bash', GBG_CMD_PLAIN)
|
|
1999
|
+
|
|
2000
|
+
if (!data.toolsSettings) data.toolsSettings = {}
|
|
2001
|
+
if (!data.toolsSettings.execute_bash) data.toolsSettings.execute_bash = {}
|
|
2002
|
+
if (!Array.isArray(data.toolsSettings.execute_bash.deniedCommands)) {
|
|
2003
|
+
data.toolsSettings.execute_bash.deniedCommands = []
|
|
2004
|
+
}
|
|
2005
|
+
if (!data.toolsSettings.execute_bash.deniedCommands.includes(GBG_DENIED_COMMANDS_PATTERN)) {
|
|
2006
|
+
data.toolsSettings.execute_bash.deniedCommands.push(GBG_DENIED_COMMANDS_PATTERN)
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
writeJSON(filePath, data)
|
|
1103
2010
|
}
|
|
1104
2011
|
|
|
1105
2012
|
// ---------------------------------------------------------------------------
|
|
@@ -1146,6 +2053,13 @@ function injectHooksDetected(cwd) {
|
|
|
1146
2053
|
check: () => fs.existsSync(path.join(root, '.windsurfrules')),
|
|
1147
2054
|
fn: injectWindsurfHooks,
|
|
1148
2055
|
},
|
|
2056
|
+
// amazonq (ROADMAP-2026-08-14, ML-3B/Wave 3, step 7 dispatch): mirrors Go's
|
|
2057
|
+
// hooks.go InjectHooksDetected entry -- dispatches injectAmazonQHooks (git branch
|
|
2058
|
+
// guard, .amazonq/settings.json) whenever the .amazonq directory is present.
|
|
2059
|
+
amazonq: {
|
|
2060
|
+
check: () => fs.existsSync(path.join(root, '.amazonq')),
|
|
2061
|
+
fn: injectAmazonQHooks,
|
|
2062
|
+
},
|
|
1149
2063
|
}
|
|
1150
2064
|
|
|
1151
2065
|
for (const [name, { check, fn }] of Object.entries(detections)) {
|
|
@@ -1162,6 +2076,8 @@ module.exports = {
|
|
|
1162
2076
|
generateAttentionScripts,
|
|
1163
2077
|
generateCredentialGuardScript,
|
|
1164
2078
|
generateGlobalCredentialGuardScript,
|
|
2079
|
+
generateGitBranchGuardScript,
|
|
2080
|
+
generateGlobalGitBranchGuardScript,
|
|
1165
2081
|
injectClaudeHooks,
|
|
1166
2082
|
injectCodexHooks,
|
|
1167
2083
|
injectGeminiHooks,
|
|
@@ -1169,8 +2085,11 @@ module.exports = {
|
|
|
1169
2085
|
injectCopilotHooks,
|
|
1170
2086
|
injectCursorHooks,
|
|
1171
2087
|
injectWindsurfHooks,
|
|
2088
|
+
injectAmazonQHooks,
|
|
1172
2089
|
injectHooksDetected,
|
|
1173
2090
|
mergeClaudeHookArray,
|
|
1174
2091
|
mergeSimpleCommandArray,
|
|
1175
2092
|
mergeCopilotHookArray,
|
|
2093
|
+
normalizeGuardPath,
|
|
2094
|
+
samePathCommand,
|
|
1176
2095
|
}
|