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
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Verificação de disponibilidade de CLI — scan puro no PATH sem subprocess
8
+ // ---------------------------------------------------------------------------
9
+
10
+ /**
11
+ * Verifica se um executável está disponível no PATH, respeitando
12
+ * TRACKFW_DISABLE_EXTERNAL_COMMANDS (mesmo padrão de discover.go).
13
+ * @param {string} name
14
+ * @returns {boolean}
15
+ */
16
+ function _defaultAvailFn(name) {
17
+ if (!name) return false;
18
+ if (process.env.TRACKFW_DISABLE_EXTERNAL_COMMANDS === '1') return false;
19
+
20
+ const dirs = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
21
+ const isWindows = process.platform === 'win32';
22
+ const exts = isWindows
23
+ ? (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';')
24
+ : [''];
25
+
26
+ for (const dir of dirs) {
27
+ for (const ext of exts) {
28
+ try {
29
+ const candidate = path.join(dir, name + ext);
30
+ fs.accessSync(candidate, fs.constants.F_OK | fs.constants.X_OK);
31
+ return true;
32
+ } catch (_) {
33
+ // continua
34
+ }
35
+ }
36
+ }
37
+ return false;
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Conversão de remote URL → base HTTPS
42
+ // ---------------------------------------------------------------------------
43
+
44
+ /**
45
+ * Converte qualquer formato de remote git (git@, ssh://, https://, http://)
46
+ * para URL base HTTPS sem .git.
47
+ *
48
+ * Normalização especial para Azure SSH:
49
+ * git@ssh.dev.azure.com:v3/org/project/repo → https://dev.azure.com/org/project/_git/repo
50
+ *
51
+ * @param {string} rawURL
52
+ * @param {string} forge
53
+ * @returns {string}
54
+ */
55
+ function remoteHTTPSBase(rawURL, forge) {
56
+ if (!rawURL) return '';
57
+ rawURL = rawURL.trim();
58
+ if (!rawURL) return '';
59
+
60
+ let host = '';
61
+ let pathStr = '';
62
+
63
+ if (rawURL.startsWith('git@')) {
64
+ // git@host:path
65
+ const rest = rawURL.slice(4);
66
+ const colonIdx = rest.indexOf(':');
67
+ if (colonIdx < 0) return '';
68
+ host = rest.slice(0, colonIdx).toLowerCase();
69
+ pathStr = rest.slice(colonIdx + 1);
70
+
71
+ } else if (rawURL.startsWith('ssh://')) {
72
+ // ssh://[user@]host/path
73
+ let rest = rawURL.slice(6);
74
+ const atIdx = rest.indexOf('@');
75
+ if (atIdx >= 0) rest = rest.slice(atIdx + 1);
76
+ const slashIdx = rest.indexOf('/');
77
+ if (slashIdx < 0) {
78
+ host = rest.toLowerCase();
79
+ } else {
80
+ host = rest.slice(0, slashIdx).toLowerCase();
81
+ pathStr = rest.slice(slashIdx + 1);
82
+ }
83
+
84
+ } else if (rawURL.startsWith('https://') || rawURL.startsWith('http://')) {
85
+ let rest = rawURL.replace(/^https?:\/\//, '');
86
+ const atIdx = rest.indexOf('@');
87
+ if (atIdx >= 0) rest = rest.slice(atIdx + 1);
88
+ const slashIdx = rest.indexOf('/');
89
+ if (slashIdx < 0) {
90
+ host = rest.toLowerCase();
91
+ } else {
92
+ host = rest.slice(0, slashIdx).toLowerCase();
93
+ pathStr = rest.slice(slashIdx + 1);
94
+ }
95
+
96
+ } else {
97
+ return '';
98
+ }
99
+
100
+ pathStr = pathStr.replace(/\.git$/, '').replace(/^\/|\/$/g, '');
101
+
102
+ // Normalização Azure SSH
103
+ if (forge === 'azure' && host === 'ssh.dev.azure.com') {
104
+ host = 'dev.azure.com';
105
+ pathStr = pathStr.replace(/^v3\//, '');
106
+ const parts = pathStr.split('/');
107
+ if (parts.length === 3) {
108
+ pathStr = `${parts[0]}/${parts[1]}/_git/${parts[2]}`;
109
+ }
110
+ }
111
+
112
+ if (!pathStr) return `https://${host}`;
113
+ return `https://${host}/${pathStr}`;
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Adapter
118
+ // ---------------------------------------------------------------------------
119
+
120
+ /**
121
+ * @typedef {Object} Adapter
122
+ * @property {string} forge
123
+ * @property {string} noun - "Pull Request" ou "Merge Request"
124
+ * @property {string} cliName - nome do executável (vazio = sem CLI)
125
+ * @property {string[]} cliArgs - argumentos para criar PR/MR
126
+ * @property {boolean} available - false quando CLI ausente
127
+ * @property {function(string, string): string} fallbackURL
128
+ */
129
+
130
+ /**
131
+ * Retorna o Adapter para o forge informado.
132
+ *
133
+ * @param {string} forge
134
+ * @param {function(string): boolean} [availFn] - injeta verificação de CLI (para testes)
135
+ * @returns {Adapter}
136
+ */
137
+ function forgeAdapter(forge, availFn) {
138
+ if (typeof availFn !== 'function') {
139
+ availFn = _defaultAvailFn;
140
+ }
141
+
142
+ let noun = 'Pull Request';
143
+ let cliName = '';
144
+ let cliArgs = [];
145
+ let available = false;
146
+
147
+ switch (forge) {
148
+ case 'github':
149
+ cliName = 'gh';
150
+ cliArgs = ['pr', 'create'];
151
+ available = availFn('gh');
152
+ break;
153
+ case 'gitlab':
154
+ noun = 'Merge Request';
155
+ cliName = 'glab';
156
+ cliArgs = ['mr', 'create'];
157
+ available = availFn('glab');
158
+ break;
159
+ case 'azure':
160
+ cliName = 'az';
161
+ cliArgs = ['repos', 'pr', 'create'];
162
+ available = availFn('az');
163
+ break;
164
+ case 'bitbucket':
165
+ // Bitbucket não possui CLI — nunca chama availFn
166
+ break;
167
+ default:
168
+ // forge desconhecido: sem CLI, sem disponibilidade
169
+ break;
170
+ }
171
+
172
+ const adapter = { forge, noun, cliName, cliArgs, available };
173
+
174
+ adapter.fallbackURL = function (remoteURL, branch) {
175
+ const base = remoteHTTPSBase(remoteURL, forge);
176
+ if (!base) return '';
177
+ switch (forge) {
178
+ case 'github': return `${base}/compare/${branch}?expand=1`;
179
+ case 'gitlab': return `${base}/-/merge_requests/new?merge_request[source_branch]=${branch}`;
180
+ case 'bitbucket': return `${base}/pull-requests/new?source=${branch}`;
181
+ case 'azure': return `${base}/pullrequestcreate?sourceRef=${branch}`;
182
+ default: return '';
183
+ }
184
+ };
185
+
186
+ return adapter;
187
+ }
188
+
189
+ module.exports = { forgeAdapter, remoteHTTPSBase };
@@ -0,0 +1,191 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * forge/resolve.js — Forge resolution for trackfw ship.
5
+ *
6
+ * Precedence (highest → lowest):
7
+ * 1. --forge flag (explicit override)
8
+ * 2. forge: field in trackfw.yaml
9
+ * 3. Host extracted from git remote get-url origin
10
+ * 4. CI configuration files in repo root (.gitlab-ci.yml / .github/workflows/)
11
+ * 5. "manual" — never an error
12
+ */
13
+
14
+ const { execSync } = require('child_process');
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ /** Accepted values for the --forge flag and forge: config field. */
19
+ const VALID_FORGES = ['github', 'gitlab', 'bitbucket', 'azure'];
20
+
21
+ /**
22
+ * Resolve the forge from the given inputs.
23
+ *
24
+ * @param {object} input
25
+ * @param {string} [input.flagForge] - value from --forge flag
26
+ * @param {string} [input.configForge] - value from trackfw.yaml forge: field
27
+ * @param {string} [input.remoteURL] - output of "git remote get-url origin"
28
+ * @param {string} [input.repoDir] - repo root for CI file detection
29
+ * @returns {{ forge: string, source: string }} Resolution object.
30
+ * forge ∈ {"github", "gitlab", "bitbucket", "azure", "manual"}
31
+ * source ∈ {"flag", "config", "remote", "ci", "none"}
32
+ * @throws {Error} if flagForge or configForge contains an invalid value.
33
+ */
34
+ function resolve(input) {
35
+ const { flagForge = '', configForge = '', remoteURL = '', repoDir = '' } = input || {};
36
+
37
+ // 1. Explicit flag wins everything.
38
+ if (flagForge) {
39
+ validateForge(flagForge);
40
+ return { forge: flagForge, source: 'flag' };
41
+ }
42
+
43
+ // 2. Config field.
44
+ if (configForge) {
45
+ validateForge(configForge);
46
+ return { forge: configForge, source: 'config' };
47
+ }
48
+
49
+ // 3. Remote URL — known hosts only.
50
+ if (remoteURL) {
51
+ const forge = forgeFromRemoteURL(remoteURL);
52
+ if (forge) return { forge, source: 'remote' };
53
+ }
54
+
55
+ // 4. CI files — desempate for self-hosted / unknown host.
56
+ if (repoDir) {
57
+ const forge = forgeFromCI(repoDir);
58
+ if (forge) return { forge, source: 'ci' };
59
+ }
60
+
61
+ // 5. Manual — not an error.
62
+ return { forge: 'manual', source: 'none' };
63
+ }
64
+
65
+ /**
66
+ * Production entry point: runs "git remote get-url origin" in repoDir,
67
+ * then calls resolve().
68
+ */
69
+ function resolveFromRepo(flagForge, configForge, repoDir) {
70
+ const remoteURL = gitRemoteURL(repoDir);
71
+ return resolve({ flagForge, configForge, remoteURL, repoDir });
72
+ }
73
+
74
+ /** @throws {Error} if forge is not in VALID_FORGES */
75
+ function validateForge(forge) {
76
+ if (!VALID_FORGES.includes(forge)) {
77
+ throw new Error(
78
+ `invalid forge "${forge}": accepted values are ${VALID_FORGES.join(', ')}`
79
+ );
80
+ }
81
+ }
82
+
83
+ function forgeFromRemoteURL(rawURL) {
84
+ const host = extractHost(rawURL);
85
+ return hostToForge(host);
86
+ }
87
+
88
+ /**
89
+ * Extracts the lowercase hostname from HTTPS, SSH (git@) or ssh:// URLs.
90
+ */
91
+ function extractHost(rawURL) {
92
+ if (!rawURL) return '';
93
+ rawURL = rawURL.trim();
94
+
95
+ // SSH short form: git@github.com:org/repo.git
96
+ if (rawURL.startsWith('git@')) {
97
+ const rest = rawURL.slice(4); // strip "git@"
98
+ const colonIdx = rest.indexOf(':');
99
+ if (colonIdx >= 0) return rest.slice(0, colonIdx).toLowerCase();
100
+ return '';
101
+ }
102
+
103
+ // SSH long form: ssh://git@github.com/org/repo.git
104
+ if (rawURL.startsWith('ssh://')) {
105
+ let rest = rawURL.slice(6); // strip "ssh://"
106
+ const atIdx = rest.indexOf('@');
107
+ if (atIdx >= 0) rest = rest.slice(atIdx + 1); // strip user@
108
+ const slashIdx = rest.indexOf('/');
109
+ return slashIdx >= 0
110
+ ? rest.slice(0, slashIdx).toLowerCase()
111
+ : rest.toLowerCase();
112
+ }
113
+
114
+ // HTTPS / HTTP
115
+ if (rawURL.startsWith('https://') || rawURL.startsWith('http://')) {
116
+ let rest = rawURL.replace(/^https?:\/\//, '');
117
+ // strip optional user:pass@
118
+ const atIdx = rest.indexOf('@');
119
+ if (atIdx >= 0) rest = rest.slice(atIdx + 1);
120
+ const slashIdx = rest.indexOf('/');
121
+ return slashIdx >= 0
122
+ ? rest.slice(0, slashIdx).toLowerCase()
123
+ : rest.toLowerCase();
124
+ }
125
+
126
+ return '';
127
+ }
128
+
129
+ /**
130
+ * Maps a known hostname to its forge identifier.
131
+ * Azure DevOps uses dev.azure.com (HTTPS) and ssh.dev.azure.com (SSH).
132
+ * Returns '' for unknown / self-hosted hosts.
133
+ */
134
+ function hostToForge(host) {
135
+ if (!host) return '';
136
+ if (host === 'github.com') return 'github';
137
+ if (host === 'gitlab.com') return 'gitlab';
138
+ if (host === 'bitbucket.org') return 'bitbucket';
139
+ if (
140
+ host === 'dev.azure.com' ||
141
+ host.endsWith('.dev.azure.com') ||
142
+ host.endsWith('.visualstudio.com')
143
+ ) {
144
+ return 'azure';
145
+ }
146
+ return '';
147
+ }
148
+
149
+ /**
150
+ * Inspects CI indicator files in repoDir.
151
+ * Priority: .gitlab-ci.yml → gitlab; .github/workflows/ → github.
152
+ */
153
+ function forgeFromCI(repoDir) {
154
+ try {
155
+ fs.statSync(path.join(repoDir, '.gitlab-ci.yml'));
156
+ return 'gitlab';
157
+ } catch (_) {}
158
+
159
+ try {
160
+ const stat = fs.statSync(path.join(repoDir, '.github', 'workflows'));
161
+ if (stat.isDirectory()) return 'github';
162
+ } catch (_) {}
163
+
164
+ return '';
165
+ }
166
+
167
+ /**
168
+ * Runs "git remote get-url origin" in repoDir.
169
+ * Returns '' on any error.
170
+ */
171
+ function gitRemoteURL(repoDir) {
172
+ try {
173
+ return execSync('git remote get-url origin', {
174
+ cwd: repoDir,
175
+ stdio: ['pipe', 'pipe', 'pipe'],
176
+ encoding: 'utf8',
177
+ }).trim();
178
+ } catch (_) {
179
+ return '';
180
+ }
181
+ }
182
+
183
+ module.exports = {
184
+ VALID_FORGES,
185
+ resolve,
186
+ resolveFromRepo,
187
+ validateForge,
188
+ extractHost,
189
+ hostToForge,
190
+ forgeFromCI,
191
+ };
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs')
4
4
  const path = require('path')
5
+ const { localDateISO } = require('./date')
5
6
 
6
7
  /**
7
8
  * Converte uma string em slug: lowercase + espaços → hifens.
@@ -9,7 +10,13 @@ const path = require('path')
9
10
  * @returns {string}
10
11
  */
11
12
  function toSlug(s) {
12
- return s.toLowerCase().replace(/ /g, '-')
13
+ // NFKD normalization + remove combining marks (diacríticos) + lowercase + non-alphanumeric → hífen
14
+ return s
15
+ .normalize('NFKD')
16
+ .replace(/[̀-ͯ]/g, '')
17
+ .toLowerCase()
18
+ .replace(/[^a-z0-9]+/g, '-')
19
+ .replace(/^-+|-+$/g, '')
13
20
  }
14
21
 
15
22
  /**
@@ -26,11 +33,13 @@ function slugToTitle(slug) {
26
33
  }
27
34
 
28
35
  /**
29
- * Retorna a data atual no formato YYYY-MM-DD.
36
+ * Retorna a data LOCAL atual no formato YYYY-MM-DD.
37
+ * Delega para localDateISO() — usa getDate/getMonth/getFullYear (hora local),
38
+ * não toISOString (UTC).
30
39
  * @returns {string}
31
40
  */
32
41
  function today() {
33
- return new Date().toISOString().slice(0, 10)
42
+ return localDateISO()
34
43
  }
35
44
 
36
45
  /**
@@ -182,4 +191,4 @@ author: ""
182
191
  return filename
183
192
  }
184
193
 
185
- module.exports = { newADR, listADRs, newADRDraft, toSlug }
194
+ module.exports = { newADR, listADRs, newADRDraft, toSlug, today }
@@ -0,0 +1,23 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * localDateISO — retorna a data LOCAL atual no formato YYYY-MM-DD.
5
+ *
6
+ * Usa getFullYear/getMonth/getDate (métodos de hora local) em vez de
7
+ * toISOString() (que retorna sempre UTC). Isso garante que uma REQ criada às
8
+ * 22h no Brasil seja datada do dia correto, independentemente do fuso da
9
+ * máquina — comportamento idêntico ao de Go (time.Now().Format("2006-01-02"))
10
+ * e Python (date.today().isoformat()).
11
+ *
12
+ * @returns {string} Data no formato "YYYY-MM-DD"
13
+ */
14
+ function localDateISO() {
15
+ const d = new Date()
16
+ return [
17
+ d.getFullYear(),
18
+ String(d.getMonth() + 1).padStart(2, '0'),
19
+ String(d.getDate()).padStart(2, '0'),
20
+ ].join('-')
21
+ }
22
+
23
+ module.exports = { localDateISO }
@@ -13,6 +13,7 @@ const GOV_DIRS = [
13
13
  'docs/roadmaps/blocked',
14
14
  'docs/roadmaps/done',
15
15
  'docs/roadmaps/abandoned',
16
+ 'vault/notes',
16
17
  ]
17
18
 
18
19
  const GLOBAL_ADRS_DIRECTIVE = 'Obrigatório: Inspecione e respeite todos os ADRs globais nos diretórios listados em adr_dirs (inclusive caminhos ~/...) antes de propor alterações de arquitetura.'
@@ -28,6 +29,7 @@ async function scaffold(cfg, cwd) {
28
29
  console.log(` ✓ ${dir}`)
29
30
  }
30
31
 
32
+ generateVaultIndex(root)
31
33
  writeTrackfwConfig(cfg, root)
32
34
  generateValidateScript(cfg, root)
33
35
  generateAttentionScripts(cfg, root)
@@ -40,6 +42,29 @@ async function scaffold(cfg, cwd) {
40
42
  injectHooksDetected(root)
41
43
  }
42
44
 
45
+ /**
46
+ * generateVaultIndex — cria vault/notes/index.md se ainda não existir.
47
+ * @param {string} root
48
+ */
49
+ function generateVaultIndex(root) {
50
+ const indexPath = path.join(root || process.cwd(), 'vault', 'notes', 'index.md')
51
+ if (fs.existsSync(indexPath)) return
52
+ const content = `# Vault de Conhecimento
53
+
54
+ > Ponto de entrada de conhecimento do projeto para agentes e pessoas.
55
+ > Cada nota documenta uma causa-raiz, decisão técnica ou restrição não óbvia.
56
+ > Crie notas com: trackfw note new "<título>"
57
+
58
+ ## Índice
59
+
60
+ <!-- As notas serão listadas abaixo. Exemplo:
61
+ - [nome-da-nota-YYYY-MM-DD](nome-da-nota-YYYY-MM-DD.md)
62
+ -->
63
+ `
64
+ fs.writeFileSync(indexPath, content, 'utf8')
65
+ console.log(' ✓ vault/notes/index.md')
66
+ }
67
+
43
68
  // ---------------------------------------------------------------------------
44
69
  // trackfw.yaml
45
70
  // ---------------------------------------------------------------------------
@@ -54,7 +79,7 @@ backend: ${cfg.backend || ''}
54
79
  backend_framework: ${cfg.backendFramework || ''}
55
80
  pkg_manager: ${cfg.pkgManager || ''}
56
81
  hooks: ${cfg.hooks || ''}
57
- ci: ${cfg.ci || ''}
82
+ ci: ${cfg.ci || ''}${cfg.forge ? `\nforge: ${cfg.forge}` : ''}
58
83
  require_req_in_commit: ${cfg.requireReqInCommit ? 'true' : 'false'}
59
84
 
60
85
  # governance paths (edit to match your project structure)
@@ -585,6 +610,66 @@ function generateClaudeMD(cfg) {
585
610
  content += 'No CI gate configured.\n'
586
611
  }
587
612
 
613
+ // Harness sections — derived from project governance conventions
614
+ content += '\n## Branch strategy\n\n'
615
+ content += 'One active branch at a time. Name it `feat/<slug>`, `fix/<slug>` or `refactor/<slug>`. '
616
+ content += 'Before creating a new branch, verify no other is genuinely open: run `git fetch origin --prune`, '
617
+ content += 'then `git branch -r --no-merged origin/main`, then for each candidate `git diff origin/main <branch> --stat`. '
618
+ content += 'An empty diff means it was squash-merged — ignore it. '
619
+ content += 'Squash merges do not mark a branch as merged, so `--no-merged` alone is not evidence. '
620
+ content += 'If the branch is stale and the diff looks inflated by main\'s own evolution, '
621
+ content += 'compare only the files the branch itself touched since the merge base.\n\n'
622
+
623
+ content += '## Definition of done\n\n'
624
+ content += 'Green build and tests do not close a microbatch. '
625
+ content += 'It is done when the requirement and the roadmap sit in the correct state folder, '
626
+ content += 'their declared status matches that folder, the final validation is recorded with evidence, '
627
+ content += 'no duplicate copy remains in another state, and `trackfw validate` reports no violations.\n\n'
628
+
629
+ content += '## Requirement scope\n\n'
630
+ content += 'Every requirement must declare an explicit negative scope: what must not be implemented. '
631
+ content += 'Boundaries prevent an implementing agent from inventing work.\n\n'
632
+
633
+ content += '## State requirements\n\n'
634
+ content += '`blocked` requires a reason and an owner. '
635
+ content += '`abandoned` requires a reason and a successor. '
636
+ content += '`wip` must reflect work that is genuinely active; '
637
+ content += 'anything stalled moves to `blocked` or `abandoned` instead of rotting in `wip`.\n\n'
638
+
639
+ content += '## Roadmap format\n\n'
640
+ content += 'Organize work as waves of microbatches. '
641
+ content += 'A wave groups microbatches that can run in parallel; a barrier separates waves. '
642
+ content += 'Microbatches sharing any file — including generated trees and build outputs — must be sequential, '
643
+ content += 'and the reason is documented. '
644
+ content += 'Each microbatch declares exact files, exact actions, measurable acceptance criteria and exact validation commands, '
645
+ content += 'so that a small model can execute it without guessing.\n\n'
646
+
647
+ content += '## When governance is not required\n\n'
648
+ content += 'A closed list of exemptions: a typo or local variable rename; a documentation-only change; '
649
+ content += 'a configuration tweak with no runtime effect; a direct revert; '
650
+ content += 'answering a question or reviewing without changes. '
651
+ content += 'Additionally, when the user reports a concrete bug, fix it directly and do not open an architectural analysis for it. '
652
+ content += '**This section takes precedence over the general rule that requires a requirement and a roadmap.** '
653
+ content += 'Anything touching business logic, an API contract, a data schema, authentication or authorization, '
654
+ content += 'localization, or user-facing behavior always requires governance, regardless of how few files it touches.\n\n'
655
+
656
+ content += '## Production incidents\n\n'
657
+ content += 'Inspect the live environment before proposing a fix: real variables, active credentials, '
658
+ content += 'granted permissions, running processes. '
659
+ content += 'Confirm the root cause against real evidence, then implement the smallest fix. '
660
+ content += 'Never edit static configuration files as a response to a root cause that has not been confirmed in the running environment.\n\n'
661
+
662
+ content += '## Iterative prototyping\n\n'
663
+ content += 'For complex or uncertain user-facing work, validate the concept with a disposable, isolated prototype '
664
+ content += 'that the user reviews visually, and only then write the decision record and the production roadmap. '
665
+ content += 'Build and test success is not evidence that an interface is right.\n\n'
666
+
667
+ content += '## Autopilot\n\n'
668
+ content += 'Ask everything you need before starting. '
669
+ content += 'Once started, do not interrupt for confirmations that could have been anticipated. '
670
+ content += 'Decide low-risk details autonomously following existing project conventions, '
671
+ content += 'and record autonomous decisions in the commit message.\n'
672
+
588
673
  injectOrUpdateRules('CLAUDE.md', content)
589
674
  console.log(' ✓ CLAUDE.md')
590
675
  }
@@ -700,9 +785,16 @@ trackfw não está instalado. Instale com:
700
785
  Com base no conteúdo da REQ, gere um roadmap seguindo **estritamente** este formato:
701
786
 
702
787
  \`\`\`markdown
788
+ ---
789
+ status: backlog
790
+ date: <YYYY-MM-DD>
791
+ req: "docs/req/<arquivo-selecionado>.md"
792
+ squad: ""
793
+ ---
794
+
703
795
  # Roadmap: <título derivado da REQ>
704
796
 
705
- > Criado em: <YYYY-MM-DD> | Status: ⬜ Backlog
797
+ > Created: <YYYY-MM-DD> | Status: backlog
706
798
 
707
799
  ## Diagnóstico / Contexto
708
800
  <resumo do problema, motivação e escopo extraídos da REQ>
@@ -720,6 +812,13 @@ trackfw não está instalado. Instale com:
720
812
  - [ ] build sem erros
721
813
  - [ ] testes verdes
722
814
  **Comandos de validação:** \`<comando de build e teste do projeto>\`
815
+
816
+ ### ML-1B — <título> (se independente de ML-1A)
817
+ ...
818
+
819
+ ## Wave 2 — <nome> (depende de Wave 1)
820
+ > Dependências: Wave 1 completa
821
+ ...
723
822
  \`\`\`
724
823
 
725
824
  **Princípios obrigatórios:**
@@ -732,10 +831,12 @@ trackfw não está instalado. Instale com:
732
831
  4. **Salvar o arquivo**
733
832
  - Calcule o slug: título em lowercase, espaços → hifens, remova caracteres especiais
734
833
  - Crie o arquivo em \`docs/roadmaps/backlog/ROADMAP-<YYYY-MM-DD>-<slug>.md\`
834
+ - Preencha \`req:\` com o caminho relativo completo da REQ selecionada
735
835
  - Use a data de hoje
736
836
 
737
837
  5. **Confirmar**
738
- Informe o caminho do arquivo criado e um resumo das Waves e total de MLs gerados.`,
838
+ Informe o caminho do arquivo criado e um resumo das Waves e total de MLs gerados.
839
+ `,
739
840
 
740
841
  'implement.md': `Você é o orquestrador de implementação do trackfw. Siga o fluxo abaixo **sem pular etapas**.
741
842
 
@@ -1006,12 +1107,35 @@ async function installAmazonQ(cwd = process.cwd()) {
1006
1107
  }
1007
1108
  }
1008
1109
 
1009
- /** Install the canonical agents and skills for one AI target. */
1010
- async function installIntegrationTarget(target, cwd = process.cwd()) {
1011
- const { execute } = require('../integrations')
1110
+ /**
1111
+ * Install the canonical agents and skills for one AI target.
1112
+ *
1113
+ * `scope` defaults to "project" for backward compatibility with the
1114
+ * compatibility entrypoints below (installAgents, installGemini, etc.), that
1115
+ * historical tests exercise directly without going through the `init`
1116
+ * wizard's scope prompt. `trackfw init` itself always passes the scope it
1117
+ * resolved (ADR-2026-07-25-escopo-de-instalacao-selecionavel-para-agents-e-
1118
+ * skills, D4).
1119
+ */
1120
+ async function installIntegrationTarget(target, cwd = process.cwd(), scope = 'project') {
1121
+ const { execute, buildPlans } = require('../integrations')
1012
1122
  const roots = { projectRoot: cwd }
1013
- execute('agents', 'install', { targets: [target], scope: 'project' }, roots)
1014
- execute('skills', 'install', { targets: [target], scope: 'project' }, roots)
1123
+ const options = { targets: [target], scope }
1124
+ // D5 — transparency: print resolved destinations before writing anything.
1125
+ // buildPlans has no side effects, so it is safe to call here purely to
1126
+ // enumerate destinations; the actual write happens below via execute().
1127
+ // Mirrors printResolvedDestinations in commands/integrations.js and Go's
1128
+ // installAITools (internal/commands/init.go) — this call site (init's AI
1129
+ // tools install) was the one place that stayed silent (divergence #2,
1130
+ // ROADMAP-2026-07-25-escopo-de-instalacao-selecionavel-para-agents-e-skills).
1131
+ const destinations = [
1132
+ ...buildPlans('agents', options).map(plan => plan.destination),
1133
+ ...buildPlans('skills', options).map(plan => plan.destination),
1134
+ ]
1135
+ console.log(`Destino (${scope}):`)
1136
+ for (const destination of [...new Set(destinations)].sort()) console.log(` ${destination}`)
1137
+ execute('agents', 'install', options, roots)
1138
+ execute('skills', 'install', options, roots)
1015
1139
  console.log(` ✓ ${target} agents and skills`)
1016
1140
  }
1017
1141
 
@@ -1116,6 +1240,7 @@ module.exports = {
1116
1240
  generateClaudeMD,
1117
1241
  generateClaudeCommands,
1118
1242
  generateClaudeCommandsForce,
1243
+ generateVaultIndex,
1119
1244
  installSkillsForce,
1120
1245
  installAgents,
1121
1246
  installGemini,