trackfw 2.16.0 → 3.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 (53) hide show
  1. package/README.md +18 -2
  2. package/package.json +1 -1
  3. package/src/commands/discover.js +18 -0
  4. package/src/commands/index.js +2 -0
  5. package/src/commands/init.js +50 -4
  6. package/src/commands/integrations.js +75 -4
  7. package/src/commands/log.js +2 -1
  8. package/src/commands/note.js +24 -0
  9. package/src/commands/req.js +7 -1
  10. package/src/commands/roadmap.js +4 -3
  11. package/src/commands/ship.js +49 -0
  12. package/src/config/index.js +5 -2
  13. package/src/forge/adapter.js +189 -0
  14. package/src/forge/resolve.js +191 -0
  15. package/src/generators/adr.js +13 -4
  16. package/src/generators/date.js +23 -0
  17. package/src/generators/init.js +133 -8
  18. package/src/generators/note.js +154 -0
  19. package/src/generators/req.js +88 -5
  20. package/src/generators/roadmap.js +96 -20
  21. package/src/i18n/locales/en-US.json +2 -1
  22. package/src/i18n/locales/es-ES.json +2 -1
  23. package/src/i18n/locales/pt-BR.json +2 -1
  24. package/src/identity/preset.js +22 -0
  25. package/src/integrations/assets/agents/architect.md +32 -0
  26. package/src/integrations/assets/agents/backend.md +32 -0
  27. package/src/integrations/assets/agents/code-quality.md +29 -0
  28. package/src/integrations/assets/agents/data.md +32 -0
  29. package/src/integrations/assets/agents/dba.md +32 -0
  30. package/src/integrations/assets/agents/frontend.md +32 -0
  31. package/src/integrations/assets/agents/iac.md +46 -0
  32. package/src/integrations/assets/agents/infra.md +34 -0
  33. package/src/integrations/assets/agents/qa.md +32 -0
  34. package/src/integrations/assets/agents/security.md +29 -0
  35. package/src/integrations/assets/agents/tooling.md +47 -0
  36. package/src/integrations/assets/agents/ux.md +29 -0
  37. package/src/integrations/assets/catalog.json +16 -2
  38. package/src/integrations/assets/skills/architecture.md +66 -0
  39. package/src/integrations/assets/skills/backend.md +67 -0
  40. package/src/integrations/assets/skills/code-quality.md +78 -0
  41. package/src/integrations/assets/skills/data.md +75 -0
  42. package/src/integrations/assets/skills/dba.md +77 -0
  43. package/src/integrations/assets/skills/frontend.md +70 -0
  44. package/src/integrations/assets/skills/iac.md +90 -0
  45. package/src/integrations/assets/skills/infra.md +62 -0
  46. package/src/integrations/assets/skills/qa.md +63 -0
  47. package/src/integrations/assets/skills/security.md +74 -0
  48. package/src/integrations/assets/skills/tooling.md +80 -0
  49. package/src/integrations/assets/skills/ux.md +70 -0
  50. package/src/integrations/render.js +57 -6
  51. package/src/ship/runner.js +454 -0
  52. package/src/validator/index.js +321 -111
  53. package/src/validator/traceid.js +1 -1
package/README.md CHANGED
@@ -128,6 +128,10 @@ trackfw validate
128
128
 
129
129
  # 6. See what is in flight
130
130
  trackfw status
