trackfw 6.8.0 → 6.9.1

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 CHANGED
@@ -722,6 +722,71 @@ The Codex integration is repository-scoped: `AGENTS.md` carries persistent instr
722
722
 
723
723
  ---
724
724
 
725
+ ## Credential guard: detection, not prevention
726
+
727
+ trackfw can generate a **credential guard** hook that blocks agents from materializing real
728
+ credentials. `trackfw validate` also reports when that guard looks compromised:
729
+
730
+ - the hook is registered but the script is **missing** or not executable;
731
+ - the script's **contents diverge** from the template this version of trackfw generates;
732
+ - `credential_guard.mode` was **downgraded** from `block` on disk relative to your last commit.
733
+
734
+ **Read this before relying on it.**
735
+
736
+ These checks are **detection, not prevention.** trackfw *tells you* something diverged — it does
737
+ **not** stop it from happening. This is not an implementation gap: it was measured that no technical
738
+ prevention is possible, within trackfw's scope, against an agent that has unrestricted write access
739
+ to your workspace. An agent that can edit your files can delete the guard script, overwrite it with
740
+ a no-op, or relax the mode — and on most agent CLIs, a hook that fails to run simply lets the tool
741
+ call proceed.
742
+
743
+ What these checks are **not** able to see:
744
+
745
+ - **changes the adversary also commits** — once it is in your `HEAD`, there is nothing to diverge
746
+ from. What remains is the audit trail: the change shows up in your diff and in review.
747
+ - **anything with no commit to compare against** — a repository with no commits, or a file that was
748
+ never versioned. The check stays silent rather than guessing.
749
+ - **the difference between tampering and you being out of date.** The script carries no version
750
+ marker, so a divergence may simply mean you have not run `trackfw update` since upgrading. That is
751
+ why the message is worded neutrally and the rule defaults to a **warning**, which does **not**
752
+ change `validate`'s exit code. If you want CI to fail on it:
753
+
754
+ ```yaml
755
+ # trackfw.yaml
756
+ rules:
757
+ credential_guard_script_integrity: error
758
+ ```
759
+
760
+ - **the check can be switched off by the same edit it is meant to catch.** Rule severity is read
761
+ from `rules:` in the `trackfw.yaml` **on disk** — so an edit that both downgrades
762
+ `credential_guard.mode` *and* sets `credential_guard_mode_downgrade: off` silences the report,
763
+ with nothing committed. Anchoring `rules:` to your last commit is a known open item.
764
+
765
+ ### If `trackfw update` suddenly reports something it used to ignore
766
+
767
+ Since the version that introduced anchoring, the credential-guard checks are resolved against **your
768
+ last commit**, not only against the file on disk — and they can **no longer be suppressed** via
769
+ `.trackfw-baseline.json`. This is deliberate: a check that can be switched off by the same
770
+ uncommitted edit it is meant to catch is not a check.
771
+
772
+ If one of them starts reporting after an upgrade, you have two legitimate ways out, and both leave a
773
+ trail:
774
+
775
+ ```yaml
776
+ # trackfw.yaml — commit this change
777
+ rules:
778
+ credential_guard_hook_resolvable: off
779
+ ```
780
+
781
+ ...or fix the underlying cause (usually `trackfw update`, to regenerate the guard script and wiring).
782
+
783
+ One caveat worth knowing: `governance_mode: lenient` still turns **every** finding into a warning,
784
+ including these. Closing that is tracked separately.
785
+
786
+ The strongest protection remains the ordinary one: the guard script and `trackfw.yaml` are
787
+ **versioned files**. Review their diffs like you review any other code. Every limitation above has
788
+ the same escape hatch: the change is *visible* in `git diff`.
789
+
725
790
  ## What trackfw is not
726
791
 
727
792
  - Not a project management SaaS — no accounts, no cloud sync, no data leaving your repository. A local dashboard is available via `trackfw serve`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trackfw",
3
- "version": "6.8.0",
3
+ "version": "6.9.1",
4
4
  "description": "Governance CLI for AI-native software delivery and AI coding agents. Enforces traceability: ADR → REQ → ROADMAP → kanban. Native support for Codex, Claude Code, Gemini CLI, Cursor, Copilot, Windsurf, Amazon Q, OpenCode, and Kiro.",
