trackfw 3.0.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 +9 -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 +40 -2
- 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 +105 -3
- 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,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
|
|
|
@@ -1139,6 +1240,7 @@ module.exports = {
|
|
|
1139
1240
|
generateClaudeMD,
|
|
1140
1241
|
generateClaudeCommands,
|
|
1141
1242
|
generateClaudeCommandsForce,
|
|
1243
|
+
generateVaultIndex,
|
|
1142
1244
|
installSkillsForce,
|
|
1143
1245
|
installAgents,
|
|
1144
1246
|
installGemini,
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const { localDateISO } = require('./date')
|
|
6
|
+
|
|
7
|
+
const VAULT_DIR = 'vault/notes'
|
|
8
|
+
const INDEX_FILE = 'vault/notes/index.md'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Converte título em slug kebab-case lowercase sem acentos.
|
|
12
|
+
* Reutiliza a mesma lógica do adr.js (toSlug).
|
|
13
|
+
* @param {string} s
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
function toSlug(s) {
|
|
17
|
+
// NFKD normalization + remove combining marks (diacríticos) + lowercase + non-alphanumeric → hífen
|
|
18
|
+
return s
|
|
19
|
+
.normalize('NFKD')
|
|
20
|
+
.replace(/[̀-ͯ]/g, '')
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
23
|
+
.replace(/^-+|-+$/g, '')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Retorna a data LOCAL atual no formato YYYY-MM-DD.
|
|
28
|
+
* Delega para localDateISO() — usa getDate/getMonth/getFullYear (hora local),
|
|
29
|
+
* não toISOString (UTC).
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
function today() {
|
|
33
|
+
return localDateISO()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Cria uma nova nota em vault/notes/<slug>-YYYY-MM-DD.md e linka no index.md.
|
|
38
|
+
* Idempotente: falha com erro claro se a nota já existir.
|
|
39
|
+
* @param {string} title
|
|
40
|
+
* @param {string} [cwd]
|
|
41
|
+
*/
|
|
42
|
+
function newNote(title, cwd) {
|
|
43
|
+
const root = cwd || process.cwd()
|
|
44
|
+
const vaultDir = path.join(root, VAULT_DIR)
|
|
45
|
+
fs.mkdirSync(vaultDir, { recursive: true })
|
|
46
|
+
|
|
47
|
+
const slug = toSlug(title)
|
|
48
|
+
const date = today()
|
|
49
|
+
const filename = `${slug}-${date}.md`
|
|
50
|
+
const notePath = path.join(vaultDir, filename)
|
|
51
|
+
|
|
52
|
+
if (fs.existsSync(notePath)) {
|
|
53
|
+
throw new Error(`nota "${filename}" já existe — não sobrescrita`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const body = `---
|
|
57
|
+
title: "${title}"
|
|
58
|
+
tags: []
|
|
59
|
+
date: ${date}
|
|
60
|
+
related: []
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
# ${title}
|
|
64
|
+
|
|
65
|
+
## Problem
|
|
66
|
+
|
|
67
|
+
<!-- Descreva o problema ou situação que motivou esta nota. -->
|
|
68
|
+
|
|
69
|
+
## Root cause
|
|
70
|
+
|
|
71
|
+
<!-- Qual foi a causa raiz identificada? -->
|
|
72
|
+
|
|
73
|
+
## Solution
|
|
74
|
+
|
|
75
|
+
<!-- Como foi resolvido ou mitigado? O que deve ser feito? -->
|
|
76
|
+
`
|
|
77
|
+
|
|
78
|
+
fs.writeFileSync(notePath, body, 'utf8')
|
|
79
|
+
appendNoteToIndex(filename, root)
|
|
80
|
+
console.log(`created ${path.join(VAULT_DIR, filename)}`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Acrescenta link para filename no vault/notes/index.md.
|
|
85
|
+
* Cria o index.md se não existir. Não duplica se já estiver linkado.
|
|
86
|
+
* @param {string} filename
|
|
87
|
+
* @param {string} [cwd]
|
|
88
|
+
*/
|
|
89
|
+
function appendNoteToIndex(filename, cwd) {
|
|
90
|
+
const root = cwd || process.cwd()
|
|
91
|
+
const indexPath = path.join(root, INDEX_FILE)
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(indexPath)) {
|
|
94
|
+
const initial = `# Vault de Conhecimento
|
|
95
|
+
|
|
96
|
+
> Ponto de entrada de conhecimento do projeto para agentes e pessoas.
|
|
97
|
+
|
|
98
|
+
## Índice
|
|
99
|
+
|
|
100
|
+
`
|
|
101
|
+
fs.writeFileSync(indexPath, initial, 'utf8')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const content = fs.readFileSync(indexPath, 'utf8')
|
|
105
|
+
const nameWithoutExt = filename.replace(/\.md$/, '')
|
|
106
|
+
|
|
107
|
+
// Verifica se já linkado (link markdown ou wikilink)
|
|
108
|
+
if (
|
|
109
|
+
content.includes(`(${filename})`) ||
|
|
110
|
+
content.includes(`[[${nameWithoutExt}]]`) ||
|
|
111
|
+
content.includes(`[[${filename}]]`)
|
|
112
|
+
) {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const link = `- [${nameWithoutExt}](${filename})\n`
|
|
117
|
+
fs.appendFileSync(indexPath, link, 'utf8')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Retorna todos os arquivos .md em vault/notes/ exceto index.md.
|
|
122
|
+
* Retorna [] se o diretório não existir.
|
|
123
|
+
* @param {string} [cwd]
|
|
124
|
+
* @returns {string[]}
|
|
125
|
+
*/
|
|
126
|
+
function noteFiles(cwd) {
|
|
127
|
+
const root = cwd || process.cwd()
|
|
128
|
+
const vaultDir = path.join(root, VAULT_DIR)
|
|
129
|
+
if (!fs.existsSync(vaultDir)) return []
|
|
130
|
+
return fs
|
|
131
|
+
.readdirSync(vaultDir)
|
|
132
|
+
.filter((f) => f.endsWith('.md') && f !== 'index.md')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Retorna true se o index.md referencia filename.
|
|
137
|
+
* @param {string} filename
|
|
138
|
+
* @param {string} [cwd]
|
|
139
|
+
* @returns {boolean}
|
|
140
|
+
*/
|
|
141
|
+
function indexContains(filename, cwd) {
|
|
142
|
+
const root = cwd || process.cwd()
|
|
143
|
+
const indexPath = path.join(root, INDEX_FILE)
|
|
144
|
+
if (!fs.existsSync(indexPath)) return false
|
|
145
|
+
const content = fs.readFileSync(indexPath, 'utf8')
|
|
146
|
+
const nameWithoutExt = filename.replace(/\.md$/, '')
|
|
147
|
+
return (
|
|
148
|
+
content.includes(`(${filename})`) ||
|
|
149
|
+
content.includes(`[[${nameWithoutExt}]]`) ||
|
|
150
|
+
content.includes(`[[${filename}]]`)
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = { newNote, appendNoteToIndex, noteFiles, indexContains, toSlug, today }
|