trackfw 6.3.1 → 6.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "trackfw",
3
- "version": "6.3.1",
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, and Amazon Q.",
3
+ "version": "6.4.0",
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",
7
7
  "adr",
@@ -0,0 +1,140 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * branch/runner.js — Core implementation of `trackfw branch new`.
5
+ *
6
+ * Moves the branch_has_wip_roadmap governance gate (already enforced by `trackfw validate` and
7
+ * `trackfw ship`) to before branch creation, instead of after. Mirrors internal/commands/branch.go
8
+ * byte-for-byte in behavior and message text — Go is the behavioral reference
9
+ * (docs/cli-parity.md: "Go is the behavioral reference").
10
+ *
11
+ * All git write operations are injectable for testability — never runs a real `git checkout -b`
12
+ * in unit tests.
13
+ */
14
+
15
+ const { spawnSync } = require('child_process')
16
+ const config = require('../config')
17
+ const validator = require('../validator')
18
+
19
+ // branchValidTypes mirrors the vocabulary already enforced by `trackfw ship` (feat|fix|refactor).
20
+ const branchValidTypes = new Set(['feat', 'fix', 'refactor'])
21
+
22
+ /**
23
+ * parseBranchSpec splits "<type>/<slug>" and validates both parts. type must be one of
24
+ * feat, fix, refactor (branchValidTypes); slug must be non-empty.
25
+ * @param {string} spec
26
+ * @returns {{ branchType: string, slug: string }}
27
+ * @throws {Error} when spec is malformed
28
+ */
29
+ function parseBranchSpec(spec) {
30
+ const value = String(spec)
31
+ const idx = value.indexOf('/')
32
+ if (idx === -1 || value.slice(0, idx) === '') {
33
+ throw new Error(`invalid branch spec "${spec}" — expected <type>/<slug> with type in feat, fix, refactor`)
34
+ }
35
+ const branchType = value.slice(0, idx)
36
+ const slug = value.slice(idx + 1)
37
+ if (!branchValidTypes.has(branchType)) {
38
+ throw new Error(`invalid branch type "${branchType}" — must be one of feat, fix, refactor`)
39
+ }
40
+ if (slug.trim() === '') {
41
+ throw new Error(`branch slug is required — expected <type>/<slug>, got "${spec}"`)
42
+ }
43
+ return { branchType, slug }
44
+ }
45
+
46
+ /**
47
+ * defaultGitCheckout runs `git checkout -b <branchName>` with inherited stdio, so Git's own output
48
+ * (including branch-already-exists errors) reaches the user unmodified.
49
+ *
50
+ * Returns Git's own numeric exit code (0 on success), never a generic 1 — this command's contract
51
+ * is to propagate Git's exit status literally (see internal/commands/branch.go's
52
+ * defaultGitCheckout, the behavioral reference: it exits the process directly with
53
+ * exitErr.ExitCode() rather than a hardcoded 1). A spawn failure (e.g. the git binary is missing)
54
+ * or a signal-terminated process has no Git-produced numeric code to propagate, so both fall back
55
+ * to 1 — the same convention already used for signal-killed gate commands
56
+ * (docs/cli-parity.md, `trackfw barrier`, "Gate process terminated by a signal ... Recorded as exit 1").
57
+ * @param {string} branchName
58
+ * @returns {number} exit code
59
+ */
60
+ function defaultGitCheckout(branchName) {
61
+ const result = spawnSync('git', ['checkout', '-b', branchName], { stdio: 'inherit' })
62
+ if (result.error) return 1
63
+ if (result.status === null) return 1
64
+ return result.status
65
+ }
66
+
67
+ /**
68
+ * runBranchNew implements the `trackfw branch new <type>/<slug>` flow described in
69
+ * docs/req/REQ-2026-08-04-comando-trackfw-branch-new-para-bloquear-criacao-de-branch-sem-req-roadmap-em-wip.md.
70
+ *
71
+ * Mirrors Go's stdout/stderr split exactly (internal/commands/branch.go + root.go Execute()):
72
+ * the primary governance/dry-run message goes to stdout via `writeln`; usage errors and the
73
+ * "blocked: ..." wrapper (Go's `Execute()` printing the returned error) go to stderr via
74
+ * `writeErr`. Git's own output is inherited directly by the child process and is never touched
75
+ * here.
76
+ *
77
+ * @param {string} spec "<type>/<slug>"
78
+ * @param {boolean} dryRun
79
+ * @param {{ loadConfig?: function, resolveWIPDirs?: function, resolveDoneDirs?: function,
80
+ * matchSlug?: function(string, string[], string[]): {matched: boolean, candidates: string[]},
81
+ * execGitCheckout?: function(string): number, writeln?: function(string): void,
82
+ * writeErr?: function(string): void }} deps
83
+ * @returns {number} exit code (0 = success, 1 = blocked/usage error, or Git's own exit code when
84
+ * `git checkout -b` fails — propagated literally, never hardcoded)
85
+ */
86
+ function runBranchNew(spec, dryRun, deps = {}) {
87
+ const loadConfig = deps.loadConfig || config.load
88
+ const resolveWIPDirs = deps.resolveWIPDirs || validator.resolveWIPDirs
89
+ const resolveDoneDirs = deps.resolveDoneDirs || validator.resolveDoneDirs
90
+ const matchSlug = deps.matchSlug || validator.branchSlugMatchesRoadmap
91
+ const execGitCheckout = deps.execGitCheckout || defaultGitCheckout
92
+ const writeln = deps.writeln || ((s) => process.stdout.write(s + '\n'))
93
+ const writeErr = deps.writeErr || ((s) => process.stderr.write(s + '\n'))
94
+
95
+ let branchType, slug
96
+ try {
97
+ ({ branchType, slug } = parseBranchSpec(spec))
98
+ } catch (err) {
99
+ writeErr(err.message)
100
+ return 1
101
+ }
102
+
103
+ const branchName = `${branchType}/${slug}`
104
+
105
+ const cfg = loadConfig()
106
+ const wipDirs = resolveWIPDirs(cfg)
107
+ const doneDirs = resolveDoneDirs(cfg)
108
+
109
+ const normalizedSlug = validator.normalizeBranchSlug(slug)
110
+ const { matched, candidates } = matchSlug(normalizedSlug, wipDirs, doneDirs)
111
+
112
+ if (!matched) {
113
+ const msg = (!candidates || candidates.length === 0)
114
+ ? validator.branchGovernanceOrientation(branchName)
115
+ : validator.branchNoMatchingRoadmapMessage(branchName, candidates)
116
+ if (dryRun) {
117
+ writeln(`[dry-run] would block: ${msg}`)
118
+ } else {
119
+ writeln(msg)
120
+ }
121
+ writeErr(`blocked: no matching roadmap in wip/ nor done/ for "${branchName}"`)
122
+ return 1
123
+ }
124
+
125
+ if (dryRun) {
126
+ writeln(`[dry-run] would create branch "${branchName}" (git checkout -b ${branchName})`)
127
+ return 0
128
+ }
129
+
130
+ // Git's own stderr (inherited stdio) already reached the user unmodified — do not reformat,
131
+ // duplicate, or replace this exit code with a generic 1.
132
+ return execGitCheckout(branchName)
133
+ }
134
+
135
+ module.exports = {
136
+ branchValidTypes,
137
+ parseBranchSpec,
138
+ defaultGitCheckout,
139
+ runBranchNew,
140
+ }
@@ -0,0 +1,46 @@
1
+ 'use strict'
2
+
3
+ // trackfw branch new <type>/<slug>
4
+ //
5
+ // CLI wiring for the `trackfw branch` command group. The testable logic lives in
6
+ // ../branch/runner.js (mirrors the ship.js / ship/runner.js split already used in this CLI).
7
+
8
+ const { Command } = require('commander')
9
+ const { runBranchNew } = require('../branch/runner')
10
+
11
+ function createBranchCommand() {
12
+ const cmd = new Command('branch')
13
+ cmd.description('Manage governed feature branches')
14
+
15
+ const newCmd = new Command('new')
16
+ newCmd
17
+ .description(
18
+ 'Create a feat/fix/refactor branch, gated on a matching REQ + roadmap already in wip/ or done/\n\n' +
19
+ 'trackfw branch new moves the branch_has_wip_roadmap governance gate (already enforced\n' +
20
+ "by 'trackfw validate' and 'trackfw ship') to before branch creation, instead of after:\n\n" +
21
+ ' 1. Validates <type> is one of feat, fix, refactor and <slug> is non-empty.\n' +
22
+ ' 2. Checks whether a roadmap in wip/ or done/ matches the given slug — the exact matching\n' +
23
+ " logic 'trackfw validate' already uses (normalized slug, filename contains match).\n" +
24
+ " 3. Without a match: blocks — 'git checkout -b' is never executed — and prints the same\n" +
25
+ " governance orientation message 'trackfw validate' already prints for this rule.\n" +
26
+ " 4. With a match: runs 'git checkout -b <type>/<slug>', propagating Git's own output and\n" +
27
+ ' exit status literally.\n\n' +
28
+ 'Create the governance artifacts first if this blocks you:\n' +
29
+ ' trackfw req new "title"\n' +
30
+ ' trackfw roadmap new "title"\n' +
31
+ ' trackfw roadmap move <name> wip'
32
+ )
33
+ .argument('<spec>', '<type>/<slug>, type in feat, fix, refactor')
34
+ .option('--dry-run', 'Report whether the branch would be created or blocked, without executing git', false)
35
+ .action((spec, options) => {
36
+ const exitCode = runBranchNew(spec, !!options.dryRun, {})
37
+ if (exitCode !== 0) {
38
+ process.exit(exitCode)
39
+ }
40
+ })
41
+
42
+ cmd.addCommand(newCmd)
43
+ return cmd
44
+ }
45
+
46
+ module.exports = createBranchCommand()
@@ -435,6 +435,12 @@ cmd.action((opts) => {
435
435
  } catch (e) {
436
436
  console.log(`⚠ agent rules inject partial: ${e.message}`);
437
437
  }
438
+ try {
439
+ const { generateAttentionScripts } = require('../generators/hooks');
440
+ generateAttentionScripts({}, cwd);
441
+ } catch (e) {
442
+ console.warn(`⚠ attention scripts: ${e.message}`);
443
+ }
438
444
  try {
439
445
  const { injectHooksDetected } = require('../generators/hooks');
440
446
  injectHooksDetected(cwd);
@@ -32,6 +32,7 @@ function createProgram() {
32
32
  program.addCommand(require('./note'))
33
33
  program.addCommand(require('./ship'))
34
34
  program.addCommand(require('./barrier'))
35
+ program.addCommand(require('./branch'))
35
36
 
36
37
  const { createServeCommand } = require('./serve')
37
38
  program.addCommand(createServeCommand())
@@ -8,7 +8,7 @@ const { resolveScope } = require('./integrations')
8
8
 
9
9
  const cmd = new Command('init')
10
10
  cmd.description(t('init.description'))
11
- cmd.option('--ai-tools <tools>', 'Comma-separated AI tools to configure (claude,codex,gemini,antigravity,cursor,copilot,windsurf,amazonq,kiro)', '')
11
+ cmd.option('--ai-tools <tools>', 'Comma-separated AI tools to configure (claude,codex,gemini,antigravity,cursor,copilot,windsurf,amazonq,opencode,kiro)', '')
12
12
  cmd.option('--identity-preset <preset>', `Agent identity preset: none, neutral, ${identityStore.presetNames().join(', ')}`, 'none')
13
13
  cmd.option('--forge <value>', 'Forge platform: github, gitlab, bitbucket, azure', '')
14
14
  cmd.action(async (options, command) => {
@@ -58,7 +58,7 @@ cmd.action(async (options, command) => {
58
58
  }
59
59
  await generators.scaffold(cfg)
60
60
  const aiTools = String(options.aiTools || '').split(',').map(tool => tool.trim()).filter(Boolean)
61
- const supported = new Set(['claude', 'codex', 'gemini', 'antigravity', 'cursor', 'copilot', 'windsurf', 'amazonq', 'kiro'])
61
+ const supported = new Set(['claude', 'codex', 'gemini', 'antigravity', 'cursor', 'copilot', 'windsurf', 'amazonq', 'opencode', 'kiro'])
62
62
  // Sem TTY, o escopo nunca é perguntado: default `global` (ADR D1/D4).
63
63
  const scope = await resolveScope({}, { interactive: false })
64
64
  const makeOnSkip = () => (_destination, reason) => {
@@ -222,6 +222,7 @@ cmd.action(async (options, command) => {
222
222
  { name: 'GitHub Copilot', value: 'copilot' },
223
223
  { name: 'Windsurf', value: 'windsurf' },
224
224
  { name: 'Amazon Q Developer', value: 'amazonq' },
225
+ { name: 'OpenCode', value: 'opencode' },
225
226
  { name: 'Kiro', value: 'kiro' },
226
227
  ],
227
228
  })
@@ -82,8 +82,9 @@ case "$ROADMAP_DIR" in
82
82
  esac
83
83
 
84
84
  TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
85
- TOOL_ESC=$(echo "$TOOL" | tr -d '\\000-\\037' | sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g')
86
- MSG_ESC=$(echo "$MSG" | tr -d '\\000-\\037' | sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g')
85
+
86
+ TOOL_ESC=$(echo "$TOOL" | tr -d '\\000-\\037' | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
87
+ MSG_ESC=$(echo "$MSG" | tr -d '\\000-\\037' | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
87
88
 
88
89
  mkdir -p "$ROADMAP_DIR"
89
90
  printf '{"tool":"%s","message":"%s","level":"action_required","timestamp":"%s"}\\n' \\
@@ -35,6 +35,9 @@ Analyze real dependencies between microbatches before assigning work. Microbatch
35
35
  ## Workflow
36
36
  Analyze the codebase and requirements; record material decisions in an ADR; create the REQ with an explicit negative scope; produce a roadmap of waves and microbatches with measurable acceptance criteria; create the branch; commit the governance artifacts before any handoff; dispatch the wave; audit each microbatch against its acceptance criteria; update the roadmap; open the pull request only on request.
37
37
 
38
+ ## Dispatch contract
39
+ Naming a specialist in prose or in a roadmap's `squad:` field is documentation, not delegation — it does not route the Agent tool call by itself. Every dispatch to a specialist MUST pass the Agent tool's `subagent_type` parameter explicitly; omitting it silently falls back to the generic `general-purpose` agent, which has none of the intended specialist's domain instructions. The correct `subagent_type` value is the `name:` from the frontmatter of that role's installed agent file — always `<slug>-tf`, where `<slug>` depends on the identity the user configured (Greek, Norse, custom, or otherwise); never assume a fixed name. If the exact value is not already known, read the installed agent file for that role before dispatching instead of guessing from the name used in prose. Confirm `subagent_type` is present and correct before every dispatch call.
40
+
38
41
  ## Post-microbatch audit
39
42
  Before releasing the next wave, verify each acceptance criterion yourself: read the changed files, confirm the build, tests and gates, and check that no forbidden file was touched. Green gates are not proof that the intended behavior was delivered — validate the real artifact, not only the test fixtures. A failed audit blocks the next wave.
40
43
 
@@ -42,6 +42,7 @@
42
42
  {"id":"copilot","name":"GitHub Copilot","surfaces":[{"id":"ide","name":"GitHub Copilot","scopes":["global","project"],"capabilities":{"agents":{"support_level":"native","representation":"custom-agent"},"skills":{"support_level":"native","representation":"agent-skill"}},"paths":{"agents":[{"scope":"global","path":"~/.copilot/agents/trackfw-{{id}}.agent.md","extension":".agent.md"},{"scope":"project","path":".github/agents/trackfw-{{id}}.agent.md","extension":".agent.md"}],"skills":[{"scope":"global","path":"~/.copilot/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".github/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}}],"auxiliary_artifacts":[{"id":"instructions","surface":"ide","scope":"project","path":".github/copilot-instructions.md","purpose":"repository instructions","required":false},{"id":"hooks","surface":"ide","scope":"project","path":".github/hooks/trackfw-attention.json","purpose":"attention hooks","required":false}]},
43
43
  {"id":"windsurf","name":"Windsurf","surfaces":[{"id":"ide","name":"Windsurf IDE","scopes":["global","project"],"capabilities":{"agents":{"support_level":"fallback","representation":"skill","fallback_representation":"specialist skill"},"skills":{"support_level":"native","representation":"skill"}},"paths":{"agents":[{"scope":"global","path":"~/.codeium/windsurf/skills/trackfw-agent-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".windsurf/skills/trackfw-agent-{{id}}/SKILL.md","extension":".md"}],"skills":[{"scope":"global","path":"~/.codeium/windsurf/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".windsurf/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}}],"auxiliary_artifacts":[{"id":"global-rules","surface":"ide","scope":"project","path":".windsurfrules","purpose":"governance rule discovery","required":false}]},
44
44
  {"id":"amazonq","name":"Amazon Q Developer","surfaces":[{"id":"cli","name":"Amazon Q Developer CLI","scopes":["global","project"],"capabilities":{"agents":{"support_level":"native","representation":"cli-agent-json"},"skills":{"support_level":"fallback","representation":"rule","fallback_representation":"workflow rule"}},"paths":{"agents":[{"scope":"global","path":"~/.aws/amazonq/cli-agents/trackfw-{{id}}.json","extension":".json"},{"scope":"project","path":".amazonq/cli-agents/trackfw-{{id}}.json","extension":".json"}],"skills":[{"scope":"global","path":"~/.aws/amazonq/rules/trackfw-{{id}}.md","extension":".md"},{"scope":"project","path":".amazonq/rules/trackfw-{{id}}.md","extension":".md"}]}}],"auxiliary_artifacts":[{"id":"guidelines","surface":"cli","scope":"project","path":".amazonq/developer/guidelines.md","purpose":"project governance guidelines","required":false}]},
45
+ {"id":"opencode","name":"OpenCode","surfaces":[{"id":"cli","name":"OpenCode CLI","scopes":["global","project"],"capabilities":{"agents":{"support_level":"native","representation":"opencode-agent"},"skills":{"support_level":"native","representation":"skill"}},"paths":{"agents":[{"scope":"global","path":"~/.config/opencode/agents/trackfw-{{id}}.md","extension":".md"},{"scope":"project","path":".opencode/agents/trackfw-{{id}}.md","extension":".md"}],"skills":[{"scope":"global","path":"~/.config/opencode/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".opencode/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}}],"auxiliary_artifacts":[{"id":"instructions","surface":"cli","scope":"project","path":"AGENTS.md","purpose":"repository instructions (shared with Codex/AGENTS.md convention)","required":false}]},
45
46
  {"id":"kiro","name":"Kiro","surfaces":[{"id":"ide","name":"Kiro IDE","scopes":["global","project"],"capabilities":{"agents":{"support_level":"native","representation":"agent-markdown"},"skills":{"support_level":"native","representation":"agent-skill"}},"paths":{"agents":[{"scope":"global","path":"~/.kiro/agents/trackfw-{{id}}.md","extension":".md"},{"scope":"project","path":".kiro/agents/trackfw-{{id}}.md","extension":".md"}],"skills":[{"scope":"global","path":"~/.kiro/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".kiro/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}},{"id":"cli","name":"Kiro CLI","scopes":["global","project"],"capabilities":{"agents":{"support_level":"native","representation":"agent-json"},"skills":{"support_level":"native","representation":"agent-skill"}},"paths":{"agents":[{"scope":"global","path":"~/.kiro/agents/trackfw-{{id}}.json","extension":".json"},{"scope":"project","path":".kiro/agents/trackfw-{{id}}.json","extension":".json"}],"skills":[{"scope":"global","path":"~/.kiro/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".kiro/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}}],"auxiliary_artifacts":[{"id":"hooks","surface":"ide","scope":"project","path":".kiro/hooks/trackfw-attention.json","purpose":"attention hooks","required":false}]}
46
47
  ]
47
48
  }
@@ -278,6 +278,38 @@ function render({ kind, content, capability, item, identity: cfg }) {
278
278
  return out
279
279
  }
280
280
 
281
+ if (capability.representation === 'opencode-agent') {
282
+ // Reconstrói o frontmatter para o OpenCode CLI (opencode.ai), seguindo o
283
+ // mesmo padrão de reconstrução-do-zero do case "agent-directory".
284
+ // Decisão registrada na Wave 1 do roadmap
285
+ // ROADMAP-2026-08-04-compatibilidade-com-opencode-opencode-ai (achado #3,
286
+ // pesquisa contra o binário real 1.18.13):
287
+ // - "tools:" é uma chave RESERVADA no schema de agente do OpenCode
288
+ // (espera um objeto de overrides por-ferramenta, ex. {bash: false},
289
+ // não uma lista de nomes estilo Claude Code) — reutilizar o
290
+ // frontmatter original faz o OpenCode recusar o carregamento INTEIRO
291
+ // do projeto ("Configuration is invalid"), não só daquele agente. Por
292
+ // isso "tools:" nunca é emitido aqui.
293
+ // - sem "mode:" explícito, o OpenCode assume mode "all" (agente
294
+ // selecionável como persona primária de chat) — os agentes trackfw
295
+ // devem ser sempre subagentes puros, nunca primários, para paridade
296
+ // com o comportamento nos demais targets. Por isso "mode: subagent" é
297
+ // sempre fixo, nunca omitido.
298
+ // - "model:" é deliberadamente OMITIDO (decisão de produto do
299
+ // orquestrador, não uma limitação técnica): o OpenCode espera
300
+ // "provider/model-id" (ex. "anthropic/claude-sonnet-4-5"), não os
301
+ // aliases curtos do catálogo canônico ("opus"/"sonnet"), e mapear
302
+ // para um provider fixo contradiria a motivação de negócio do REQ
303
+ // (permitir que o usuário roteie os agentes trackfw para o modelo
304
+ // open-source/local que ele já configurou em opencode.json). Omitir
305
+ // deixa o OpenCode resolver pelo default já configurado pelo usuário.
306
+ // - "memory:" também não faz sentido no schema do OpenCode e é
307
+ // descartado junto com "tools:".
308
+ let out = `---\ndescription: ${description}\nmode: subagent\n---\n`
309
+ if (body) out += `${body}\n`
310
+ return out
311
+ }
312
+
281
313
  if (!id) return normalize(content)
282
314
  const withBody = insertBodyPrefix(content, greeting)
283
315
  const withFrontmatter = rewriteFrontmatterFields(withBody, name, description)
@@ -1042,6 +1042,45 @@ function validateFilenameUniqueness() {
1042
1042
  return violations
1043
1043
  }
1044
1044
 
1045
+ // branchSlugMatchesRoadmap verifica se branchSlug (já normalizado via normalizeBranchSlug) casa com o
1046
+ // nome de algum roadmap .md encontrado em wipDirs ou doneDirs. Reutilizada por
1047
+ // validateBranchHasWIPRoadmap e pelo comando `trackfw branch new` — nunca duplicar esta lógica.
1048
+ //
1049
+ // Espelha internal/validator/validator.go:BranchSlugMatchesRoadmap. Retorna { matched, candidates }:
1050
+ // matched indica se algum candidato casou com o slug; candidates lista todos os roadmaps .md
1051
+ // encontrados em wipDirs+doneDirs (para diagnóstico/mensagem de orientação quando matched é false).
1052
+ function branchSlugMatchesRoadmap(branchSlug, wipDirs, doneDirs) {
1053
+ const dirs = [...wipDirs, ...doneDirs]
1054
+ const candidates = []
1055
+ let matched = false
1056
+ for (const dir of dirs) {
1057
+ const files = listDir(dir).filter(f => f.endsWith('.md'))
1058
+ candidates.push(...files)
1059
+ if (files.some(file => normalizeBranchSlug(file).includes(branchSlug))) matched = true
1060
+ }
1061
+ return { matched, candidates }
1062
+ }
1063
+
1064
+ // branchGovernanceOrientation is the guidance message printed when a feat/fix/refactor branch has
1065
+ // no roadmap in wip/ nor done/ at all (candidates is empty). Shared by validateBranchHasWIPRoadmap
1066
+ // and `trackfw branch new` — never duplicate this string. Byte-identical to Go's
1067
+ // BranchGovernanceOrientation.
1068
+ function branchGovernanceOrientation(branch) {
1069
+ return `branch "${branch}" is a feat/fix/refactor branch but no roadmap is in wip/ nor done/ — create governance artifacts first:\n trackfw req new "title"\n trackfw roadmap new "title"\n trackfw roadmap move <name> wip`
1070
+ }
1071
+
1072
+ // branchNoMatchingRoadmapMessage is the guidance message printed when roadmaps exist in wip/ or
1073
+ // done/ but none of them match the branch's slug. Shared by validateBranchHasWIPRoadmap and
1074
+ // `trackfw branch new` — never duplicate this string. Byte-identical to Go's
1075
+ // BranchNoMatchingRoadmapMessage. Does not mutate candidates.
1076
+ function branchNoMatchingRoadmapMessage(branch, candidates) {
1077
+ // P3: sort for deterministic output regardless of filesystem ordering.
1078
+ const sorted = [...candidates].sort()
1079
+ const display = sorted.slice(0, 3)
1080
+ const suffix = sorted.length > 3 ? `, e mais ${sorted.length - 3}` : ''
1081
+ return `branch "${branch}" has no matching roadmap in wip/ nor done/ (found: ${display.join(', ')}${suffix}) — include the branch slug in the roadmap filename or set TRACKFW_BRANCH explicitly in CI`
1082
+ }
1083
+
1045
1084
  // validateBranchHasWIPRoadmap — verifica que branch feat/fix/refactor tem ao menos um roadmap em
1046
1085
  // wip/ ou done/ cujo slug case com a branch. Aceita done/ para permitir encerramento do roadmap na
1047
1086
  // própria branch, conforme a Definition of Done, sem reprovar o gate.
@@ -1067,22 +1106,13 @@ function validateBranchHasWIPRoadmap() {
1067
1106
  const wipDirs = resolveWIPDirs(cfg)
1068
1107
  const doneDirs = resolveDoneDirs(cfg)
1069
1108
  const branchSlug = normalizeBranchSlug(branch.split('/', 2)[1])
1070
- // candidates reúne todos os roadmaps encontrados em wip/ e done/.
1071
- const candidates = []
1072
- for (const dir of [...wipDirs, ...doneDirs]) {
1073
- const files = listDir(dir).filter(f => f.endsWith('.md'))
1074
- candidates.push(...files)
1075
- if (files.some(file => normalizeBranchSlug(file).includes(branchSlug))) return []
1076
- }
1109
+ const { matched, candidates } = branchSlugMatchesRoadmap(branchSlug, wipDirs, doneDirs)
1110
+ if (matched) return []
1077
1111
 
1078
1112
  if (candidates.length === 0) {
1079
- return [`branch "${branch}" is a feat/fix/refactor branch but no roadmap is in wip/ nor done/ — create governance artifacts first:\n trackfw req new "title"\n trackfw roadmap new "title"\n trackfw roadmap move <name> wip`]
1113
+ return [branchGovernanceOrientation(branch)]
1080
1114
  }
1081
- // P3: sort for deterministic output regardless of filesystem ordering.
1082
- const sorted = [...candidates].sort()
1083
- const display = sorted.slice(0, 3)
1084
- const suffix = sorted.length > 3 ? `, e mais ${sorted.length - 3}` : ''
1085
- return [`branch "${branch}" has no matching roadmap in wip/ nor done/ (found: ${display.join(', ')}${suffix}) — include the branch slug in the roadmap filename or set TRACKFW_BRANCH explicitly in CI`]
1115
+ return [branchNoMatchingRoadmapMessage(branch, candidates)]
1086
1116
  }
1087
1117
 
1088
1118
  function normalizeBranchSlug(value) {
@@ -1451,6 +1481,11 @@ module.exports = {
1451
1481
  validateFolderStatusCoherence,
1452
1482
  validateFilenameUniqueness,
1453
1483
  validateBranchHasWIPRoadmap,
1484
+ // novas funções — trackfw branch new (extraídas do gate branch_has_wip_roadmap)
1485
+ branchSlugMatchesRoadmap,
1486
+ branchGovernanceOrientation,
1487
+ branchNoMatchingRoadmapMessage,
1488
+ normalizeBranchSlug,
1454
1489
  // novas funções ML-2B
1455
1490
  contentHasMarker,
1456
1491
  ruleSeverity,