trackfw 6.5.0 → 6.6.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/package.json +1 -1
- package/src/commands/adr.js +28 -4
- package/src/commands/req.js +16 -1
- package/src/commands/update-harness.js +14 -1
- package/src/commands/update.js +79 -0
- package/src/generators/adr.js +9 -5
- package/src/generators/codex.js +1 -25
- package/src/i18n/locales/en-US.json +4 -1
- package/src/i18n/locales/es-ES.json +4 -1
- package/src/i18n/locales/pt-BR.json +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackfw",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.6.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",
|
package/src/commands/adr.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const os = require('os')
|
|
3
5
|
const { Command } = require('commander')
|
|
4
6
|
const { input } = require('@inquirer/prompts')
|
|
5
7
|
const generators = require('../generators/adr')
|
|
@@ -8,9 +10,29 @@ const { t } = require('../i18n')
|
|
|
8
10
|
const cmd = new Command('adr')
|
|
9
11
|
cmd.description(t('adr.description'))
|
|
10
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Resolve o diretório de ADRs conforme --scope.
|
|
15
|
+
* 'project' (default) preserva o comportamento atual (adrDirs[0] do trackfw.yaml).
|
|
16
|
+
* 'global' escreve/lista em ~/.trackfw/adr, sem exigir trackfw.yaml no cwd.
|
|
17
|
+
* @param {string} scope
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
function resolveAdrDir(scope) {
|
|
21
|
+
if (scope === 'global') {
|
|
22
|
+
return path.join(os.homedir(), '.trackfw', 'adr')
|
|
23
|
+
}
|
|
24
|
+
if (scope === 'project') {
|
|
25
|
+
return require('../config').load().adrDirs[0]
|
|
26
|
+
}
|
|
27
|
+
console.error(`Invalid --scope value: "${scope}". Expected "project" or "global".`)
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
|
|
11
31
|
cmd.command('new <title>')
|
|
12
32
|
.description(t('adr.new.description'))
|
|
13
|
-
.
|
|
33
|
+
.option('--scope <scope>', 'ADR scope: project (default) or global (~/.trackfw/adr)', 'project')
|
|
34
|
+
.action(async (title, options) => {
|
|
35
|
+
const adrDir = resolveAdrDir(options.scope)
|
|
14
36
|
const content = { title }
|
|
15
37
|
// wizard interativo se TTY
|
|
16
38
|
if (process.stdin.isTTY) {
|
|
@@ -19,13 +41,15 @@ cmd.command('new <title>')
|
|
|
19
41
|
content.consequences = await input({ message: t('adr.new.prompt.consequences'), default: '' })
|
|
20
42
|
content.alternatives = await input({ message: t('adr.new.prompt.alternatives'), default: '' })
|
|
21
43
|
}
|
|
22
|
-
await generators.newADR(content)
|
|
44
|
+
await generators.newADR(content, adrDir)
|
|
23
45
|
})
|
|
24
46
|
|
|
25
47
|
cmd.command('list')
|
|
26
48
|
.description(t('adr.list.description'))
|
|
27
|
-
.
|
|
28
|
-
|
|
49
|
+
.option('--scope <scope>', 'ADR scope: project (default) or global (~/.trackfw/adr)', 'project')
|
|
50
|
+
.action(async (options) => {
|
|
51
|
+
const adrDir = resolveAdrDir(options.scope)
|
|
52
|
+
await generators.listADRs(adrDir)
|
|
29
53
|
})
|
|
30
54
|
|
|
31
55
|
module.exports = cmd
|
package/src/commands/req.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const os = require('os')
|
|
2
4
|
const { Command } = require('commander')
|
|
3
5
|
const { listREQs, moveREQ } = require('../generators/req')
|
|
4
6
|
const { t } = require('../i18n')
|
|
@@ -26,6 +28,19 @@ cmd.command('new <title>')
|
|
|
26
28
|
// Form 2 — critérios de aceite
|
|
27
29
|
content.criteria = await input({ message: t('req.new.prompt.criteria'), default: '- [ ]\n- [ ]' })
|
|
28
30
|
|
|
31
|
+
// Escopo dos ADR drafts desta sessão (uma única pergunta, vale para todos)
|
|
32
|
+
const adrScope = await select({
|
|
33
|
+
message: t('req.new.prompt.adrScope'),
|
|
34
|
+
choices: [
|
|
35
|
+
{ name: t('req.new.prompt.adrScopeLocal'), value: 'local' },
|
|
36
|
+
{ name: t('req.new.prompt.adrScopeGlobal'), value: 'global' },
|
|
37
|
+
],
|
|
38
|
+
default: 'local',
|
|
39
|
+
})
|
|
40
|
+
const adrDir = adrScope === 'global'
|
|
41
|
+
? path.join(os.homedir(), '.trackfw', 'adr')
|
|
42
|
+
: require('../config').load().adrDirs[0]
|
|
43
|
+
|
|
29
44
|
// Perguntas dinâmicas por probe
|
|
30
45
|
const generatedADRs = []
|
|
31
46
|
for (const probe of probes) {
|
|
@@ -40,7 +55,7 @@ cmd.command('new <title>')
|
|
|
40
55
|
})
|
|
41
56
|
if (answer) {
|
|
42
57
|
try {
|
|
43
|
-
const basename = await adrGenerators.newADRDraft(answer)
|
|
58
|
+
const basename = await adrGenerators.newADRDraft(answer, adrDir)
|
|
44
59
|
if (basename) generatedADRs.push(basename)
|
|
45
60
|
} catch (e) {
|
|
46
61
|
console.warn(t('req.new.adrWarning', { slug: answer, message: e.message }))
|
|
@@ -6,7 +6,7 @@ const path = require('path')
|
|
|
6
6
|
const identityStore = require('../identity')
|
|
7
7
|
const { catalog, buildPlans, IntegrationManager, globalGroupPath } = require('../integrations')
|
|
8
8
|
const { tildeify, validateTargets, buildDocument, humanReport, silenceConsole } = require('../lib/update-engine')
|
|
9
|
-
const { mergeClaudeHookArray, mergeSimpleCommandArray, mergeCopilotHookArray } = require('../generators/hooks')
|
|
9
|
+
const { mergeClaudeHookArray, mergeSimpleCommandArray, mergeCopilotHookArray, generateGlobalCredentialGuardScript } = require('../generators/hooks')
|
|
10
10
|
|
|
11
11
|
// `trackfw update harness` is the global counterpart to `trackfw update` —
|
|
12
12
|
// see docs/cli-parity.md, "`trackfw update` vs `trackfw update harness`".
|
|
@@ -581,6 +581,19 @@ function run(options) {
|
|
|
581
581
|
const dryRun = Boolean(options.dryRun)
|
|
582
582
|
const installMissing = Boolean(options.installMissing)
|
|
583
583
|
|
|
584
|
+
// The per-CLI *-credential-guard targets below only wire hook entries that
|
|
585
|
+
// point at ~/.trackfw/scripts/trackfw-credential-guard.sh — none of them
|
|
586
|
+
// write the script itself (ADR-2026-08-06, decision #2/#3). Without this
|
|
587
|
+
// call the wiring is installed but every hook invocation fails with
|
|
588
|
+
// "No such file or directory" because the script never exists.
|
|
589
|
+
if (!dryRun) {
|
|
590
|
+
if (options.json) {
|
|
591
|
+
silenceConsole(() => generateGlobalCredentialGuardScript(homeRoot))
|
|
592
|
+
} else {
|
|
593
|
+
generateGlobalCredentialGuardScript(homeRoot)
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
584
597
|
// Identidade resolvida do disco antes de buildPlans — pular esta etapa
|
|
585
598
|
// reverteria silenciosamente os nomes customizados para os defaults
|
|
586
599
|
// neutros (mesma justificativa de npm/src/integrations/index.js:execute).
|
package/src/commands/update.js
CHANGED
|
@@ -29,6 +29,83 @@ function loadUpdateConfig(rootDir) {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// ensureGlobalAdrDirRegistered — mirrors internal/generators/update.go's
|
|
33
|
+
// ensureGlobalADRDirRegistered (Go implementation, same REQ, ML-1A). Registers
|
|
34
|
+
// ~/.trackfw/adr in the project's trackfw.yaml `adr_dirs` list, but ONLY when
|
|
35
|
+
// that directory exists AND contains at least one `ADR-*.md` file — an empty
|
|
36
|
+
// or absent global ADR dir is a no-op, never written. The edit is surgical
|
|
37
|
+
// (line-based text splice, not a YAML parse+reserialize) to preserve 100% of
|
|
38
|
+
// the user's existing trackfw.yaml content (comments, key order, formatting).
|
|
39
|
+
function ensureGlobalAdrDirRegistered(cwd) {
|
|
40
|
+
const home = os.homedir();
|
|
41
|
+
const globalDir = path.join(home, '.trackfw', 'adr');
|
|
42
|
+
if (!fs.existsSync(globalDir)) return;
|
|
43
|
+
|
|
44
|
+
let hasAdr = false;
|
|
45
|
+
try {
|
|
46
|
+
hasAdr = fs.readdirSync(globalDir).some((f) => /^ADR-.*\.md$/.test(f));
|
|
47
|
+
} catch (_) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!hasAdr) return;
|
|
51
|
+
|
|
52
|
+
const yamlPath = path.join(cwd, 'trackfw.yaml');
|
|
53
|
+
let content;
|
|
54
|
+
try {
|
|
55
|
+
content = fs.readFileSync(yamlPath, 'utf8');
|
|
56
|
+
} catch (_) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const resolvesToGlobal = (entry) => {
|
|
61
|
+
const trimmed = entry.trim();
|
|
62
|
+
if (trimmed === '~/.trackfw/adr') return true;
|
|
63
|
+
const expanded = trimmed.startsWith('~') ? path.join(home, trimmed.slice(1)) : trimmed;
|
|
64
|
+
return path.resolve(expanded) === path.resolve(globalDir);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const lines = content.split('\n');
|
|
68
|
+
let adrDirsIndex = -1;
|
|
69
|
+
for (let i = 0; i < lines.length; i++) {
|
|
70
|
+
if (/^adr_dirs:\s*$/.test(lines[i])) {
|
|
71
|
+
adrDirsIndex = i;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (adrDirsIndex === -1) {
|
|
77
|
+
// No adr_dirs key at all — the implicit default is `docs/adr`; append a
|
|
78
|
+
// new block preserving that default explicitly, plus the global entry.
|
|
79
|
+
let newContent = content;
|
|
80
|
+
if (newContent.length && !newContent.endsWith('\n')) newContent += '\n';
|
|
81
|
+
newContent += 'adr_dirs:\n - docs/adr\n - ~/.trackfw/adr\n';
|
|
82
|
+
fs.writeFileSync(yamlPath, newContent, 'utf8');
|
|
83
|
+
console.log(' ✓ adr_dirs: ~/.trackfw/adr registrado');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const itemRe = /^(\s*)-\s*(.+?)\s*$/;
|
|
88
|
+
let end = adrDirsIndex + 1;
|
|
89
|
+
let lastItemIndex = -1;
|
|
90
|
+
let indent = ' ';
|
|
91
|
+
while (end < lines.length) {
|
|
92
|
+
const m = lines[end].match(itemRe);
|
|
93
|
+
if (!m) break;
|
|
94
|
+
indent = m[1] || indent;
|
|
95
|
+
if (resolvesToGlobal(m[2])) {
|
|
96
|
+
// Already registered under this or an equivalent path form — no-op.
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
lastItemIndex = end;
|
|
100
|
+
end++;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const insertAt = lastItemIndex === -1 ? adrDirsIndex + 1 : lastItemIndex + 1;
|
|
104
|
+
lines.splice(insertAt, 0, `${indent}- ~/.trackfw/adr`);
|
|
105
|
+
fs.writeFileSync(yamlPath, lines.join('\n'), 'utf8');
|
|
106
|
+
console.log(' ✓ adr_dirs: ~/.trackfw/adr registrado');
|
|
107
|
+
}
|
|
108
|
+
|
|
32
109
|
function updateHooksSurgical(cfg, rootDir) {
|
|
33
110
|
const hooks = cfg.hooks || '';
|
|
34
111
|
if (hooks === 'husky') {
|
|
@@ -243,6 +320,8 @@ cmd.action((mode, options) => {
|
|
|
243
320
|
process.exit(1);
|
|
244
321
|
}
|
|
245
322
|
|
|
323
|
+
ensureGlobalAdrDirRegistered(cwd);
|
|
324
|
+
|
|
246
325
|
let wanted;
|
|
247
326
|
try {
|
|
248
327
|
const requested = options.targets ? String(options.targets).split(',').map((s) => s.trim()).filter(Boolean) : [];
|
package/src/generators/adr.js
CHANGED
|
@@ -43,13 +43,13 @@ function today() {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* Cria um novo ADR em
|
|
46
|
+
* Cria um novo ADR em <adrDir>/ADR-YYYY-MM-DD-<slug>.md.
|
|
47
47
|
* Campos vazios recebem placeholder HTML.
|
|
48
48
|
* @param {{ title: string, context?: string, decision?: string, consequences?: string, alternatives?: string }} content
|
|
49
|
+
* @param {string} adrDir Diretório de destino (resolvido pelo chamador conforme --scope)
|
|
49
50
|
* @returns {Promise<void>}
|
|
50
51
|
*/
|
|
51
|
-
async function newADR(content) {
|
|
52
|
-
const adrDir = require('../config').load().adrDirs[0]
|
|
52
|
+
async function newADR(content, adrDir) {
|
|
53
53
|
fs.mkdirSync(adrDir, { recursive: true })
|
|
54
54
|
|
|
55
55
|
const slug = toSlug(content.title)
|
|
@@ -142,10 +142,14 @@ function parseADRStatus(filepath) {
|
|
|
142
142
|
* Cria um ADR com Status: Draft a partir de um slug.
|
|
143
143
|
* Idempotente: se já existe ADR-*-<slug>.md, pula e imprime mensagem.
|
|
144
144
|
* @param {string} slug
|
|
145
|
+
* @param {string} [adrDir] Diretório de destino (resolvido pelo chamador conforme escopo).
|
|
146
|
+
* Se omitido, preserva o comportamento anterior (adrDirs[0] do trackfw.yaml).
|
|
145
147
|
* @returns {Promise<string>} basename do arquivo criado
|
|
146
148
|
*/
|
|
147
|
-
async function newADRDraft(slug) {
|
|
148
|
-
|
|
149
|
+
async function newADRDraft(slug, adrDir) {
|
|
150
|
+
if (!adrDir) {
|
|
151
|
+
adrDir = require('../config').load().adrDirs[0]
|
|
152
|
+
}
|
|
149
153
|
fs.mkdirSync(adrDir, { recursive: true })
|
|
150
154
|
|
|
151
155
|
// Verificar idempotência: buscar arquivo existente com o mesmo slug
|
package/src/generators/codex.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const fs = require('fs')
|
|
4
|
-
const path = require('path')
|
|
5
|
-
const { injectCodexHooks } = require('./hooks')
|
|
6
|
-
const { injectRulesForTool } = require('./init')
|
|
7
|
-
|
|
8
3
|
const skills = {
|
|
9
4
|
'trackfw-governance': `---
|
|
10
5
|
name: trackfw-governance
|
|
@@ -81,23 +76,4 @@ Review the branch as an owner. Return findings first, ordered by severity, with
|
|
|
81
76
|
`,
|
|
82
77
|
}
|
|
83
78
|
|
|
84
|
-
|
|
85
|
-
const root = cwd || process.cwd()
|
|
86
|
-
const { execute } = require('../integrations')
|
|
87
|
-
execute('agents', 'install', { targets: ['codex'], scope: 'project' }, { projectRoot: root })
|
|
88
|
-
execute('skills', 'install', { targets: ['codex'], scope: 'project' }, { projectRoot: root })
|
|
89
|
-
injectRulesForTool('codex', root)
|
|
90
|
-
|
|
91
|
-
const configPath = path.join(root, '.codex', 'config.toml')
|
|
92
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true })
|
|
93
|
-
let config = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : ''
|
|
94
|
-
if (!/^\[agents\]\s*$/m.test(config)) {
|
|
95
|
-
config = config.replace(/\s*$/, '') + '\n\n[agents]\nmax_threads = 6\nmax_depth = 1\n'
|
|
96
|
-
fs.writeFileSync(configPath, config, 'utf8')
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
injectCodexHooks(root)
|
|
100
|
-
console.log(' ✓ Codex: AGENTS.md, skills, custom agents and hooks')
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
module.exports = { installCodex, legacyCodexFixtures: { skills, agents } }
|
|
79
|
+
module.exports = { legacyCodexFixtures: { skills, agents } }
|
|
@@ -63,7 +63,10 @@
|
|
|
63
63
|
"domainQuestion_persistence": "Which database engine will be used?",
|
|
64
64
|
"domainQuestion_api": "Which API protocol will be used?",
|
|
65
65
|
"domainQuestion_deploy": "What is the deployment target?",
|
|
66
|
-
"domainQuestion_events": "Which event broker will be used?"
|
|
66
|
+
"domainQuestion_events": "Which event broker will be used?",
|
|
67
|
+
"adrScope": "Scope of ADRs for this REQ",
|
|
68
|
+
"adrScopeLocal": "Local (default)",
|
|
69
|
+
"adrScopeGlobal": "Global (~/.trackfw/adr, cross-project)"
|
|
67
70
|
},
|
|
68
71
|
"detectedDomains": "Detected domains: {{domains}}",
|
|
69
72
|
"created": "✓ REQ created: {{path}}",
|
|
@@ -63,7 +63,10 @@
|
|
|
63
63
|
"domainQuestion_persistence": "¿Qué motor de base de datos se usará?",
|
|
64
64
|
"domainQuestion_api": "¿Qué protocolo de API se usará?",
|
|
65
65
|
"domainQuestion_deploy": "¿Cuál es el destino de despliegue?",
|
|
66
|
-
"domainQuestion_events": "¿Qué message broker se usará?"
|
|
66
|
+
"domainQuestion_events": "¿Qué message broker se usará?",
|
|
67
|
+
"adrScope": "Alcance de los ADRs de este REQ",
|
|
68
|
+
"adrScopeLocal": "Local (predeterminado)",
|
|
69
|
+
"adrScopeGlobal": "Global (~/.trackfw/adr, entre proyectos)"
|
|
67
70
|
},
|
|
68
71
|
"detectedDomains": "Dominios detectados: {{domains}}",
|
|
69
72
|
"created": "✓ REQ creado: {{path}}",
|
|
@@ -63,7 +63,10 @@
|
|
|
63
63
|
"domainQuestion_persistence": "Qual engine de banco de dados será usada?",
|
|
64
64
|
"domainQuestion_api": "Qual protocolo de API será usado?",
|
|
65
65
|
"domainQuestion_deploy": "Qual é o destino de deploy?",
|
|
66
|
-
"domainQuestion_events": "Qual message broker será usado?"
|
|
66
|
+
"domainQuestion_events": "Qual message broker será usado?",
|
|
67
|
+
"adrScope": "Escopo dos ADRs desta REQ",
|
|
68
|
+
"adrScopeLocal": "Local (padrão)",
|
|
69
|
+
"adrScopeGlobal": "Global (~/.trackfw/adr, entre projetos)"
|
|
67
70
|
},
|
|
68
71
|
"detectedDomains": "Domínios detectados: {{domains}}",
|
|
69
72
|
"created": "✓ REQ criado: {{path}}",
|