131
+
132
+ # 7. Governed commit + push + open PR/MR (feat/fix/refactor branches only)
133
+ git add -p # stage your changes explicitly
134
+ trackfw ship -m "feat(auth): add login flow"
131
135
  ```
132
136
 
133
137
  ---
@@ -146,6 +150,7 @@ trackfw status
146
150
  | `trackfw roadmap move <name> <state>` | Move roadmap between states |
147
151
  | `trackfw roadmap list` | List all roadmaps grouped by state |
148
152
  | `trackfw validate` | Check governance consistency (use as CI gate) |
153
+ | `trackfw ship -m "msg"` | Governed `git commit + push + open PR/MR` — enforces branch pattern and governance gate; resolves forge (GitHub/GitLab/Bitbucket/Azure) automatically or via `--forge`; falls back to a browser URL when the forge CLI is absent |
149
154
  | `trackfw context` | Print a structured summary of the project's governance state (REQs, Roadmaps, ADRs with counts and statuses) |
150
155
  | `trackfw serve` | Start a local governance dashboard (no cloud, no accounts) |
151
156
  | `trackfw status` | Show wip, blocked, REQs waiting on ADRs |
@@ -363,14 +368,25 @@ are never replaced or removed unless `--force` is explicit, and unmanaged files
363
368
  are never removed. Known historical templates are adopted without overwriting;
364
369
  unknown unmanaged content cannot be adopted by `update`, even with `--force`.
365
370
 
366
- The 10 roles installed for each tool: **architect · backend · frontend · qa · infra · security · code-quality · dba · ux · data**
371
+ Without `--scope`, `install`/`update` default to `global` (`~/.claude/...`)
372
+ when stdin is not a TTY, and otherwise prompt interactively with `global`
373
+ pre-selected — the resolved destination paths are printed before anything is
374
+ written. `uninstall` is the one exception: without `--scope` and without a
375
+ TTY it fails instead of guessing, since silently defaulting a destructive
376
+ operation could delete artifacts from the user's home directory. `list` never
377
+ prompts and always assumes `global` unless `--scope` is given, so it reports
378
+ the same destinations `install` actually wrote to.
379
+
380
+ The **12 roles** installed for each tool: **architect · backend · frontend · qa · infra · security · code-quality · dba · ux · data · iac · tooling**
381
+
382
+ The **17 skills** cover governance process (governance, implement, plan, release, review) and technical specialties (backend-skill, code-quality-skill, data-skill, dba-skill, frontend-skill, iac-skill, infra-skill, qa-skill, security-skill, tooling-skill, ux-skill, vault-skill).
367
383
 
368
384
  ---
369
385
 
370
386
  ## Agent identity — give your agents a name
371
387
 
372
388
  By default the agents are functional and impersonal: `trackfw-architect`,
373
- `trackfw-backend`, … Agent identity lets you name all ten, pick how they
389
+ `trackfw-backend`, … Agent identity lets you name all twelve, pick how they
374
390
  address you, and call them by name.
375
391
 
376
392
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trackfw",
3
- "version": "2.16.0",
3
+ "version": "3.1.0",
4
4
  "description": "Governance CLI for AI-native software delivery and AI coding agents. Enforces traceability: ADR → REQ → ROADMAP → kanban. Native support for Codex, Claude Code, Gemini CLI, Cursor, Copilot, Windsurf, and Amazon Q.",
5
5
  "keywords": [
6
6
  "cli",
@@ -4,6 +4,7 @@ const { Command } = require('commander');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const { execSync } = require('child_process');
7
+ const { resolve: resolveForge } = require('../forge/resolve');
7
8
 
8
9
  function scan(rootDir) {
9
10
  const r = {
@@ -20,6 +21,7 @@ function scan(rootDir) {
20
21
  governanceScore: 0,
21
22
  hookFramework: 'none',
22
23
  ciSystem: 'none',
24
+ forge: '',
23
25
  };
24
26
 
25
27
  // trackfw.yaml
@@ -108,6 +110,18 @@ function scan(rootDir) {
108
110
  r.ciSystem = 'none';
109
111
  }
110
112
 
113
+ // Forge detection — reuse forge/resolve.js (no duplicate parse).
114
+ // gitRemoteURL returns '' on any error; CI detection is filesystem-based.
115
+ try {
116
+ const { resolveFromRepo } = require('../forge/resolve');
117
+ const res = resolveFromRepo('', '', rootDir);
118
+ if (res.source !== 'none') {
119
+ r.forge = res.forge;
120
+ }
121
+ } catch (_) {
122
+ // forge detection is best-effort; never block scan on it
123
+ }
124
+
111
125
  r.governanceScore = calcScore(r);
112
126
  return r;
113
127
  }
@@ -146,6 +160,10 @@ function generateYAML(r) {
146
160
  out += `hooks: ${r.hookFramework}\n`;
147
161
  out += `ci: ${r.ciSystem}\n`;
148
162
 
163
+ if (r.forge) {
164
+ out += `forge: ${r.forge}\n`;
165
+ }
166
+
149
167
  return out;
150
168
  }
151
169
 
@@ -29,6 +29,8 @@ function createProgram() {
29
29
  program.addCommand(require('./version'))
30
30
  program.addCommand(require('./agents'))
31
31
  program.addCommand(require('./skills'))
32
+ program.addCommand(require('./note'))
33
+ program.addCommand(require('./ship'))
32
34
 
33
35
  const { createServeCommand } = require('./serve')
34
36
  program.addCommand(createServeCommand())
@@ -4,11 +4,13 @@ const { t } = require('../i18n')
4
4
  const identityStore = require('../identity')
5
5
  const identityWizard = require('./identity-wizard')
6
6
  const { resolveIdentityPreset, identityFileExists } = identityWizard
7
+ const { resolveScope } = require('./integrations')
7
8
 
8
9
  const cmd = new Command('init')
9
10
  cmd.description(t('init.description'))
10
11
  cmd.option('--ai-tools <tools>', 'Comma-separated AI tools to configure (claude,codex,gemini,antigravity,cursor,copilot,windsurf,amazonq,kiro)', '')
11
12
  cmd.option('--identity-preset <preset>', `Agent identity preset: none, neutral, ${identityStore.presetNames().join(', ')}`, 'none')
13
+ cmd.option('--forge <value>', 'Forge platform: github, gitlab, bitbucket, azure', '')
12
14
  cmd.action(async (options, command) => {
13
15
  const os = require('os')
14
16
  const path = require('path')
@@ -28,6 +30,14 @@ cmd.action(async (options, command) => {
28
30
  }
29
31
  }
30
32
 
33
+ // --forge: validate unconditionally above the non-TTY early return so that
34
+ // an invalid value fails loudly in CI instead of silently no-op'ing.
35
+ const forgeChanged = command.getOptionValueSource('forge') === 'cli'
36
+ if (forgeChanged && options.forge) {
37
+ const { validateForge } = require('../forge/resolve')
38
+ validateForge(options.forge) // throws on invalid value
39
+ }
40
+
31
41
  // Pula o wizard de identidade inteiramente quando a flag foi passada
32
42
  // explicitamente (já tratado acima) ou quando um arquivo de identidade já
33
43
  // existe — re-executar init nunca deve sobrescrever silenciosamente uma
@@ -44,13 +54,16 @@ cmd.action(async (options, command) => {
44
54
  pkgManager: 'npm',
45
55
  hooks: 'none',
46
56
  ci: 'none',
57
+ forge: forgeChanged ? options.forge : '',
47
58
  }
48
59
  await generators.scaffold(cfg)
49
60
  const aiTools = String(options.aiTools || '').split(',').map(tool => tool.trim()).filter(Boolean)
50
61
  const supported = new Set(['claude', 'codex', 'gemini', 'antigravity', 'cursor', 'copilot', 'windsurf', 'amazonq', 'kiro'])
62
+ // Sem TTY, o escopo nunca é perguntado: default `global` (ADR D1/D4).
63
+ const scope = await resolveScope({}, { interactive: false })
51
64
  for (const tool of aiTools) {
52
65
  if (!supported.has(tool)) throw new Error(`Unsupported AI tool: ${tool}`)
53
- await generators.installIntegrationTarget(tool, process.cwd())
66
+ await generators.installIntegrationTarget(tool, process.cwd(), scope)
54
67
  }
55
68
  console.log(`\n${t('init.success')}`)
56
69
  require('../generators/init').printArchitectNextSteps(process.cwd())
@@ -59,7 +72,17 @@ cmd.action(async (options, command) => {
59
72
 
60
73
  const { input, select, checkbox } = require('@inquirer/prompts')
61
74
 
62
- let projectName, projectType, frontend, pkgManager, backend, backendFramework, hooks, ci, aiTools, requireReqInCommit
75
+ // Detect forge from the current dir to prefill the wizard default.
76
+ const { resolveFromRepo: resolveForgeFromRepo } = require('../forge/resolve')
77
+ let detectedForge = ''
78
+ if (!forgeChanged) {
79
+ try {
80
+ const res = resolveForgeFromRepo('', '', process.cwd())
81
+ if (res.source !== 'none') detectedForge = res.forge
82
+ } catch (_) {}
83
+ }
84
+
85
+ let projectName, projectType, frontend, pkgManager, backend, backendFramework, hooks, ci, forgeValue, aiTools, requireReqInCommit, scope
63
86
 
64
87
  try {
65
88
  projectName = await input({
@@ -160,6 +183,22 @@ cmd.action(async (options, command) => {
160
183
  ],
161
184
  })
162
185
 
186
+ if (!forgeChanged) {
187
+ forgeValue = await select({
188
+ message: t('init.prompt.forge'),
189
+ default: detectedForge,
190
+ choices: [
191
+ { name: 'Auto-detect (omit key)', value: '' },
192
+ { name: 'GitHub', value: 'github' },
193
+ { name: 'GitLab', value: 'gitlab' },
194
+ { name: 'Bitbucket', value: 'bitbucket' },
195
+ { name: 'Azure DevOps', value: 'azure' },
196
+ ],
197
+ })
198
+ } else {
199
+ forgeValue = options.forge
200
+ }
201
+
163
202
  requireReqInCommit = false
164
203
  if (hooks !== 'none') {
165
204
  const { confirm: confirmPrompt } = require('@inquirer/prompts')
@@ -184,6 +223,11 @@ cmd.action(async (options, command) => {
184
223
  ],
185
224
  })
186
225
 
226
+ // Escopo de instalação (ADR D4): só pergunta quando há de fato algo para
227
+ // instalar. `resolveScope` é a mesma função (mesmas strings/mecanismo de
228
+ // prompt) usada por `agents install` / `skills install`.
229
+ scope = aiTools.length ? await resolveScope({}, { interactive: true }) : 'global'
230
+
187
231
  } catch (err) {
188
232
  // Fallback quando stdin fecha inesperadamente (ex: pipe em TTY simulado)
189
233
  const cfg = {
@@ -194,6 +238,7 @@ cmd.action(async (options, command) => {
194
238
  pkgManager: 'npm',
195
239
  hooks: 'none',
196
240
  ci: 'none',
241
+ forge: forgeChanged ? options.forge : '',
197
242
  }
198
243
  await generators.scaffold(cfg)
199
244
  console.log(`\n${t('init.success')}`)
@@ -221,6 +266,7 @@ cmd.action(async (options, command) => {
221
266
  pkgManager: 'npm',
222
267
  hooks: 'none',
223
268
  ci: 'none',
269
+ forge: forgeChanged ? options.forge : '',
224
270
  })
225
271
  console.log(`\n${t('init.success')}`)
226
272
  require('../generators/init').printArchitectNextSteps(process.cwd())
@@ -228,10 +274,10 @@ cmd.action(async (options, command) => {
228
274
  }
229
275
  }
230
276
 
231
- const cfg = { projectName, projectType, frontend, backend, backendFramework, pkgManager, hooks, ci, requireReqInCommit }
277
+ const cfg = { projectName, projectType, frontend, backend, backendFramework, pkgManager, hooks, ci, forge: forgeValue || '', requireReqInCommit }
232
278
  await generators.scaffold(cfg)
233
279
 
234
- for (const tool of (aiTools || [])) await generators.installIntegrationTarget(tool, process.cwd())
280
+ for (const tool of (aiTools || [])) await generators.installIntegrationTarget(tool, process.cwd(), scope)
235
281
 
236
282
  console.log(`\n${t('init.success')}`)
237
283
  require('../generators/init').printArchitectNextSteps(process.cwd())
@@ -2,7 +2,7 @@
2
2
 
3
3
  const os = require('node:os')
4
4
  const { Command } = require('commander')
5
- const { catalog, execute, parseSurfaces } = require('../integrations')
5
+ const { catalog, execute, parseSurfaces, buildPlans } = require('../integrations')
6
6
  const identityStore = require('../identity')
7
7
  const identityWizard = require('./identity-wizard')
8
8
  const { t } = require('../i18n')
@@ -27,6 +27,66 @@ async function promptSelection(kind, options, prompts = require('@inquirer/promp
27
27
  options.items = await checkbox({ message: `${kind} to manage`, choices: catalog[kind].map(item => ({ name: item.name, value: item.id })), required: true })
28
28
  }
29
29
 
30
+ // resolveScope decide o escopo de instalação (ADR-2026-07-25-escopo-de-
31
+ // instalacao-selecionavel-para-agents-e-skills):
32
+ //
33
+ // - `--scope` explícito (`options.scope !== undefined`) é sempre respeitado
34
+ // e nunca dispara prompt — apenas validado contra os valores aceitos. A
35
+ // detecção é por *flag-set* (undefined), nunca por comparação de valor,
36
+ // para não confundir um `--scope project` explícito com o default.
37
+ // - Sem flag e `interactive` desligado (comando `list`, D6): default
38
+ // `global`, nunca lança erro (list nunca é destrutivo).
39
+ // - Sem flag e stdin não é um TTY (D1): default `global` — exceto para
40
+ // `uninstall` (ADR D8), que falha exigindo `--scope` explícito. D1 foi
41
+ // aprovado no enquadramento "onde instalar"; aplicá-lo a uninstall
42
+ // permitiria que um script de CI que hoje limpa `.claude/agents/` do
43
+ // repositório passasse a apagar arquivos do home do usuário.
44
+ // - Sem flag, `interactive` ligado e TTY (D2): pergunta o escopo, com
45
+ // `global` pré-selecionado, para toda operação incluindo uninstall — o
46
+ // usuário vê a escolha antes de destruir. Usa o mesmo mecanismo de prompt
47
+ // (`@inquirer/prompts`) já empregado por `identity-wizard.js`, sem
48
+ // dependência nova.
49
+ //
50
+ // `interactive` é passado pelo chamador como `false` para `list` — comando de
51
+ // leitura que nunca deve bloquear em prompt, mas que ainda assim precisa do
52
+ // mesmo default `global` para não reportar deployments divergentes dos que o
53
+ // `install` gravou (D6). `operation` identifica install/update/uninstall/list
54
+ // para o branch de D8. Espelha
55
+ // internal/commands/integrations_flags.go:resolveScope.
56
+ async function resolveScope(options, { interactive = true, operation, prompts = require('@inquirer/prompts') } = {}) {
57
+ if (options.scope !== undefined) {
58
+ if (options.scope !== 'project' && options.scope !== 'global') throw new Error(`Unsupported scope: ${options.scope}`)
59
+ return options.scope
60
+ }
61
+ if (!interactive) return 'global'
62
+ if (!process.stdin.isTTY) {
63
+ if (operation === 'uninstall') throw new Error('uninstall requires --scope in non-interactive mode')
64
+ return 'global'
65
+ }
66
+ const { select } = prompts
67
+ return select({
68
+ message: 'Onde instalar os artefatos?',
69
+ default: 'global',
70
+ choices: [
71
+ { name: 'Pasta do usuário (~/.claude) — vale para todos os projetos', value: 'global' },
72
+ { name: 'Este projeto (.claude) — apenas neste repositório', value: 'project' },
73
+ ],
74
+ })
75
+ }
76
+
77
+ // printResolvedDestinations imprime, antes da gravação, o escopo escolhido e
78
+ // os caminhos de destino que serão afetados (ADR D5) — nunca em modo --json,
79
+ // que já é o canal determinístico consumido por scripts. Recalcula os planos
80
+ // via buildPlans (sem side effect) só para enumerar destinos; a gravação em
81
+ // si acontece depois, em execute().
82
+ function printResolvedDestinations(kind, options) {
83
+ if (options.json) return
84
+ const plans = buildPlans(kind, options)
85
+ const destinations = [...new Set(plans.map(plan => plan.destination))].sort()
86
+ console.log(`Destino (${options.scope}):`)
87
+ for (const destination of destinations) console.log(` ${destination}`)
88
+ }
89
+
30
90
  async function promptAmbiguousSurfaces(kind, options, prompts = require('@inquirer/prompts')) {
31
91
  const { select } = prompts
32
92
  const selected = parseSurfaces(options.surfaces)
@@ -47,7 +107,7 @@ function createLifecycleCommand(kind) {
47
107
  const command = new Command(operation)
48
108
  .option('--targets <targets>', 'Comma-separated target CLIs', csv)
49
109
  .option('--items <items>', `Comma-separated ${kind} IDs`, csv)
50
- .option('--scope <scope>', 'Installation scope: project or global', 'project')
110
+ .option('--scope <scope>', 'Installation scope: project or global (default: global; asks interactively)')
51
111
  .option('--surface <target=surface>', 'Surface selection (repeatable)', collect, [])
52
112
  .option('--json', 'Print deterministic JSON')
53
113
  .option('--force', 'Replace or remove modified artifacts')
@@ -65,7 +125,14 @@ function createLifecycleCommand(kind) {
65
125
 
66
126
  command.action(async (options, cmd) => {
67
127
  options.surfaces = options.surface || []
68
- if (options.scope !== 'project' && options.scope !== 'global') throw new Error(`Unsupported scope: ${options.scope}`)
128
+
129
+ // Gate de escopo (ADR D1-D3, D6): resolvido incondicionalmente aqui,
130
+ // antes de qualquer seleção de targets/surfaces ou construção de
131
+ // planos, e independentemente de --targets já ter sido informado —
132
+ // caso contrário o caso mais comum (`agents install --targets claude`)
133
+ // nunca passaria por prompt algum. `list` (mutation === false) nunca
134
+ // pergunta (comando de leitura), apenas adota o default `global`.
135
+ options.scope = await resolveScope(options, { interactive: mutation, operation })
69
136
 
70
137
  // O booleano de --identity nunca deve chegar a execute()/buildPlans()
71
138
  // sob a chave "identity" — essa chave ali é reservada para uma Config
@@ -112,6 +179,10 @@ function createLifecycleCommand(kind) {
112
179
  }
113
180
  }
114
181
 
182
+ // D5: caminhos de destino impressos antes da gravação, apenas para
183
+ // operações de mutação (install/update/uninstall) e fora de --json.
184
+ if (mutation) printResolvedDestinations(kind, options)
185
+
115
186
  const output = execute(kind, operation, options)
116
187
  console.log(options.json ? JSON.stringify(output) : human(output))
117
188
  })
@@ -120,4 +191,4 @@ function createLifecycleCommand(kind) {
120
191
  return root
121
192
  }
122
193
 
123
- module.exports = { createLifecycleCommand, csv, human, promptSelection, promptAmbiguousSurfaces }
194
+ module.exports = { createLifecycleCommand, csv, human, promptSelection, promptAmbiguousSurfaces, resolveScope }
@@ -3,13 +3,14 @@ const { Command } = require('commander')
3
3
  const fs = require('fs')
4
4
  const path = require('path')
5
5
  const { t } = require('../i18n')
6
+ const config = require('../config')
6
7
 
7
8
  const cmd = new Command('log')
8
9
  cmd.description(t('log.description'))
9
10
  cmd.option('--tail <n>', t('log.tail'), '20')
10
11
  cmd.action(async (opts) => {
11
12
  const tail = parseInt(opts.tail, 10)
12
- const logPath = path.join('docs', 'roadmaps', '.trackfw-log')
13
+ const logPath = path.join(config.load().roadmapDir, '.trackfw-log')
13
14
 
14
15
  if (!fs.existsSync(logPath)) {
15
16
  console.log(t('log.empty'))
@@ -0,0 +1,24 @@
1
+ 'use strict'
2
+
3
+ const { Command } = require('commander')
4
+ const { newNote } = require('../generators/note')
5
+
6
+ function createNoteCommand() {
7
+ const note = new Command('note').description('Manage vault knowledge notes')
8
+
9
+ note
10
+ .command('new <title>')
11
+ .description('Create a new knowledge note in vault/notes/')
12
+ .action((title) => {
13
+ try {
14
+ newNote(title)
15
+ } catch (err) {
16
+ console.error(`error: ${err.message}`)
17
+ process.exit(1)
18
+ }
19
+ })
20
+
21
+ return note
22
+ }
23
+
24
+ module.exports = createNoteCommand()
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
  const { Command } = require('commander')
3
- const { listREQs } = require('../generators/req')
3
+ const { listREQs, moveREQ } = require('../generators/req')
4
4
  const { t } = require('../i18n')
5
5
 
6
6
  const cmd = new Command('req')
@@ -67,4 +67,10 @@ cmd.command('list')
67
67
  listREQs(require('../config').load().reqDir)
68
68
  })
69
69
 
70
+ cmd.command('move <name> <status>')
71
+ .description('Update a REQ status in place')
72
+ .action(async (name, status) => {
73
+ moveREQ(name, status)
74
+ })
75
+
70
76
  module.exports = cmd
@@ -8,15 +8,16 @@ cmd.description(t('roadmap.description'))
8
8
 
9
9
  cmd.command('new')
10
10
  .description(t('roadmap.new.description'))
11
- .option('-t, --title <title>', 'Roadmap title')
11
+ .argument('[title]', 'Roadmap title')
12
+ .option('-t, --title <title>', 'Roadmap title (alias)')
12
13
  .option('-r, --req <path>', 'Path to the linked REQ')
13
14
  .option('--from-req <path>', 'Generate roadmap with ML stubs from REQ acceptance criteria')
14
- .action(async (opts) => {
15
+ .action(async (titleArg, opts) => {
15
16
  if (opts.fromReq) {
16
17
  newRoadmapFromReq(opts.fromReq)
17
18
  return
18
19
  }
19
- const title = opts.title || 'New Roadmap'
20
+ const title = titleArg || opts.title || 'New Roadmap'
20
21
  const reqPath = opts.req || ''
21
22
  newRoadmap(title, reqPath)
22
23
  })
@@ -0,0 +1,49 @@
1
+ 'use strict'
2
+
3
+ const { Command } = require('commander')
4
+ const { runShip } = require('../ship/runner')
5
+ const config = require('../config')
6
+
7
+ function createShipCommand() {
8
+ const cmd = new Command('ship')
9
+ cmd
10
+ .description(
11
+ 'trackfw ship runs a governed delivery sequence:\n\n' +
12
+ ' 1. Validates branch name — must match feat|fix|refactor/<slug>\n' +
13
+ ' 2. Validates governance — REQ + roadmap in wip/ must exist\n' +
14
+ ' (hard gate: not affected by lenient mode or per-rule severity)\n' +
15
+ ' 3. Detects pending squash-merges in other branches (advisory only)\n' +
16
+ ' 4. Reviews what is staged (git status --short + git diff --cached --stat)\n' +
17
+ ' 5. Commits with Conventional Commits format (-m is required)\n' +
18
+ ' 6. Pushes to origin (adds -u if no upstream is configured yet)\n' +
19
+ ' 7. Opens PR/MR via the resolved forge CLI (or prints URL if CLI is absent)\n\n' +
20
+ "Stage your files explicitly before running ship.\n" +
21
+ "This command never executes 'git add .' or 'git add -A'."
22
+ )
23
+ .option('-m, --message <msg>', 'Commit message (Conventional Commits format required)')
24
+ .option('--dry-run', 'Print what would be done without executing write commands', false)
25
+ .option('--no-pr', 'Skip PR/MR creation after push')
26
+ .option('--forge <forge>', 'Override forge detection (github, gitlab, bitbucket, azure)', '')
27
+ .action((options) => {
28
+ const cfg = config.load()
29
+ const exitCode = runShip(
30
+ {
31
+ message: options.message || '',
32
+ dryRun: options.dryRun || false,
33
+ noPR: options.pr === false,
34
+ forge: options.forge || '',
35
+ },
36
+ {
37
+ configForge: cfg.forge || '',
38
+ repoDir: '.',
39
+ }
40
+ )
41
+ if (exitCode !== 0) {
42
+ process.exit(exitCode)
43
+ }
44
+ })
45
+
46
+ return cmd
47
+ }
48
+
49
+ module.exports = createShipCommand()
@@ -26,9 +26,11 @@ function defaults() {
26
26
  lenientUntil: '',
27
27
  wipLimit: 1,
28
28
  wipBySquad: false,
29
+ staleWipDays: 7,
29
30
  requireReqInCommit: false,
30
31
  strictCiPaths: false,
31
32
  traceIdField: '',
33
+ forge: '',
32
34
  // NOVOS campos:
33
35
  linkFields: {
34
36
  req: ['REQ:'],
@@ -42,7 +44,7 @@ function defaults() {
42
44
  wip_limit: 'error',
43
45
  stale_wip: 'warning',
44
46
  adr_orphan: 'warning',
45
- ref_targets_exist: 'warning',
47
+ ref_targets_exist: 'error',
46
48
  folder_status: 'warning',
47
49
  filename_uniqueness: 'error',
48
50
  blocked_by_draft_adr: 'error',
@@ -191,9 +193,11 @@ function parse(content, cfg) {
191
193
  case 'lenient_until': cfg.lenientUntil = val; break;
192
194
  case 'wip_limit': { const n = parseInt(val, 10); if (n > 0) cfg.wipLimit = n; break; }
193
195
  case 'wip_by_squad': cfg.wipBySquad = val === 'true'; break;
196
+ case 'stale_wip_days': { const n = parseInt(val, 10); if (n > 0) cfg.staleWipDays = n; break; }
194
197
  case 'require_req_in_commit': cfg.requireReqInCommit = val === 'true'; break;
195
198
  case 'strict_ci_paths': cfg.strictCiPaths = val === 'true'; break;
196
199
  case 'trace_id_field': cfg.traceIdField = val.replace(/^["']|["']$/g, ''); break;
200
+ case 'forge': cfg.forge = val.replace(/^["']|["']$/g, ''); break;
197
201
  case 'link_fields': inLinkFields = true; break;
198
202
  case 'acceptance_markers': inAcceptanceMarkers = true; acceptanceMarkers = []; break;
199
203
  case 'rules': inRules = true; rules = {}; break;
@@ -208,4 +212,3 @@ const NAMESPACING_FLAT = 'flat';
208
212
  const NAMESPACING_BY_AGENT = 'by_agent';
209
213
 
210
214
  module.exports = { load, reset, defaults, expandPath, NAMESPACING_FLAT, NAMESPACING_BY_AGENT };
211
-