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.
Files changed (38) hide show
  1. package/README.md +0 -3
  2. package/bin/trackfw +38 -1
  3. package/package.json +1 -1
  4. package/src/branch/prune.js +450 -0
  5. package/src/branch/runner.js +33 -20
  6. package/src/changelog/index.js +114 -0
  7. package/src/commands/branch.js +59 -9
  8. package/src/commands/changelog.js +29 -0
  9. package/src/commands/commit.js +60 -0
  10. package/src/commands/discover.js +75 -0
  11. package/src/commands/doctor.js +32 -0
  12. package/src/commands/index.js +23 -4
  13. package/src/commands/integrations.js +23 -1
  14. package/src/commands/serve.js +52 -4
  15. package/src/commands/ship.js +4 -3
  16. package/src/commands/thirdparty.js +384 -0
  17. package/src/commands/update-harness.js +320 -12
  18. package/src/commands/update.js +3 -1
  19. package/src/commit/runner.js +352 -0
  20. package/src/config/index.js +27 -0
  21. package/src/generators/hooks.js +925 -6
  22. package/src/generators/init.js +28 -6
  23. package/src/i18n/locales/en-US.json +0 -21
  24. package/src/i18n/locales/es-ES.json +0 -21
  25. package/src/i18n/locales/pt-BR.json +0 -21
  26. package/src/integrations/doctor.js +0 -0
  27. package/src/integrations/index.js +19 -2
  28. package/src/integrations/manager.js +133 -19
  29. package/src/lib/fatal-error.js +64 -0
  30. package/src/lib/unknown-command.js +82 -0
  31. package/src/ship/runner.js +259 -95
  32. package/src/thirdparty/fetch.js +140 -0
  33. package/src/thirdparty/markers.js +207 -0
  34. package/src/thirdparty/provenance.js +114 -0
  35. package/src/thirdparty/quarantine.js +113 -0
  36. package/src/thirdparty/references.js +194 -0
  37. package/src/validator/index.js +1060 -27
  38. package/src/commands/plugins.js +0 -184
@@ -6,7 +6,7 @@ const path = require('path')
6
6
  const identityStore = require('../identity')
7
7
  const { catalog, buildPlans, IntegrationManager, globalGroupPath } = require('../integrations')
8
8
  const { tildeify, validateTargets, buildDocument, humanReport, silenceConsole } = require('../lib/update-engine')
9
- const { mergeClaudeHookArray, mergeSimpleCommandArray, mergeCopilotHookArray, generateGlobalCredentialGuardScript } = require('../generators/hooks')
9
+ const { mergeClaudeHookArray, mergeSimpleCommandArray, mergeCopilotHookArray, generateGlobalCredentialGuardScript, generateGlobalGitBranchGuardScript } = require('../generators/hooks')
10
10
 
11
11
  // `trackfw update harness` is the global counterpart to `trackfw update` —
12
12
  // see docs/cli-parity.md, "`trackfw update` vs `trackfw update harness`".
@@ -462,6 +462,288 @@ function credentialGuardTargetKiro(homeRoot, { dryRun, installMissing }) {
462
462
  }
463
463
  }
464
464
 
