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
package/README.md
CHANGED
|
@@ -128,6 +128,10 @@ trackfw validate
|
|
|
128
128
|
|
|
129
129
|
# 6. See what is in flight
|
|
130
130
|
trackfw status
|
|
131
|
+
|
|
132
|
+
# 7. Governed commit + push + open PR/MR (feat/fix/refactor branches only)
|
|
133
|
+
git add -p # stage your changes explicitly
|
|
134
|
+
trackfw ship -m "feat(auth): add login flow"
|
|
131
135
|
```
|
|
132
136
|
|
|
133
137
|
---
|
|
@@ -146,6 +150,7 @@ trackfw status
|
|
|
146
150
|
| `trackfw roadmap move <name> <state>` | Move roadmap between states |
|
|
147
151
|
| `trackfw roadmap list` | List all roadmaps grouped by state |
|
|
148
152
|
| `trackfw validate` | Check governance consistency (use as CI gate) |
|
|
153
|
+
| `trackfw ship -m "msg"` | Governed `git commit + push + open PR/MR` — enforces branch pattern and governance gate; resolves forge (GitHub/GitLab/Bitbucket/Azure) automatically or via `--forge`; falls back to a browser URL when the forge CLI is absent |
|
|
149
154
|
| `trackfw context` | Print a structured summary of the project's governance state (REQs, Roadmaps, ADRs with counts and statuses) |
|
|
150
155
|
| `trackfw serve` | Start a local governance dashboard (no cloud, no accounts) |
|
|
151
156
|
| `trackfw status` | Show wip, blocked, REQs waiting on ADRs |
|
|
@@ -363,14 +368,25 @@ are never replaced or removed unless `--force` is explicit, and unmanaged files
|
|
|
363
368
|
are never removed. Known historical templates are adopted without overwriting;
|
|
364
369
|
unknown unmanaged content cannot be adopted by `update`, even with `--force`.
|
|
365
370
|
|
|
366
|
-
|
|
371
|
+
Without `--scope`, `install`/`update` default to `global` (`~/.claude/...`)
|
|
372
|
+
when stdin is not a TTY, and otherwise prompt interactively with `global`
|
|
373
|
+
pre-selected — the resolved destination paths are printed before anything is
|
|
374
|
+
written. `uninstall` is the one exception: without `--scope` and without a
|
|
375
|
+
TTY it fails instead of guessing, since silently defaulting a destructive
|
|
376
|
+
operation could delete artifacts from the user's home directory. `list` never
|
|
377
|
+
prompts and always assumes `global` unless `--scope` is given, so it reports
|
|
378
|
+
the same destinations `install` actually wrote to.
|
|
379
|
+
|
|
380
|
+
The **12 roles** installed for each tool: **architect · backend · frontend · qa · infra · security · code-quality · dba · ux · data · iac · tooling**
|
|
381
|
+
|
|
382
|
+
The **17 skills** cover governance process (governance, implement, plan, release, review) and technical specialties (backend-skill, code-quality-skill, data-skill, dba-skill, frontend-skill, iac-skill, infra-skill, qa-skill, security-skill, tooling-skill, ux-skill, vault-skill).
|
|
367
383
|
|
|
368
384
|
---
|
|
369
385
|
|
|
370
386
|
## Agent identity — give your agents a name
|
|
371
387
|
|
|
372
388
|
By default the agents are functional and impersonal: `trackfw-architect`,
|
|
373
|
-
`trackfw-backend`, … Agent identity lets you name all
|
|
389
|
+
`trackfw-backend`, … Agent identity lets you name all twelve, pick how they
|
|
374
390
|
address you, and call them by name.
|
|
375
391
|
|
|
376
392
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackfw",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.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, and Amazon Q.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
package/src/commands/discover.js
CHANGED
|
@@ -4,6 +4,7 @@ const { Command } = require('commander');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { execSync } = require('child_process');
|
|
7
|
+
const { resolve: resolveForge } = require('../forge/resolve');
|
|
7
8
|
|
|
8
9
|
function scan(rootDir) {
|
|
9
10
|
const r = {
|
|
@@ -20,6 +21,7 @@ function scan(rootDir) {
|
|
|
20
21
|
governanceScore: 0,
|
|
21
22
|
hookFramework: 'none',
|
|
22
23
|
ciSystem: 'none',
|
|
24
|
+
forge: '',
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
// trackfw.yaml
|
|
@@ -108,6 +110,18 @@ function scan(rootDir) {
|
|
|
108
110
|
r.ciSystem = 'none';
|
|
109
111
|
}
|
|
110
112
|
|
|
113
|
+
// Forge detection — reuse forge/resolve.js (no duplicate parse).
|
|
114
|
+
// gitRemoteURL returns '' on any error; CI detection is filesystem-based.
|
|
115
|
+
try {
|
|
116
|
+
const { resolveFromRepo } = require('../forge/resolve');
|
|
117
|
+
const res = resolveFromRepo('', '', rootDir);
|
|
118
|
+
if (res.source !== 'none') {
|
|
119
|
+
r.forge = res.forge;
|
|
120
|
+
}
|
|
121
|
+
} catch (_) {
|
|
122
|
+
// forge detection is best-effort; never block scan on it
|
|
123
|
+
}
|
|
124
|
+
|
|
111
125
|
r.governanceScore = calcScore(r);
|
|
112
126
|
return r;
|
|
113
127
|
}
|
|
@@ -146,6 +160,10 @@ function generateYAML(r) {
|
|
|
146
160
|
out += `hooks: ${r.hookFramework}\n`;
|
|
147
161
|
out += `ci: ${r.ciSystem}\n`;
|
|
148
162
|
|
|
163
|
+
if (r.forge) {
|
|
164
|
+
out += `forge: ${r.forge}\n`;
|
|
165
|
+
}
|
|
166
|
+
|
|
149
167
|
return out;
|
|
150
168
|
}
|
|
151
169
|
|
package/src/commands/index.js
CHANGED
|
@@ -29,6 +29,8 @@ function createProgram() {
|
|
|
29
29
|
program.addCommand(require('./version'))
|
|
30
30
|
program.addCommand(require('./agents'))
|
|
31
31
|
program.addCommand(require('./skills'))
|
|
32
|
+
program.addCommand(require('./note'))
|
|
33
|
+
program.addCommand(require('./ship'))
|
|
32
34
|
|
|
33
35
|
const { createServeCommand } = require('./serve')
|
|
34
36
|
program.addCommand(createServeCommand())
|
package/src/commands/init.js
CHANGED
|
@@ -4,11 +4,13 @@ const { t } = require('../i18n')
|
|
|
4
4
|
const identityStore = require('../identity')
|
|
5
5
|
const identityWizard = require('./identity-wizard')
|
|
6
6
|
const { resolveIdentityPreset, identityFileExists } = identityWizard
|
|
7
|
+
const { resolveScope } = require('./integrations')
|
|
7
8
|
|
|
8
9
|
const cmd = new Command('init')
|
|
9
10
|
cmd.description(t('init.description'))
|
|
10
11
|
cmd.option('--ai-tools <tools>', 'Comma-separated AI tools to configure (claude,codex,gemini,antigravity,cursor,copilot,windsurf,amazonq,kiro)', '')
|
|
11
12
|
cmd.option('--identity-preset <preset>', `Agent identity preset: none, neutral, ${identityStore.presetNames().join(', ')}`, 'none')
|
|
13
|
+
cmd.option('--forge <value>', 'Forge platform: github, gitlab, bitbucket, azure', '')
|
|
12
14
|
cmd.action(async (options, command) => {
|
|
13
15
|
const os = require('os')
|
|
14
16
|
const path = require('path')
|
|
@@ -28,6 +30,14 @@ cmd.action(async (options, command) => {
|
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
// --forge: validate unconditionally above the non-TTY early return so that
|
|
34
|
+
// an invalid value fails loudly in CI instead of silently no-op'ing.
|
|
35
|
+
const forgeChanged = command.getOptionValueSource('forge') === 'cli'
|
|
36
|
+
if (forgeChanged && options.forge) {
|
|
37
|
+
const { validateForge } = require('../forge/resolve')
|
|
38
|
+
validateForge(options.forge) // throws on invalid value
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
// Pula o wizard de identidade inteiramente quando a flag foi passada
|
|
32
42
|
// explicitamente (já tratado acima) ou quando um arquivo de identidade já
|
|
33
43
|
// existe — re-executar init nunca deve sobrescrever silenciosamente uma
|
|
@@ -44,13 +54,16 @@ cmd.action(async (options, command) => {
|
|
|
44
54
|
pkgManager: 'npm',
|
|
45
55
|
hooks: 'none',
|
|
46
56
|
ci: 'none',
|
|
57
|
+
forge: forgeChanged ? options.forge : '',
|
|
47
58
|
}
|
|
48
59
|
await generators.scaffold(cfg)
|
|
49
60
|
const aiTools = String(options.aiTools || '').split(',').map(tool => tool.trim()).filter(Boolean)
|
|
50
61
|
const supported = new Set(['claude', 'codex', 'gemini', 'antigravity', 'cursor', 'copilot', 'windsurf', 'amazonq', 'kiro'])
|
|
62
|
+
// Sem TTY, o escopo nunca é perguntado: default `global` (ADR D1/D4).
|
|
63
|
+
const scope = await resolveScope({}, { interactive: false })
|
|
51
64
|
for (const tool of aiTools) {
|
|
52
65
|
if (!supported.has(tool)) throw new Error(`Unsupported AI tool: ${tool}`)
|
|
53
|
-
await generators.installIntegrationTarget(tool, process.cwd())
|
|
66
|
+
await generators.installIntegrationTarget(tool, process.cwd(), scope)
|
|
54
67
|
}
|
|
55
68
|
console.log(`\n${t('init.success')}`)
|
|
56
69
|
require('../generators/init').printArchitectNextSteps(process.cwd())
|
|
@@ -59,7 +72,17 @@ cmd.action(async (options, command) => {
|
|
|
59
72
|
|
|
60
73
|
const { input, select, checkbox } = require('@inquirer/prompts')
|
|
61
74
|
|
|
62
|
-
|
|
75
|
+
// Detect forge from the current dir to prefill the wizard default.
|
|
76
|
+
const { resolveFromRepo: resolveForgeFromRepo } = require('../forge/resolve')
|
|
77
|
+
let detectedForge = ''
|
|
78
|
+
if (!forgeChanged) {
|
|
79
|
+
try {
|
|
80
|
+
const res = resolveForgeFromRepo('', '', process.cwd())
|
|
81
|
+
if (res.source !== 'none') detectedForge = res.forge
|
|
82
|
+
} catch (_) {}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let projectName, projectType, frontend, pkgManager, backend, backendFramework, hooks, ci, forgeValue, aiTools, requireReqInCommit, scope
|
|
63
86
|
|
|
64
87
|
try {
|
|
65
88
|
projectName = await input({
|
|
@@ -160,6 +183,22 @@ cmd.action(async (options, command) => {
|
|
|
160
183
|
],
|
|
161
184
|
})
|
|
162
185
|
|
|
186
|
+
if (!forgeChanged) {
|
|
187
|
+
forgeValue = await select({
|
|
188
|
+
message: t('init.prompt.forge'),
|
|
189
|
+
default: detectedForge,
|
|
190
|
+
choices: [
|
|
191
|
+
{ name: 'Auto-detect (omit key)', value: '' },
|
|
192
|
+
{ name: 'GitHub', value: 'github' },
|
|
193
|
+
{ name: 'GitLab', value: 'gitlab' },
|
|
194
|
+
{ name: 'Bitbucket', value: 'bitbucket' },
|
|
195
|
+
{ name: 'Azure DevOps', value: 'azure' },
|
|
196
|
+
],
|
|
197
|
+
})
|
|
198
|
+
} else {
|
|
199
|
+
forgeValue = options.forge
|
|
200
|
+
}
|
|
201
|
+
|
|
163
202
|
requireReqInCommit = false
|
|
164
203
|
if (hooks !== 'none') {
|
|
165
204
|
const { confirm: confirmPrompt } = require('@inquirer/prompts')
|
|
@@ -184,6 +223,11 @@ cmd.action(async (options, command) => {
|
|
|
184
223
|
],
|
|
185
224
|
})
|
|
186
225
|
|
|
226
|
+
// Escopo de instalação (ADR D4): só pergunta quando há de fato algo para
|
|
227
|
+
// instalar. `resolveScope` é a mesma função (mesmas strings/mecanismo de
|
|
228
|
+
// prompt) usada por `agents install` / `skills install`.
|
|
229
|
+
scope = aiTools.length ? await resolveScope({}, { interactive: true }) : 'global'
|
|
230
|
+
|
|
187
231
|
} catch (err) {
|
|
188
232
|
// Fallback quando stdin fecha inesperadamente (ex: pipe em TTY simulado)
|
|
189
233
|
const cfg = {
|
|
@@ -194,6 +238,7 @@ cmd.action(async (options, command) => {
|
|
|
194
238
|
pkgManager: 'npm',
|
|
195
239
|
hooks: 'none',
|
|
196
240
|
ci: 'none',
|
|
241
|
+
forge: forgeChanged ? options.forge : '',
|
|
197
242
|
}
|
|
198
243
|
await generators.scaffold(cfg)
|
|
199
244
|
console.log(`\n${t('init.success')}`)
|
|
@@ -221,6 +266,7 @@ cmd.action(async (options, command) => {
|
|
|
221
266
|
pkgManager: 'npm',
|
|
222
267
|
hooks: 'none',
|
|
223
268
|
ci: 'none',
|
|
269
|
+
forge: forgeChanged ? options.forge : '',
|
|
224
270
|
})
|
|
225
271
|
console.log(`\n${t('init.success')}`)
|
|
226
272
|
require('../generators/init').printArchitectNextSteps(process.cwd())
|
|
@@ -228,10 +274,10 @@ cmd.action(async (options, command) => {
|
|
|
228
274
|
}
|
|
229
275
|
}
|
|
230
276
|
|
|
231
|
-
const cfg = { projectName, projectType, frontend, backend, backendFramework, pkgManager, hooks, ci, requireReqInCommit }
|
|
277
|
+
const cfg = { projectName, projectType, frontend, backend, backendFramework, pkgManager, hooks, ci, forge: forgeValue || '', requireReqInCommit }
|
|
232
278
|
await generators.scaffold(cfg)
|
|
233
279
|
|
|
234
|
-
for (const tool of (aiTools || [])) await generators.installIntegrationTarget(tool, process.cwd())
|
|
280
|
+
for (const tool of (aiTools || [])) await generators.installIntegrationTarget(tool, process.cwd(), scope)
|
|
235
281
|
|
|
236
282
|
console.log(`\n${t('init.success')}`)
|
|
237
283
|
require('../generators/init').printArchitectNextSteps(process.cwd())
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const os = require('node:os')
|
|
4
4
|
const { Command } = require('commander')
|
|
5
|
-
const { catalog, execute, parseSurfaces } = require('../integrations')
|
|
5
|
+
const { catalog, execute, parseSurfaces, buildPlans } = require('../integrations')
|
|
6
6
|
const identityStore = require('../identity')
|
|
7
7
|
const identityWizard = require('./identity-wizard')
|
|
8
8
|
const { t } = require('../i18n')
|
|
@@ -27,6 +27,66 @@ async function promptSelection(kind, options, prompts = require('@inquirer/promp
|
|
|
27
27
|
options.items = await checkbox({ message: `${kind} to manage`, choices: catalog[kind].map(item => ({ name: item.name, value: item.id })), required: true })
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// resolveScope decide o escopo de instalação (ADR-2026-07-25-escopo-de-
|
|
31
|
+
// instalacao-selecionavel-para-agents-e-skills):
|
|
32
|
+
//
|
|
33
|
+
// - `--scope` explícito (`options.scope !== undefined`) é sempre respeitado
|
|
34
|
+
// e nunca dispara prompt — apenas validado contra os valores aceitos. A
|
|
35
|
+
// detecção é por *flag-set* (undefined), nunca por comparação de valor,
|
|
36
|
+
// para não confundir um `--scope project` explícito com o default.
|
|
37
|
+
// - Sem flag e `interactive` desligado (comando `list`, D6): default
|
|
38
|
+
// `global`, nunca lança erro (list nunca é destrutivo).
|
|
39
|
+
// - Sem flag e stdin não é um TTY (D1): default `global` — exceto para
|
|
40
|
+
// `uninstall` (ADR D8), que falha exigindo `--scope` explícito. D1 foi
|
|
41
|
+
// aprovado no enquadramento "onde instalar"; aplicá-lo a uninstall
|
|
42
|
+
// permitiria que um script de CI que hoje limpa `.claude/agents/` do
|
|
43
|
+
// repositório passasse a apagar arquivos do home do usuário.
|
|
44
|
+
// - Sem flag, `interactive` ligado e TTY (D2): pergunta o escopo, com
|
|
45
|
+
// `global` pré-selecionado, para toda operação incluindo uninstall — o
|
|
46
|
+
// usuário vê a escolha antes de destruir. Usa o mesmo mecanismo de prompt
|
|
47
|
+
// (`@inquirer/prompts`) já empregado por `identity-wizard.js`, sem
|
|
48
|
+
// dependência nova.
|
|
49
|
+
//
|
|
50
|
+
// `interactive` é passado pelo chamador como `false` para `list` — comando de
|
|
51
|
+
// leitura que nunca deve bloquear em prompt, mas que ainda assim precisa do
|
|
52
|
+
// mesmo default `global` para não reportar deployments divergentes dos que o
|
|
53
|
+
// `install` gravou (D6). `operation` identifica install/update/uninstall/list
|
|
54
|
+
// para o branch de D8. Espelha
|
|
55
|
+
// internal/commands/integrations_flags.go:resolveScope.
|
|
56
|
+
async function resolveScope(options, { interactive = true, operation, prompts = require('@inquirer/prompts') } = {}) {
|
|
57
|
+
if (options.scope !== undefined) {
|
|
58
|
+
if (options.scope !== 'project' && options.scope !== 'global') throw new Error(`Unsupported scope: ${options.scope}`)
|
|
59
|
+
return options.scope
|
|
60
|
+
}
|
|
61
|
+
if (!interactive) return 'global'
|
|
62
|
+
if (!process.stdin.isTTY) {
|
|
63
|
+
if (operation === 'uninstall') throw new Error('uninstall requires --scope in non-interactive mode')
|
|
64
|
+
return 'global'
|
|
65
|
+
}
|
|
66
|
+
const { select } = prompts
|
|
67
|
+
return select({
|
|
68
|
+
message: 'Onde instalar os artefatos?',
|
|
69
|
+
default: 'global',
|
|
70
|
+
choices: [
|
|
71
|
+
{ name: 'Pasta do usuário (~/.claude) — vale para todos os projetos', value: 'global' },
|
|
72
|
+
{ name: 'Este projeto (.claude) — apenas neste repositório', value: 'project' },
|
|
73
|
+
],
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// printResolvedDestinations imprime, antes da gravação, o escopo escolhido e
|
|
78
|
+
// os caminhos de destino que serão afetados (ADR D5) — nunca em modo --json,
|
|
79
|
+
// que já é o canal determinístico consumido por scripts. Recalcula os planos
|
|
80
|
+
// via buildPlans (sem side effect) só para enumerar destinos; a gravação em
|
|
81
|
+
// si acontece depois, em execute().
|
|
82
|
+
function printResolvedDestinations(kind, options) {
|
|
83
|
+
if (options.json) return
|
|
84
|
+
const plans = buildPlans(kind, options)
|
|
85
|
+
const destinations = [...new Set(plans.map(plan => plan.destination))].sort()
|
|
86
|
+
console.log(`Destino (${options.scope}):`)
|
|
87
|
+
for (const destination of destinations) console.log(` ${destination}`)
|
|
88
|
+
}
|
|
89
|
+
|
|
30
90
|
async function promptAmbiguousSurfaces(kind, options, prompts = require('@inquirer/prompts')) {
|
|
31
91
|
const { select } = prompts
|
|
32
92
|
const selected = parseSurfaces(options.surfaces)
|
|
@@ -47,7 +107,7 @@ function createLifecycleCommand(kind) {
|
|
|
47
107
|
const command = new Command(operation)
|
|
48
108
|
.option('--targets <targets>', 'Comma-separated target CLIs', csv)
|
|
49
109
|
.option('--items <items>', `Comma-separated ${kind} IDs`, csv)
|
|
50
|
-
.option('--scope <scope>', 'Installation scope: project or global
|
|
110
|
+
.option('--scope <scope>', 'Installation scope: project or global (default: global; asks interactively)')
|
|
51
111
|
.option('--surface <target=surface>', 'Surface selection (repeatable)', collect, [])
|
|
52
112
|
.option('--json', 'Print deterministic JSON')
|
|
53
113
|
.option('--force', 'Replace or remove modified artifacts')
|
|
@@ -65,7 +125,14 @@ function createLifecycleCommand(kind) {
|
|
|
65
125
|
|
|
66
126
|
command.action(async (options, cmd) => {
|
|
67
127
|
options.surfaces = options.surface || []
|
|
68
|
-
|
|
128
|
+
|
|
129
|
+
// Gate de escopo (ADR D1-D3, D6): resolvido incondicionalmente aqui,
|
|
130
|
+
// antes de qualquer seleção de targets/surfaces ou construção de
|
|
131
|
+
// planos, e independentemente de --targets já ter sido informado —
|
|
132
|
+
// caso contrário o caso mais comum (`agents install --targets claude`)
|
|
133
|
+
// nunca passaria por prompt algum. `list` (mutation === false) nunca
|
|
134
|
+
// pergunta (comando de leitura), apenas adota o default `global`.
|
|
135
|
+
options.scope = await resolveScope(options, { interactive: mutation, operation })
|
|
69
136
|
|
|
70
137
|
// O booleano de --identity nunca deve chegar a execute()/buildPlans()
|
|
71
138
|
// sob a chave "identity" — essa chave ali é reservada para uma Config
|
|
@@ -112,6 +179,10 @@ function createLifecycleCommand(kind) {
|
|
|
112
179
|
}
|
|
113
180
|
}
|
|
114
181
|
|
|
182
|
+
// D5: caminhos de destino impressos antes da gravação, apenas para
|
|
183
|
+
// operações de mutação (install/update/uninstall) e fora de --json.
|
|
184
|
+
if (mutation) printResolvedDestinations(kind, options)
|
|
185
|
+
|
|
115
186
|
const output = execute(kind, operation, options)
|
|
116
187
|
console.log(options.json ? JSON.stringify(output) : human(output))
|
|
117
188
|
})
|
|
@@ -120,4 +191,4 @@ function createLifecycleCommand(kind) {
|
|
|
120
191
|
return root
|
|
121
192
|
}
|
|
122
193
|
|
|
123
|
-
module.exports = { createLifecycleCommand, csv, human, promptSelection, promptAmbiguousSurfaces }
|
|
194
|
+
module.exports = { createLifecycleCommand, csv, human, promptSelection, promptAmbiguousSurfaces, resolveScope }
|
package/src/commands/log.js
CHANGED
|
@@ -3,13 +3,14 @@ const { Command } = require('commander')
|
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const path = require('path')
|
|
5
5
|
const { t } = require('../i18n')
|
|
6
|
+
const config = require('../config')
|
|
6
7
|
|
|
7
8
|
const cmd = new Command('log')
|
|
8
9
|
cmd.description(t('log.description'))
|
|
9
10
|
cmd.option('--tail <n>', t('log.tail'), '20')
|
|
10
11
|
cmd.action(async (opts) => {
|
|
11
12
|
const tail = parseInt(opts.tail, 10)
|
|
12
|
-
const logPath = path.join(
|
|
13
|
+
const logPath = path.join(config.load().roadmapDir, '.trackfw-log')
|
|
13
14
|
|
|
14
15
|
if (!fs.existsSync(logPath)) {
|
|
15
16
|
console.log(t('log.empty'))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander')
|
|
4
|
+
const { newNote } = require('../generators/note')
|
|
5
|
+
|
|
6
|
+
function createNoteCommand() {
|
|
7
|
+
const note = new Command('note').description('Manage vault knowledge notes')
|
|
8
|
+
|
|
9
|
+
note
|
|
10
|
+
.command('new <title>')
|
|
11
|
+
.description('Create a new knowledge note in vault/notes/')
|
|
12
|
+
.action((title) => {
|
|
13
|
+
try {
|
|
14
|
+
newNote(title)
|
|
15
|
+
} catch (err) {
|
|
16
|
+
console.error(`error: ${err.message}`)
|
|
17
|
+
process.exit(1)
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return note
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = createNoteCommand()
|
package/src/commands/req.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
const { Command } = require('commander')
|
|
3
|
-
const { listREQs } = require('../generators/req')
|
|
3
|
+
const { listREQs, moveREQ } = require('../generators/req')
|
|
4
4
|
const { t } = require('../i18n')
|
|
5
5
|
|
|
6
6
|
const cmd = new Command('req')
|
|
@@ -67,4 +67,10 @@ cmd.command('list')
|
|
|
67
67
|
listREQs(require('../config').load().reqDir)
|
|
68
68
|
})
|
|
69
69
|
|
|
70
|
+
cmd.command('move <name> <status>')
|
|
71
|
+
.description('Update a REQ status in place')
|
|
72
|
+
.action(async (name, status) => {
|
|
73
|
+
moveREQ(name, status)
|
|
74
|
+
})
|
|
75
|
+
|
|
70
76
|
module.exports = cmd
|
package/src/commands/roadmap.js
CHANGED
|
@@ -8,15 +8,16 @@ cmd.description(t('roadmap.description'))
|
|
|
8
8
|
|
|
9
9
|
cmd.command('new')
|
|
10
10
|
.description(t('roadmap.new.description'))
|
|
11
|
-
.
|
|
11
|
+
.argument('[title]', 'Roadmap title')
|
|
12
|
+
.option('-t, --title <title>', 'Roadmap title (alias)')
|
|
12
13
|
.option('-r, --req <path>', 'Path to the linked REQ')
|
|
13
14
|
.option('--from-req <path>', 'Generate roadmap with ML stubs from REQ acceptance criteria')
|
|
14
|
-
.action(async (opts) => {
|
|
15
|
+
.action(async (titleArg, opts) => {
|
|
15
16
|
if (opts.fromReq) {
|
|
16
17
|
newRoadmapFromReq(opts.fromReq)
|
|
17
18
|
return
|
|
18
19
|
}
|
|
19
|
-
const title = opts.title || 'New Roadmap'
|
|
20
|
+
const title = titleArg || opts.title || 'New Roadmap'
|
|
20
21
|
const reqPath = opts.req || ''
|
|
21
22
|
newRoadmap(title, reqPath)
|
|
22
23
|
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander')
|
|
4
|
+
const { runShip } = require('../ship/runner')
|
|
5
|
+
const config = require('../config')
|
|
6
|
+
|
|
7
|
+
function createShipCommand() {
|
|
8
|
+
const cmd = new Command('ship')
|
|
9
|
+
cmd
|
|
10
|
+
.description(
|
|
11
|
+
'trackfw ship runs a governed delivery sequence:\n\n' +
|
|
12
|
+
' 1. Validates branch name — must match feat|fix|refactor/<slug>\n' +
|
|
13
|
+
' 2. Validates governance — REQ + roadmap in wip/ must exist\n' +
|
|
14
|
+
' (hard gate: not affected by lenient mode or per-rule severity)\n' +
|
|
15
|
+
' 3. Detects pending squash-merges in other branches (advisory only)\n' +
|
|
16
|
+
' 4. Reviews what is staged (git status --short + git diff --cached --stat)\n' +
|
|
17
|
+
' 5. Commits with Conventional Commits format (-m is required)\n' +
|
|
18
|
+
' 6. Pushes to origin (adds -u if no upstream is configured yet)\n' +
|
|
19
|
+
' 7. Opens PR/MR via the resolved forge CLI (or prints URL if CLI is absent)\n\n' +
|
|
20
|
+
"Stage your files explicitly before running ship.\n" +
|
|
21
|
+
"This command never executes 'git add .' or 'git add -A'."
|
|
22
|
+
)
|
|
23
|
+
.option('-m, --message <msg>', 'Commit message (Conventional Commits format required)')
|
|
24
|
+
.option('--dry-run', 'Print what would be done without executing write commands', false)
|
|
25
|
+
.option('--no-pr', 'Skip PR/MR creation after push')
|
|
26
|
+
.option('--forge <forge>', 'Override forge detection (github, gitlab, bitbucket, azure)', '')
|
|
27
|
+
.action((options) => {
|
|
28
|
+
const cfg = config.load()
|
|
29
|
+
const exitCode = runShip(
|
|
30
|
+
{
|
|
31
|
+
message: options.message || '',
|
|
32
|
+
dryRun: options.dryRun || false,
|
|
33
|
+
noPR: options.pr === false,
|
|
34
|
+
forge: options.forge || '',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
configForge: cfg.forge || '',
|
|
38
|
+
repoDir: '.',
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
if (exitCode !== 0) {
|
|
42
|
+
process.exit(exitCode)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return cmd
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = createShipCommand()
|
package/src/config/index.js
CHANGED
|
@@ -26,9 +26,11 @@ function defaults() {
|
|
|
26
26
|
lenientUntil: '',
|
|
27
27
|
wipLimit: 1,
|
|
28
28
|
wipBySquad: false,
|
|
29
|
+
staleWipDays: 7,
|
|
29
30
|
requireReqInCommit: false,
|
|
30
31
|
strictCiPaths: false,
|
|
31
32
|
traceIdField: '',
|
|
33
|
+
forge: '',
|
|
32
34
|
// NOVOS campos:
|
|
33
35
|
linkFields: {
|
|
34
36
|
req: ['REQ:'],
|
|
@@ -42,7 +44,7 @@ function defaults() {
|
|
|
42
44
|
wip_limit: 'error',
|
|
43
45
|
stale_wip: 'warning',
|
|
44
46
|
adr_orphan: 'warning',
|
|
45
|
-
ref_targets_exist: '
|
|
47
|
+
ref_targets_exist: 'error',
|
|
46
48
|
folder_status: 'warning',
|
|
47
49
|
filename_uniqueness: 'error',
|
|
48
50
|
blocked_by_draft_adr: 'error',
|
|
@@ -191,9 +193,11 @@ function parse(content, cfg) {
|
|
|
191
193
|
case 'lenient_until': cfg.lenientUntil = val; break;
|
|
192
194
|
case 'wip_limit': { const n = parseInt(val, 10); if (n > 0) cfg.wipLimit = n; break; }
|
|
193
195
|
case 'wip_by_squad': cfg.wipBySquad = val === 'true'; break;
|
|
196
|
+
case 'stale_wip_days': { const n = parseInt(val, 10); if (n > 0) cfg.staleWipDays = n; break; }
|
|
194
197
|
case 'require_req_in_commit': cfg.requireReqInCommit = val === 'true'; break;
|
|
195
198
|
case 'strict_ci_paths': cfg.strictCiPaths = val === 'true'; break;
|
|
196
199
|
case 'trace_id_field': cfg.traceIdField = val.replace(/^["']|["']$/g, ''); break;
|
|
200
|
+
case 'forge': cfg.forge = val.replace(/^["']|["']$/g, ''); break;
|
|
197
201
|
case 'link_fields': inLinkFields = true; break;
|
|
198
202
|
case 'acceptance_markers': inAcceptanceMarkers = true; acceptanceMarkers = []; break;
|
|
199
203
|
case 'rules': inRules = true; rules = {}; break;
|
|
@@ -208,4 +212,3 @@ const NAMESPACING_FLAT = 'flat';
|
|
|
208
212
|
const NAMESPACING_BY_AGENT = 'by_agent';
|
|
209
213
|
|
|
210
214
|
module.exports = { load, reset, defaults, expandPath, NAMESPACING_FLAT, NAMESPACING_BY_AGENT };
|
|
211
|
-
|