5
5
  "keywords": [
6
6
  "cli",
@@ -277,6 +277,28 @@ function parse(content, cfg) {
277
277
  return false;
278
278
  }
279
279
 
280
+ // parseRulesFromContent parses only the `rules:` mapping out of arbitrary trackfw.yaml content
281
+ // (e.g. a git-HEAD blob obtained via `git show HEAD:./trackfw.yaml`, not the CWD file load()
282
+ // reads) and returns it as name->severity. Used by the validator's HEAD-anchored credential-guard
283
+ // rules — see ADR-2026-08-12-severidade-das-regras-de-credential-guard-resolvida-pela-mais-estrita-
284
+ // entre-head-e-disco.md — which need `rules:` as it existed at a specific git ref, not the CWD, so
285
+ // they cannot go through the load() singleton (always reads the CWD file and caches it once per
286
+ // process). Reuses parse() over an ephemeral cfg object, mirroring Go's ParseRulesFromContent, so
287
+ // this and load() can never diverge on how `rules:` itself is read — purely additive, does not
288
+ // touch load()/parse() otherwise. Malformed content (parse() returning true) is treated the same
289
+ // as "no rules:" — an empty map — since the caller only wants a best-effort read of a historical
290
+ // git blob, not a fatal exit like load() has for the live CWD file.
291
+ function parseRulesFromContent(content) {
292
+ // parse() assumes cfg already has the nested-object shape defaults() provides for
293
+ // credentialGuard/update/sync/linkFields (it assigns into them, e.g. cfg.update.hooks = ...,
294
+ // never creates them) — so this cannot be a bare { rules: {} } literal or parse() throws on any
295
+ // content that sets one of those nested keys. rules starts empty (not seeded from defaults()),
296
+ // deliberately: the caller wants exactly what `rules:` in content declares, nothing else.
297
+ const cfg = { rules: {}, credentialGuard: {}, update: {}, sync: {}, linkFields: {} };
298
+ parse(content, cfg);
299
+ return cfg.rules;
300
+ }
301
+
280
302
  const NAMESPACING_FLAT = 'flat';
281
303
  const NAMESPACING_BY_AGENT = 'by_agent';
282
304
 
@@ -285,6 +307,7 @@ module.exports = {
285
307
  reset,
286
308
  defaults,
287
309
  expandPath,
310
+ parseRulesFromContent,
288
311
  NAMESPACING_FLAT,
289
312
  NAMESPACING_BY_AGENT,
290
313
  MALFORMED_CONFIG_MESSAGE,
@@ -3,7 +3,7 @@ name: trackfw-code-quality
3
3
  description: Code quality specialist for maintainability, static analysis, refactoring and quality gates.
4
4
  model: sonnet
5
5
  memory: project
6
- tools: Read, Grep, Glob, Bash, WebSearch, AskUserQuestion
6
+ tools: Read, Grep, Glob, Bash, WebSearch, AskUserQuestion, Write, Edit
7
7
  ---
8
8
 
9
9
  # Code Quality
@@ -24,13 +24,20 @@ Append an entry to `docs/agents-working-context.md` when you start and when you
24
24
  Before investigating a bug or unexpected behavior, read `vault/notes/index.md` when it exists and open the related notes. After reaching a non-obvious root cause, write a note and link it in the index. Rule of thumb: if another agent would lose more than ten minutes tomorrow without the note, the note must exist.
25
25
 
26
26
  ## Governance prerequisite
27
- Do not edit code without a requirement and a roadmap already in the `wip` state. Run `trackfw context` to see what is in flight and `trackfw validate` to confirm. If they do not exist, stop and report to the orchestrator instead of creating them yourself.
27
+ Do not produce deliverables without a requirement and a roadmap already in the `wip` state. Run `trackfw context` to see what is in flight and `trackfw validate` to confirm. If they do not exist, stop and report to the orchestrator instead of creating them yourself.
28
28
 
29
29
  ## Reporting boundary
30
- You do not modify code. Report findings ordered by severity, each with concrete evidence (file, line, and the observed behavior), and hand off the fix to the role that owns the code. Never weaken a control, a test or a permission to make something pass.
30
+ You do not modify **product code** — `internal/`, `npm/src/`, `pypi/trackfw/` and their tests. Report
31
+ findings ordered by severity, each with concrete evidence (file, line, and the observed behavior),
32
+ and hand off the fix to the role that owns the code. Never weaken a control, a test or a permission
33
+ to make something pass.
34
+
35
+ You **do** write your own artifacts, and refusing to is a scope error in the opposite direction: your
36
+ report or assessment, the entry in `docs/agents-working-context.md`, and any documentation the
37
+ orchestrator assigns you. Writing these is not "modifying code".
31
38
 
32
39
  ## Git authority
33
- This role never executes Git operations — no `branch`, `commit`, `push`, `checkout`, `merge`, `rebase` or `stash`. `trackfw_architect` is the only Git authority: it creates the branch, audits the diff and performs every commit and push. Act only on a self-contained handoff from `trackfw_architect`; refuse to implement anything without one.
40
+ This role never executes Git operations — no `branch`, `commit`, `push`, `checkout`, `merge`, `rebase` or `stash`. `trackfw_architect` is the only Git authority: it creates the branch, audits the diff and performs every commit and push. Act only on a self-contained handoff from `trackfw_architect`; refuse to act without one.
34
41
 
35
42
  ## Definition of done
36
43
  Green build and tests do not close a microbatch. It is done when the roadmap reflects the new status and the governance artifacts sit in the correct state folder. Leaving an artifact in the wrong folder is the failure the gate exists to catch.
@@ -3,7 +3,7 @@ name: trackfw-security
3
3
  description: Security reviewer for trust boundaries, secrets, injection, permissions and supply-chain risk.
4
4
  model: sonnet
5
5
  memory: project
6
- tools: Read, Grep, Glob, Bash, WebSearch, AskUserQuestion
6
+ tools: Read, Grep, Glob, Bash, WebSearch, AskUserQuestion, Write, Edit
7
7
  ---
8
8
 
9
9
  # Security
@@ -24,13 +24,20 @@ Append an entry to `docs/agents-working-context.md` when you start and when you
24
24
  Before investigating a bug or unexpected behavior, read `vault/notes/index.md` when it exists and open the related notes. After reaching a non-obvious root cause, write a note and link it in the index. Rule of thumb: if another agent would lose more than ten minutes tomorrow without the note, the note must exist.
25
25
 
26
26
  ## Governance prerequisite
27
- Do not edit code without a requirement and a roadmap already in the `wip` state. Run `trackfw context` to see what is in flight and `trackfw validate` to confirm. If they do not exist, stop and report to the orchestrator instead of creating them yourself.
27
+ Do not produce deliverables without a requirement and a roadmap already in the `wip` state. Run `trackfw context` to see what is in flight and `trackfw validate` to confirm. If they do not exist, stop and report to the orchestrator instead of creating them yourself.
28
28
 
29
29
  ## Reporting boundary
30
- You do not modify code. Report findings ordered by severity, each with concrete evidence (file, line, and the observed behavior), and hand off the fix to the role that owns the code. Never weaken a control, a test or a permission to make something pass.
30
+ You do not modify **product code** — `internal/`, `npm/src/`, `pypi/trackfw/` and their tests. Report
31
+ findings ordered by severity, each with concrete evidence (file, line, and the observed behavior),
32
+ and hand off the fix to the role that owns the code. Never weaken a control, a test or a permission
33
+ to make something pass.
34
+
35
+ You **do** write your own artifacts, and refusing to is a scope error in the opposite direction: your
36
+ report or assessment, the entry in `docs/agents-working-context.md`, and any documentation the
37
+ orchestrator assigns you. Writing these is not "modifying code".
31
38
 
32
39
  ## Git authority
33
- This role never executes Git operations — no `branch`, `commit`, `push`, `checkout`, `merge`, `rebase` or `stash`. `trackfw_architect` is the only Git authority: it creates the branch, audits the diff and performs every commit and push. Act only on a self-contained handoff from `trackfw_architect`; refuse to implement anything without one.
40
+ This role never executes Git operations — no `branch`, `commit`, `push`, `checkout`, `merge`, `rebase` or `stash`. `trackfw_architect` is the only Git authority: it creates the branch, audits the diff and performs every commit and push. Act only on a self-contained handoff from `trackfw_architect`; refuse to act without one.
34
41
 
35
42
  ## Definition of done
36
43
  Green build and tests do not close a microbatch. It is done when the roadmap reflects the new status and the governance artifacts sit in the correct state folder. Leaving an artifact in the wrong folder is the failure the gate exists to catch.
@@ -3,7 +3,7 @@ name: trackfw-ux
3
3
  description: UX and UI specialist for research, interaction design, design systems and accessibility.
4
4
  model: sonnet
5
5
  memory: project
6
- tools: Read, Grep, Glob, Bash, WebSearch, AskUserQuestion
6
+ tools: Read, Grep, Glob, Bash, WebSearch, AskUserQuestion, Write, Edit
7
7
  ---
8
8
 
9
9
  # UX
@@ -24,13 +24,20 @@ Append an entry to `docs/agents-working-context.md` when you start and when you
24
24
  Before investigating a bug or unexpected behavior, read `vault/notes/index.md` when it exists and open the related notes. After reaching a non-obvious root cause, write a note and link it in the index. Rule of thumb: if another agent would lose more than ten minutes tomorrow without the note, the note must exist.
25
25
 
26
26
  ## Governance prerequisite
27
- Do not edit code without a requirement and a roadmap already in the `wip` state. Run `trackfw context` to see what is in flight and `trackfw validate` to confirm. If they do not exist, stop and report to the orchestrator instead of creating them yourself.
27
+ Do not produce deliverables without a requirement and a roadmap already in the `wip` state. Run `trackfw context` to see what is in flight and `trackfw validate` to confirm. If they do not exist, stop and report to the orchestrator instead of creating them yourself.
28
28
 
29
29
  ## Reporting boundary
30
- You do not modify code. Report findings ordered by severity, each with concrete evidence (file, line, and the observed behavior), and hand off the fix to the role that owns the code. Never weaken a control, a test or a permission to make something pass.
30
+ You do not modify **product code** — `internal/`, `npm/src/`, `pypi/trackfw/` and their tests. Report
31
+ findings ordered by severity, each with concrete evidence (file, line, and the observed behavior),
32
+ and hand off the fix to the role that owns the code. Never weaken a control, a test or a permission
33
+ to make something pass.
34
+
35
+ You **do** write your own artifacts, and refusing to is a scope error in the opposite direction: your
36
+ report or assessment, the entry in `docs/agents-working-context.md`, and any documentation the
37
+ orchestrator assigns you. Writing these is not "modifying code".
31
38
 
32
39
  ## Git authority
33
- This role never executes Git operations — no `branch`, `commit`, `push`, `checkout`, `merge`, `rebase` or `stash`. `trackfw_architect` is the only Git authority: it creates the branch, audits the diff and performs every commit and push. Act only on a self-contained handoff from `trackfw_architect`; refuse to implement anything without one.
40
+ This role never executes Git operations — no `branch`, `commit`, `push`, `checkout`, `merge`, `rebase` or `stash`. `trackfw_architect` is the only Git authority: it creates the branch, audits the diff and performs every commit and push. Act only on a self-contained handoff from `trackfw_architect`; refuse to act without one.
34
41
 
35
42
  ## Definition of done
36
43
  Green build and tests do not close a microbatch. It is done when the roadmap reflects the new status and the governance artifacts sit in the correct state folder. Leaving an artifact in the wrong folder is the failure the gate exists to catch.
@@ -0,0 +1,72 @@
1
+ 'use strict'
2
+
3
+ const { execFileSync } = require('child_process')
4
+
5
+ // ROADMAP-2026-08-12-ancorar-rules-no-head-para-as-regras-de-credential-guard, ML-1B.
6
+ // ADR: docs/adr/ADR-2026-08-12-severidade-das-regras-de-credential-guard-resolvida-pela-mais-
7
+ // estrita-entre-head-e-disco.md (Emenda 3).
8
+ //
9
+ // Achado do ML-3B, reproduzido por Zeus: headTrackfwYAML() e isGitWorktree() (index.js) invocavam
10
+ // execSync('git ...') sem limpar o ambiente herdado do processo — GIT_DIR/GIT_WORK_TREE
11
+ // redirecionados para outro repositório git (sem trackfw.yaml versionado) faziam a resolução do
12
+ // HEAD falhar EM SILÊNCIO, e credentialGuardRuleSeverity caía só no disco: derrota o mecanismo M4
13
+ // inteiro sem nenhum commit e sem sequer editar trackfw.yaml. Exposição NOVA para
14
+ // credential_guard_script_integrity e credential_guard_hook_resolvable — elas não dependiam de git
15
+ // antes do M4.
16
+ //
17
+ // TODA invocação de git em npm/src/validator passa a ir por gitOutput() abaixo — nunca chamar
18
+ // execSync/execFileSync('git', ...) diretamente em index.js.
19
+
20
+ // GIT_ENV_PREFIX é o prefixo de TODA variável de ambiente que o git(1) reconhece como
21
+ // configuração (GIT_DIR, GIT_WORK_TREE, GIT_CONFIG_*, GIT_CEILING_DIRECTORIES, etc.).
22
+ //
23
+ // A tentativa inicial deste ML era uma lista fechada (GIT_DIR, GIT_WORK_TREE, GIT_COMMON_DIR,
24
+ // GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY, GIT_ALTERNATE_OBJECT_DIRECTORIES,
25
+ // GIT_CEILING_DIRECTORIES, GIT_NAMESPACE) justificada por "variáveis que redirecionam ONDE o
26
+ // repositório é lido". Essa justificativa estava ERRADA: o vetor real não é redirecionamento, é
27
+ // fazer o `git` sair com status != 0 por QUALQUER motivo — toda chamada deste módulo trata falha
28
+ // do subprocesso como "sem âncora, silêncio" (headTrackfwYAML) ou "fallback para disco"
29
+ // (credentialGuardRuleSeverity), então qualquer variável capaz de tornar o git fatal já é um
30
+ // bypass, redirecionando ou não. Provado com GIT_CONFIG_COUNT=abc (fora da lista fechada — injeta
31
+ // config arbitrária via GIT_CONFIG_COUNT/GIT_CONFIG_KEY_n/GIT_CONFIG_VALUE_n, e um valor
32
+ // malformado faz `git rev-parse --is-inside-work-tree` sair com "fatal: unable to parse
33
+ // command-line config", exit 128): a lista fechada não a cobria, e
34
+ // `credential_guard_mode_downgrade` silenciava por inteiro (headTrackfwYAML retornava ok=false),
35
+ // não só a severidade das outras duas regras.
36
+ //
37
+ // Por isso a abordagem correta é NEGATIVA por prefixo, não uma enumeração positiva: nenhuma
38
+ // invocação deste módulo (`rev-parse`, `show`, `log`, `symbolic-ref`) depende de qualquer GIT_*
39
+ // herdada do ambiente para funcionar corretamente — `-C dir` já ancora explicitamente o
40
+ // repositório, então git redescobre a partir de dir como se tivesse sido iniciado lá. Contextos
41
+ // legítimos que setam GIT_* (hooks do próprio git, `git submodule foreach`, worktrees vinculadas)
42
+ // continuam funcionando sem essas variáveis porque a descoberta normal a partir de `-C dir` já
43
+ // resolve o mesmo repositório.
44
+ const GIT_ENV_PREFIX = 'GIT_'
45
+
46
+ // cleanGitEnv retorna uma cópia de process.env sem nenhuma variável cujo nome comece com
47
+ // GIT_ENV_PREFIX — usado como `env` de toda invocação de git deste módulo.
48
+ function cleanGitEnv() {
49
+ const env = {}
50
+ for (const key of Object.keys(process.env)) {
51
+ if (!key.startsWith(GIT_ENV_PREFIX)) {
52
+ env[key] = process.env[key]
53
+ }
54
+ }
55
+ return env
56
+ }
57
+
58
+ // gitOutput executa `git -C dir ...args` ancorado explicitamente em dir via `-C` — nunca
59
+ // dependendo só do cwd do processo/descoberta implícita — e com toda variável GIT_* removida
60
+ // do ambiente herdado. Lança (mesmo comportamento de execSync) se o comando sair com status != 0.
61
+ // dir vazio/undefined cai para process.cwd(), o mesmo comportamento que todo call site já assumia
62
+ // implicitamente antes deste ML.
63
+ function gitOutput(dir, args) {
64
+ const root = dir || process.cwd()
65
+ return execFileSync('git', ['-C', root, ...args], {
66
+ encoding: 'utf8',
67
+ stdio: ['pipe', 'pipe', 'pipe'],
68
+ env: cleanGitEnv()
69
+ })
70
+ }
71
+
72
+ module.exports = { gitOutput, cleanGitEnv, GIT_ENV_PREFIX }
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fs = require('fs')
4
4
  const path = require('path')
5
- const { execSync } = require('child_process')
5
+ const { gitOutput } = require('./git-exec')
6
6
  const config = require('../config')
7
7
  const { checkTraceIds } = require('./traceid')
8
8
 
@@ -138,10 +138,7 @@ function findAdrFile(basename) {
138
138
  // Retorna null em caso de erro ou se não houver commits.
139
139
  function gitLastModifiedTime(filePath) {
140
140
  try {
141
- const out = execSync(`git log -1 --format=%ct -- "${filePath}"`, {
142
- encoding: 'utf8',
143
- stdio: ['pipe', 'pipe', 'pipe']
144
- }).trim()
141
+ const out = gitOutput('.', ['log', '-1', '--format=%ct', '--', filePath]).trim()
145
142
  if (out) return parseInt(out, 10) * 1000 // converter para ms
146
143
  } catch (_) {}
147
144
  return null
@@ -1085,11 +1082,10 @@ function branchNoMatchingRoadmapMessage(branch, candidates) {
1085
1082
  // wip/ ou done/ cujo slug case com a branch. Aceita done/ para permitir encerramento do roadmap na
1086
1083
  // própria branch, conforme a Definition of Done, sem reprovar o gate.
1087
1084
  function validateBranchHasWIPRoadmap() {
1088
- const { execSync } = require('child_process')
1089
1085
  let branch = process.env.TRACKFW_BRANCH || ''
1090
1086
  if (!branch && isGitWorktree(process.cwd())) {
1091
1087
  try {
1092
- branch = execSync('git symbolic-ref --short HEAD', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
1088
+ branch = gitOutput(process.cwd(), ['symbolic-ref', '--short', 'HEAD']).trim()
1093
1089
  } catch {
1094
1090
  branch = ''
1095
1091
  }
@@ -1152,15 +1148,428 @@ function validateNoteOrphan(cwd) {
1152
1148
  return msgs
1153
1149
  }
1154
1150
 
1151
+ // CREDENTIAL_GUARD_SCRIPT_MARKER é o nome do script que a regra credential_guard_hook_resolvable
1152
+ // procura dentro dos comandos de hook de projeto.
1153
+ const CREDENTIAL_GUARD_SCRIPT_MARKER = 'trackfw-credential-guard.sh'
1154
+
1155
+ // CREDENTIAL_GUARD_HOOK_FILES é a lista fechada dos arquivos de hook de PROJETO que o trackfw
1156
+ // gera hoje e que podem conter uma entrada de credential-guard (ROADMAP-2026-08-12-mitigacao-do
1157
+ // -fail-open-do-credential-guard, ML-1A). Hooks de escopo GLOBAL (~/.trackfw/..., trackfw update
1158
+ // harness) ficam fora — caso distinto, fora do repositório do usuário, e a checagem de dedup
1159
+ // globalCredentialGuardInstalled*() já os pula de propósito nas entradas de projeto.
1160
+ const CREDENTIAL_GUARD_HOOK_FILES = [
1161
+ { relPath: '.claude/settings.json', cli: 'Claude Code' },
1162
+ { relPath: '.codex/hooks.json', cli: 'Codex CLI' },
1163
+ { relPath: '.gemini/settings.json', cli: 'Gemini CLI' },
1164
+ { relPath: '.cursor/hooks.json', cli: 'Cursor' },
1165
+ { relPath: '.github/hooks/trackfw-attention.json', cli: 'GitHub Copilot CLI' },
1166
+ { relPath: '.kiro/hooks/trackfw-attention.json', cli: 'Kiro' },
1167
+ ]
1168
+
1169
+ // resolveCredentialGuardHookPath resolve o valor bruto de um comando de hook (string extraída do
1170
+ // JSON) para um caminho de arquivo absoluto, usando exatamente as 3 formas de prefixo que o
1171
+ // trackfw emite hoje (docs/cli-parity.md, "Mecanismo de resolução de caminho dos hooks de
1172
+ // projeto, por CLI"):
1173
+ // 1. "$CLAUDE_PROJECT_DIR/…" / "$GEMINI_PROJECT_DIR/…" — placeholder de env var expandido em
1174
+ // runtime pelo próprio CLI, substituído aqui pela raiz do projeto.
1175
+ // 2. '"$(git rev-parse --show-toplevel)/…"' — substituição de shell entre aspas literais
1176
+ // (Codex). As aspas fazem parte do valor emitido e são removidas antes de resolver contra a
1177
+ // raiz do projeto.
1178
+ // 3. Caminho relativo puro, sem prefixo nenhum (Cursor/Copilot/Kiro) — resolvido diretamente
1179
+ // contra a raiz do projeto.
1180
+ // Qualquer valor que não bata em nenhuma das 3 formas retorna null — o chamador NÃO deve tratar
1181
+ // isso como violação. Não é função desta regra adivinhar wiring próprio do usuário fora dos
1182
+ // formatos que o trackfw gera.
1183
+ function resolveCredentialGuardHookPath(raw, root) {
1184
+ const claudePrefix = '$CLAUDE_PROJECT_DIR/'
1185
+ const geminiPrefix = '$GEMINI_PROJECT_DIR/'
1186
+ const codexPrefix = '"$(git rev-parse --show-toplevel)/'
1187
+
1188
+ if (raw.startsWith(claudePrefix)) {
1189
+ return path.join(root, raw.slice(claudePrefix.length))
1190
+ }
1191
+ if (raw.startsWith(geminiPrefix)) {
1192
+ return path.join(root, raw.slice(geminiPrefix.length))
1193
+ }
1194
+ if (raw.startsWith(codexPrefix) && raw.endsWith('"')) {
1195
+ const inner = raw.slice(codexPrefix.length, raw.length - 1)
1196
+ return path.join(root, inner)
1197
+ }
1198
+ if (!raw.startsWith('$') && !raw.startsWith('"') && !path.isAbsolute(raw)) {
1199
+ // Caminho relativo puro — Cursor (beforeShellExecution/preToolUse), GitHub Copilot CLI
1200
+ // (campo "bash"), Kiro (action.command).
1201
+ return path.join(root, raw)
1202
+ }
1203
+ return null
1204
+ }
1205
+
1206
+ // collectCredentialGuardCommands percorre recursivamente um valor JSON já decodificado e coleta
1207
+ // todo valor-string que referencia trackfw-credential-guard.sh, independentemente do nome do
1208
+ // campo que o contém.
1209
+ //
1210
+ // Os 6 formatos de hook usam campos diferentes para o comando: "command" (Claude/Codex/
1211
+ // Gemini/Cursor), "bash" (GitHub Copilot CLI), "action.command" (Kiro). Varrer por VALOR em vez
1212
+ // de por caminho de chave evita acoplar esta regra à forma exata de cada schema.
1213
+ function collectCredentialGuardCommands(value, out) {
1214
+ if (typeof value === 'string') {
1215
+ if (value.includes(CREDENTIAL_GUARD_SCRIPT_MARKER)) out.push(value)
1216
+ return
1217
+ }
1218
+ if (Array.isArray(value)) {
1219
+ for (const item of value) collectCredentialGuardCommands(item, out)
1220
+ return
1221
+ }
1222
+ if (value && typeof value === 'object') {
1223
+ for (const key of Object.keys(value)) collectCredentialGuardCommands(value[key], out)
1224
+ }
1225
+ }
1226
+
1227
+ // validateCredentialGuardHookResolvable é a regra "credential_guard_hook_resolvable": para cada
1228
+ // arquivo de hook de PROJETO que existir, extrai os comandos que referenciam
1229
+ // trackfw-credential-guard.sh, resolve o caminho e verifica que o script existe e é executável.
1230
+ //
1231
+ // Riscos de regressão mapeados no roadmap:
1232
+ // - A regra só avalia entradas que EXISTEM. Ausência de entrada de guard é estado legítimo
1233
+ // (guard global instalado via `trackfw update harness`) — nunca é violação por si só.
1234
+ // - Arquivo de hook ausente é pulado em silêncio.
1235
+ // - Arquivo de hook presente mas com JSON inválido é pulado em silêncio — validar a forma do
1236
+ // JSON não é escopo desta regra.
1237
+ function validateCredentialGuardHookResolvable(cwd) {
1238
+ const root = cwd || process.cwd()
1239
+ const msgs = []
1240
+
1241
+ for (const hf of CREDENTIAL_GUARD_HOOK_FILES) {
1242
+ const fullPath = path.join(root, hf.relPath)
1243
+ let content
1244
+ try {
1245
+ content = fs.readFileSync(fullPath, 'utf8')
1246
+ } catch (e) {
1247
+ if (e.code === 'ENOENT') continue
1248
+ continue
1249
+ }
1250
+
1251
+ let parsed
1252
+ try {
1253
+ parsed = JSON.parse(content)
1254
+ } catch (_) {
1255
+ continue
1256
+ }
1257
+
1258
+ const commands = []
1259
+ collectCredentialGuardCommands(parsed, commands)
1260
+
1261
+ const seen = new Set()
1262
+ for (const raw of commands) {
1263
+ if (seen.has(raw)) continue
1264
+ seen.add(raw)
1265
+
1266
+ const resolved = resolveCredentialGuardHookPath(raw, root)
1267
+ if (resolved === null) continue
1268
+
1269
+ let stat = null
1270
+ try {
1271
+ stat = fs.statSync(resolved)
1272
+ } catch (_) {
1273
+ stat = null
1274
+ }
1275
+
1276
+ if (!stat) {
1277
+ msgs.push(`${hf.relPath} (${hf.cli}) references trackfw-credential-guard.sh resolved to "${resolved}", but the script does not exist — run \`trackfw update\` to regenerate it`)
1278
+ } else if ((stat.mode & 0o111) === 0) {
1279
+ msgs.push(`${hf.relPath} (${hf.cli}) references trackfw-credential-guard.sh resolved to "${resolved}", but the script is not executable — run \`trackfw update\` to regenerate it`)
1280
+ }
1281
+ }
1282
+ }
1283
+
1284
+ return msgs
1285
+ }
1286
+
1155
1287
  function isGitWorktree(dir) {
1156
1288
  try {
1157
- const out = execSync('git rev-parse --is-inside-work-tree', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], cwd: dir })
1289
+ const out = gitOutput(dir, ['rev-parse', '--is-inside-work-tree'])
1158
1290
  return String(out).trim() === 'true'
1159
1291
  } catch {
1160
1292
  return false
1161
1293
  }
1162
1294
  }
1163
1295
 
1296
+ // ROADMAP-2026-08-12-deteccao-de-adulteracao-do-credential-guard-regra-de-validate, ML-1A.
1297
+ // ADR: docs/adr/ADR-2026-08-12-nao-ha-prevencao-contra-agente-induzido-com-escrita-irrestrita-a-
1298
+ // resposta-e-deteccao-ancorada-no-git.md (Emenda 1: âncora POR ALVO, decidida na Barreira B0).
1299
+ // Ver internal/validator/validator_credential_guard_integrity.go para o raciocínio completo
1300
+ // (âncoras, severidades, e por que "trackfw.yaml sem a chave" é lido como HEAD sem a chave, não
1301
+ // disco sem a chave) — replicado aqui byte-a-byte na semântica, não na forma.
1302
+
1303
+ // CREDENTIAL_GUARD_SCRIPT_REFERENCE is a validator-local copy of the same template composed in
1304
+ // generators/hooks.js (CREDENTIAL_GUARD_SCRIPT = CG_HEADER + CG_PROJECT_GUARD + CG_DETECTION_CORE
1305
+ // + CG_PROJECT_TAIL). Not required directly from generators/hooks.js because
1306
+ // generateCredentialGuardScript() writes to disk AND console.log()s a success line on every call
1307
+ // -- calling it from inside `trackfw validate` would leak a stray " \u2713
1308
+ // scripts/trackfw-credential-guard.sh" line into validate's output on every run, corrupting the
1309
+ // exact success message Cenario 29 fixes byte-for-byte. Kept as a literal copy (same choice made
1310
+ // in internal/validator/validator_credential_guard_integrity_reference.go for Go, for the same
1311
+ // reason) instead -- drift is caught by test/validator-credential-guard-script-reference.test.js,
1312
+ // which regenerates the real script via generateCredentialGuardScript() into a temp dir and
1313
+ // asserts byte-equality against this constant.
1314
+ const CREDENTIAL_GUARD_SCRIPT_REFERENCE = `#!/usr/bin/env bash
1315
+ # trackfw credential guard — PreToolUse/PostToolUse hook
1316
+ set -euo pipefail
1317
+
1318
+ INPUT=$(cat)
1319
+
1320
+ # Script is intentionally a no-op when executed outside the project root
1321
+ [ -f "trackfw.yaml" ] || exit 0
1322
+
1323
+ JWT_PATTERN='eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+'
1324
+ AWS_KEY_PATTERN='AKIA[0-9A-Z]{16}'
1325
+
1326
+ MATCH=""
1327
+ if printf '%s' "$INPUT" | grep -qE "$JWT_PATTERN"; then
1328
+ MATCH="JWT"
1329
+ elif printf '%s' "$INPUT" | grep -qE "$AWS_KEY_PATTERN"; then
1330
+ MATCH="AWS access key"
1331
+ fi
1332
+
1333
+ # The raw payload is JSON: any double quote inside the underlying tool_input.command is
1334
+ # escaped as \\" -- unescape those before scanning for redirect targets, or a quoted target
1335
+ # like "$TMPFILE" is seen as starting with a literal backslash instead of a variable
1336
+ # reference.
1337
+ RAW=$(printf '%s' "$INPUT" | sed 's/\\\\"/"/g')
1338
+
1339
+ # Ignore matches that are only ever written to an ephemeral destination
1340
+ # (mktemp-derived path or /dev/null). A match with no redirect at all
1341
+ # (printed to stdout, e.g.) or redirected to a plain file path still
1342
+ # alerts -- that is the incident this hook guards against.
1343
+ is_ephemeral_target() {
1344
+ local target
1345
+ target=$(printf '%s' "$1" | tr -d "\\"'" | sed -E 's/[},]+$//')
1346
+ case "$target" in
1347
+ /dev/null) return 0 ;;
1348
+ *mktemp*) return 0 ;;
1349
+ esac
1350
+ if printf '%s' "$target" | grep -qE '^\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?$'; then
1351
+ local varname pattern
1352
+ varname=$(printf '%s' "$target" | sed -E 's/^\\$\\{?([A-Za-z_][A-Za-z0-9_]*)\\}?$/\\1/')
1353
+ pattern="*\${varname}="'$(mktemp'"*"
1354
+ case "$RAW" in
1355
+ $pattern) return 0 ;;
1356
+ esac
1357
+ fi
1358
+ return 1
1359
+ }
1360
+
1361
+ REDIRECTS=$(printf '%s' "$RAW" | grep -oE '[0-9]?>>?[[:space:]]*[^[:space:]|&;,:]+' || true)
1362
+
1363
+ # Second detection layer: only runs when the payload scan above found nothing -- keeps the common
1364
+ # case (match already found) cheap and avoids reading files unnecessarily. Files above the size cap
1365
+ # are skipped silently.
1366
+ scan_file_for_pattern() {
1367
+ local path size
1368
+ path=$(printf '%s' "$1" | tr -d "\\"'" | sed -E 's/[},]+$//')
1369
+ [ -n "$path" ] && [ -f "$path" ] || return 1
1370
+ size=$(wc -c < "$path" 2>/dev/null | tr -d '[:space:]')
1371
+ size=\${size:-0}
1372
+ [ "$size" -lt 1048576 ] || return 1
1373
+ if grep -qE "$JWT_PATTERN" "$path" 2>/dev/null; then
1374
+ MATCH="JWT"
1375
+ return 0
1376
+ fi
1377
+ if grep -qE "$AWS_KEY_PATTERN" "$path" 2>/dev/null; then
1378
+ MATCH="AWS access key"
1379
+ return 0
1380
+ fi
1381
+ return 1
1382
+ }
1383
+
1384
+ if [ -z "$MATCH" ] && [ -n "$REDIRECTS" ]; then
1385
+ while IFS= read -r line; do
1386
+ if [ -z "$line" ]; then
1387
+ continue
1388
+ fi
1389
+ target=$(printf '%s' "$line" | sed -E 's/^[0-9]?>>?[[:space:]]*//')
1390
+ if ! is_ephemeral_target "$target"; then
1391
+ scan_file_for_pattern "$target" && break
1392
+ fi
1393
+ done <<< "$REDIRECTS"
1394
+ fi
1395
+
1396
+ if [ -z "$MATCH" ]; then
1397
+ CMD_LINE=$(printf '%s' "$RAW" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')
1398
+ if [ -n "$CMD_LINE" ]; then
1399
+ set -- $CMD_LINE
1400
+ cmd_name="\${1:-}"
1401
+ case "$cmd_name" in
1402
+ cat|head|tail|jq|grep)
1403
+ shift
1404
+ for token in "$@"; do
1405
+ scan_file_for_pattern "$token" && break
1406
+ done
1407
+ ;;
1408
+ esac
1409
+ fi
1410
+ fi
1411
+
1412
+ [ -n "$MATCH" ] || exit 0
1413
+
1414
+ HAS_REDIRECT=0
1415
+ EXEMPT=1
1416
+ if [ -n "$REDIRECTS" ]; then
1417
+ while IFS= read -r line; do
1418
+ if [ -z "$line" ]; then
1419
+ continue
1420
+ fi
1421
+ HAS_REDIRECT=1
1422
+ target=$(printf '%s' "$line" | sed -E 's/^[0-9]?>>?[[:space:]]*//')
1423
+ if ! is_ephemeral_target "$target"; then
1424
+ EXEMPT=0
1425
+ fi
1426
+ done <<< "$REDIRECTS"
1427
+ fi
1428
+
1429
+ if [ "$HAS_REDIRECT" -eq 1 ] && [ "$EXEMPT" -eq 1 ]; then
1430
+ exit 0
1431
+ fi
1432
+
1433
+ DEFAULT_MODE="warn"
1434
+ MODE=$(grep -A 5 '^credential_guard:' trackfw.yaml 2>/dev/null | grep 'mode:' | head -1 | sed -E 's/^[[:space:]]*mode:[[:space:]]*//; s/[[:space:]]*#.*$//' | tr -d "\\"'" || true)
1435
+ case "$MODE" in
1436
+ warn|block) ;;
1437
+ *) MODE="$DEFAULT_MODE" ;;
1438
+ esac
1439
+
1440
+ if [ "$MODE" = "block" ]; then
1441
+ echo "trackfw-credential-guard: blocked - possible $MATCH detected in tool payload." >&2
1442
+ exit 2
1443
+ fi
1444
+
1445
+ echo "trackfw-credential-guard: warning - possible $MATCH detected in tool payload." >&2
1446
+
1447
+ ROADMAP_DIR=$(grep '^roadmap_dir:' trackfw.yaml 2>/dev/null | head -1 | sed 's/^roadmap_dir:[[:space:]]*//; s/[[:space:]]*#.*$//' | tr -d '"' | tr -d "'" || true)
1448
+ ROADMAP_DIR=\${ROADMAP_DIR:-docs/roadmaps}
1449
+
1450
+ case "$ROADMAP_DIR" in
1451
+ /*|../*|*/../*|*/..|..) ROADMAP_DIR="docs/roadmaps" ;;
1452
+ esac
1453
+
1454
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1455
+ MSG="Possible $MATCH detected in tool payload - review before materializing credentials in plain text."
1456
+ MSG_ESC=$(echo "$MSG" | tr -d '\\000-\\037' | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
1457
+
1458
+ mkdir -p "$ROADMAP_DIR"
1459
+ printf '{"tool":"credential-guard","message":"%s","level":"action_required","timestamp":"%s"}\\n' \\
1460
+ "$MSG_ESC" \\
1461
+ "$TIMESTAMP" > "$ROADMAP_DIR/.trackfw-credential-guard.json"
1462
+
1463
+ exit 0
1464
+ `
1465
+
1466
+ // validateCredentialGuardScriptIntegrity é a regra "credential_guard_script_integrity": compara
1467
+ // scripts/trackfw-credential-guard.sh em disco contra o template que esta versão do trackfw
1468
+ // geraria. Silenciosa quando o script não existe — ausência é escopo de
1469
+ // credential_guard_hook_resolvable, não desta regra.
1470
+ function validateCredentialGuardScriptIntegrity(cwd) {
1471
+ const root = cwd || process.cwd()
1472
+ const relPath = 'scripts/trackfw-credential-guard.sh'
1473
+ let content
1474
+ try {
1475
+ content = fs.readFileSync(path.join(root, relPath), 'utf8')
1476
+ } catch (e) {
1477
+ if (e.code === 'ENOENT') return []
1478
+ return [inspectionDiagnostic('credential_guard_script_integrity', relPath, e)]
1479
+ }
1480
+
1481
+ if (content === CREDENTIAL_GUARD_SCRIPT_REFERENCE) return []
1482
+
1483
+ return [
1484
+ `${relPath} content diverges from the template this version of trackfw generates — ` +
1485
+ 'if you did not edit this file by hand, run `trackfw update` to regenerate it',
1486
+ ]
1487
+ }
1488
+
1489
+ // CREDENTIAL_GUARD_MODE_LOOKBEHIND_LINES mirrors the shell script's own resolution of
1490
+ // credential_guard.mode (`grep -A 5 '^credential_guard:'`): the mode key is found on the matched
1491
+ // line or within the 5 lines following it.
1492
+ const CREDENTIAL_GUARD_MODE_LOOKBEHIND_LINES = 5
1493
+
1494
+ // extractCredentialGuardMode replica a leitura leve (grep/sed) que o próprio script faz —
1495
+ // deliberadamente não um parser YAML completo, para que a noção desta regra de "para o que
1496
+ // credential_guard.mode resolve" bata com o que roda de fato no hook.
1497
+ function extractCredentialGuardMode(content) {
1498
+ const lines = content.split('\n')
1499
+ const start = lines.findIndex(l => l.startsWith('credential_guard:'))
1500
+ if (start === -1) return { mode: '', ok: false }
1501
+
1502
+ const end = Math.min(start + 1 + CREDENTIAL_GUARD_MODE_LOOKBEHIND_LINES, lines.length)
1503
+ for (const line of lines.slice(start, end)) {
1504
+ const trimmed = line.trim()
1505
+ if (!trimmed.includes('mode:')) continue
1506
+ let rest = trimmed.startsWith('mode:') ? trimmed.slice('mode:'.length) : trimmed
1507
+ rest = rest.trim()
1508
+ const hashIdx = rest.indexOf('#')
1509
+ if (hashIdx >= 0) rest = rest.slice(0, hashIdx).trim()
1510
+ rest = rest.replace(/^["']+|["']+$/g, '')
1511
+ return { mode: rest, ok: true }
1512
+ }
1513
+ return { mode: '', ok: false }
1514
+ }
1515
+
1516
+ // headTrackfwYAML retorna o conteúdo de trackfw.yaml no HEAD do git, resolvido relativo a cwd
1517
+ // (não necessariamente a raiz do repo — `trackfw validate` pode rodar de um subdiretório). ok é
1518
+ // false sempre que não há âncora usável: não é worktree git, sem commits, ou trackfw.yaml não
1519
+ // versionado no HEAD — todos "sem âncora, silêncio", nunca erro.
1520
+ function headTrackfwYAML(cwd) {
1521
+ const root = cwd || process.cwd()
1522
+ if (!isGitWorktree(root)) return { content: '', ok: false }
1523
+ try {
1524
+ gitOutput(root, ['rev-parse', '--verify', 'HEAD'])
1525
+ } catch {
1526
+ return { content: '', ok: false }
1527
+ }
1528
+ try {
1529
+ const out = gitOutput(root, ['show', 'HEAD:./trackfw.yaml'])
1530
+ return { content: out, ok: true }
1531
+ } catch {
1532
+ return { content: '', ok: false }
1533
+ }
1534
+ }
1535
+
1536
+ // validateCredentialGuardModeDowngrade é a regra "credential_guard_mode_downgrade": dispara apenas
1537
+ // quando credential_guard.mode era explicitamente "block" no HEAD e o trackfw.yaml atual em disco
1538
+ // não resolve mais para "block" (warn explícito, valor não reconhecido, ou chave/arquivo
1539
+ // ausente — todos os quais o próprio script resolveria como "warn", o DEFAULT_MODE da variante de
1540
+ // projeto).
1541
+ //
1542
+ // Silenciosa sempre que HEAD não é "block": isso é "sem âncora para detectar downgrade", não
1543
+ // "nada errado". A ausência da chave em DISCO nunca é tratada como silêncio — é exatamente a via
1544
+ // que esta regra existe para cobrir.
1545
+ function validateCredentialGuardModeDowngrade(cwd) {
1546
+ const root = cwd || process.cwd()
1547
+ const head = headTrackfwYAML(root)
1548
+ if (!head.ok) return []
1549
+
1550
+ const headMode = extractCredentialGuardMode(head.content)
1551
+ if (headMode.mode !== 'block') return []
1552
+
1553
+ let diskContent
1554
+ try {
1555
+ diskContent = fs.readFileSync(path.join(root, 'trackfw.yaml'), 'utf8')
1556
+ } catch (e) {
1557
+ if (e.code === 'ENOENT') return [credentialGuardModeDowngradeMessage()]
1558
+ return [inspectionDiagnostic('credential_guard_mode_downgrade', 'trackfw.yaml', e)]
1559
+ }
1560
+
1561
+ const diskMode = extractCredentialGuardMode(diskContent)
1562
+ if (diskMode.mode === 'block') return []
1563
+
1564
+ return [credentialGuardModeDowngradeMessage()]
1565
+ }
1566
+
1567
+ function credentialGuardModeDowngradeMessage() {
1568
+ return 'trackfw.yaml sets credential_guard.mode: block at the git HEAD commit, but the ' +
1569
+ 'current file does not resolve to block — if this was intentional, commit the change; ' +
1570
+ 'otherwise investigate before treating the credential guard as active'
1571
+ }
1572
+
1164
1573
  // _itemMeta: mapa de message → { rule, file } para enriquecer saída JSON.
1165
1574
  // Populado em applyRule e nos pushs diretos do validateUnfiltered.
1166
1575
  // Permanece em memória apenas durante a execução de uma chamada validate*.
@@ -1185,11 +1594,87 @@ function resetMeta() {
1185
1594
  // RULE_DEFAULTS mapeia regras cujo default NÃO é 'error'.
1186
1595
  const RULE_DEFAULTS = {
1187
1596
  note_orphan: 'warning',
1597
+ // ROADMAP-2026-08-12-deteccao-de-adulteracao-do-credential-guard-regra-de-validate, ML-1A,
1598
+ // ADR-2026-08-12 Emenda 3: the script carries no version marker, so this rule cannot tell
1599
+ // legitimate drift (trackfw not updated yet) from tampering — kept a warning, never an error.
1600
+ // credential_guard_mode_downgrade is deliberately absent: it falls through to ruleSeverity's
1601
+ // 'error' default.
1602
+ credential_guard_script_integrity: 'warning',
1603
+ }
1604
+
1605
+ // ROADMAP-2026-08-12-ancorar-rules-no-head-para-as-regras-de-credential-guard, ML-1A.
1606
+ // ADR: docs/adr/ADR-2026-08-12-severidade-das-regras-de-credential-guard-resolvida-pela-mais-
1607
+ // estrita-entre-head-e-disco.md.
1608
+ //
1609
+ // As 3 regras abaixo resolvem severidade de forma DIFERENTE de todas as outras ~38: comparam HEAD
1610
+ // contra disco e adotam a MAIS ESTRITA das duas, em vez de ler só o disco. Deliberado, não bug —
1611
+ // sem isso, estas 3 regras podem ser desligadas pela mesma edição NÃO COMMITADA que elas deveriam
1612
+ // denunciar (`rules: credential_guard_mode_downgrade: off` em trackfw.yaml, nunca commitado). Toda
1613
+ // outra regra continua passando por diskRuleSeverity, byte-idêntico a antes deste ADR.
1614
+ const CREDENTIAL_GUARD_ANCHORED_RULES = new Set([
1615
+ 'credential_guard_hook_resolvable',
1616
+ 'credential_guard_script_integrity',
1617
+ 'credential_guard_mode_downgrade',
1618
+ ])
1619
+
1620
+ // credentialGuardSeverityRank ordena severidades da menos para a mais estrita, para a comparação
1621
+ // "mais estrita vence" de credentialGuardRuleSeverity. Qualquer valor fora de 'off'/'warning' só
1622
+ // significa 'error' na prática — applyRule já trata qualquer valor não reconhecido como violation,
1623
+ // então este ranking espelha esse mesmo fallback em vez de introduzir um contrato mais rígido.
1624
+ function credentialGuardSeverityRank(s) {
1625
+ if (s === 'off') return 0
1626
+ if (s === 'warning') return 1
1627
+ return 2
1628
+ }
1629
+
1630
+ // credentialGuardStricterSeverity retorna a mais estrita entre a e b ('error' > 'warning' > 'off').
1631
+ function credentialGuardStricterSeverity(a, b) {
1632
+ return credentialGuardSeverityRank(a) >= credentialGuardSeverityRank(b) ? a : b
1633
+ }
1634
+
1635
+ // credentialGuardDefaultSeverity é o mesmo fallback "RULE_DEFAULTS > error" que diskRuleSeverity
1636
+ // usa quando trackfw.yaml não tem rules: <name> — extraído para credentialGuardRuleSeverity poder
1637
+ // aplicá-lo igualmente ao lado HEAD (que não tem equivalente de RULE_DEFAULTS próprio, já que
1638
+ // config.parseRulesFromContent só devolve o que rules: em si contém).
1639
+ function credentialGuardDefaultSeverity(name) {
1640
+ return RULE_DEFAULTS[name] || 'error'
1641
+ }
1642
+
1643
+ // credentialGuardRuleSeverity resolve a severidade de uma das 3 CREDENTIAL_GUARD_ANCHORED_RULES
1644
+ // como a MAIS ESTRITA entre HEAD e disco — direcional, não "ignora disco e usa só HEAD" (ver o
1645
+ // parecer §2 e o ADR — o caso comum, HEAD sem menção à regra, precisa resolver para o default, ou
1646
+ // seja o valor mais estrito possível, senão o disco venceria de volta silenciosamente sempre).
1647
+ //
1648
+ // Sem HEAD (não é git worktree, sem commits, ou trackfw.yaml não versionado no HEAD —
1649
+ // headTrackfwYAML's 3 casos de "sem âncora"): cai no disco puro, igual a qualquer outra regra.
1650
+ // ADR ponto de decisão 4: limite aceito, não um bypass acionável por adversário — nenhum desses 3
1651
+ // casos é alcançável por uma edição não commitada de trackfw.yaml sozinha.
1652
+ function credentialGuardRuleSeverity(name) {
1653
+ const diskSeverity = diskRuleSeverity(name)
1654
+
1655
+ const head = headTrackfwYAML()
1656
+ if (!head.ok) return diskSeverity
1657
+
1658
+ const headRules = config.parseRulesFromContent(head.content)
1659
+ const headSeverity = headRules[name] || credentialGuardDefaultSeverity(name)
1660
+
1661
+ return credentialGuardStricterSeverity(headSeverity, diskSeverity)
1188
1662
  }
1189
1663
 
1190
1664
  // ruleSeverity retorna a severidade configurada para uma regra ('error'|'warning'|'off').
1191
1665
  // Prioridade: trackfw.yaml rules: > RULE_DEFAULTS > 'error'.
1666
+ //
1667
+ // Para as 3 CREDENTIAL_GUARD_ANCHORED_RULES, delega a credentialGuardRuleSeverity acima — ver o
1668
+ // comentário logo antes dessa constante para o porquê. Toda outra regra segue para
1669
+ // diskRuleSeverity, textualmente idêntico ao corpo desta função antes do ADR-2026-08-12.
1192
1670
  function ruleSeverity(name) {
1671
+ if (CREDENTIAL_GUARD_ANCHORED_RULES.has(name)) return credentialGuardRuleSeverity(name)
1672
+ return diskRuleSeverity(name)
1673
+ }
1674
+
1675
+ // diskRuleSeverity é a resolução ordinária, só-disco, usada por toda regra exceto as 3
1676
+ // CREDENTIAL_GUARD_ANCHORED_RULES: trackfw.yaml rules: (CWD) > RULE_DEFAULTS > 'error'.
1677
+ function diskRuleSeverity(name) {
1193
1678
  const cfg = config.load()
1194
1679
  if (cfg.rules[name]) return cfg.rules[name]
1195
1680
  if (RULE_DEFAULTS[name]) return RULE_DEFAULTS[name]
@@ -1255,6 +1740,9 @@ async function validateUnfiltered() {
1255
1740
  applyRule('blocked_by_draft_adr', validateREQsNotBlockedByDraftADRs(), violations, warnings)
1256
1741
  applyRule('adr_accepted_when_req_done', validateADRAcceptedWhenREQDone(), violations, warnings)
1257
1742
  applyRule('note_orphan', validateNoteOrphan(), violations, warnings)
1743
+ applyRule('credential_guard_hook_resolvable', validateCredentialGuardHookResolvable(), violations, warnings)
1744
+ applyRule('credential_guard_script_integrity', validateCredentialGuardScriptIntegrity(), violations, warnings)
1745
+ applyRule('credential_guard_mode_downgrade', validateCredentialGuardModeDowngrade(), violations, warnings)
1258
1746
 
1259
1747
  // Regras configuráveis via applyRule (popula _itemMeta automaticamente)
1260
1748
  applyRule('req_has_adr', validateREQsHaveADR(), violations, warnings)
@@ -1288,6 +1776,16 @@ async function validateUnfiltered() {
1288
1776
 
1289
1777
  // validate executa todas as validações, aplica ratchet (baseline) e modo lenient.
1290
1778
  // Retorna { violations, warnings }.
1779
+ //
1780
+ // ADR-2026-08-12-severidade-das-regras-de-credential-guard-resolvida-pela-mais-estrita-entre-head-
1781
+ // e-disco: carve-out do baseline — violations/warnings de uma das 3
1782
+ // CREDENTIAL_GUARD_ANCHORED_RULES NUNCA são toleradas via .trackfw-baseline.json, não importa o
1783
+ // que o arquivo contenha para elas. Mecanismo DIFERENTE do HEAD-vs-disco em
1784
+ // credentialGuardRuleSeverity: .trackfw-baseline.json é .gitignore'd DE PROPÓSITO ("baseline local
1785
+ // de violations toleradas (nao versionado)"), então não há HEAD desse arquivo para comparar —
1786
+ // "exigir commit" simplesmente não se aplica a um arquivo que o projeto decidiu nunca versionar. A
1787
+ // única forma de fechar esse canal é excluir estas 3 regras da elegibilidade de ratchet, por nome,
1788
+ // independente do conteúdo da mensagem — daí a checagem via getItemMeta(msg).rule abaixo.
1291
1789
  async function validate() {
1292
1790
  const result = await validateUnfiltered()
1293
1791
  let { violations, warnings } = result
@@ -1296,9 +1794,9 @@ async function validate() {
1296
1794
  const baseline = loadBaseline()
1297
1795
  if (baseline) {
1298
1796
  const baselineSet = new Set(baseline.violations || [])
1299
- violations = violations.filter(v => !baselineSet.has(v))
1797
+ violations = violations.filter(v => !baselineSet.has(v) || CREDENTIAL_GUARD_ANCHORED_RULES.has(getItemMeta(v).rule))
1300
1798
  const baselineWarnSet = new Set(baseline.warnings || [])
1301
- warnings = warnings.filter(w => !baselineWarnSet.has(w))
1799
+ warnings = warnings.filter(w => !baselineWarnSet.has(w) || CREDENTIAL_GUARD_ANCHORED_RULES.has(getItemMeta(w).rule))
1302
1800
  }
1303
1801
 
1304
1802
  // Modo lenient: mover violations para warnings, exit code 0
@@ -1505,4 +2003,13 @@ module.exports = {
1505
2003
  // ML-1D (2026-08-01 — reconciliação de paridade: frontmatter-first)
1506
2004
  extractFrontmatterField,
1507
2005
  resolveAdrStatus,
2006
+ // ROADMAP-2026-08-12-mitigacao-do-fail-open-do-credential-guard, ML-1A
2007
+ resolveCredentialGuardHookPath,
2008
+ collectCredentialGuardCommands,
2009
+ validateCredentialGuardHookResolvable,
2010
+ // ROADMAP-2026-08-12-deteccao-de-adulteracao-do-credential-guard-regra-de-validate, ML-1A
2011
+ validateCredentialGuardScriptIntegrity,
2012
+ validateCredentialGuardModeDowngrade,
2013
+ extractCredentialGuardMode,
2014
+ CREDENTIAL_GUARD_SCRIPT_REFERENCE,
1508
2015
  }