trackfw 7.0.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,48 @@ const { handleAttention } = require('../serve/api_attention')
13
13
 
14
14
  const STATIC_DIR = path.join(__dirname, '..', 'serve', 'static')
15
15
 
16
+ // Aviso pinado, byte-idêntico entre os 3 runtimes (Go, Node.js, Python) — ver
17
+ // docs/cli-parity.md "`trackfw serve` — endereço de escuta, `--host` e
18
+ // aviso de exposição". Emitido quando --host resolve para uma interface
19
+ // diferente de loopback.
20
+ function exposureWarning(host, port) {
21
+ return `WARNING: trackfw serve is binding to ${host}:${port} — the governance chain (ADRs, REQs, roadmaps) will be readable without authentication by any device that can reach it.`
22
+ }
23
+
24
+ // Texto de --help pinado, byte-idêntico entre os 3 runtimes.
25
+ const SERVE_HOST_FLAG_HELP = 'Host to bind to (loopback only by default; use 0.0.0.0 to expose on the network)'
26
+
27
+ // Espelha internal/serve/serve.go IsLoopbackHost — 'localhost' ou IP loopback
28
+ // (IPv4 127.0.0.0/8 inteiro, ou IPv6 ::1), igual a net.IP.IsLoopback() do Go
29
+ // e ipaddress.ip_address(...).is_loopback do Python.
30
+ function isLoopbackHost(host) {
31
+ const net = require('net')
32
+ if (host === 'localhost') return true
33
+ if (net.isIPv4(host)) {
34
+ return host.split('.')[0] === '127'
35
+ }
36
+ if (net.isIPv6(host)) {
37
+ return host === '::1' || host === '0:0:0:0:0:0:0:1'
38
+ }
39
+ return false
40
+ }
41
+
42
+ // Espelha internal/serve/serve.go DisplayURL — URL a imprimir e a abrir no
43
+ // browser. 'localhost' é mantido só para 'localhost' ou IPv4 loopback
44
+ // (127.0.0.0/8), para não mudar a saída do caso comum; hosts IPv6 usam
45
+ // colchetes; qualquer outro host é impresso como está.
46
+ function displayUrl(host, port) {
47
+ const net = require('net')
48
+ if (host === 'localhost') return `http://localhost:${port}`
49
+ if (net.isIPv4(host)) {
50
+ return host.split('.')[0] === '127' ? `http://localhost:${port}` : `http://${host}:${port}`
51
+ }
52
+ if (net.isIPv6(host)) {
53
+ return `http://[${host}]:${port}`
54
+ }
55
+ return `http://${host}:${port}`
56
+ }
57
+
16
58
  // Mapa de extensão → Content-Type
17
59
  const MIME = {
18
60
  '.html': 'text/html; charset=utf-8',
@@ -141,19 +183,25 @@ function createServeCommand() {
141
183
  cmd
142
184
  .description('Inicia o servidor HTTP do trackfw dashboard (kanban + chain + metrics)')
143
185
  .option('--port <port>', 'Porta do servidor', '8080')
186
+ .option('--host <host>', SERVE_HOST_FLAG_HELP, '127.0.0.1')
144
187
  .option('--no-open', 'Não abrir o browser automaticamente')
145
188
  .action((opts) => {
146
189
  const cfg = config.load()
147
190
  const port = parseInt(opts.port, 10) || 8080
191
+ const host = opts.host
148
192
 
149
193
  const server = createServer(cfg, port)
150
194
 
151
- server.listen(port, '127.0.0.1', () => {
152
- console.log(`trackfw serve: http://localhost:${port}`)
195
+ if (!isLoopbackHost(host)) {
196
+ console.error(exposureWarning(host, port))
197
+ }
198
+
199
+ server.listen(port, host, () => {
200
+ const url = displayUrl(host, port)
201
+ console.log(`trackfw serve: ${url}`)
153
202
 
154
203
  if (opts.open !== false) {
155
204
  // Tentar abrir o browser
156
- const url = `http://localhost:${port}`
157
205
  const { exec } = require('child_process')
158
206
  const platform = process.platform
159
207
  let openCmd
@@ -187,4 +235,4 @@ function createServeCommand() {
187
235
  return cmd
188
236
  }
189
237
 
190
- module.exports = { createServeCommand, createServer }
238
+ module.exports = { createServeCommand, createServer, isLoopbackHost, displayUrl }
@@ -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 }))
@@ -627,4 +927,10 @@ module.exports = {
627
927
  credentialGuardTargetCursor,
628
928
  credentialGuardTargetCopilot,
629
929
  credentialGuardTargetKiro,
930
+ gitBranchGuardTargetClaude,
931
+ gitBranchGuardTargetCodex,
932
+ gitBranchGuardTargetGemini,
933
+ gitBranchGuardTargetCursor,
934
+ gitBranchGuardTargetCopilot,
935
+ gitBranchGuardTargetKiro,
630
936
  }