trackfw 6.3.0 → 6.3.1
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 +25 -1
- package/package.json +1 -1
- package/src/commands/req.js +7 -2
- package/src/generators/req.js +160 -26
package/README.md
CHANGED
|
@@ -144,7 +144,8 @@ trackfw ship -m "feat(auth): add login flow"
|
|
|
144
144
|
| `trackfw adr new "title"` | Create a new Architecture Decision Record |
|
|
145
145
|
| `trackfw adr list` | List all ADRs with status |
|
|
146
146
|
| `trackfw req new "title"` | Create a REQ with guided ADR discovery |
|
|
147
|
-
| `trackfw req list` | List all REQs with status |
|
|
147
|
+
| `trackfw req list` | List all REQs with status, discovered across flat, per-state, and by_agent layouts |
|
|
148
|
+
| `trackfw req move <name> <status>` | Update a REQ's status; physically relocates the file when it already lives in a recognized state subfolder (see [Multi-agent namespacing](#multi-agent-namespacing)) |
|
|
148
149
|
| `trackfw roadmap new "title"` | Create a roadmap in `backlog/` |
|
|
149
150
|
| `trackfw roadmap show <name>` | Print a roadmap with its current state |
|
|
150
151
|
| `trackfw roadmap move <name> <state>` | Move roadmap between states |
|
|
@@ -210,6 +211,29 @@ agents: [claude, gemini, copilot]
|
|
|
210
211
|
|
|
211
212
|
Artifacts are organized by agent: `docs/roadmaps/claude/wip/`, `docs/req/gemini/done/`. `trackfw validate` and `trackfw context` are fully by_agent-aware — no false positives.
|
|
212
213
|
|
|
214
|
+
REQs reuse this same `roadmap_namespacing` setting — there is no separate `req_namespacing` key. When
|
|
215
|
+
`by_agent` is configured, REQs may live under `req_dir/<agent>/<state>/` just like roadmaps do.
|
|
216
|
+
|
|
217
|
+
`trackfw req list` and `trackfw req move` discover REQs across three layouts at once (each is a fixed,
|
|
218
|
+
non-recursive glob — a REQ nested deeper than these patterns is not found), with no extra flag required:
|
|
219
|
+
|
|
220
|
+
- **Flat (legacy):** `req_dir/*.md` — REQs loose directly in the REQ directory.
|
|
221
|
+
- **Per-state:** `req_dir/<state>/*.md` — organized by state, without an agent segment.
|
|
222
|
+
- **By agent:** `req_dir/<agent>/<state>/*.md` — used when `roadmap_namespacing: by_agent` is set.
|
|
223
|
+
|
|
224
|
+
`trackfw req move <name> <status>` behaves conditionally depending on where the REQ currently lives:
|
|
225
|
+
|
|
226
|
+
- If the REQ already sits inside a recognized state subfolder (per-state or by_agent layout above), the
|
|
227
|
+
move **physically relocates the file** to the target state's folder, mirroring `trackfw roadmap move` —
|
|
228
|
+
the folder is the source of truth for state. In this mode `<status>` must be one of the six governance
|
|
229
|
+
states (`backlog`, `analyzing`, `wip`, `blocked`, `done`, `abandoned`) — any other value is rejected
|
|
230
|
+
with `invalid state`.
|
|
231
|
+
- If the REQ is loose in `req_dir/` (flat legacy layout), the move rewrites the `status:` frontmatter
|
|
232
|
+
field **in place** and does not move or create any folder. `<status>` is written verbatim in this
|
|
233
|
+
mode — it accepts the free-form values existing REQs already use (`Open`, `Done`, ...), not just the
|
|
234
|
+
six governance state names. Existing flat REQs are never migrated automatically — you are not forced
|
|
235
|
+
to reorganize a project's existing REQs to adopt this behavior.
|
|
236
|
+
|
|
213
237
|
### Bidirectional traceability (`trace_id_field`)
|
|
214
238
|
|
|
215
239
|
```yaml
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackfw",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.1",
|
|
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/req.js
CHANGED
|
@@ -64,13 +64,18 @@ cmd.command('new <title>')
|
|
|
64
64
|
cmd.command('list')
|
|
65
65
|
.description(t('req.list.description'))
|
|
66
66
|
.action(async () => {
|
|
67
|
-
listREQs(require('../config').load()
|
|
67
|
+
listREQs(require('../config').load())
|
|
68
68
|
})
|
|
69
69
|
|
|
70
70
|
cmd.command('move <name> <status>')
|
|
71
71
|
.description('Update a REQ status in place')
|
|
72
72
|
.action(async (name, status) => {
|
|
73
|
-
|
|
73
|
+
try {
|
|
74
|
+
moveREQ(name, status)
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error(`Error: ${err.message}`)
|
|
77
|
+
process.exitCode = 1
|
|
78
|
+
}
|
|
74
79
|
})
|
|
75
80
|
|
|
76
81
|
module.exports = cmd
|
package/src/generators/req.js
CHANGED
|
@@ -2,27 +2,86 @@
|
|
|
2
2
|
const fs = require('fs')
|
|
3
3
|
const path = require('path')
|
|
4
4
|
const { localDateISO } = require('./date')
|
|
5
|
+
const roadmapGen = require('./roadmap')
|
|
6
|
+
const config = require('../config')
|
|
7
|
+
|
|
8
|
+
const VALID_STATES = roadmapGen.VALID_STATES
|
|
9
|
+
const STATE_ORDER = roadmapGen.STATE_ORDER
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
12
|
+
* listREQFiles — descoberta recursiva de REQs nos 3 layouts suportados, mesmo algoritmo usado
|
|
13
|
+
* em Go/Python (ADR-2026-08-04): flat (reqDir/*.md), por-estado (reqDir/<estado>/*.md) e,
|
|
14
|
+
* quando roadmapNamespacing === 'by_agent', by_agent (reqDir/<agente>/<estado>/*.md).
|
|
15
|
+
* Os três conjuntos não são mutuamente exclusivos — concatena todos.
|
|
16
|
+
* @param {object} cfg — config completo (ver npm/src/config)
|
|
17
|
+
* @returns {string[]} paths completos, na ordem flat → por-estado → by_agent
|
|
10
18
|
*/
|
|
11
|
-
function
|
|
12
|
-
|
|
19
|
+
function listREQFiles(cfg) {
|
|
20
|
+
const reqDir = cfg.reqDir
|
|
21
|
+
const files = []
|
|
22
|
+
|
|
23
|
+
// (a) flat legado — reqDir/*.md
|
|
13
24
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
for (const f of fs.readdirSync(reqDir)) {
|
|
26
|
+
if (!f.endsWith('.md')) continue
|
|
27
|
+
const full = path.join(reqDir, f)
|
|
28
|
+
try {
|
|
29
|
+
if (!fs.statSync(full).isDirectory()) files.push(full)
|
|
30
|
+
} catch (_) { /* ignora */ }
|
|
31
|
+
}
|
|
32
|
+
} catch (_) { /* reqDir não existe */ }
|
|
33
|
+
|
|
34
|
+
// (b) por-estado, sem agente — reqDir/<estado>/*.md
|
|
35
|
+
for (const state of STATE_ORDER) {
|
|
36
|
+
const dir = path.join(reqDir, state)
|
|
37
|
+
try {
|
|
38
|
+
for (const f of fs.readdirSync(dir)) {
|
|
39
|
+
if (f.endsWith('.md')) files.push(path.join(dir, f))
|
|
40
|
+
}
|
|
41
|
+
} catch (_) { /* estado não existe */ }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// (c) by_agent — reqDir/<agente>/<estado>/*.md
|
|
45
|
+
if (cfg.roadmapNamespacing === config.NAMESPACING_BY_AGENT) {
|
|
46
|
+
let agents = cfg.agents || []
|
|
47
|
+
if (agents.length === 0) {
|
|
48
|
+
try {
|
|
49
|
+
agents = fs.readdirSync(reqDir).filter(f => {
|
|
50
|
+
try { return fs.statSync(path.join(reqDir, f)).isDirectory() } catch (_) { return false }
|
|
51
|
+
})
|
|
52
|
+
} catch (_) { agents = [] }
|
|
53
|
+
}
|
|
54
|
+
for (const agent of agents) {
|
|
55
|
+
for (const state of STATE_ORDER) {
|
|
56
|
+
const dir = path.join(reqDir, agent, state)
|
|
57
|
+
try {
|
|
58
|
+
for (const f of fs.readdirSync(dir)) {
|
|
59
|
+
if (f.endsWith('.md')) files.push(path.join(dir, f))
|
|
60
|
+
}
|
|
61
|
+
} catch (_) { /* agente/estado não existe */ }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
17
64
|
}
|
|
18
65
|
|
|
66
|
+
return files
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* listREQs — lista REQs (recursivo nos 3 layouts), imprimindo filename e status (coluna 60 chars).
|
|
71
|
+
* Extrai status da linha `> Date: ... | Status: ...`.
|
|
72
|
+
* Se nenhum REQ encontrado: imprime "No REQs found in <reqDir>".
|
|
73
|
+
* @param {object} cfg — config completo (ver npm/src/config)
|
|
74
|
+
*/
|
|
75
|
+
function listREQs(cfg) {
|
|
76
|
+
const files = listREQFiles(cfg)
|
|
77
|
+
|
|
19
78
|
if (files.length === 0) {
|
|
20
|
-
console.log(`No REQs found in ${
|
|
79
|
+
console.log(`No REQs found in ${cfg.reqDir}`)
|
|
21
80
|
return
|
|
22
81
|
}
|
|
23
82
|
|
|
24
|
-
for (const
|
|
25
|
-
const
|
|
83
|
+
for (const filepath of files) {
|
|
84
|
+
const filename = path.basename(filepath)
|
|
26
85
|
const status = parseREQStatus(filepath)
|
|
27
86
|
console.log(`${filename.padEnd(60)} ${status}`)
|
|
28
87
|
}
|
|
@@ -105,30 +164,105 @@ function rewriteREQStatus(source, status) {
|
|
|
105
164
|
return { content: `---\n${lines.join('\n')}${rest}`, changed: true }
|
|
106
165
|
}
|
|
107
166
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
167
|
+
/**
|
|
168
|
+
* findREQ — busca recursiva nos 3 layouts (flat → por-estado → by_agent), retornando o primeiro
|
|
169
|
+
* path cujo basename contém `name` (case-insensitive).
|
|
170
|
+
* @param {string} name
|
|
171
|
+
* @param {object} cfg — config completo (ver npm/src/config)
|
|
172
|
+
* @returns {string} path completo
|
|
173
|
+
*/
|
|
174
|
+
function findREQ(name, cfg) {
|
|
175
|
+
const files = listREQFiles(cfg)
|
|
115
176
|
const lower = name.toLowerCase()
|
|
116
|
-
const found = files.find(f => f.toLowerCase().includes(lower))
|
|
117
|
-
if (!found) throw new Error(`REQ "${name}" not found in ${reqDir}`)
|
|
118
|
-
return
|
|
177
|
+
const found = files.find(f => path.basename(f).toLowerCase().includes(lower))
|
|
178
|
+
if (!found) throw new Error(`REQ "${name}" not found in ${cfg.reqDir}`)
|
|
179
|
+
return found
|
|
119
180
|
}
|
|
120
181
|
|
|
182
|
+
/**
|
|
183
|
+
* appendREQTransitionLog — append em <reqDir>/.trackfw-log, mesmo formato de
|
|
184
|
+
* appendTransitionLog (roadmap.js), em arquivo de log separado (escopo de REQ, não roadmap).
|
|
185
|
+
*/
|
|
186
|
+
function appendREQTransitionLog(cfg, basename, fromState, toState) {
|
|
187
|
+
const now = new Date()
|
|
188
|
+
const yyyy = now.getFullYear()
|
|
189
|
+
const mm = String(now.getMonth() + 1).padStart(2, '0')
|
|
190
|
+
const dd = String(now.getDate()).padStart(2, '0')
|
|
191
|
+
const hh = String(now.getHours()).padStart(2, '0')
|
|
192
|
+
const min = String(now.getMinutes()).padStart(2, '0')
|
|
193
|
+
const timestamp = `${yyyy}-${mm}-${dd} ${hh}:${min}`
|
|
194
|
+
const line = `${timestamp} ${basename.padEnd(50)} ${fromState} → ${toState}\n`
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const lp = path.join(cfg.reqDir, '.trackfw-log')
|
|
198
|
+
fs.mkdirSync(path.dirname(lp), { recursive: true })
|
|
199
|
+
fs.appendFileSync(lp, line, 'utf8')
|
|
200
|
+
} catch (_) { /* best-effort, mesmo padrão do roadmap */ }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* moveREQ — reescreve status: e, condicionalmente, move fisicamente o arquivo
|
|
205
|
+
* (ADR-2026-08-04, decisão D3):
|
|
206
|
+
* - REQ solta em `reqDir/` → modo in-place (comportamento legado: só reescreve status, sem mover).
|
|
207
|
+
* - REQ já organizada em `reqDir/<estado>/` ou `reqDir/<agente>/<estado>/` → move fisicamente
|
|
208
|
+
* para o novo estado, preservando o layout (por-estado ou by_agent), e loga a transição.
|
|
209
|
+
*/
|
|
121
210
|
function moveREQ(name, status) {
|
|
122
211
|
if (!String(status || '').trim()) throw new Error('status is required')
|
|
123
|
-
const
|
|
124
|
-
const filepath = findREQ(name,
|
|
212
|
+
const cfg = require('../config').load()
|
|
213
|
+
const filepath = findREQ(name, cfg)
|
|
125
214
|
const source = fs.readFileSync(filepath, 'utf8')
|
|
126
215
|
const result = rewriteREQStatus(source, status)
|
|
127
216
|
if (!result.changed) {
|
|
128
217
|
throw new Error(`REQ "${path.basename(filepath)}" has no frontmatter status/header Status to update`)
|
|
129
218
|
}
|
|
130
|
-
|
|
131
|
-
|
|
219
|
+
|
|
220
|
+
const basename = path.basename(filepath)
|
|
221
|
+
const parentDir = path.dirname(filepath)
|
|
222
|
+
const grandparentDir = path.dirname(parentDir)
|
|
223
|
+
const greatGrandparentDir = path.dirname(grandparentDir)
|
|
224
|
+
const reqDirAbs = path.resolve(cfg.reqDir)
|
|
225
|
+
const fromState = path.basename(parentDir)
|
|
226
|
+
|
|
227
|
+
if (path.resolve(parentDir) === reqDirAbs) {
|
|
228
|
+
// modo in-place — REQ solta em reqDir/, comportamento legado (sem mover)
|
|
229
|
+
fs.writeFileSync(filepath, result.content, 'utf8')
|
|
230
|
+
console.log(`✓ updated ${basename} status → ${status}`)
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!VALID_STATES.includes(status)) {
|
|
235
|
+
throw new Error(`invalid state "${status}" — valid states: ${VALID_STATES.join(', ')}`)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let targetDir = null
|
|
239
|
+
let logBasename = basename
|
|
240
|
+
|
|
241
|
+
if (path.resolve(grandparentDir) === reqDirAbs && VALID_STATES.includes(fromState)) {
|
|
242
|
+
// layout por-estado — reqDir/<estado>/
|
|
243
|
+
targetDir = path.join(cfg.reqDir, status)
|
|
244
|
+
} else if (VALID_STATES.includes(fromState) && path.resolve(greatGrandparentDir) === reqDirAbs) {
|
|
245
|
+
// layout by_agent — reqDir/<agente>/<estado>/
|
|
246
|
+
const agent = path.basename(grandparentDir)
|
|
247
|
+
targetDir = path.join(cfg.reqDir, agent, status)
|
|
248
|
+
logBasename = `${agent}/${basename}`
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!targetDir) {
|
|
252
|
+
// layout não reconhecido — fallback seguro para in-place (não inventa destino)
|
|
253
|
+
fs.writeFileSync(filepath, result.content, 'utf8')
|
|
254
|
+
console.log(`✓ updated ${basename} status → ${status}`)
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fs.mkdirSync(targetDir, { recursive: true })
|
|
259
|
+
const dst = path.join(targetDir, basename)
|
|
260
|
+
fs.writeFileSync(dst, result.content, 'utf8')
|
|
261
|
+
if (path.resolve(dst) !== path.resolve(filepath)) {
|
|
262
|
+
fs.unlinkSync(filepath)
|
|
263
|
+
}
|
|
264
|
+
appendREQTransitionLog(cfg, logBasename, fromState, status)
|
|
265
|
+
console.log(`✓ moved ${basename} → ${targetDir}`)
|
|
132
266
|
}
|
|
133
267
|
|
|
134
268
|
/**
|
|
@@ -328,4 +462,4 @@ function detectDomains(intention) {
|
|
|
328
462
|
)
|
|
329
463
|
}
|
|
330
464
|
|
|
331
|
-
module.exports = { listREQs, parseREQStatus, rewriteREQStatus, moveREQ, newREQ, PROBES_CATALOG, detectDomains, localDateISO, toSlug }
|
|
465
|
+
module.exports = { listREQs, listREQFiles, findREQ, parseREQStatus, rewriteREQStatus, moveREQ, newREQ, PROBES_CATALOG, detectDomains, localDateISO, toSlug }
|