465
+ // ---------------------------------------------------------------------------
466
+ // git-branch-guard global-scope wiring (ROADMAP-2026-08-17 Wave 2/ML-2A).
467
+ // Mirrors the six credentialGuardTarget<Tool> functions above, entry-for-
468
+ // entry — same 4-state contract, same displayPath per tool, same reuse of
469
+ // mergeClaudeHookArray/mergeSimpleCommandArray/mergeCopilotHookArray (those
470
+ // helpers only need a scriptPath, which here is trackfw-git-branch-guard.sh
471
+ // instead of trackfw-credential-guard.sh). Mirrors
472
+ // internal/generators/update.go:harnessGitBranchGuardTarget<Tool>.
473
+ //
474
+ // Kiro is the one structural exception: credentialGuardTargetKiro rewrites
475
+ // ~/.kiro/hooks/trackfw-credential-guard.json WHOLESALE every run (never
476
+ // merges), so sharing that file with a second wholesale writer would make
477
+ // the two targets flap between each other's desired 2-entry document forever
478
+ // (idempotency failure). gitBranchGuardTargetKiro therefore writes its OWN
479
+ // dedicated file, ~/.kiro/hooks/trackfw-git-branch-guard.json, same
480
+ // {"version":"v1","hooks":[pre,post]} schema, hook names
481
+ // "trackfw-git-branch-guard-global-pre"/"-global-post".
482
+ // ---------------------------------------------------------------------------
483
+
484
+ function gitBranchGuardTargetClaude(homeRoot, { dryRun, installMissing }) {
485
+ const id = 'claude-git-branch-guard'
486
+ const filePath = path.join(homeRoot, '.claude', 'settings.json')
487
+ const displayPath = tildeify(homeRoot, filePath)
488
+ const scriptPath = path.join(homeRoot, '.trackfw', 'scripts', 'trackfw-git-branch-guard.sh')
489
+
490
+ let root
491
+ let raw = null
492
+ try {
493
+ if (fs.existsSync(filePath)) {
494
+ raw = fs.readFileSync(filePath, 'utf8')
495
+ root = raw.length ? JSON.parse(raw) : {}
496
+ } else {
497
+ if (!installMissing) return { id, state: 'missing', path: displayPath }
498
+ if (dryRun) return { id, state: 'updated', path: displayPath }
499
+ root = {}
500
+ if (!root.hooks) root.hooks = {}
501
+ root.hooks.PreToolUse = mergeClaudeHookArray(root.hooks.PreToolUse, 'Bash', scriptPath)
502
+ root.hooks.PostToolUse = mergeClaudeHookArray(root.hooks.PostToolUse, 'Bash', scriptPath)
503
+ const desired = JSON.stringify(root, null, 2) + '\n'
504
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
505
+ fs.writeFileSync(filePath, desired, 'utf8')
506
+ return { id, state: 'updated', path: displayPath }
507
+ }
508
+ } catch (e) {
509
+ return { id, state: 'failed', path: displayPath, message: e.message }
510
+ }
511
+
512
+ try {
513
+ if (!root.hooks) root.hooks = {}
514
+ root.hooks.PreToolUse = mergeClaudeHookArray(root.hooks.PreToolUse, 'Bash', scriptPath)
515
+ root.hooks.PostToolUse = mergeClaudeHookArray(root.hooks.PostToolUse, 'Bash', scriptPath)
516
+ const desired = JSON.stringify(root, null, 2) + '\n'
517
+ if (desired === raw) return { id, state: 'skipped', path: displayPath }
518
+ if (dryRun) return { id, state: 'updated', path: displayPath }
519
+ fs.writeFileSync(filePath, desired, 'utf8')
520
+ return { id, state: 'updated', path: displayPath }
521
+ } catch (e) {
522
+ return { id, state: 'failed', path: displayPath, message: e.message }
523
+ }
524
+ }
525
+
526
+ function gitBranchGuardTargetCodex(homeRoot, { dryRun, installMissing }) {
527
+ const id = 'codex-git-branch-guard'
528
+ const filePath = path.join(homeRoot, '.codex', 'hooks.json')
529
+ const displayPath = tildeify(homeRoot, filePath)
530
+ const scriptPath = path.join(homeRoot, '.trackfw', 'scripts', 'trackfw-git-branch-guard.sh')
531
+
532
+ let root
533
+ let raw = null
534
+ try {
535
+ if (fs.existsSync(filePath)) {
536
+ raw = fs.readFileSync(filePath, 'utf8')
537
+ root = raw.length ? JSON.parse(raw) : {}
538
+ } else {
539
+ if (!installMissing) return { id, state: 'missing', path: displayPath }
540
+ if (dryRun) return { id, state: 'updated', path: displayPath }
541
+ root = {}
542
+ if (!root.hooks) root.hooks = {}
543
+ root.hooks.PreToolUse = mergeClaudeHookArray(root.hooks.PreToolUse, 'Bash', scriptPath)
544
+ root.hooks.PostToolUse = mergeClaudeHookArray(root.hooks.PostToolUse, 'Bash', scriptPath)
545
+ const desired = JSON.stringify(root, null, 2) + '\n'
546
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
547
+ fs.writeFileSync(filePath, desired, 'utf8')
548
+ return { id, state: 'updated', path: displayPath }
549
+ }
550
+ } catch (e) {
551
+ return { id, state: 'failed', path: displayPath, message: e.message }
552
+ }
553
+
554
+ try {
555
+ if (!root.hooks) root.hooks = {}
556
+ root.hooks.PreToolUse = mergeClaudeHookArray(root.hooks.PreToolUse, 'Bash', scriptPath)
557
+ root.hooks.PostToolUse = mergeClaudeHookArray(root.hooks.PostToolUse, 'Bash', scriptPath)
558
+ const desired = JSON.stringify(root, null, 2) + '\n'
559
+ if (desired === raw) return { id, state: 'skipped', path: displayPath }
560
+ if (dryRun) return { id, state: 'updated', path: displayPath }
561
+ fs.writeFileSync(filePath, desired, 'utf8')
562
+ return { id, state: 'updated', path: displayPath }
563
+ } catch (e) {
564
+ return { id, state: 'failed', path: displayPath, message: e.message }
565
+ }
566
+ }
567
+
568
+ function gitBranchGuardTargetGemini(homeRoot, { dryRun, installMissing }) {
569
+ const id = 'gemini-git-branch-guard'
570
+ const filePath = path.join(homeRoot, '.gemini', 'settings.json')
571
+ const displayPath = tildeify(homeRoot, filePath)
572
+ const scriptPath = path.join(homeRoot, '.trackfw', 'scripts', 'trackfw-git-branch-guard.sh')
573
+
574
+ let root
575
+ let raw = null
576
+ try {
577
+ if (fs.existsSync(filePath)) {
578
+ raw = fs.readFileSync(filePath, 'utf8')
579
+ root = raw.length ? JSON.parse(raw) : {}
580
+ } else {
581
+ if (!installMissing) return { id, state: 'missing', path: displayPath }
582
+ if (dryRun) return { id, state: 'updated', path: displayPath }
583
+ root = {}
584
+ if (!root.hooks) root.hooks = {}
585
+ root.hooks.BeforeTool = mergeClaudeHookArray(root.hooks.BeforeTool, 'run_shell_command', scriptPath)
586
+ root.hooks.AfterTool = mergeClaudeHookArray(root.hooks.AfterTool, 'run_shell_command', scriptPath)
587
+ const desired = JSON.stringify(root, null, 2) + '\n'
588
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
589
+ fs.writeFileSync(filePath, desired, 'utf8')
590
+ return { id, state: 'updated', path: displayPath }
591
+ }
592
+ } catch (e) {
593
+ return { id, state: 'failed', path: displayPath, message: e.message }
594
+ }
595
+
596
+ try {
597
+ if (!root.hooks) root.hooks = {}
598
+ root.hooks.BeforeTool = mergeClaudeHookArray(root.hooks.BeforeTool, 'run_shell_command', scriptPath)
599
+ root.hooks.AfterTool = mergeClaudeHookArray(root.hooks.AfterTool, 'run_shell_command', scriptPath)
600
+ const desired = JSON.stringify(root, null, 2) + '\n'
601
+ if (desired === raw) return { id, state: 'skipped', path: displayPath }
602
+ if (dryRun) return { id, state: 'updated', path: displayPath }
603
+ fs.writeFileSync(filePath, desired, 'utf8')
604
+ return { id, state: 'updated', path: displayPath }
605
+ } catch (e) {
606
+ return { id, state: 'failed', path: displayPath, message: e.message }
607
+ }
608
+ }
609
+
610
+ function gitBranchGuardTargetCursor(homeRoot, { dryRun, installMissing }) {
611
+ const id = 'cursor-git-branch-guard'
612
+ const filePath = path.join(homeRoot, '.cursor', 'hooks.json')
613
+ const displayPath = tildeify(homeRoot, filePath)
614
+ const scriptPath = path.join(homeRoot, '.trackfw', 'scripts', 'trackfw-git-branch-guard.sh')
615
+
616
+ let root
617
+ let raw = null
618
+ try {
619
+ if (fs.existsSync(filePath)) {
620
+ raw = fs.readFileSync(filePath, 'utf8')
621
+ root = raw.length ? JSON.parse(raw) : {}
622
+ } else {
623
+ if (!installMissing) return { id, state: 'missing', path: displayPath }
624
+ if (dryRun) return { id, state: 'updated', path: displayPath }
625
+ root = {}
626
+ if (typeof root.version === 'undefined') root.version = 1
627
+ if (!root.hooks) root.hooks = {}
628
+ root.hooks.beforeShellExecution = mergeSimpleCommandArray(root.hooks.beforeShellExecution, scriptPath)
629
+ root.hooks.afterShellExecution = mergeSimpleCommandArray(root.hooks.afterShellExecution, scriptPath)
630
+ const desired = JSON.stringify(root, null, 2) + '\n'
631
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
632
+ fs.writeFileSync(filePath, desired, 'utf8')
633
+ return { id, state: 'updated', path: displayPath }
634
+ }
635
+ } catch (e) {
636
+ return { id, state: 'failed', path: displayPath, message: e.message }
637
+ }
638
+
639
+ try {
640
+ if (typeof root.version === 'undefined') root.version = 1
641
+ if (!root.hooks) root.hooks = {}
642
+ root.hooks.beforeShellExecution = mergeSimpleCommandArray(root.hooks.beforeShellExecution, scriptPath)
643
+ root.hooks.afterShellExecution = mergeSimpleCommandArray(root.hooks.afterShellExecution, scriptPath)
644
+ const desired = JSON.stringify(root, null, 2) + '\n'
645
+ if (desired === raw) return { id, state: 'skipped', path: displayPath }
646
+ if (dryRun) return { id, state: 'updated', path: displayPath }
647
+ fs.writeFileSync(filePath, desired, 'utf8')
648
+ return { id, state: 'updated', path: displayPath }
649
+ } catch (e) {
650
+ return { id, state: 'failed', path: displayPath, message: e.message }
651
+ }
652
+ }
653
+
654
+ function gitBranchGuardTargetCopilot(homeRoot, { dryRun, installMissing }) {
655
+ const id = 'copilot-git-branch-guard'
656
+ const filePath = path.join(homeRoot, '.copilot', 'settings.json')
657
+ const displayPath = tildeify(homeRoot, filePath)
658
+ const scriptPath = path.join(homeRoot, '.trackfw', 'scripts', 'trackfw-git-branch-guard.sh')
659
+
660
+ let root
661
+ let raw = null
662
+ try {
663
+ if (fs.existsSync(filePath)) {
664
+ raw = fs.readFileSync(filePath, 'utf8')
665
+ root = raw.length ? JSON.parse(raw) : {}
666
+ } else {
667
+ if (!installMissing) return { id, state: 'missing', path: displayPath }
668
+ if (dryRun) return { id, state: 'updated', path: displayPath }
669
+ root = {}
670
+ if (!root.hooks) root.hooks = {}
671
+ root.hooks.preToolUse = mergeCopilotHookArray(root.hooks.preToolUse, scriptPath)
672
+ root.hooks.postToolUse = mergeCopilotHookArray(root.hooks.postToolUse, scriptPath)
673
+ const desired = JSON.stringify(root, null, 2) + '\n'
674
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
675
+ fs.writeFileSync(filePath, desired, 'utf8')
676
+ return { id, state: 'updated', path: displayPath }
677
+ }
678
+ } catch (e) {
679
+ return { id, state: 'failed', path: displayPath, message: e.message }
680
+ }
681
+
682
+ try {
683
+ if (!root.hooks) root.hooks = {}
684
+ root.hooks.preToolUse = mergeCopilotHookArray(root.hooks.preToolUse, scriptPath)
685
+ root.hooks.postToolUse = mergeCopilotHookArray(root.hooks.postToolUse, scriptPath)
686
+ const desired = JSON.stringify(root, null, 2) + '\n'
687
+ if (desired === raw) return { id, state: 'skipped', path: displayPath }
688
+ if (dryRun) return { id, state: 'updated', path: displayPath }
689
+ fs.writeFileSync(filePath, desired, 'utf8')
690
+ return { id, state: 'updated', path: displayPath }
691
+ } catch (e) {
692
+ return { id, state: 'failed', path: displayPath, message: e.message }
693
+ }
694
+ }
695
+
696
+ // gitBranchGuardTargetKiro writes a DEDICATED file,
697
+ // ~/.kiro/hooks/trackfw-git-branch-guard.json — deliberately NOT
698
+ // credentialGuardTargetKiro's file. See this section's header comment for
699
+ // why sharing would break idempotency.
700
+ function gitBranchGuardTargetKiro(homeRoot, { dryRun, installMissing }) {
701
+ const id = 'kiro-git-branch-guard'
702
+ const filePath = path.join(homeRoot, '.kiro', 'hooks', 'trackfw-git-branch-guard.json')
703
+ const displayPath = tildeify(homeRoot, filePath)
704
+ const scriptPath = path.join(homeRoot, '.trackfw', 'scripts', 'trackfw-git-branch-guard.sh')
705
+
706
+ const desired = JSON.stringify(
707
+ {
708
+ version: 'v1',
709
+ hooks: [
710
+ {
711
+ name: 'trackfw-git-branch-guard-global-pre',
712
+ description: 'Blocks branch-creation git subcommands issued outside trackfw branch new (global, all trackfw projects)',
713
+ trigger: 'PreToolUse',
714
+ matcher: 'shell',
715
+ action: { type: 'command', command: scriptPath },
716
+ },
717
+ {
718
+ name: 'trackfw-git-branch-guard-global-post',
719
+ description: 'Warns on branch-creation git subcommands issued outside trackfw branch new (global, all trackfw projects)',
720
+ trigger: 'PostToolUse',
721
+ matcher: 'shell',
722
+ action: { type: 'command', command: scriptPath },
723
+ },
724
+ ],
725
+ },
726
+ null,
727
+ 2
728
+ ) + '\n'
729
+
730
+ try {
731
+ const exists = fs.existsSync(filePath)
732
+ const actual = exists ? fs.readFileSync(filePath, 'utf8') : null
733
+
734
+ if (!exists && !installMissing) return { id, state: 'missing', path: displayPath }
735
+ if (exists && actual === desired) return { id, state: 'skipped', path: displayPath }
736
+
737
+ if (dryRun) return { id, state: 'updated', path: displayPath }
738
+
739
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
740
+ fs.writeFileSync(filePath, desired, 'utf8')
741
+ return { id, state: 'updated', path: displayPath }
742
+ } catch (e) {
743
+ return { id, state: 'failed', path: displayPath, message: e.message }
744
+ }
745
+ }
746
+
465
747
  // catalogBundleTarget — one target per (tool, kind) pair at global scope.
