trackfw 6.9.1 → 7.0.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/README.md CHANGED
@@ -157,9 +157,6 @@ trackfw ship -m "feat(auth): add login flow"
157
157
  | `trackfw serve` | Start a local governance dashboard (no cloud, no accounts) |
158
158
  | `trackfw status` | Show wip, blocked, REQs waiting on ADRs |
159
159
  | `trackfw log [--tail N]` | Show roadmap state transition history |
160
- | `trackfw plugins list` | List installed plugins |
161
- | `trackfw plugins add <user/repo>` | Install a plugin from GitHub Releases |
162
- | `trackfw plugins remove <name>` | Remove an installed plugin |
163
160
  | `trackfw agents list` | List available agents and deployment state across AI CLIs |
164
161
  | `trackfw agents install` | Install selected specialist agents |
165
162
  | `trackfw agents update` | Safely update managed agents |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trackfw",
3
- "version": "6.9.1",
3
+ "version": "7.0.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, Amazon Q, OpenCode, and Kiro.",
5
5
  "keywords": [
6
6
  "cli",
@@ -16,12 +16,20 @@ const { spawnSync } = require('child_process')
16
16
  const config = require('../config')
17
17
  const validator = require('../validator')
18
18
 
19
- // branchValidTypes mirrors the vocabulary already enforced by `trackfw ship` (feat|fix|refactor).
20
- const branchValidTypes = new Set(['feat', 'fix', 'refactor'])
19
+ // branchValidTypes is the full vocabulary accepted by `trackfw branch new`. feat/fix/refactor are
20
+ // gated on a matching REQ + roadmap already in wip/ or done/ (branchGatedTypes below); chore/docs
21
+ // are housekeeping types — already treated as roadmap-exempt by `trackfw ship` and `trackfw
22
+ // commit` — and create the branch without that gate.
23
+ const branchValidTypes = new Set(['feat', 'fix', 'refactor', 'chore', 'docs'])
24
+
25
+ // branchGatedTypes is the subset of branchValidTypes that requires a matching REQ + roadmap
26
+ // already in wip/ or done/ before the branch is created. Keep this in sync with the pattern
27
+ // `trackfw ship`/`trackfw commit` use to decide when the branch_has_wip_roadmap gate applies.
28
+ const branchGatedTypes = new Set(['feat', 'fix', 'refactor'])
21
29
 
22
30
  /**
23
31
  * parseBranchSpec splits "<type>/<slug>" and validates both parts. type must be one of
24
- * feat, fix, refactor (branchValidTypes); slug must be non-empty.
32
+ * feat, fix, refactor, chore, docs (branchValidTypes); slug must be non-empty.
25
33
  * @param {string} spec
26
34
  * @returns {{ branchType: string, slug: string }}
27
35
  * @throws {Error} when spec is malformed
@@ -30,12 +38,12 @@ function parseBranchSpec(spec) {
30
38
  const value = String(spec)
31
39
  const idx = value.indexOf('/')
32
40
  if (idx === -1 || value.slice(0, idx) === '') {
33
- throw new Error(`invalid branch spec "${spec}" — expected <type>/<slug> with type in feat, fix, refactor`)
41
+ throw new Error(`invalid branch spec "${spec}" — expected <type>/<slug> with type in feat, fix, refactor, chore, docs`)
34
42
  }
35
43
  const branchType = value.slice(0, idx)
36
44
  const slug = value.slice(idx + 1)
37
45
  if (!branchValidTypes.has(branchType)) {
38
- throw new Error(`invalid branch type "${branchType}" — must be one of feat, fix, refactor`)
46
+ throw new Error(`invalid branch type "${branchType}" — must be one of feat, fix, refactor, chore, docs`)
39
47
  }
40
48
  if (slug.trim() === '') {
41
49
  throw new Error(`branch slug is required — expected <type>/<slug>, got "${spec}"`)
@@ -102,24 +110,28 @@ function runBranchNew(spec, dryRun, deps = {}) {
102
110
 
103
111
  const branchName = `${branchType}/${slug}`
104
112
 
105
- const cfg = loadConfig()
106
- const wipDirs = resolveWIPDirs(cfg)
107
- const doneDirs = resolveDoneDirs(cfg)
113
+ // chore/docs are housekeeping types — already treated as roadmap-exempt by `trackfw ship`
114
+ // and `trackfw commit` — so the branch_has_wip_roadmap gate below does not apply to them.
115
+ if (branchGatedTypes.has(branchType)) {
116
+ const cfg = loadConfig()
117
+ const wipDirs = resolveWIPDirs(cfg)
118
+ const doneDirs = resolveDoneDirs(cfg)
108
119
 
109
- const normalizedSlug = validator.normalizeBranchSlug(slug)
110
- const { matched, candidates } = matchSlug(normalizedSlug, wipDirs, doneDirs)
120
+ const normalizedSlug = validator.normalizeBranchSlug(slug)
121
+ const { matched, candidates } = matchSlug(normalizedSlug, wipDirs, doneDirs)
111
122
 
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)
123
+ if (!matched) {
124
+ const msg = (!candidates || candidates.length === 0)
125
+ ? validator.branchGovernanceOrientation(branchName)
126
+ : validator.branchNoMatchingRoadmapMessage(branchName, candidates)
127
+ if (dryRun) {
128
+ writeln(`[dry-run] would block: ${msg}`)
129
+ } else {
130
+ writeln(msg)
131
+ }
132
+ writeErr(`blocked: no matching roadmap in wip/ nor done/ for "${branchName}"`)
133
+ return 1
120
134
  }
121
- writeErr(`blocked: no matching roadmap in wip/ nor done/ for "${branchName}"`)
122
- return 1
123
135
  }
124
136
 
125
137
  if (dryRun) {
@@ -134,6 +146,7 @@ function runBranchNew(spec, dryRun, deps = {}) {
134
146
 
135
147
  module.exports = {
136
148
  branchValidTypes,
149
+ branchGatedTypes,
137
150
  parseBranchSpec,
138
151
  defaultGitCheckout,
139
152
  runBranchNew,
@@ -0,0 +1,114 @@
1
+ 'use strict'
2
+
3
+ // changelog — parsing e extração de seções do CHANGELOG.md no formato
4
+ // Keep a Changelog (https://keepachangelog.com/en/1.1.0/).
5
+ // Port 1:1 de internal/changelog/changelog.go — manter em sincronia.
6
+
7
+ const fs = require('fs')
8
+ const path = require('path')
9
+
10
+ // sectionHeaderRE casa cabeçalhos de seção no formato "## [x.y.z] - YYYY-MM-DD"
11
+ // ou "## [Unreleased]" (sem data).
12
+ const sectionHeaderRE = /^## \[([^\]]+)\](?: - (\d{4}-\d{2}-\d{2}))?/
13
+
14
+ /**
15
+ * parseSections — separa o conteúdo de um CHANGELOG.md em seções, uma por
16
+ * cabeçalho "## [...]" encontrado. Texto antes da primeira seção (título do
17
+ * arquivo, preâmbulo) é descartado.
18
+ * @param {string} content
19
+ * @returns {Array<{version: string, date: string, body: string}>}
20
+ */
21
+ function parseSections(content) {
22
+ const lines = content.split('\n')
23
+
24
+ const sections = []
25
+ let current = null
26
+ let bodyLines = []
27
+
28
+ const flush = () => {
29
+ if (current !== null) {
30
+ current.body = bodyLines.join('\n')
31
+ sections.push(current)
32
+ }
33
+ }
34
+
35
+ for (const line of lines) {
36
+ const m = sectionHeaderRE.exec(line)
37
+ if (m !== null) {
38
+ flush()
39
+ current = { version: m[1], date: m[2] || '', body: '' }
40
+ bodyLines = []
41
+ continue
42
+ }
43
+ if (current !== null) {
44
+ bodyLines.push(line)
45
+ }
46
+ }
47
+ flush()
48
+
49
+ return sections
50
+ }
51
+
52
+ /**
53
+ * firstSection — retorna a primeira seção da lista.
54
+ * Erro se a lista vier vazia.
55
+ * @param {Array<{version: string, date: string, body: string}>} sections
56
+ * @returns {{version: string, date: string, body: string}}
57
+ */
58
+ function firstSection(sections) {
59
+ if (sections.length === 0) {
60
+ throw new Error('CHANGELOG.md has no version sections')
61
+ }
62
+ return sections[0]
63
+ }
64
+
65
+ /**
66
+ * findVersion — busca a seção com version igual ao argumento, normalizando um
67
+ * prefixo "v"/"V" opcional no argumento do usuário antes de comparar.
68
+ * @param {Array<{version: string, date: string, body: string}>} sections
69
+ * @param {string} version
70
+ * @returns {{version: string, date: string, body: string}}
71
+ */
72
+ function findVersion(sections, version) {
73
+ let normalized = version
74
+ if (normalized.length > 0 && (normalized[0] === 'v' || normalized[0] === 'V')) {
75
+ normalized = normalized.slice(1)
76
+ }
77
+ for (const s of sections) {
78
+ if (s.version === normalized || s.version === version) {
79
+ return s
80
+ }
81
+ }
82
+ throw new Error(`version "${version}" not found in CHANGELOG.md`)
83
+ }
84
+
85
+ /**
86
+ * formatSection — reconstrói o texto formatado de uma seção, reproduzindo o
87
+ * cabeçalho original.
88
+ * @param {{version: string, date: string, body: string}} section
89
+ * @returns {string}
90
+ */
91
+ function formatSection(section) {
92
+ const dateSuffix = section.date !== '' ? ` - ${section.date}` : ''
93
+ const body = section.body.replace(/^\n+/, '')
94
+ return `## [${section.version}]${dateSuffix}\n\n${body.replace(/\n+$/, '') + '\n'}`
95
+ }
96
+
97
+ /**
98
+ * read — lê o CHANGELOG.md na raiz informada.
99
+ * @param {string} root
100
+ * @returns {string}
101
+ */
102
+ function read(root) {
103
+ const filePath = path.join(root, 'CHANGELOG.md')
104
+ try {
105
+ return fs.readFileSync(filePath, 'utf8')
106
+ } catch (err) {
107
+ if (err.code === 'ENOENT') {
108
+ throw new Error('CHANGELOG.md not found — nothing to show')
109
+ }
110
+ throw err
111
+ }
112
+ }
113
+
114
+ module.exports = { parseSections, firstSection, findVersion, formatSection, read }
@@ -15,22 +15,25 @@ function createBranchCommand() {
15
15
  const newCmd = new Command('new')
16
16
  newCmd
17
17
  .description(
18
- 'Create a feat/fix/refactor branch, gated on a matching REQ + roadmap already in wip/ or done/\n\n' +
18
+ 'Create a feat/fix/refactor/chore/docs branch; feat/fix/refactor gated on a matching REQ + roadmap already in wip/ or done/\n\n' +
19
19
  'trackfw branch new moves the branch_has_wip_roadmap governance gate (already enforced\n' +
20
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' +
21
+ ' 1. Validates <type> is one of feat, fix, refactor, chore, docs and <slug> is non-empty.\n' +
22
+ ' 2. For feat, fix, refactor: checks whether a roadmap in wip/ or done/ matches the given\n' +
23
+ " slug — the exact matching logic 'trackfw validate' already uses (normalized slug,\n" +
24
+ " filename contains match). Without a match: blocks — 'git checkout -b' is never\n" +
25
+ " executed — and prints the same governance orientation message 'trackfw validate'\n" +
26
+ " already prints for this rule.\n" +
27
+ " 3. For chore, docs: housekeeping types already treated as roadmap-exempt by 'trackfw ship'\n" +
28
+ " and 'trackfw commit' — the branch is created without the roadmap gate.\n" +
29
+ " 4. With a match (or for chore/docs): runs 'git checkout -b <type>/<slug>', propagating\n" +
30
+ " Git's own output and exit status literally.\n\n" +
28
31
  'Create the governance artifacts first if this blocks you:\n' +
29
32
  ' trackfw req new "title"\n' +
30
33
  ' trackfw roadmap new "title"\n' +
31
34
  ' trackfw roadmap move <name> wip'
32
35
  )
33
- .argument('<spec>', '<type>/<slug>, type in feat, fix, refactor')
36
+ .argument('<spec>', '<type>/<slug>, type in feat, fix, refactor, chore, docs')
34
37
  .option('--dry-run', 'Report whether the branch would be created or blocked, without executing git', false)
35
38
  .action((spec, options) => {
36
39
  const exitCode = runBranchNew(spec, !!options.dryRun, {})
@@ -0,0 +1,29 @@
1
+ 'use strict'
2
+ const { Command } = require('commander')
3
+ const { read, parseSections, firstSection, findVersion, formatSection } = require('../changelog')
4
+
5
+ const cmd = new Command('changelog')
6
+ cmd.description('Show entries from CHANGELOG.md')
7
+ // enablePositionalOptions escopa a flag local "--version" a este subcomando,
8
+ // evitando que o root a intercepte como a flag global "trackfw --version".
9
+ cmd.enablePositionalOptions()
10
+ cmd.option('--version <x.y.z>', 'Show a specific version section')
11
+ cmd.option('--all', 'Show the entire CHANGELOG.md')
12
+ cmd.action(async (opts) => {
13
+ try {
14
+ const root = process.cwd()
15
+ const content = read(root)
16
+ if (opts.all) {
17
+ process.stdout.write(content)
18
+ return
19
+ }
20
+ const sections = parseSections(content)
21
+ const section = opts.version ? findVersion(sections, opts.version) : firstSection(sections)
22
+ process.stdout.write(formatSection(section))
23
+ } catch (err) {
24
+ console.error(`Error: ${err.message}`)
25
+ process.exitCode = 1
26
+ }
27
+ })
28
+
29
+ module.exports = cmd
@@ -0,0 +1,60 @@
1
+ 'use strict'
2
+
3
+ // trackfw commit -m "<message>"
4
+ //
5
+ // CLI wiring for the `trackfw commit` command. The testable logic lives in
6
+ // ../commit/runner.js (mirrors the branch.js / branch/runner.js split already used in this CLI).
7
+
8
+ const { Command } = require('commander')
9
+ const { runCommit, buildSuggestedMessage } = require('../commit/runner')
10
+
11
+ function createCommitCommand() {
12
+ const cmd = new Command('commit')
13
+ cmd
14
+ .description(
15
+ "trackfw commit is the missing intermediate step between raw 'git commit' and\n" +
16
+ "'trackfw ship': it commits staged changes directly, but blocks the commit before it happens\n" +
17
+ 'when governance is missing, instead of letting it land and only catching it later:\n\n' +
18
+ " 1. On 'main'/'master': always blocked — commit directly on the default branch is never\n" +
19
+ ' permitted.\n' +
20
+ ' 2. On a feat/fix/refactor branch: requires a roadmap matching the branch slug already in\n' +
21
+ " wip/ or done/ — the exact matching logic 'trackfw branch new' and 'trackfw validate'\n" +
22
+ ' already use. Without a match, blocks with the same governance orientation message.\n' +
23
+ ' 3. On any other branch (e.g. doc/housekeeping branches): allowed without requiring a\n' +
24
+ ' roadmap — a warning is logged, but the commit proceeds.\n' +
25
+ " 4. When allowed: runs 'git commit -m <message>', propagating Git's own output and exit\n" +
26
+ ' status literally.\n\n' +
27
+ "'--suggest' takes a completely separate path: it prints a heuristic Conventional Commits\n" +
28
+ "skeleton built from 'git diff --cached --name-status' (type + staged file list) and exits\n" +
29
+ 'without ever committing — no LLM call, just a structural heuristic. It is not a\n' +
30
+ 'ready-to-use message; review and edit before using it with -m. When \'--suggest\' is set,\n' +
31
+ "'-m' (if also passed) is ignored and no commit ever happens.\n\n" +
32
+ 'Create the governance artifacts first if this blocks you:\n' +
33
+ ' trackfw req new "title"\n' +
34
+ ' trackfw roadmap new "title"\n' +
35
+ ' trackfw roadmap move <name> wip'
36
+ )
37
+ .option('-m, --message <msg>', 'Commit message (required)')
38
+ .option('--suggest', 'Print a heuristic Conventional Commits message skeleton from staged files and exit without committing (ignores -m)')
39
+ .action((options) => {
40
+ if (options.suggest) {
41
+ try {
42
+ const suggestion = buildSuggestedMessage({})
43
+ process.stdout.write(suggestion + '\n')
44
+ return
45
+ } catch (err) {
46
+ process.stderr.write(err.message + '\n')
47
+ process.exit(1)
48
+ }
49
+ }
50
+
51
+ const exitCode = runCommit(options.message || '', {})
52
+ if (exitCode !== 0) {
53
+ process.exit(exitCode)
54
+ }
55
+ })
56
+
57
+ return cmd
58
+ }
59
+
60
+ module.exports = createCommitCommand()
@@ -22,6 +22,11 @@ function scan(rootDir) {
22
22
  hookFramework: 'none',
23
23
  ciSystem: 'none',
24
24
  forge: '',
25
+ // suggestedTestFramework é uma sugestão best-effort (nunca erro, '' quando nada bate) baseada
26
+ // em arquivos de configuração presentes na raiz do projeto. É apenas impressa como sugestão
27
+ // pelo comando `discover` — nunca escrita automaticamente em trackfw.yaml (a convenção de
28
+ // agent_conventions deve ser sempre declarada pelo time, não inferida).
29
+ suggestedTestFramework: '',
25
30
  };
26
31
 
27
32
  // trackfw.yaml
@@ -110,6 +115,9 @@ function scan(rootDir) {
110
115
  r.ciSystem = 'none';
111
116
  }
112
117
 
118
+ // Suggested test framework — best-effort heuristic, never an error.
119
+ r.suggestedTestFramework = detectTestFramework(rootDir);
120
+
113
121
  // Forge detection — reuse forge/resolve.js (no duplicate parse).
114
122
  // gitRemoteURL returns '' on any error; CI detection is filesystem-based.
115
123
  try {
@@ -338,6 +346,60 @@ function countMD(dir) {
338
346
  return n;
339
347
  }
340
348
 
349
+ // detectTestFramework é uma heurística best-effort para sugerir um framework de teste com base
350
+ // em arquivos de configuração presentes na raiz do projeto. Nunca retorna erro — retorna '' quando
351
+ // nenhum arquivo-gatilho é encontrado. Ordem de precedência: jest, vitest, pytest, go test.
352
+ function detectTestFramework(rootDir) {
353
+ if (isFile(path.join(rootDir, 'jest.config.js')) || isFile(path.join(rootDir, 'jest.config.ts'))) {
354
+ return 'jest';
355
+ }
356
+ if (isFile(path.join(rootDir, 'vitest.config.js')) || isFile(path.join(rootDir, 'vitest.config.ts'))) {
357
+ return 'vitest';
358
+ }
359
+ if (isFile(path.join(rootDir, 'pytest.ini'))) {
360
+ return 'pytest';
361
+ }
362
+ if (hasFileWithSubstring(path.join(rootDir, 'pyproject.toml'), '[tool.pytest')) {
363
+ return 'pytest';
364
+ }
365
+ if (hasFileWithSubstring(path.join(rootDir, 'setup.cfg'), '[tool:pytest]')) {
366
+ return 'pytest';
367
+ }
368
+ if (isFile(path.join(rootDir, 'go.mod')) && hasGoTestFile(rootDir)) {
369
+ return 'go test';
370
+ }
371
+ return '';
372
+ }
373
+
374
+ // hasFileWithSubstring lê path e retorna true se seu conteúdo contém sub. Retorna false
375
+ // silenciosamente se o arquivo não existir ou não puder ser lido (best-effort).
376
+ function hasFileWithSubstring(p, sub) {
377
+ let content;
378
+ try { content = fs.readFileSync(p, 'utf8'); } catch { return false; }
379
+ return content.includes(sub);
380
+ }
381
+
382
+ // hasGoTestFile percorre rootDir recursivamente procurando qualquer arquivo *_test.go.
383
+ function hasGoTestFile(rootDir) {
384
+ let found = false;
385
+ function walk(d) {
386
+ if (found) return;
387
+ let entries;
388
+ try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
389
+ for (const e of entries) {
390
+ if (found) return;
391
+ if (e.isDirectory()) {
392
+ walk(path.join(d, e.name));
393
+ } else if (e.name.endsWith('_test.go')) {
394
+ found = true;
395
+ return;
396
+ }
397
+ }
398
+ }
399
+ walk(rootDir);
400
+ return found;
401
+ }
402
+
341
403
  function listSubDirs(dir) {
342
404
  try {
343
405
  return fs.readdirSync(dir).filter(f => {
@@ -412,6 +474,12 @@ cmd.action((opts) => {
412
474
  console.log('⚠ No CI system detected');
413
475
  }
414
476
 
477
+ // suggested test framework — printed only, never written to trackfw.yaml automatically
478
+ // (agent_conventions must always be declared by the team).
479
+ if (r.suggestedTestFramework) {
480
+ console.log(`Suggested test framework: ${r.suggestedTestFramework} (add to trackfw.yaml as agent_conventions: if correct)`);
481
+ }
482
+
415
483
  console.log(`\nGovernance Score: ${r.governanceScore}/100`);
416
484
 
417
485
  if (opts.init) {
@@ -500,3 +568,4 @@ module.exports.generateBootstrapLog = generateBootstrapLog;
500
568
  module.exports.writeValidateScript = writeValidateScript;
501
569
  module.exports.writeCIWorkflow = writeCIWorkflow;
502
570
  module.exports.writeCIWorkflowForce = writeCIWorkflowForce;
571
+ module.exports.detectTestFramework = detectTestFramework;
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { Command } = require('commander')
4
4
  const { version } = require('../../package.json')
5
+ const { formatUnknownCommandError } = require('../lib/unknown-command')
5
6
 
6
7
  function createProgram() {
7
8
  const program = new Command()
@@ -9,6 +10,25 @@ function createProgram() {
9
10
  .name('trackfw')
10
11
  .description('trackfw — governed software delivery framework\nADR → REQ → ROADMAP → kanban')
11
12
  .version(`trackfw ${version}`)
13
+ // enablePositionalOptions: garante que uma flag de subcomando (ex.: o
14
+ // "changelog --version <x.y.z>") não seja capturada pela flag global
15
+ // "--version" do root — sem isto, commander casa "--version" contra o
16
+ // primeiro registro conhecido em toda a árvore de comandos.
17
+ program.enablePositionalOptions()
18
+
19
+ // Comando desconhecido — mensagem canônica compartilhada pelos 3 CLIs
20
+ // (ADR-2026-08-15-remocao-do-subsistema-de-plugins-em-vez-de-gate-de-binario-
21
+ // de-terceiro.md D3). Registrar um listener 'command:*' intercepta ANTES do
22
+ // commander chamar seu próprio Command.unknownCommand() — evita reformatar
23
+ // uma mensagem já impressa por ele (o padrão que causava dupla-impressão no
24
+ // Go, ver internal/commands/root.go) e evita depender do texto/threshold
25
+ // próprio de commander.suggestSimilar, que diverge de cobra/argparse.
26
+ program.on('command:*', (operands) => {
27
+ const typed = operands[0]
28
+ const candidates = program.commands.map((cmd) => cmd.name())
29
+ process.stderr.write(formatUnknownCommandError(typed, candidates, program.name()) + '\n')
30
+ process.exit(1)
31
+ })
12
32
 
13
33
  program.addCommand(require('./init'))
14
34
  program.addCommand(require('./adr'))
@@ -16,8 +36,8 @@ function createProgram() {
16
36
  program.addCommand(require('./roadmap'))
17
37
  program.addCommand(require('./validate'))
18
38
  program.addCommand(require('./status'))
39
+ program.addCommand(require('./changelog'))
19
40
  program.addCommand(require('./log'))
20
- program.addCommand(require('./plugins'))
21
41
  program.addCommand(require('./discover'))
22
42
  program.addCommand(require('./update'))
23
43
  program.addCommand(require('./metrics'))
@@ -33,13 +53,11 @@ function createProgram() {
33
53
  program.addCommand(require('./ship'))
34
54
  program.addCommand(require('./barrier'))
35
55
  program.addCommand(require('./branch'))
56
+ program.addCommand(require('./commit'))
36
57
 
37
58
  const { createServeCommand } = require('./serve')
38
59
  program.addCommand(createServeCommand())
39
60
 
40
- // plugin dispatch — comandos desconhecidos tentam executar plugin
41
- program.hook('preSubcommand', () => {})
42
-
43
61
  return program
44
62
  }
45
63
 
@@ -6,6 +6,7 @@ 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')
9
+ const { createThirdPartyCommand } = require('./thirdparty')
9
10
 
10
11
  const csv = value => String(value).split(',').map(entry => entry.trim()).filter(Boolean)
11
12
  const collect = (value, previous) => previous.concat(value)
@@ -102,6 +103,12 @@ async function promptAmbiguousSurfaces(kind, options, prompts = require('@inquir
102
103
 
103
104
  function createLifecycleCommand(kind) {
104
105
  const root = new Command(kind).description(`Manage trackfw ${kind}`)
106
+ // third-party fetch/install (D1): reachable from both `trackfw agents
107
+ // third-party` and `trackfw skills third-party`, under the same
108
+ // two-phase quarantine gate. Mirrors
109
+ // internal/commands/integrations_flags.go registering `third-party` in
110
+ // newIntegrationsLifecycleCmd.
111
+ root.addCommand(createThirdPartyCommand(kind))
105
112
  for (const operation of ['list', 'install', 'uninstall', 'update']) {
106
113
  const mutation = operation !== 'list'
107
114
  const command = new Command(operation)
@@ -9,9 +9,10 @@ function createShipCommand() {
9
9
  cmd
10
10
  .description(
11
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' +
12
+ ' 1. Validates branch name — must match feat|fix|refactor|chore|docs/<slug>\n' +
13
+ ' 2. Validates governance — REQ + roadmap in wip/ must exist for feat/fix/refactor branches\n' +
14
+ ' (hard gate: not affected by lenient mode or per-rule severity); chore/docs branches skip\n' +
15
+ " this check, mirroring 'trackfw commit'\n" +
15
16
  ' 3. Detects pending squash-merges in other branches (advisory only)\n' +
16
17
  ' 4. Reviews what is staged (git status --short + git diff --cached --stat)\n' +
17
18
  ' 5. Commits with Conventional Commits format (-m is required)\n' +