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.
- package/README.md +18 -2
- package/package.json +1 -1
- package/src/commands/discover.js +18 -0
- package/src/commands/index.js +2 -0
- package/src/commands/init.js +50 -4
- package/src/commands/integrations.js +75 -4
- package/src/commands/log.js +2 -1
- package/src/commands/note.js +24 -0
- package/src/commands/req.js +7 -1
- package/src/commands/roadmap.js +4 -3
- package/src/commands/ship.js +49 -0
- package/src/config/index.js +5 -2
- package/src/forge/adapter.js +189 -0
- package/src/forge/resolve.js +191 -0
- package/src/generators/adr.js +13 -4
- package/src/generators/date.js +23 -0
- package/src/generators/init.js +133 -8
- package/src/generators/note.js +154 -0
- package/src/generators/req.js +88 -5
- package/src/generators/roadmap.js +96 -20
- package/src/i18n/locales/en-US.json +2 -1
- package/src/i18n/locales/es-ES.json +2 -1
- package/src/i18n/locales/pt-BR.json +2 -1
- package/src/identity/preset.js +22 -0
- package/src/integrations/assets/agents/architect.md +32 -0
- package/src/integrations/assets/agents/backend.md +32 -0
- package/src/integrations/assets/agents/code-quality.md +29 -0
- package/src/integrations/assets/agents/data.md +32 -0
- package/src/integrations/assets/agents/dba.md +32 -0
- package/src/integrations/assets/agents/frontend.md +32 -0
- package/src/integrations/assets/agents/iac.md +46 -0
- package/src/integrations/assets/agents/infra.md +34 -0
- package/src/integrations/assets/agents/qa.md +32 -0
- package/src/integrations/assets/agents/security.md +29 -0
- package/src/integrations/assets/agents/tooling.md +47 -0
- package/src/integrations/assets/agents/ux.md +29 -0
- package/src/integrations/assets/catalog.json +16 -2
- package/src/integrations/assets/skills/architecture.md +66 -0
- package/src/integrations/assets/skills/backend.md +67 -0
- package/src/integrations/assets/skills/code-quality.md +78 -0
- package/src/integrations/assets/skills/data.md +75 -0
- package/src/integrations/assets/skills/dba.md +77 -0
- package/src/integrations/assets/skills/frontend.md +70 -0
- package/src/integrations/assets/skills/iac.md +90 -0
- package/src/integrations/assets/skills/infra.md +62 -0
- package/src/integrations/assets/skills/qa.md +63 -0
- package/src/integrations/assets/skills/security.md +74 -0
- package/src/integrations/assets/skills/tooling.md +80 -0
- package/src/integrations/assets/skills/ux.md +70 -0
- package/src/integrations/render.js +57 -6
- package/src/ship/runner.js +454 -0
- package/src/validator/index.js +321 -111
- 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
|
+
};
|
package/src/generators/adr.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 }
|
package/src/generators/init.js
CHANGED
|
@@ -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
|
-
>
|
|
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
|
-
/**
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
-
|
|
1014
|
-
|
|
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,
|