466
748
  // Uses IntegrationManager.inspect (read-only) to classify every catalog
467
749
  // item under that pair, then only calls manager.update() for the subset
@@ -506,18 +788,20 @@ function catalogBundleTarget(toolId, kind, homeRoot, identityConfig, { dryRun, i
506
788
  }
507
789
 
508
790
  // HARNESS_TARGET_IDS — mirrors internal/generators/update.go:HarnessTargetIDs.
509
- // "codex-credential-guard"/"gemini-credential-guard" are each inserted
510
- // immediately BEFORE their tool's "-agents"/"-skills" pair (same relative
511
- // position as claude-credential-guard, which precedes claude-agents/
512
- // claude-skills) — see buildHarnessTargetIDs's comment in update.go for the
513
- // full rationale.
514
- const HARNESS_TARGET_IDS = ['claude-skill', 'claude-credential-guard']
791
+ // "codex-credential-guard"/"codex-git-branch-guard",
792
+ // "gemini-credential-guard"/"gemini-git-branch-guard", etc. are each
793
+ // inserted immediately BEFORE their tool's "-agents"/"-skills" pair (same
794
+ // relative position as claude-credential-guard/claude-git-branch-guard,
795
+ // which precede claude-agents/claude-skills), with credential-guard always
796
+ // preceding git-branch-guard within a tool — see buildHarnessTargetIDs's
797
+ // comment in update.go for the full rationale.
798
+ const HARNESS_TARGET_IDS = ['claude-skill', 'claude-credential-guard', 'claude-git-branch-guard']
515
799
  for (const target of catalog.targets) {
516
- if (target.id === 'codex') HARNESS_TARGET_IDS.push('codex-credential-guard')
517
- if (target.id === 'gemini') HARNESS_TARGET_IDS.push('gemini-credential-guard')
518
- if (target.id === 'cursor') HARNESS_TARGET_IDS.push('cursor-credential-guard')
519
- if (target.id === 'copilot') HARNESS_TARGET_IDS.push('copilot-credential-guard')
520
- if (target.id === 'kiro') HARNESS_TARGET_IDS.push('kiro-credential-guard')
800
+ if (target.id === 'codex') HARNESS_TARGET_IDS.push('codex-credential-guard', 'codex-git-branch-guard')
801
+ if (target.id === 'gemini') HARNESS_TARGET_IDS.push('gemini-credential-guard', 'gemini-git-branch-guard')
802
+ if (target.id === 'cursor') HARNESS_TARGET_IDS.push('cursor-credential-guard', 'cursor-git-branch-guard')
803
+ if (target.id === 'copilot') HARNESS_TARGET_IDS.push('copilot-credential-guard', 'copilot-git-branch-guard')
804
+ if (target.id === 'kiro') HARNESS_TARGET_IDS.push('kiro-credential-guard', 'kiro-git-branch-guard')
521
805
  HARNESS_TARGET_IDS.push(`${target.id}-agents`, `${target.id}-skills`)
522
806
  }
523
807
 
@@ -531,22 +815,38 @@ function buildHarnessTargets(homeRoot, identityConfig, { dryRun, installMissing
531
815
  const targets = []
532
816
  if (include('claude-skill')) targets.push(claudeSkillTarget(homeRoot, { dryRun, installMissing }))
533
817
  if (include('claude-credential-guard')) targets.push(credentialGuardTargetClaude(homeRoot, { dryRun, installMissing }))
818
+ if (include('claude-git-branch-guard')) targets.push(gitBranchGuardTargetClaude(homeRoot, { dryRun, installMissing }))
534
819
  for (const target of catalog.targets) {
535
820
  if (target.id === 'codex' && include('codex-credential-guard')) {
536
821
  targets.push(credentialGuardTargetCodex(homeRoot, { dryRun, installMissing }))
537
822
  }
823
+ if (target.id === 'codex' && include('codex-git-branch-guard')) {
824
+ targets.push(gitBranchGuardTargetCodex(homeRoot, { dryRun, installMissing }))
825
+ }
538
826
  if (target.id === 'gemini' && include('gemini-credential-guard')) {
539
827
  targets.push(credentialGuardTargetGemini(homeRoot, { dryRun, installMissing }))
540
828
  }
829
+ if (target.id === 'gemini' && include('gemini-git-branch-guard')) {
830
+ targets.push(gitBranchGuardTargetGemini(homeRoot, { dryRun, installMissing }))
831
+ }
541
832
  if (target.id === 'cursor' && include('cursor-credential-guard')) {
542
833
  targets.push(credentialGuardTargetCursor(homeRoot, { dryRun, installMissing }))
543
834
  }
835
+ if (target.id === 'cursor' && include('cursor-git-branch-guard')) {
836
+ targets.push(gitBranchGuardTargetCursor(homeRoot, { dryRun, installMissing }))
837
+ }
544
838
  if (target.id === 'copilot' && include('copilot-credential-guard')) {
545
839
  targets.push(credentialGuardTargetCopilot(homeRoot, { dryRun, installMissing }))
546
840
  }
841
+ if (target.id === 'copilot' && include('copilot-git-branch-guard')) {
842
+ targets.push(gitBranchGuardTargetCopilot(homeRoot, { dryRun, installMissing }))
843
+ }
547
844
  if (target.id === 'kiro' && include('kiro-credential-guard')) {
548
845
  targets.push(credentialGuardTargetKiro(homeRoot, { dryRun, installMissing }))
549
846
  }
847
+ if (target.id === 'kiro' && include('kiro-git-branch-guard')) {
848
+ targets.push(gitBranchGuardTargetKiro(homeRoot, { dryRun, installMissing }))
849
+ }
550
850
  const agentsId = `${target.id}-agents`
551
851
  const skillsId = `${target.id}-skills`
552
852
  if (include(agentsId)) targets.push(catalogBundleTarget(target.id, 'agents', homeRoot, identityConfig, { dryRun, installMissing }))
@@ -589,8 +889,10 @@ function run(options) {
589
889
  if (!dryRun) {
590
890
  if (options.json) {
591
891
  silenceConsole(() => generateGlobalCredentialGuardScript(homeRoot))
892
+ silenceConsole(() => generateGlobalGitBranchGuardScript(homeRoot))
592
893
  } else {
593
894
  generateGlobalCredentialGuardScript(homeRoot)
895
+ generateGlobalGitBranchGuardScript(homeRoot)
594
896
  }
595
897
  }
596
898
 
@@ -625,4 +927,10 @@ module.exports = {
625
927
  credentialGuardTargetCursor,
626
928
  credentialGuardTargetCopilot,
627
929
  credentialGuardTargetKiro,
930
+ gitBranchGuardTargetClaude,
931
+ gitBranchGuardTargetCodex,
932
+ gitBranchGuardTargetGemini,
933
+ gitBranchGuardTargetCursor,
934
+ gitBranchGuardTargetCopilot,
935
+ gitBranchGuardTargetKiro,
628
936
  }
@@ -207,7 +207,7 @@ function buildProjectTargets(cwd, cfg, identityConfig, { dryRun, installMissing
207
207
 
208
208
  if (include('agent-hooks')) targets.push(runFileTarget({
209
209
  id: 'agent-hooks',
210
- path: '.claude/settings.json, .codex/hooks.json, .gemini/settings.json, .kiro/hooks/trackfw-attention.json, .github/hooks/trackfw-attention.json, .cursor/hooks.json, scripts/trackfw-attention-*.sh, scripts/trackfw-credential-guard.sh',
210
+ path: '.claude/settings.json, .codex/hooks.json, .gemini/settings.json, .kiro/hooks/trackfw-attention.json, .github/hooks/trackfw-attention.json, .cursor/hooks.json, scripts/trackfw-attention-*.sh, scripts/trackfw-credential-guard.sh, scripts/trackfw-git-branch-guard.sh',
211
211
  root: cwd,
212
212
  relPaths: [
213
213
  '.claude/settings.json',
@@ -219,11 +219,13 @@ function buildProjectTargets(cwd, cfg, identityConfig, { dryRun, installMissing
219
219
  'scripts/trackfw-attention-signal.sh',
220
220
  'scripts/trackfw-attention-cleanup.sh',
221
221
  'scripts/trackfw-credential-guard.sh',
222
+ 'scripts/trackfw-git-branch-guard.sh',
222
223
  ],
223
224
  apply: (root) => {
224
225
  hooksGen.injectHooksDetected(root);
225
226
  hooksGen.generateAttentionScripts(cfg, root);
226
227
  hooksGen.generateCredentialGuardScript(root);
228
+ hooksGen.generateGitBranchGuardScript(root);
227
229
  },
228
230
  dryRun,
229
231
  installMissing,