trackfw 6.3.0 → 6.4.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 +25 -1
- package/package.json +2 -2
- package/src/branch/runner.js +140 -0
- package/src/commands/branch.js +46 -0
- package/src/commands/discover.js +6 -0
- package/src/commands/index.js +1 -0
- package/src/commands/init.js +3 -2
- package/src/commands/req.js +7 -2
- package/src/generators/hooks.js +3 -2
- package/src/generators/req.js +160 -26
- package/src/integrations/assets/agents/architect.md +3 -0
- package/src/integrations/assets/catalog.json +1 -0
- package/src/integrations/render.js +32 -0
- package/src/validator/index.js +48 -13
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackfw",
|
|
3
|
-
"version": "6.
|
|
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,
|
|
3
|
+
"version": "6.4.0",
|
|
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",
|
|
7
7
|
"adr",
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* branch/runner.js — Core implementation of `trackfw branch new`.
|
|
5
|
+
*
|
|
6
|
+
* Moves the branch_has_wip_roadmap governance gate (already enforced by `trackfw validate` and
|
|
7
|
+
* `trackfw ship`) to before branch creation, instead of after. Mirrors internal/commands/branch.go
|
|
8
|
+
* byte-for-byte in behavior and message text — Go is the behavioral reference
|
|
9
|
+
* (docs/cli-parity.md: "Go is the behavioral reference").
|
|
10
|
+
*
|
|
11
|
+
* All git write operations are injectable for testability — never runs a real `git checkout -b`
|
|
12
|
+
* in unit tests.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { spawnSync } = require('child_process')
|
|
16
|
+
const config = require('../config')
|
|
17
|
+
const validator = require('../validator')
|
|
18
|
+
|
|
19
|
+
// branchValidTypes mirrors the vocabulary already enforced by `trackfw ship` (feat|fix|refactor).
|
|
20
|
+
const branchValidTypes = new Set(['feat', 'fix', 'refactor'])
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* parseBranchSpec splits "<type>/<slug>" and validates both parts. type must be one of
|
|
24
|
+
* feat, fix, refactor (branchValidTypes); slug must be non-empty.
|
|
25
|
+
* @param {string} spec
|
|
26
|
+
* @returns {{ branchType: string, slug: string }}
|
|
27
|
+
* @throws {Error} when spec is malformed
|
|
28
|
+
*/
|
|
29
|
+
function parseBranchSpec(spec) {
|
|
30
|
+
const value = String(spec)
|
|
31
|
+
const idx = value.indexOf('/')
|
|
32
|
+
if (idx === -1 || value.slice(0, idx) === '') {
|
|
33
|
+
throw new Error(`invalid branch spec "${spec}" — expected <type>/<slug> with type in feat, fix, refactor`)
|
|
34
|
+
}
|
|
35
|
+
const branchType = value.slice(0, idx)
|
|
36
|
+
const slug = value.slice(idx + 1)
|
|
37
|
+
if (!branchValidTypes.has(branchType)) {
|
|
38
|
+
throw new Error(`invalid branch type "${branchType}" — must be one of feat, fix, refactor`)
|
|
39
|
+
}
|
|
40
|
+
if (slug.trim() === '') {
|
|
41
|
+
throw new Error(`branch slug is required — expected <type>/<slug>, got "${spec}"`)
|
|
42
|
+
}
|
|
43
|
+
return { branchType, slug }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* defaultGitCheckout runs `git checkout -b <branchName>` with inherited stdio, so Git's own output
|
|
48
|
+
* (including branch-already-exists errors) reaches the user unmodified.
|
|
49
|
+
*
|
|
50
|
+
* Returns Git's own numeric exit code (0 on success), never a generic 1 — this command's contract
|
|
51
|
+
* is to propagate Git's exit status literally (see internal/commands/branch.go's
|
|
52
|
+
* defaultGitCheckout, the behavioral reference: it exits the process directly with
|
|
53
|
+
* exitErr.ExitCode() rather than a hardcoded 1). A spawn failure (e.g. the git binary is missing)
|
|
54
|
+
* or a signal-terminated process has no Git-produced numeric code to propagate, so both fall back
|
|
55
|
+
* to 1 — the same convention already used for signal-killed gate commands
|
|
56
|
+
* (docs/cli-parity.md, `trackfw barrier`, "Gate process terminated by a signal ... Recorded as exit 1").
|
|
57
|
+
* @param {string} branchName
|
|
58
|
+
* @returns {number} exit code
|
|
59
|
+
*/
|
|
60
|
+
function defaultGitCheckout(branchName) {
|
|
61
|
+
const result = spawnSync('git', ['checkout', '-b', branchName], { stdio: 'inherit' })
|
|
62
|
+
if (result.error) return 1
|
|
63
|
+
if (result.status === null) return 1
|
|
64
|
+
return result.status
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* runBranchNew implements the `trackfw branch new <type>/<slug>` flow described in
|
|
69
|
+
* docs/req/REQ-2026-08-04-comando-trackfw-branch-new-para-bloquear-criacao-de-branch-sem-req-roadmap-em-wip.md.
|
|
70
|
+
*
|
|
71
|
+
* Mirrors Go's stdout/stderr split exactly (internal/commands/branch.go + root.go Execute()):
|
|
72
|
+
* the primary governance/dry-run message goes to stdout via `writeln`; usage errors and the
|
|
73
|
+
* "blocked: ..." wrapper (Go's `Execute()` printing the returned error) go to stderr via
|
|
74
|
+
* `writeErr`. Git's own output is inherited directly by the child process and is never touched
|
|
75
|
+
* here.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} spec "<type>/<slug>"
|
|
78
|
+
* @param {boolean} dryRun
|
|
79
|
+
* @param {{ loadConfig?: function, resolveWIPDirs?: function, resolveDoneDirs?: function,
|
|
80
|
+
* matchSlug?: function(string, string[], string[]): {matched: boolean, candidates: string[]},
|
|
81
|
+
* execGitCheckout?: function(string): number, writeln?: function(string): void,
|
|
82
|
+
* writeErr?: function(string): void }} deps
|
|
83
|
+
* @returns {number} exit code (0 = success, 1 = blocked/usage error, or Git's own exit code when
|
|
84
|
+
* `git checkout -b` fails — propagated literally, never hardcoded)
|
|
85
|
+
*/
|
|
86
|
+
function runBranchNew(spec, dryRun, deps = {}) {
|
|
87
|
+
const loadConfig = deps.loadConfig || config.load
|
|
88
|
+
const resolveWIPDirs = deps.resolveWIPDirs || validator.resolveWIPDirs
|
|
89
|
+
const resolveDoneDirs = deps.resolveDoneDirs || validator.resolveDoneDirs
|
|
90
|
+
const matchSlug = deps.matchSlug || validator.branchSlugMatchesRoadmap
|
|
91
|
+
const execGitCheckout = deps.execGitCheckout || defaultGitCheckout
|
|
92
|
+
const writeln = deps.writeln || ((s) => process.stdout.write(s + '\n'))
|
|
93
|
+
const writeErr = deps.writeErr || ((s) => process.stderr.write(s + '\n'))
|
|
94
|
+
|
|
95
|
+
let branchType, slug
|
|
96
|
+
try {
|
|
97
|
+
({ branchType, slug } = parseBranchSpec(spec))
|
|
98
|
+
} catch (err) {
|
|
99
|
+
writeErr(err.message)
|
|
100
|
+
return 1
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const branchName = `${branchType}/${slug}`
|
|
104
|
+
|
|
105
|
+
const cfg = loadConfig()
|
|
106
|
+
const wipDirs = resolveWIPDirs(cfg)
|
|
107
|
+
const doneDirs = resolveDoneDirs(cfg)
|
|
108
|
+
|
|
109
|
+
const normalizedSlug = validator.normalizeBranchSlug(slug)
|
|
110
|
+
const { matched, candidates } = matchSlug(normalizedSlug, wipDirs, doneDirs)
|
|
111
|
+
|
|
112
|
+
if (!matched) {
|
|
113
|
+
const msg = (!candidates || candidates.length === 0)
|
|
114
|
+
? validator.branchGovernanceOrientation(branchName)
|
|
115
|
+
: validator.branchNoMatchingRoadmapMessage(branchName, candidates)
|
|
116
|
+
if (dryRun) {
|
|
117
|
+
writeln(`[dry-run] would block: ${msg}`)
|
|
118
|
+
} else {
|
|
119
|
+
writeln(msg)
|
|
120
|
+
}
|
|
121
|
+
writeErr(`blocked: no matching roadmap in wip/ nor done/ for "${branchName}"`)
|
|
122
|
+
return 1
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (dryRun) {
|
|
126
|
+
writeln(`[dry-run] would create branch "${branchName}" (git checkout -b ${branchName})`)
|
|
127
|
+
return 0
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Git's own stderr (inherited stdio) already reached the user unmodified — do not reformat,
|
|
131
|
+
// duplicate, or replace this exit code with a generic 1.
|
|
132
|
+
return execGitCheckout(branchName)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
branchValidTypes,
|
|
137
|
+
parseBranchSpec,
|
|
138
|
+
defaultGitCheckout,
|
|
139
|
+
runBranchNew,
|
|
140
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// trackfw branch new <type>/<slug>
|
|
4
|
+
//
|
|
5
|
+
// CLI wiring for the `trackfw branch` command group. The testable logic lives in
|
|
6
|
+
// ../branch/runner.js (mirrors the ship.js / ship/runner.js split already used in this CLI).
|
|
7
|
+
|
|
8
|
+
const { Command } = require('commander')
|
|
9
|
+
const { runBranchNew } = require('../branch/runner')
|
|
10
|
+
|
|
11
|
+
function createBranchCommand() {
|
|
12
|
+
const cmd = new Command('branch')
|
|
13
|
+
cmd.description('Manage governed feature branches')
|
|
14
|
+
|
|
15
|
+
const newCmd = new Command('new')
|
|
16
|
+
newCmd
|
|
17
|
+
.description(
|
|
18
|
+
'Create a feat/fix/refactor branch, gated on a matching REQ + roadmap already in wip/ or done/\n\n' +
|
|
19
|
+
'trackfw branch new moves the branch_has_wip_roadmap governance gate (already enforced\n' +
|
|
20
|
+
"by 'trackfw validate' and 'trackfw ship') to before branch creation, instead of after:\n\n" +
|
|
21
|
+
' 1. Validates <type> is one of feat, fix, refactor and <slug> is non-empty.\n' +
|
|
22
|
+
' 2. Checks whether a roadmap in wip/ or done/ matches the given slug — the exact matching\n' +
|
|
23
|
+
" logic 'trackfw validate' already uses (normalized slug, filename contains match).\n" +
|
|
24
|
+
" 3. Without a match: blocks — 'git checkout -b' is never executed — and prints the same\n" +
|
|
25
|
+
" governance orientation message 'trackfw validate' already prints for this rule.\n" +
|
|
26
|
+
" 4. With a match: runs 'git checkout -b <type>/<slug>', propagating Git's own output and\n" +
|
|
27
|
+
' exit status literally.\n\n' +
|
|
28
|
+
'Create the governance artifacts first if this blocks you:\n' +
|
|
29
|
+
' trackfw req new "title"\n' +
|
|
30
|
+
' trackfw roadmap new "title"\n' +
|
|
31
|
+
' trackfw roadmap move <name> wip'
|
|
32
|
+
)
|
|
33
|
+
.argument('<spec>', '<type>/<slug>, type in feat, fix, refactor')
|
|
34
|
+
.option('--dry-run', 'Report whether the branch would be created or blocked, without executing git', false)
|
|
35
|
+
.action((spec, options) => {
|
|
36
|
+
const exitCode = runBranchNew(spec, !!options.dryRun, {})
|
|
37
|
+
if (exitCode !== 0) {
|
|
38
|
+
process.exit(exitCode)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
cmd.addCommand(newCmd)
|
|
43
|
+
return cmd
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = createBranchCommand()
|
package/src/commands/discover.js
CHANGED
|
@@ -435,6 +435,12 @@ cmd.action((opts) => {
|
|
|
435
435
|
} catch (e) {
|
|
436
436
|
console.log(`⚠ agent rules inject partial: ${e.message}`);
|
|
437
437
|
}
|
|
438
|
+
try {
|
|
439
|
+
const { generateAttentionScripts } = require('../generators/hooks');
|
|
440
|
+
generateAttentionScripts({}, cwd);
|
|
441
|
+
} catch (e) {
|
|
442
|
+
console.warn(`⚠ attention scripts: ${e.message}`);
|
|
443
|
+
}
|
|
438
444
|
try {
|
|
439
445
|
const { injectHooksDetected } = require('../generators/hooks');
|
|
440
446
|
injectHooksDetected(cwd);
|
package/src/commands/index.js
CHANGED
|
@@ -32,6 +32,7 @@ function createProgram() {
|
|
|
32
32
|
program.addCommand(require('./note'))
|
|
33
33
|
program.addCommand(require('./ship'))
|
|
34
34
|
program.addCommand(require('./barrier'))
|
|
35
|
+
program.addCommand(require('./branch'))
|
|
35
36
|
|
|
36
37
|
const { createServeCommand } = require('./serve')
|
|
37
38
|
program.addCommand(createServeCommand())
|
package/src/commands/init.js
CHANGED
|
@@ -8,7 +8,7 @@ const { resolveScope } = require('./integrations')
|
|
|
8
8
|
|
|
9
9
|
const cmd = new Command('init')
|
|
10
10
|
cmd.description(t('init.description'))
|
|
11
|
-
cmd.option('--ai-tools <tools>', 'Comma-separated AI tools to configure (claude,codex,gemini,antigravity,cursor,copilot,windsurf,amazonq,kiro)', '')
|
|
11
|
+
cmd.option('--ai-tools <tools>', 'Comma-separated AI tools to configure (claude,codex,gemini,antigravity,cursor,copilot,windsurf,amazonq,opencode,kiro)', '')
|
|
12
12
|
cmd.option('--identity-preset <preset>', `Agent identity preset: none, neutral, ${identityStore.presetNames().join(', ')}`, 'none')
|
|
13
13
|
cmd.option('--forge <value>', 'Forge platform: github, gitlab, bitbucket, azure', '')
|
|
14
14
|
cmd.action(async (options, command) => {
|
|
@@ -58,7 +58,7 @@ cmd.action(async (options, command) => {
|
|
|
58
58
|
}
|
|
59
59
|
await generators.scaffold(cfg)
|
|
60
60
|
const aiTools = String(options.aiTools || '').split(',').map(tool => tool.trim()).filter(Boolean)
|
|
61
|
-
const supported = new Set(['claude', 'codex', 'gemini', 'antigravity', 'cursor', 'copilot', 'windsurf', 'amazonq', 'kiro'])
|
|
61
|
+
const supported = new Set(['claude', 'codex', 'gemini', 'antigravity', 'cursor', 'copilot', 'windsurf', 'amazonq', 'opencode', 'kiro'])
|
|
62
62
|
// Sem TTY, o escopo nunca é perguntado: default `global` (ADR D1/D4).
|
|
63
63
|
const scope = await resolveScope({}, { interactive: false })
|
|
64
64
|
const makeOnSkip = () => (_destination, reason) => {
|
|
@@ -222,6 +222,7 @@ cmd.action(async (options, command) => {
|
|
|
222
222
|
{ name: 'GitHub Copilot', value: 'copilot' },
|
|
223
223
|
{ name: 'Windsurf', value: 'windsurf' },
|
|
224
224
|
{ name: 'Amazon Q Developer', value: 'amazonq' },
|
|
225
|
+
{ name: 'OpenCode', value: 'opencode' },
|
|
225
226
|
{ name: 'Kiro', value: 'kiro' },
|
|
226
227
|
],
|
|
227
228
|
})
|
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/hooks.js
CHANGED
|
@@ -82,8 +82,9 @@ case "$ROADMAP_DIR" in
|
|
|
82
82
|
esac
|
|
83
83
|
|
|
84
84
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
|
|
86
|
+
TOOL_ESC=$(echo "$TOOL" | tr -d '\\000-\\037' | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
|
|
87
|
+
MSG_ESC=$(echo "$MSG" | tr -d '\\000-\\037' | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
|
|
87
88
|
|
|
88
89
|
mkdir -p "$ROADMAP_DIR"
|
|
89
90
|
printf '{"tool":"%s","message":"%s","level":"action_required","timestamp":"%s"}\\n' \\
|
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 }
|
|
@@ -35,6 +35,9 @@ Analyze real dependencies between microbatches before assigning work. Microbatch
|
|
|
35
35
|
## Workflow
|
|
36
36
|
Analyze the codebase and requirements; record material decisions in an ADR; create the REQ with an explicit negative scope; produce a roadmap of waves and microbatches with measurable acceptance criteria; create the branch; commit the governance artifacts before any handoff; dispatch the wave; audit each microbatch against its acceptance criteria; update the roadmap; open the pull request only on request.
|
|
37
37
|
|
|
38
|
+
## Dispatch contract
|
|
39
|
+
Naming a specialist in prose or in a roadmap's `squad:` field is documentation, not delegation — it does not route the Agent tool call by itself. Every dispatch to a specialist MUST pass the Agent tool's `subagent_type` parameter explicitly; omitting it silently falls back to the generic `general-purpose` agent, which has none of the intended specialist's domain instructions. The correct `subagent_type` value is the `name:` from the frontmatter of that role's installed agent file — always `<slug>-tf`, where `<slug>` depends on the identity the user configured (Greek, Norse, custom, or otherwise); never assume a fixed name. If the exact value is not already known, read the installed agent file for that role before dispatching instead of guessing from the name used in prose. Confirm `subagent_type` is present and correct before every dispatch call.
|
|
40
|
+
|
|
38
41
|
## Post-microbatch audit
|
|
39
42
|
Before releasing the next wave, verify each acceptance criterion yourself: read the changed files, confirm the build, tests and gates, and check that no forbidden file was touched. Green gates are not proof that the intended behavior was delivered — validate the real artifact, not only the test fixtures. A failed audit blocks the next wave.
|
|
40
43
|
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
{"id":"copilot","name":"GitHub Copilot","surfaces":[{"id":"ide","name":"GitHub Copilot","scopes":["global","project"],"capabilities":{"agents":{"support_level":"native","representation":"custom-agent"},"skills":{"support_level":"native","representation":"agent-skill"}},"paths":{"agents":[{"scope":"global","path":"~/.copilot/agents/trackfw-{{id}}.agent.md","extension":".agent.md"},{"scope":"project","path":".github/agents/trackfw-{{id}}.agent.md","extension":".agent.md"}],"skills":[{"scope":"global","path":"~/.copilot/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".github/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}}],"auxiliary_artifacts":[{"id":"instructions","surface":"ide","scope":"project","path":".github/copilot-instructions.md","purpose":"repository instructions","required":false},{"id":"hooks","surface":"ide","scope":"project","path":".github/hooks/trackfw-attention.json","purpose":"attention hooks","required":false}]},
|
|
43
43
|
{"id":"windsurf","name":"Windsurf","surfaces":[{"id":"ide","name":"Windsurf IDE","scopes":["global","project"],"capabilities":{"agents":{"support_level":"fallback","representation":"skill","fallback_representation":"specialist skill"},"skills":{"support_level":"native","representation":"skill"}},"paths":{"agents":[{"scope":"global","path":"~/.codeium/windsurf/skills/trackfw-agent-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".windsurf/skills/trackfw-agent-{{id}}/SKILL.md","extension":".md"}],"skills":[{"scope":"global","path":"~/.codeium/windsurf/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".windsurf/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}}],"auxiliary_artifacts":[{"id":"global-rules","surface":"ide","scope":"project","path":".windsurfrules","purpose":"governance rule discovery","required":false}]},
|
|
44
44
|
{"id":"amazonq","name":"Amazon Q Developer","surfaces":[{"id":"cli","name":"Amazon Q Developer CLI","scopes":["global","project"],"capabilities":{"agents":{"support_level":"native","representation":"cli-agent-json"},"skills":{"support_level":"fallback","representation":"rule","fallback_representation":"workflow rule"}},"paths":{"agents":[{"scope":"global","path":"~/.aws/amazonq/cli-agents/trackfw-{{id}}.json","extension":".json"},{"scope":"project","path":".amazonq/cli-agents/trackfw-{{id}}.json","extension":".json"}],"skills":[{"scope":"global","path":"~/.aws/amazonq/rules/trackfw-{{id}}.md","extension":".md"},{"scope":"project","path":".amazonq/rules/trackfw-{{id}}.md","extension":".md"}]}}],"auxiliary_artifacts":[{"id":"guidelines","surface":"cli","scope":"project","path":".amazonq/developer/guidelines.md","purpose":"project governance guidelines","required":false}]},
|
|
45
|
+
{"id":"opencode","name":"OpenCode","surfaces":[{"id":"cli","name":"OpenCode CLI","scopes":["global","project"],"capabilities":{"agents":{"support_level":"native","representation":"opencode-agent"},"skills":{"support_level":"native","representation":"skill"}},"paths":{"agents":[{"scope":"global","path":"~/.config/opencode/agents/trackfw-{{id}}.md","extension":".md"},{"scope":"project","path":".opencode/agents/trackfw-{{id}}.md","extension":".md"}],"skills":[{"scope":"global","path":"~/.config/opencode/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".opencode/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}}],"auxiliary_artifacts":[{"id":"instructions","surface":"cli","scope":"project","path":"AGENTS.md","purpose":"repository instructions (shared with Codex/AGENTS.md convention)","required":false}]},
|
|
45
46
|
{"id":"kiro","name":"Kiro","surfaces":[{"id":"ide","name":"Kiro IDE","scopes":["global","project"],"capabilities":{"agents":{"support_level":"native","representation":"agent-markdown"},"skills":{"support_level":"native","representation":"agent-skill"}},"paths":{"agents":[{"scope":"global","path":"~/.kiro/agents/trackfw-{{id}}.md","extension":".md"},{"scope":"project","path":".kiro/agents/trackfw-{{id}}.md","extension":".md"}],"skills":[{"scope":"global","path":"~/.kiro/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".kiro/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}},{"id":"cli","name":"Kiro CLI","scopes":["global","project"],"capabilities":{"agents":{"support_level":"native","representation":"agent-json"},"skills":{"support_level":"native","representation":"agent-skill"}},"paths":{"agents":[{"scope":"global","path":"~/.kiro/agents/trackfw-{{id}}.json","extension":".json"},{"scope":"project","path":".kiro/agents/trackfw-{{id}}.json","extension":".json"}],"skills":[{"scope":"global","path":"~/.kiro/skills/trackfw-{{id}}/SKILL.md","extension":".md"},{"scope":"project","path":".kiro/skills/trackfw-{{id}}/SKILL.md","extension":".md"}]}}],"auxiliary_artifacts":[{"id":"hooks","surface":"ide","scope":"project","path":".kiro/hooks/trackfw-attention.json","purpose":"attention hooks","required":false}]}
|
|
46
47
|
]
|
|
47
48
|
}
|
|
@@ -278,6 +278,38 @@ function render({ kind, content, capability, item, identity: cfg }) {
|
|
|
278
278
|
return out
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
if (capability.representation === 'opencode-agent') {
|
|
282
|
+
// Reconstrói o frontmatter para o OpenCode CLI (opencode.ai), seguindo o
|
|
283
|
+
// mesmo padrão de reconstrução-do-zero do case "agent-directory".
|
|
284
|
+
// Decisão registrada na Wave 1 do roadmap
|
|
285
|
+
// ROADMAP-2026-08-04-compatibilidade-com-opencode-opencode-ai (achado #3,
|
|
286
|
+
// pesquisa contra o binário real 1.18.13):
|
|
287
|
+
// - "tools:" é uma chave RESERVADA no schema de agente do OpenCode
|
|
288
|
+
// (espera um objeto de overrides por-ferramenta, ex. {bash: false},
|
|
289
|
+
// não uma lista de nomes estilo Claude Code) — reutilizar o
|
|
290
|
+
// frontmatter original faz o OpenCode recusar o carregamento INTEIRO
|
|
291
|
+
// do projeto ("Configuration is invalid"), não só daquele agente. Por
|
|
292
|
+
// isso "tools:" nunca é emitido aqui.
|
|
293
|
+
// - sem "mode:" explícito, o OpenCode assume mode "all" (agente
|
|
294
|
+
// selecionável como persona primária de chat) — os agentes trackfw
|
|
295
|
+
// devem ser sempre subagentes puros, nunca primários, para paridade
|
|
296
|
+
// com o comportamento nos demais targets. Por isso "mode: subagent" é
|
|
297
|
+
// sempre fixo, nunca omitido.
|
|
298
|
+
// - "model:" é deliberadamente OMITIDO (decisão de produto do
|
|
299
|
+
// orquestrador, não uma limitação técnica): o OpenCode espera
|
|
300
|
+
// "provider/model-id" (ex. "anthropic/claude-sonnet-4-5"), não os
|
|
301
|
+
// aliases curtos do catálogo canônico ("opus"/"sonnet"), e mapear
|
|
302
|
+
// para um provider fixo contradiria a motivação de negócio do REQ
|
|
303
|
+
// (permitir que o usuário roteie os agentes trackfw para o modelo
|
|
304
|
+
// open-source/local que ele já configurou em opencode.json). Omitir
|
|
305
|
+
// deixa o OpenCode resolver pelo default já configurado pelo usuário.
|
|
306
|
+
// - "memory:" também não faz sentido no schema do OpenCode e é
|
|
307
|
+
// descartado junto com "tools:".
|
|
308
|
+
let out = `---\ndescription: ${description}\nmode: subagent\n---\n`
|
|
309
|
+
if (body) out += `${body}\n`
|
|
310
|
+
return out
|
|
311
|
+
}
|
|
312
|
+
|
|
281
313
|
if (!id) return normalize(content)
|
|
282
314
|
const withBody = insertBodyPrefix(content, greeting)
|
|
283
315
|
const withFrontmatter = rewriteFrontmatterFields(withBody, name, description)
|
package/src/validator/index.js
CHANGED
|
@@ -1042,6 +1042,45 @@ function validateFilenameUniqueness() {
|
|
|
1042
1042
|
return violations
|
|
1043
1043
|
}
|
|
1044
1044
|
|
|
1045
|
+
// branchSlugMatchesRoadmap verifica se branchSlug (já normalizado via normalizeBranchSlug) casa com o
|
|
1046
|
+
// nome de algum roadmap .md encontrado em wipDirs ou doneDirs. Reutilizada por
|
|
1047
|
+
// validateBranchHasWIPRoadmap e pelo comando `trackfw branch new` — nunca duplicar esta lógica.
|
|
1048
|
+
//
|
|
1049
|
+
// Espelha internal/validator/validator.go:BranchSlugMatchesRoadmap. Retorna { matched, candidates }:
|
|
1050
|
+
// matched indica se algum candidato casou com o slug; candidates lista todos os roadmaps .md
|
|
1051
|
+
// encontrados em wipDirs+doneDirs (para diagnóstico/mensagem de orientação quando matched é false).
|
|
1052
|
+
function branchSlugMatchesRoadmap(branchSlug, wipDirs, doneDirs) {
|
|
1053
|
+
const dirs = [...wipDirs, ...doneDirs]
|
|
1054
|
+
const candidates = []
|
|
1055
|
+
let matched = false
|
|
1056
|
+
for (const dir of dirs) {
|
|
1057
|
+
const files = listDir(dir).filter(f => f.endsWith('.md'))
|
|
1058
|
+
candidates.push(...files)
|
|
1059
|
+
if (files.some(file => normalizeBranchSlug(file).includes(branchSlug))) matched = true
|
|
1060
|
+
}
|
|
1061
|
+
return { matched, candidates }
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// branchGovernanceOrientation is the guidance message printed when a feat/fix/refactor branch has
|
|
1065
|
+
// no roadmap in wip/ nor done/ at all (candidates is empty). Shared by validateBranchHasWIPRoadmap
|
|
1066
|
+
// and `trackfw branch new` — never duplicate this string. Byte-identical to Go's
|
|
1067
|
+
// BranchGovernanceOrientation.
|
|
1068
|
+
function branchGovernanceOrientation(branch) {
|
|
1069
|
+
return `branch "${branch}" is a feat/fix/refactor branch but no roadmap is in wip/ nor done/ — create governance artifacts first:\n trackfw req new "title"\n trackfw roadmap new "title"\n trackfw roadmap move <name> wip`
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// branchNoMatchingRoadmapMessage is the guidance message printed when roadmaps exist in wip/ or
|
|
1073
|
+
// done/ but none of them match the branch's slug. Shared by validateBranchHasWIPRoadmap and
|
|
1074
|
+
// `trackfw branch new` — never duplicate this string. Byte-identical to Go's
|
|
1075
|
+
// BranchNoMatchingRoadmapMessage. Does not mutate candidates.
|
|
1076
|
+
function branchNoMatchingRoadmapMessage(branch, candidates) {
|
|
1077
|
+
// P3: sort for deterministic output regardless of filesystem ordering.
|
|
1078
|
+
const sorted = [...candidates].sort()
|
|
1079
|
+
const display = sorted.slice(0, 3)
|
|
1080
|
+
const suffix = sorted.length > 3 ? `, e mais ${sorted.length - 3}` : ''
|
|
1081
|
+
return `branch "${branch}" has no matching roadmap in wip/ nor done/ (found: ${display.join(', ')}${suffix}) — include the branch slug in the roadmap filename or set TRACKFW_BRANCH explicitly in CI`
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1045
1084
|
// validateBranchHasWIPRoadmap — verifica que branch feat/fix/refactor tem ao menos um roadmap em
|
|
1046
1085
|
// wip/ ou done/ cujo slug case com a branch. Aceita done/ para permitir encerramento do roadmap na
|
|
1047
1086
|
// própria branch, conforme a Definition of Done, sem reprovar o gate.
|
|
@@ -1067,22 +1106,13 @@ function validateBranchHasWIPRoadmap() {
|
|
|
1067
1106
|
const wipDirs = resolveWIPDirs(cfg)
|
|
1068
1107
|
const doneDirs = resolveDoneDirs(cfg)
|
|
1069
1108
|
const branchSlug = normalizeBranchSlug(branch.split('/', 2)[1])
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
for (const dir of [...wipDirs, ...doneDirs]) {
|
|
1073
|
-
const files = listDir(dir).filter(f => f.endsWith('.md'))
|
|
1074
|
-
candidates.push(...files)
|
|
1075
|
-
if (files.some(file => normalizeBranchSlug(file).includes(branchSlug))) return []
|
|
1076
|
-
}
|
|
1109
|
+
const { matched, candidates } = branchSlugMatchesRoadmap(branchSlug, wipDirs, doneDirs)
|
|
1110
|
+
if (matched) return []
|
|
1077
1111
|
|
|
1078
1112
|
if (candidates.length === 0) {
|
|
1079
|
-
return [
|
|
1113
|
+
return [branchGovernanceOrientation(branch)]
|
|
1080
1114
|
}
|
|
1081
|
-
|
|
1082
|
-
const sorted = [...candidates].sort()
|
|
1083
|
-
const display = sorted.slice(0, 3)
|
|
1084
|
-
const suffix = sorted.length > 3 ? `, e mais ${sorted.length - 3}` : ''
|
|
1085
|
-
return [`branch "${branch}" has no matching roadmap in wip/ nor done/ (found: ${display.join(', ')}${suffix}) — include the branch slug in the roadmap filename or set TRACKFW_BRANCH explicitly in CI`]
|
|
1115
|
+
return [branchNoMatchingRoadmapMessage(branch, candidates)]
|
|
1086
1116
|
}
|
|
1087
1117
|
|
|
1088
1118
|
function normalizeBranchSlug(value) {
|
|
@@ -1451,6 +1481,11 @@ module.exports = {
|
|
|
1451
1481
|
validateFolderStatusCoherence,
|
|
1452
1482
|
validateFilenameUniqueness,
|
|
1453
1483
|
validateBranchHasWIPRoadmap,
|
|
1484
|
+
// novas funções — trackfw branch new (extraídas do gate branch_has_wip_roadmap)
|
|
1485
|
+
branchSlugMatchesRoadmap,
|
|
1486
|
+
branchGovernanceOrientation,
|
|
1487
|
+
branchNoMatchingRoadmapMessage,
|
|
1488
|
+
normalizeBranchSlug,
|
|
1454
1489
|
// novas funções ML-2B
|
|
1455
1490
|
contentHasMarker,
|
|
1456
1491
|
ruleSeverity,
|