trackfw 7.1.0 → 7.3.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/package.json +1 -1
- package/src/commands/audit-surface.js +520 -0
- package/src/commands/barrier.js +107 -8
- package/src/commands/commit.js +7 -3
- package/src/commands/doctor.js +17 -3
- package/src/commands/index.js +3 -0
- package/src/commands/integrations.js +80 -1
- package/src/commands/push.js +51 -0
- package/src/commands/release.js +47 -0
- package/src/commands/ship.js +10 -2
- package/src/commands/thirdparty.js +7 -2
- package/src/commands/update-harness.js +4 -1
- package/src/commands/update.js +21 -2
- package/src/config/index.js +96 -2
- package/src/generators/hooks.js +194 -13
- package/src/generators/init.js +84 -16
- package/src/generators/roadmap.js +53 -2
- package/src/integrations/assets/agents/architect.md +17 -3
- package/src/integrations/assets/agents/security.md +3 -0
- package/src/integrations/doctor.js +0 -0
- package/src/integrations/index.js +1 -1
- package/src/integrations/render.js +116 -2
- package/src/integrations/scaffold_doctor.js +366 -0
- package/src/lib/update-engine.js +13 -1
- package/src/push/runner.js +223 -0
- package/src/release/runner.js +474 -0
- package/src/ship/runner.js +177 -21
- package/src/validator/index.js +270 -13
package/src/ship/runner.js
CHANGED
|
@@ -18,6 +18,68 @@ const { evaluateBranchIntegration, DECISION: BRANCH_PRUNE_DECISION } = require('
|
|
|
18
18
|
// In --dry-run mode these are printed but not executed.
|
|
19
19
|
const GIT_WRITE_COMMANDS = new Set(['commit', 'push', 'fetch'])
|
|
20
20
|
|
|
21
|
+
// force-with-lease refusal messages. Named constants so the ML-1B parity gate has a single
|
|
22
|
+
// place to compare byte-for-byte across the 3 CLIs. Byte-identical to Go's
|
|
23
|
+
// forceLease*Msg/Fmt constants (internal/commands/ship.go) and Python's FORCE_LEASE_* below.
|
|
24
|
+
// Never expose "--force" (raw) as a flag — see
|
|
25
|
+
// ADR-2026-08-19-caminho-governado-para-push-forcado-e-tag-de-release.md.
|
|
26
|
+
const FORCE_LEASE_NO_FORGE_CLI_MSG =
|
|
27
|
+
'trackfw ship --force-with-lease requires a forge CLI (gh, glab, or az) to confirm an open pull/merge request before rewriting remote history. No forge CLI is available for this repository — install and authenticate it, or push without --force-with-lease.'
|
|
28
|
+
|
|
29
|
+
function forceLeaseNoPROpenMsg(branch) {
|
|
30
|
+
return `trackfw ship --force-with-lease refuses to run: branch "${branch}" has no open pull/merge request. Open the PR/MR first (trackfw ship without --force-with-lease, or your forge's web UI), then retry.`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function forceLeaseCannotVerifyMsg(branch, cliName, errMessage) {
|
|
34
|
+
return `trackfw ship --force-with-lease could not verify whether branch "${branch}" has an open pull/merge request (${cliName} CLI error: ${errMessage}). Refusing rather than risking a force push without a verified PR — check your ${cliName} CLI authentication and retry.`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* defaultCheckPROpen queries the resolved forge CLI for an open PR/MR whose source branch is
|
|
39
|
+
* `branch`, using the same list-based query shape for every forge: empty result means "no PR"
|
|
40
|
+
* (exit 0), any non-zero exit or unparseable output means "cannot verify" (thrown — never
|
|
41
|
+
* conflated with "no PR"). bitbucket and "manual" never reach here: runShip only calls
|
|
42
|
+
* checkPROpen when adapter.available is true, and bitbucket's adapter is always available=false.
|
|
43
|
+
* @param {object} adapter
|
|
44
|
+
* @param {string} branch
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
function defaultCheckPROpen(adapter, branch) {
|
|
48
|
+
let args
|
|
49
|
+
switch (adapter.forge) {
|
|
50
|
+
case 'github':
|
|
51
|
+
args = ['pr', 'list', '--head', branch, '--state', 'open', '--json', 'number']
|
|
52
|
+
break
|
|
53
|
+
case 'gitlab':
|
|
54
|
+
// glab mr list: --source-branch filters by source branch, --state opened matches
|
|
55
|
+
// gh's "open" state, -F json requests machine-readable output (glab's own flag,
|
|
56
|
+
// not an external jq/GNU dependency).
|
|
57
|
+
args = ['mr', 'list', '--source-branch', branch, '--state', 'opened', '-F', 'json']
|
|
58
|
+
break
|
|
59
|
+
case 'azure':
|
|
60
|
+
// az defaults to --output json; passed explicitly here for clarity, not reliance
|
|
61
|
+
// on the ambient default.
|
|
62
|
+
args = ['repos', 'pr', 'list', '--source-branch', branch, '--status', 'active', '--output', 'json']
|
|
63
|
+
break
|
|
64
|
+
default:
|
|
65
|
+
throw new Error(`no PR/MR query defined for forge "${adapter.forge}"`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const result = spawnSync(adapter.cliName, args, { encoding: 'utf8' })
|
|
69
|
+
if (result.status !== 0) {
|
|
70
|
+
const msg = (result.stderr || '').trim() || `${adapter.cliName} exited with ${result.status}`
|
|
71
|
+
throw new Error(msg)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let parsed
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(result.stdout || '[]')
|
|
77
|
+
} catch (e) {
|
|
78
|
+
throw new Error(`could not parse ${adapter.cliName} output: ${e.message}`)
|
|
79
|
+
}
|
|
80
|
+
return Array.isArray(parsed) && parsed.length > 0
|
|
81
|
+
}
|
|
82
|
+
|
|
21
83
|
/**
|
|
22
84
|
* isGitWriteCmd returns true when the first arg is a write-mode git subcommand.
|
|
23
85
|
* @param {string[]} args
|
|
@@ -240,10 +302,17 @@ function allDocOnly(files) {
|
|
|
240
302
|
*/
|
|
241
303
|
const COMMIT_MESSAGE_SEP = '\x1e'
|
|
242
304
|
|
|
305
|
+
// GIT_SYMBOLIC_REF_ORIGIN_HEAD_PREFIX is the fixed prefix `git symbolic-ref
|
|
306
|
+
// refs/remotes/origin/HEAD` always returns before the branch name, because "origin" is the
|
|
307
|
+
// literal ref namespace queried — never derived from the output itself. Stripping this exact
|
|
308
|
+
// prefix (instead of cutting at the last '/') is what makes defaultBaseBranch correct for a
|
|
309
|
+
// default branch that itself contains a slash (e.g. "release/7.2"): lastIndexOf('/') used to
|
|
310
|
+
// cut at "7.2", discarding "release/". Mirrors Go's ship.go gitSymbolicRefOriginHeadPrefix.
|
|
311
|
+
const GIT_SYMBOLIC_REF_ORIGIN_HEAD_PREFIX = 'refs/remotes/origin/'
|
|
312
|
+
|
|
243
313
|
/**
|
|
244
314
|
* defaultBaseBranch resolves the repository's default branch for `git log <base>..HEAD`.
|
|
245
|
-
* Tries `git symbolic-ref refs/remotes/origin/HEAD`
|
|
246
|
-
* only the name after the last slash is kept) and falls back to "main" when that fails or
|
|
315
|
+
* Tries `git symbolic-ref refs/remotes/origin/HEAD` and falls back to "main" when that fails or
|
|
247
316
|
* yields nothing.
|
|
248
317
|
* @param {function} execGit
|
|
249
318
|
* @returns {string}
|
|
@@ -252,9 +321,9 @@ function defaultBaseBranch(execGit) {
|
|
|
252
321
|
const { stdout, error } = execGit(['symbolic-ref', 'refs/remotes/origin/HEAD'])
|
|
253
322
|
if (error) return 'main'
|
|
254
323
|
const out = (stdout || '').trim()
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return
|
|
324
|
+
if (!out.startsWith(GIT_SYMBOLIC_REF_ORIGIN_HEAD_PREFIX)) return 'main'
|
|
325
|
+
const name = out.slice(GIT_SYMBOLIC_REF_ORIGIN_HEAD_PREFIX.length)
|
|
326
|
+
return name === '' ? 'main' : name
|
|
258
327
|
}
|
|
259
328
|
|
|
260
329
|
/**
|
|
@@ -444,6 +513,59 @@ function runShip(opts, deps = {}) {
|
|
|
444
513
|
writeln('Governance: OK')
|
|
445
514
|
}
|
|
446
515
|
|
|
516
|
+
// ─── Step 2.5: force-with-lease gate ───────────────────────────────────────
|
|
517
|
+
// Runs before any write (commit/push) — a refusal here must never leave a local
|
|
518
|
+
// commit the caller cannot push. Read-only, so it runs in --dry-run too, same
|
|
519
|
+
// posture as the read-only calls in Step 0 / Step 7.
|
|
520
|
+
//
|
|
521
|
+
// forceLeaseAdapter/forceLeaseResolution are reused by Step 7 below to avoid a
|
|
522
|
+
// second forge resolution and a duplicate "Forge: ..." line, and because Step 7
|
|
523
|
+
// must skip PR/MR creation entirely once this gate has confirmed one is already
|
|
524
|
+
// open — creating a second one would be a spurious failure on every successful
|
|
525
|
+
// force push.
|
|
526
|
+
let forceLeaseAdapter = null
|
|
527
|
+
let forceLeaseResolution = null
|
|
528
|
+
if (opts.forceWithLease) {
|
|
529
|
+
const { stdout: gateRemoteURLRaw } = execGit(['remote', 'get-url', 'origin'])
|
|
530
|
+
const gateRemoteURL = (gateRemoteURLRaw || '').trim()
|
|
531
|
+
|
|
532
|
+
let resolution
|
|
533
|
+
try {
|
|
534
|
+
resolution = forgeResolve({
|
|
535
|
+
flagForge: opts.forge || '',
|
|
536
|
+
configForge: deps.configForge || '',
|
|
537
|
+
remoteURL: gateRemoteURL,
|
|
538
|
+
repoDir: deps.repoDir || '',
|
|
539
|
+
})
|
|
540
|
+
} catch (resErr) {
|
|
541
|
+
writeErr(resErr.message)
|
|
542
|
+
return 1
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const adapter = forgeAdapter(resolution.forge, deps.availFn || undefined)
|
|
546
|
+
if (resolution.forge === 'manual' || !adapter.available) {
|
|
547
|
+
writeErr(FORCE_LEASE_NO_FORGE_CLI_MSG)
|
|
548
|
+
return 1
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const checkPROpen = deps.checkPROpen || defaultCheckPROpen
|
|
552
|
+
let open
|
|
553
|
+
try {
|
|
554
|
+
open = checkPROpen(adapter, branch)
|
|
555
|
+
} catch (prErr) {
|
|
556
|
+
writeErr(forceLeaseCannotVerifyMsg(branch, adapter.cliName, prErr.message))
|
|
557
|
+
return 1
|
|
558
|
+
}
|
|
559
|
+
if (!open) {
|
|
560
|
+
writeErr(forceLeaseNoPROpenMsg(branch))
|
|
561
|
+
return 1
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
writeln(`force-with-lease: open ${adapter.noun} confirmed for "${branch}" (${resolution.forge}).`)
|
|
565
|
+
forceLeaseAdapter = adapter
|
|
566
|
+
forceLeaseResolution = resolution
|
|
567
|
+
}
|
|
568
|
+
|
|
447
569
|
// ─── Step 3: Squash-merge detection ────────────────────────────────────────
|
|
448
570
|
if (opts.dryRun) {
|
|
449
571
|
writeln('[dry-run] git fetch origin --prune')
|
|
@@ -466,7 +588,14 @@ function runShip(opts, deps = {}) {
|
|
|
466
588
|
writeln('────────────────────────────────────────────────────────\n')
|
|
467
589
|
|
|
468
590
|
// Reuses stagedFiles read at the top of the function (Step 0) — never re-query git here.
|
|
469
|
-
|
|
591
|
+
//
|
|
592
|
+
// --force-with-lease push-only mode: a rebase that resolved conflicts already
|
|
593
|
+
// committed the result (the index is clean afterwards) — there is nothing left to
|
|
594
|
+
// stage or commit, only to push. Only --force-with-lease grants this exception;
|
|
595
|
+
// without it, "nothing staged" still aborts exactly as before (non-regression).
|
|
596
|
+
const pushOnly = opts.forceWithLease && stagedFiles.length === 0
|
|
597
|
+
|
|
598
|
+
if (stagedFiles.length === 0 && !opts.forceWithLease) {
|
|
470
599
|
writeErr(
|
|
471
600
|
'nothing is staged — stage your files explicitly before running ship:\n' +
|
|
472
601
|
' git add <file1> <file2> ...\n' +
|
|
@@ -476,26 +605,35 @@ function runShip(opts, deps = {}) {
|
|
|
476
605
|
}
|
|
477
606
|
|
|
478
607
|
// ─── Step 5: Commit ────────────────────────────────────────────────────────
|
|
479
|
-
if (
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
608
|
+
if (pushOnly) {
|
|
609
|
+
writeln('Nothing staged — --force-with-lease pushes existing commits only, no new commit.')
|
|
610
|
+
} else {
|
|
611
|
+
if (!opts.message) {
|
|
612
|
+
writeErr(
|
|
613
|
+
'commit message is required — use -m:\n' +
|
|
614
|
+
' trackfw ship -m "feat(<scope>): <description>"'
|
|
615
|
+
)
|
|
616
|
+
return 1
|
|
617
|
+
}
|
|
486
618
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
619
|
+
const { error: commitErr } = git(['commit', '-m', opts.message])
|
|
620
|
+
if (commitErr) {
|
|
621
|
+
writeErr(`git commit failed: ${commitErr.message}`)
|
|
622
|
+
return 1
|
|
623
|
+
}
|
|
492
624
|
|
|
493
|
-
|
|
494
|
-
|
|
625
|
+
if (!opts.dryRun) {
|
|
626
|
+
writeln(`Committed: ${opts.message}`)
|
|
627
|
+
}
|
|
495
628
|
}
|
|
496
629
|
|
|
497
630
|
// ─── Step 6: Push ──────────────────────────────────────────────────────────
|
|
498
|
-
|
|
631
|
+
let pushArgs = buildPushArgs(branch, execGit)
|
|
632
|
+
if (opts.forceWithLease) {
|
|
633
|
+
// Fixed position: push --force-with-lease [-u] origin <branch> — identical
|
|
634
|
+
// across the 3 CLIs (ML-1B's parity gate compares this literally).
|
|
635
|
+
pushArgs = [pushArgs[0], '--force-with-lease', ...pushArgs.slice(1)]
|
|
636
|
+
}
|
|
499
637
|
const { error: pushErr } = git(pushArgs)
|
|
500
638
|
if (pushErr) {
|
|
501
639
|
writeErr(`git push failed: ${pushErr.message}`)
|
|
@@ -507,6 +645,17 @@ function runShip(opts, deps = {}) {
|
|
|
507
645
|
}
|
|
508
646
|
|
|
509
647
|
// ─── Step 7: Open PR/MR ────────────────────────────────────────────────────
|
|
648
|
+
// --force-with-lease only ever reaches here after Step 2.5 confirmed a PR/MR is
|
|
649
|
+
// already open on this branch — creating another one would be a spurious failure
|
|
650
|
+
// on every successful force push. Reuses the adapter/resolution Step 2.5 already
|
|
651
|
+
// computed instead of resolving the forge a second time.
|
|
652
|
+
if (opts.forceWithLease) {
|
|
653
|
+
writeln(`Forge: ${forceLeaseResolution.forge} (source: ${forceLeaseResolution.source})`)
|
|
654
|
+
writeln(`${forceLeaseAdapter.noun} already open — skipping creation (--force-with-lease).`)
|
|
655
|
+
writeln('\nship complete.')
|
|
656
|
+
return 0
|
|
657
|
+
}
|
|
658
|
+
|
|
510
659
|
const { stdout: remoteURLRaw } = execGit(['remote', 'get-url', 'origin'])
|
|
511
660
|
const remoteURL = (remoteURLRaw || '').trim()
|
|
512
661
|
|
|
@@ -615,4 +764,11 @@ module.exports = {
|
|
|
615
764
|
buildPRBody,
|
|
616
765
|
COMMIT_MESSAGE_SEP,
|
|
617
766
|
detectPendingSquashMerges,
|
|
767
|
+
defaultCheckPROpen,
|
|
768
|
+
FORCE_LEASE_NO_FORGE_CLI_MSG,
|
|
769
|
+
forceLeaseNoPROpenMsg,
|
|
770
|
+
forceLeaseCannotVerifyMsg,
|
|
771
|
+
// Exported for reuse by push/runner.js (AC2 of REQ-2026-08-22-trackfw-push).
|
|
772
|
+
buildPushArgs,
|
|
773
|
+
defaultExecGit,
|
|
618
774
|
}
|
package/src/validator/index.js
CHANGED
|
@@ -1174,13 +1174,21 @@ const GIT_BRANCH_GUARD_SCRIPT_MARKER = 'trackfw-git-branch-guard.sh'
|
|
|
1174
1174
|
// sibling is a structurally malformed entry the CLI silently never executes (hades-tf ML-4A
|
|
1175
1175
|
// barrier finding), not merely "absent". false only for Cursor, whose schema
|
|
1176
1176
|
// ({"command":...}) never carries a "type" field at all.
|
|
1177
|
+
//
|
|
1178
|
+
// requiresVarOrShellPrefix (ROADMAP-2026-08-21 ML-1B, port of Go's
|
|
1179
|
+
// credentialGuardHookFile.requiresVarOrShellPrefix): true for Claude/Codex/Gemini — the
|
|
1180
|
+
// ADR-2026-08-11 requires these CLIs to anchor the hook path with $VAR/... or "$(git ...)/..."
|
|
1181
|
+
// because their hooks run from the agent's cwd, not necessarily the project root. A bare
|
|
1182
|
+
// relative path ("scripts/...") silently fails from any subdirectory (REQ-2026-08-17 root
|
|
1183
|
+
// cause). false for Cursor/Copilot/Kiro, where the bare relative path IS the correct form —
|
|
1184
|
+
// flagging them would be a false-positive (the dominant risk of this REQ).
|
|
1177
1185
|
const CREDENTIAL_GUARD_HOOK_FILES = [
|
|
1178
|
-
{ relPath: '.claude/settings.json', cli: 'Claude Code', requiresCommandType: true },
|
|
1179
|
-
{ relPath: '.codex/hooks.json', cli: 'Codex CLI', requiresCommandType: true },
|
|
1180
|
-
{ relPath: '.gemini/settings.json', cli: 'Gemini CLI', requiresCommandType: true },
|
|
1181
|
-
{ relPath: '.cursor/hooks.json', cli: 'Cursor', requiresCommandType: false },
|
|
1182
|
-
{ relPath: '.github/hooks/trackfw-attention.json', cli: 'GitHub Copilot CLI', requiresCommandType: true },
|
|
1183
|
-
{ relPath: '.kiro/hooks/trackfw-attention.json', cli: 'Kiro', requiresCommandType: true },
|
|
1186
|
+
{ relPath: '.claude/settings.json', cli: 'Claude Code', requiresCommandType: true, requiresVarOrShellPrefix: true },
|
|
1187
|
+
{ relPath: '.codex/hooks.json', cli: 'Codex CLI', requiresCommandType: true, requiresVarOrShellPrefix: true },
|
|
1188
|
+
{ relPath: '.gemini/settings.json', cli: 'Gemini CLI', requiresCommandType: true, requiresVarOrShellPrefix: true },
|
|
1189
|
+
{ relPath: '.cursor/hooks.json', cli: 'Cursor', requiresCommandType: false, requiresVarOrShellPrefix: false },
|
|
1190
|
+
{ relPath: '.github/hooks/trackfw-attention.json', cli: 'GitHub Copilot CLI', requiresCommandType: true, requiresVarOrShellPrefix: false },
|
|
1191
|
+
{ relPath: '.kiro/hooks/trackfw-attention.json', cli: 'Kiro', requiresCommandType: true, requiresVarOrShellPrefix: false },
|
|
1184
1192
|
]
|
|
1185
1193
|
|
|
1186
1194
|
// resolveCredentialGuardHookPath resolve o valor bruto de um comando de hook (string extraída do
|
|
@@ -1212,14 +1220,84 @@ function resolveCredentialGuardHookPath(raw, root) {
|
|
|
1212
1220
|
const inner = raw.slice(codexPrefix.length, raw.length - 1)
|
|
1213
1221
|
return path.join(root, inner)
|
|
1214
1222
|
}
|
|
1215
|
-
if (!raw.startsWith('$') && !raw.startsWith('"') && !path.isAbsolute(raw)) {
|
|
1223
|
+
if (!raw.startsWith('$') && !raw.startsWith('"') && !path.isAbsolute(raw) && !raw.startsWith('~/')) {
|
|
1216
1224
|
// Caminho relativo puro — Cursor (beforeShellExecution/preToolUse), GitHub Copilot CLI
|
|
1217
1225
|
// (campo "bash"), Kiro (action.command).
|
|
1226
|
+
// ~/… é excluído: é classe 1 (tilde expande para $HOME — ancorado) mas o validator não expande
|
|
1227
|
+
// o til; ok=false (null) silencia sem acusar.
|
|
1218
1228
|
return path.join(root, raw)
|
|
1219
1229
|
}
|
|
1220
1230
|
return null
|
|
1221
1231
|
}
|
|
1222
1232
|
|
|
1233
|
+
// HOOK_ANCHORAGE_CLASS_* classifica a semântica de ancoragem de um valor de comando de hook.
|
|
1234
|
+
// Avaliada apenas para CLIs com requiresVarOrShellPrefix=true. Decisão: ADR-2026-08-22.
|
|
1235
|
+
const HOOK_ANCHORAGE_CLASS_ANCHORED = 1
|
|
1236
|
+
const HOOK_ANCHORAGE_CLASS_CWD_DEPENDENT = 2
|
|
1237
|
+
const HOOK_ANCHORAGE_CLASS_UNDECIDABLE = 3
|
|
1238
|
+
|
|
1239
|
+
// stripOuterQuotesForClassify remove aspas duplas envolventes de raw, se presentes. Necessário
|
|
1240
|
+
// porque "$PWD/scripts/…" entre aspas (achado D.3) deve receber o mesmo veredito que $PWD/…
|
|
1241
|
+
// sem aspas: as aspas são sintaxe, não semântica de ancoragem.
|
|
1242
|
+
function stripOuterQuotesForClassify(raw) {
|
|
1243
|
+
if (raw.length >= 2 && raw[0] === '"' && raw[raw.length - 1] === '"') {
|
|
1244
|
+
return raw.slice(1, raw.length - 1)
|
|
1245
|
+
}
|
|
1246
|
+
return raw
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// hookValueWasQuoted reporta se raw tinha aspas duplas externas que stripOuterQuotesForClassify
|
|
1250
|
+
// removeria. Usado para distinguir ~/… (tilde expande para $HOME — classe 1) de "~/…" (tilde
|
|
1251
|
+
// NÃO expande dentro de aspas duplas — classe 2).
|
|
1252
|
+
function hookValueWasQuoted(raw) {
|
|
1253
|
+
return raw.length >= 2 && raw[0] === '"' && raw[raw.length - 1] === '"'
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// classifyHookAnchorage retorna a classe de ancoragem de rawStripped (com aspas externas já
|
|
1257
|
+
// removidas). wasQuoted indica se o valor original tinha aspas externas (obtido com
|
|
1258
|
+
// hookValueWasQuoted antes de chamar stripOuterQuotesForClassify). Ver HOOK_ANCHORAGE_CLASS_*
|
|
1259
|
+
// e ADR-2026-08-22.
|
|
1260
|
+
function classifyHookAnchorage(rawStripped, wasQuoted) {
|
|
1261
|
+
// Classe 1 — ancora na raiz do projeto.
|
|
1262
|
+
if (
|
|
1263
|
+
rawStripped.startsWith('$CLAUDE_PROJECT_DIR/') ||
|
|
1264
|
+
rawStripped.startsWith('$GEMINI_PROJECT_DIR/') ||
|
|
1265
|
+
rawStripped.startsWith('$(git rev-parse --show-toplevel)/') ||
|
|
1266
|
+
path.isAbsolute(rawStripped)
|
|
1267
|
+
) {
|
|
1268
|
+
return HOOK_ANCHORAGE_CLASS_ANCHORED
|
|
1269
|
+
}
|
|
1270
|
+
// ~/… sem aspas: tilde expande para $HOME em qualquer shell POSIX — semanticamente ancorado.
|
|
1271
|
+
// "~/…" com aspas: tilde NÃO expande dentro de aspas duplas, logo a forma quebra — classe 2.
|
|
1272
|
+
if (rawStripped.startsWith('~/') && !wasQuoted) {
|
|
1273
|
+
return HOOK_ANCHORAGE_CLASS_ANCHORED
|
|
1274
|
+
}
|
|
1275
|
+
// Classe 2 — expande a partir do cwd.
|
|
1276
|
+
// ${PWD}/… tem a mesma semântica de $PWD/… (PWD é mandado pelo POSIX, sempre o cwd).
|
|
1277
|
+
if (
|
|
1278
|
+
rawStripped.startsWith('$PWD/') ||
|
|
1279
|
+
rawStripped.startsWith('${PWD}/') ||
|
|
1280
|
+
rawStripped.startsWith('./') ||
|
|
1281
|
+
rawStripped.startsWith('../') ||
|
|
1282
|
+
(!rawStripped.startsWith('$') && !path.isAbsolute(rawStripped))
|
|
1283
|
+
) {
|
|
1284
|
+
return HOOK_ANCHORAGE_CLASS_CWD_DEPENDENT
|
|
1285
|
+
}
|
|
1286
|
+
// Classe 3 — indecidível; silêncio declarado.
|
|
1287
|
+
return HOOK_ANCHORAGE_CLASS_UNDECIDABLE
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// cwdDependentReason retorna o sufixo de mensagem específico por forma para violações de
|
|
1291
|
+
// classe 2, iniciando em "with a …". Dois formatos: formas contendo $PWD ou ${PWD} (em qualquer
|
|
1292
|
+
// posição, inclusive em wrappers sh -c / env) recebem a mensagem do $PWD; demais recebem
|
|
1293
|
+
// "bare relative path". Usa includes (não startsWith) para cobrir sh -c "$PWD/…" e env FOO=x $PWD/….
|
|
1294
|
+
function cwdDependentReason(rawStripped) {
|
|
1295
|
+
if (rawStripped.includes('$PWD') || rawStripped.includes('${PWD}')) {
|
|
1296
|
+
return 'with a $PWD path — $PWD expands to the current working directory, not the project root; run `trackfw update` to fix it'
|
|
1297
|
+
}
|
|
1298
|
+
return "with a bare relative path — this command only resolves from the project root and will silently fail when the agent's cwd is a subdirectory; run `trackfw update` to fix it"
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1223
1301
|
// collectCommandsWithMarker percorre recursivamente um valor JSON já decodificado e coleta todo
|
|
1224
1302
|
// valor-string que contém marker, independentemente do nome do campo que o contém — junto com um
|
|
1225
1303
|
// sinal estrutural (typeIsCommand) de que o objeto imediato que o contém também tem
|
|
@@ -1323,6 +1401,18 @@ function validateGuardHookResolvable(scriptMarker, cwd) {
|
|
|
1323
1401
|
if (seen.has(seenKey)) continue
|
|
1324
1402
|
seen.add(seenKey)
|
|
1325
1403
|
|
|
1404
|
+
// ADR-2026-08-22: classificar por ancoragem ANTES de resolver. Aspas externas são
|
|
1405
|
+
// sintaxe, não semântica — removê-las antes garante que "$PWD/…" receba o mesmo veredito.
|
|
1406
|
+
// wasQuoted distingue ~/… (classe 1) de "~/…" (classe 2).
|
|
1407
|
+
const wasQuoted = hookValueWasQuoted(m.raw)
|
|
1408
|
+
const rawStripped = stripOuterQuotesForClassify(m.raw)
|
|
1409
|
+
const anchorageClass = classifyHookAnchorage(rawStripped, wasQuoted)
|
|
1410
|
+
if (hf.requiresVarOrShellPrefix && anchorageClass === HOOK_ANCHORAGE_CLASS_CWD_DEPENDENT) {
|
|
1411
|
+
msgs.push(`${hf.relPath} (${hf.cli}) references ${scriptMarker} ${cwdDependentReason(rawStripped)}`)
|
|
1412
|
+
continue
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// Classe 1 (ancorado) e classe 3 (indecidível) prosseguem para a resolução existente.
|
|
1326
1416
|
const resolved = resolveCredentialGuardHookPath(m.raw, root)
|
|
1327
1417
|
if (resolved === null) continue
|
|
1328
1418
|
|
|
@@ -1901,6 +1991,24 @@ match_subcommand() {
|
|
|
1901
1991
|
;;
|
|
1902
1992
|
esac
|
|
1903
1993
|
done
|
|
1994
|
+
# git checkout -- <path> | git checkout . descarta alterações não commitadas do
|
|
1995
|
+
# caminho indicado, de forma irreversível, no worktree compartilhado — bloqueia
|
|
1996
|
+
# quando '--' aparece em qualquer posição (forma explícita de pathspec) ou quando
|
|
1997
|
+
# '.' aparece como token isolado. 'git checkout <branch>' sem nenhum dos dois
|
|
1998
|
+
# segue liberado por decisão (distinguir branch de caminho sem '--' é ambíguo, e
|
|
1999
|
+
# adivinhar produziria falso-positivo).
|
|
2000
|
+
checkout_path=0
|
|
2001
|
+
for tok2 in "$@"; do
|
|
2002
|
+
case "$tok2" in
|
|
2003
|
+
--|.)
|
|
2004
|
+
checkout_path=1
|
|
2005
|
+
;;
|
|
2006
|
+
esac
|
|
2007
|
+
done
|
|
2008
|
+
if [ "$checkout_path" = "1" ]; then
|
|
2009
|
+
echo "checkout-path"
|
|
2010
|
+
return 0
|
|
2011
|
+
fi
|
|
1904
2012
|
;;
|
|
1905
2013
|
switch)
|
|
1906
2014
|
for tok2 in "$@"; do
|
|
@@ -1912,6 +2020,87 @@ match_subcommand() {
|
|
|
1912
2020
|
esac
|
|
1913
2021
|
done
|
|
1914
2022
|
;;
|
|
2023
|
+
stash)
|
|
2024
|
+
# git stash: liberado só para leitura (list/show) — bloqueia a forma bare
|
|
2025
|
+
# (equivale a "push"), push, save, clear e drop. Decisão de KG: bloquear a
|
|
2026
|
+
# classe inteira, não só os literais medidos (ver REQ). Repositório com um único
|
|
2027
|
+
# worktree compartilhado entre subagentes paralelos — um stash de um agente
|
|
2028
|
+
# remove as alterações não commitadas de todos os outros.
|
|
2029
|
+
stash_sub="\${1:-}"
|
|
2030
|
+
case "$stash_sub" in
|
|
2031
|
+
list|show)
|
|
2032
|
+
;;
|
|
2033
|
+
*)
|
|
2034
|
+
echo "stash"
|
|
2035
|
+
return 0
|
|
2036
|
+
;;
|
|
2037
|
+
esac
|
|
2038
|
+
;;
|
|
2039
|
+
reset)
|
|
2040
|
+
# Só --hard bloqueia, em qualquer posição de token — --soft/--mixed (inclusive
|
|
2041
|
+
# sem flag, que é --mixed implícito) seguem liberados: --soft é o contorno
|
|
2042
|
+
# padrão para reempurrar trabalho staged via ` + "`" + `trackfw ship -m "..."` + "`" + ` (ainda falta commitar após --soft).
|
|
2043
|
+
for tok2 in "$@"; do
|
|
2044
|
+
case "$tok2" in
|
|
2045
|
+
--hard)
|
|
2046
|
+
echo "reset-hard"
|
|
2047
|
+
return 0
|
|
2048
|
+
;;
|
|
2049
|
+
esac
|
|
2050
|
+
done
|
|
2051
|
+
;;
|
|
2052
|
+
clean)
|
|
2053
|
+
# Bloqueia qualquer forma com force (-f, -fd, -fx, --force) ou -x/-X, EXCETO
|
|
2054
|
+
# quando -n/--dry-run também está presente (dry-run nunca apaga nada).
|
|
2055
|
+
clean_dry=0
|
|
2056
|
+
clean_force=0
|
|
2057
|
+
for tok2 in "$@"; do
|
|
2058
|
+
case "$tok2" in
|
|
2059
|
+
-n|--dry-run)
|
|
2060
|
+
clean_dry=1
|
|
2061
|
+
;;
|
|
2062
|
+
-f*|--force|--force=*|-x|-X)
|
|
2063
|
+
clean_force=1
|
|
2064
|
+
;;
|
|
2065
|
+
esac
|
|
2066
|
+
done
|
|
2067
|
+
if [ "$clean_dry" != "1" ] && [ "$clean_force" = "1" ]; then
|
|
2068
|
+
echo "clean-force"
|
|
2069
|
+
return 0
|
|
2070
|
+
fi
|
|
2071
|
+
;;
|
|
2072
|
+
restore)
|
|
2073
|
+
# git restore --staged SOZINHO nunca toca o working tree (mexe só no
|
|
2074
|
+
# index), então segue liberado mesmo com path. Mas --worktree/-W (com ou
|
|
2075
|
+
# sem --staged junto) SEMPRE afeta o working tree — inclusive
|
|
2076
|
+
# "--staged --worktree", que restaura os dois — então bloqueia sempre que
|
|
2077
|
+
# --worktree/-W aparecer, e também no caso padrão (sem --staged em
|
|
2078
|
+
# nenhuma forma) com um argumento posicional (o path).
|
|
2079
|
+
restore_staged=0
|
|
2080
|
+
restore_worktree=0
|
|
2081
|
+
restore_positional=0
|
|
2082
|
+
for tok2 in "$@"; do
|
|
2083
|
+
case "$tok2" in
|
|
2084
|
+
--staged)
|
|
2085
|
+
restore_staged=1
|
|
2086
|
+
;;
|
|
2087
|
+
--worktree|-W)
|
|
2088
|
+
restore_worktree=1
|
|
2089
|
+
;;
|
|
2090
|
+
-*)
|
|
2091
|
+
;;
|
|
2092
|
+
*)
|
|
2093
|
+
restore_positional=1
|
|
2094
|
+
;;
|
|
2095
|
+
esac
|
|
2096
|
+
done
|
|
2097
|
+
if [ "$restore_positional" = "1" ]; then
|
|
2098
|
+
if [ "$restore_worktree" = "1" ] || [ "$restore_staged" != "1" ]; then
|
|
2099
|
+
echo "restore-path"
|
|
2100
|
+
return 0
|
|
2101
|
+
fi
|
|
2102
|
+
fi
|
|
2103
|
+
;;
|
|
1915
2104
|
branch)
|
|
1916
2105
|
# git branch é majoritariamente leitura (sem args, -a, -r, -l, --list, -v/-vv,
|
|
1917
2106
|
# --show-current, --contains, --no-contains, --merged, --no-merged, --sort=,
|
|
@@ -1967,8 +2156,44 @@ match_subcommand() {
|
|
|
1967
2156
|
;;
|
|
1968
2157
|
esac
|
|
1969
2158
|
done
|
|
2159
|
+
elif [ "\${1:-}" = "remove" ]; then
|
|
2160
|
+
# git worktree remove SEM -f/--force já recusa sozinho quando há alteração não
|
|
2161
|
+
# commitada no worktree indicado — só a forma com force é irreversível o bastante
|
|
2162
|
+
# para bloquear aqui.
|
|
2163
|
+
shift
|
|
2164
|
+
for tok2 in "$@"; do
|
|
2165
|
+
case "$tok2" in
|
|
2166
|
+
-f|--force)
|
|
2167
|
+
echo "worktree-remove-force"
|
|
2168
|
+
return 0
|
|
2169
|
+
;;
|
|
2170
|
+
esac
|
|
2171
|
+
done
|
|
1970
2172
|
fi
|
|
1971
2173
|
;;
|
|
2174
|
+
update-ref)
|
|
2175
|
+
# git update-ref reescreve um ref (inclusive refs/remotes/origin/*) sem tocar o
|
|
2176
|
+
# objeto apontado nem exigir push — foi o mecanismo que tornou alcançável o exploit
|
|
2177
|
+
# descrito no ADR-2026-08-19-caminho-governado-para-push-forcado-e-tag-de-release.md
|
|
2178
|
+
# (Emenda 1): forjar origin/<base> localmente para desviar o commit-alvo de trackfw
|
|
2179
|
+
# release tag. Sem forma de leitura equivalente a bloquear seletivamente — a
|
|
2180
|
+
# subcommand inteira é escrita — bloqueia sempre, sem exceção de token.
|
|
2181
|
+
echo "update-ref"
|
|
2182
|
+
return 0
|
|
2183
|
+
;;
|
|
2184
|
+
rm)
|
|
2185
|
+
# git rm -f/--force apaga do working tree e do index de forma irreversível, mesma
|
|
2186
|
+
# classe de git clean -f/git reset --hard já bloqueados acima — sem exceção para
|
|
2187
|
+
# --cached (destrancar do index sem -f já segue liberado por não precisar de force).
|
|
2188
|
+
for tok2 in "$@"; do
|
|
2189
|
+
case "$tok2" in
|
|
2190
|
+
-f*|--force|--force=*)
|
|
2191
|
+
echo "rm-force"
|
|
2192
|
+
return 0
|
|
2193
|
+
;;
|
|
2194
|
+
esac
|
|
2195
|
+
done
|
|
2196
|
+
;;
|
|
1972
2197
|
esac
|
|
1973
2198
|
done <<EOF
|
|
1974
2199
|
$normalized
|
|
@@ -1980,22 +2205,46 @@ SUBCOMMAND=$(match_subcommand "$CMD_RAW") || exit 0
|
|
|
1980
2205
|
|
|
1981
2206
|
case "$SUBCOMMAND" in
|
|
1982
2207
|
checkout-b)
|
|
1983
|
-
REASON="trackfw: git checkout -b bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_REF_BACKTICK + `. Ver CLAUDE.md §1."
|
|
2208
|
+
REASON="trackfw: git checkout -b bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_REF_BACKTICK + `. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1984
2209
|
;;
|
|
1985
2210
|
switch-c)
|
|
1986
|
-
REASON="trackfw: git switch -c bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_REF_BACKTICK + `. Ver CLAUDE.md §1."
|
|
2211
|
+
REASON="trackfw: git switch -c bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_REF_BACKTICK + `. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1987
2212
|
;;
|
|
1988
2213
|
branch-create)
|
|
1989
|
-
REASON="trackfw: git branch bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_REF_BACKTICK + `. Ver CLAUDE.md §1."
|
|
2214
|
+
REASON="trackfw: git branch bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_REF_BACKTICK + `. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1990
2215
|
;;
|
|
1991
2216
|
worktree-add-b)
|
|
1992
|
-
REASON="trackfw: git worktree add -b bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_REF_BACKTICK + `. Ver CLAUDE.md §1."
|
|
2217
|
+
REASON="trackfw: git worktree add -b bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_REF_BACKTICK + `. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1993
2218
|
;;
|
|
1994
2219
|
commit)
|
|
1995
|
-
REASON="trackfw: git commit bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw commit -m '<mensagem>'` + GBG_REF_BACKTICK + `. Ver CLAUDE.md §1."
|
|
2220
|
+
REASON="trackfw: git commit bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw commit -m '<mensagem>'` + GBG_REF_BACKTICK + `. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1996
2221
|
;;
|
|
1997
2222
|
push)
|
|
1998
|
-
REASON="trackfw: git push bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw ship` + GBG_REF_BACKTICK +
|
|
2223
|
+
REASON="trackfw: git push bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw push` + GBG_REF_BACKTICK + ` (para empurrar commits já criados), ` + GBG_REF_BACKTICK + `trackfw ship` + GBG_REF_BACKTICK + ` (para commit+push+PR em uma etapa) ou ` + GBG_REF_BACKTICK + `trackfw release tag` + GBG_REF_BACKTICK + ` (para publicar uma tag de release). Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
2224
|
+
;;
|
|
2225
|
+
stash)
|
|
2226
|
+
REASON="trackfw: git stash bruto bloqueado — worktree compartilhado entre subagentes, um stash remove as alterações não commitadas de todos os outros. ` + GBG_REF_BACKTICK + `git stash list` + GBG_REF_BACKTICK + `/` + GBG_REF_BACKTICK + `git stash show` + GBG_REF_BACKTICK + ` seguem liberados; para guardar trabalho em progresso, use uma branch própria via ` + GBG_REF_BACKTICK + `trackfw branch new` + GBG_REF_BACKTICK + ` e commit nela. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
2227
|
+
;;
|
|
2228
|
+
reset-hard)
|
|
2229
|
+
REASON="trackfw: git reset --hard bruto bloqueado — descarta de forma irreversível as alterações não commitadas de todo o worktree compartilhado. ` + GBG_REF_BACKTICK + `git reset --soft` + GBG_REF_BACKTICK + `/` + GBG_REF_BACKTICK + `--mixed` + GBG_REF_BACKTICK + ` seguem liberados (ex.: ` + GBG_REF_BACKTICK + `git reset --soft HEAD~1` + GBG_REF_BACKTICK + ` é o caminho padrão; use ` + GBG_REF_BACKTICK + `trackfw ship -m "..."` + GBG_REF_BACKTICK + ` para commitar e empurrar). Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
2230
|
+
;;
|
|
2231
|
+
clean-force)
|
|
2232
|
+
REASON="trackfw: git clean -f/-x bruto bloqueado — apaga arquivos não rastreados do worktree compartilhado, de forma irreversível. ` + GBG_REF_BACKTICK + `git clean -n` + GBG_REF_BACKTICK + `/` + GBG_REF_BACKTICK + `--dry-run` + GBG_REF_BACKTICK + ` segue liberado para revisar antes o que seria apagado. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
2233
|
+
;;
|
|
2234
|
+
restore-path)
|
|
2235
|
+
REASON="trackfw: git restore <path> bruto bloqueado — descarta de forma irreversível as alterações não commitadas do caminho indicado. ` + GBG_REF_BACKTICK + `git restore --staged` + GBG_REF_BACKTICK + ` (não toca o working tree) segue liberado; para descartar de fato, confirme antes com o usuário. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
2236
|
+
;;
|
|
2237
|
+
checkout-path)
|
|
2238
|
+
REASON="trackfw: git checkout -- <path>/git checkout . bruto bloqueado — descarta de forma irreversível as alterações não commitadas do caminho indicado. ` + GBG_REF_BACKTICK + `git checkout <branch>` + GBG_REF_BACKTICK + `/` + GBG_REF_BACKTICK + `git switch <branch>` + GBG_REF_BACKTICK + ` seguem liberados; para descartar de fato, confirme antes com o usuário. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
2239
|
+
;;
|
|
2240
|
+
update-ref)
|
|
2241
|
+
REASON="trackfw: git update-ref bruto bloqueado — reescreve um ref (inclusive refs/remotes/origin/*) sem tocar o objeto apontado nem exigir push, o que permite forjar o commit-alvo que ` + GBG_REF_BACKTICK + `trackfw release tag` + GBG_REF_BACKTICK + ` publicaria. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
2242
|
+
;;
|
|
2243
|
+
worktree-remove-force)
|
|
2244
|
+
REASON="trackfw: git worktree remove -f/--force bruto bloqueado — remove um worktree e descarta de forma irreversível qualquer alteração não commitada nele. ` + GBG_REF_BACKTICK + `git worktree remove` + GBG_REF_BACKTICK + ` sem force segue liberado (recusa sozinho quando há algo não commitado). Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
2245
|
+
;;
|
|
2246
|
+
rm-force)
|
|
2247
|
+
REASON="trackfw: git rm -f/--force bruto bloqueado — apaga arquivos do working tree e do index de forma irreversível, mesma classe de ` + GBG_REF_BACKTICK + `git clean -f` + GBG_REF_BACKTICK + `/` + GBG_REF_BACKTICK + `git reset --hard` + GBG_REF_BACKTICK + ` já bloqueados. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1999
2248
|
;;
|
|
2000
2249
|
*)
|
|
2001
2250
|
exit 0
|
|
@@ -3021,6 +3270,14 @@ module.exports = {
|
|
|
3021
3270
|
resolveAdrStatus,
|
|
3022
3271
|
// ROADMAP-2026-08-12-mitigacao-do-fail-open-do-credential-guard, ML-1A
|
|
3023
3272
|
resolveCredentialGuardHookPath,
|
|
3273
|
+
// ADR-2026-08-22 (ML-1A): classificação por ancoragem
|
|
3274
|
+
HOOK_ANCHORAGE_CLASS_ANCHORED,
|
|
3275
|
+
HOOK_ANCHORAGE_CLASS_CWD_DEPENDENT,
|
|
3276
|
+
HOOK_ANCHORAGE_CLASS_UNDECIDABLE,
|
|
3277
|
+
stripOuterQuotesForClassify,
|
|
3278
|
+
hookValueWasQuoted,
|
|
3279
|
+
classifyHookAnchorage,
|
|
3280
|
+
cwdDependentReason,
|
|
3024
3281
|
collectCredentialGuardCommands,
|
|
3025
3282
|
validateCredentialGuardHookResolvable,
|
|
3026
3283
|
// ROADMAP-2026-08-12-deteccao-de-adulteracao-do-credential-guard-regra-de-validate, ML-1A
|