trackfw 2.11.0 → 2.12.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 +14 -0
- package/package.json +12 -2
- package/src/commands/discover.js +28 -0
- package/src/commands/update.js +9 -0
- package/src/generators/hooks.js +302 -0
- package/src/generators/init.js +11 -0
- package/src/validator/index.js +29 -0
package/README.md
CHANGED
|
@@ -374,6 +374,20 @@ The governance structure (`docs/adr/`, `docs/req/`, `docs/roadmaps/`) is always
|
|
|
374
374
|
|
|
375
375
|
---
|
|
376
376
|
|
|
377
|
+
## Compared to alternatives
|
|
378
|
+
|
|
379
|
+
| Tool | What it does | What's missing |
|
|
380
|
+
|---|---|---|
|
|
381
|
+
| **adr-tools** | Creates ADR files | No link to requirements or roadmaps |
|
|
382
|
+
| **madr** | ADR template format | No enforcement, no delivery tracking |
|
|
383
|
+
| **Linear / Jira** | Task tracking | No traceability to architectural decisions |
|
|
384
|
+
| **Kosli** | SDLC compliance for regulated industries | SaaS, accounts, cost — not for every team |
|
|
385
|
+
| **trackfw** | Enforces the full chain: decision → requirement → roadmap → delivery | — |
|
|
386
|
+
|
|
387
|
+
trackfw is the only open-source CLI that links ADRs to requirements, requirements to roadmaps, and enforces the chain as a pre-commit and CI gate — with native support for AI coding assistants.
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
377
391
|
## Contributing
|
|
378
392
|
|
|
379
393
|
```bash
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackfw",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "CLI
|
|
3
|
+
"version": "2.12.1",
|
|
4
|
+
"description": "Governance CLI for AI-native software delivery. Enforces traceability: ADR → REQ → ROADMAP → kanban. Pre-commit gate + CI integration. Native support for Claude Code, Gemini CLI, Cursor, Copilot, Windsurf, Amazon Q.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
7
7
|
"adr",
|
|
@@ -13,6 +13,16 @@
|
|
|
13
13
|
"ai-agents",
|
|
14
14
|
"developer-tools",
|
|
15
15
|
"kanban",
|
|
16
|
+
"traceability",
|
|
17
|
+
"software-governance",
|
|
18
|
+
"ai-governance",
|
|
19
|
+
"pre-commit",
|
|
20
|
+
"developer-experience",
|
|
21
|
+
"technical-debt",
|
|
22
|
+
"claude-code",
|
|
23
|
+
"gemini",
|
|
24
|
+
"cursor",
|
|
25
|
+
"copilot",
|
|
16
26
|
"req",
|
|
17
27
|
"trackfw"
|
|
18
28
|
],
|
package/src/commands/discover.js
CHANGED
|
@@ -211,11 +211,32 @@ function installHook(framework, rootDir) {
|
|
|
211
211
|
if (fs.existsSync(pkgJson)) {
|
|
212
212
|
installHusky(rootDir);
|
|
213
213
|
} else {
|
|
214
|
+
// Node.js disponível mas sem package.json → husky via npx
|
|
215
|
+
try {
|
|
216
|
+
execSync('node --version', { stdio: 'pipe' });
|
|
217
|
+
installHuskyNPX(rootDir);
|
|
218
|
+
return;
|
|
219
|
+
} catch (_) {}
|
|
220
|
+
// fallback: lefthook
|
|
214
221
|
installLefthook(rootDir);
|
|
215
222
|
}
|
|
216
223
|
}
|
|
217
224
|
}
|
|
218
225
|
|
|
226
|
+
function installHuskyNPX(rootDir) {
|
|
227
|
+
console.log('ℹ node detected — using husky via npx (no package.json required)');
|
|
228
|
+
try {
|
|
229
|
+
execSync('npx husky init', { cwd: rootDir, stdio: 'inherit' });
|
|
230
|
+
} catch (_) {
|
|
231
|
+
console.log('⚠ npx husky init failed — install manually: npx husky init');
|
|
232
|
+
}
|
|
233
|
+
const huskyDir = path.join(rootDir, '.husky');
|
|
234
|
+
fs.mkdirSync(huskyDir, { recursive: true });
|
|
235
|
+
const hookPath = path.join(huskyDir, 'pre-commit');
|
|
236
|
+
fs.appendFileSync(hookPath, '\nscripts/trackfw-validate.sh\n', { mode: 0o755 });
|
|
237
|
+
console.log('✓ trackfw entry added to .husky/pre-commit');
|
|
238
|
+
}
|
|
239
|
+
|
|
219
240
|
function installHusky(rootDir) {
|
|
220
241
|
try {
|
|
221
242
|
execSync('npm install --save-dev husky', { cwd: rootDir, stdio: 'inherit' });
|
|
@@ -396,6 +417,13 @@ cmd.action((opts) => {
|
|
|
396
417
|
} catch (e) {
|
|
397
418
|
console.log(`⚠ agent rules inject partial: ${e.message}`);
|
|
398
419
|
}
|
|
420
|
+
try {
|
|
421
|
+
const { injectHooksDetected } = require('../generators/hooks');
|
|
422
|
+
injectHooksDetected(cwd);
|
|
423
|
+
console.log('✓ agent hooks injected');
|
|
424
|
+
} catch (e) {
|
|
425
|
+
console.warn(`⚠ agent hooks inject partial: ${e.message}`);
|
|
426
|
+
}
|
|
399
427
|
try {
|
|
400
428
|
const generators = require('../generators/init')
|
|
401
429
|
generators.printArchitectNextSteps(cwd)
|
package/src/commands/update.js
CHANGED
|
@@ -72,6 +72,15 @@ cmd.action(() => {
|
|
|
72
72
|
console.log(` ⚠ agent rules: ${e.message}`);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// 1b. Agent hooks (attention signal/cleanup)
|
|
76
|
+
try {
|
|
77
|
+
const { injectHooksDetected } = require('../generators/hooks');
|
|
78
|
+
injectHooksDetected(cwd);
|
|
79
|
+
console.log(' ✓ agent hooks atualizados');
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.warn(` ⚠ agent hooks: ${e.message}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
75
84
|
// 2. Validate script (trackfw-owned, overwrite)
|
|
76
85
|
try {
|
|
77
86
|
discover.writeValidateScript(cwd);
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Helpers
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
/** Lê JSON de arquivo (retorna {} se não existir ou inválido) */
|
|
11
|
+
function readJSON(filePath) {
|
|
12
|
+
try {
|
|
13
|
+
const raw = fs.readFileSync(filePath, 'utf8')
|
|
14
|
+
return JSON.parse(raw)
|
|
15
|
+
} catch (_) {
|
|
16
|
+
return {}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Escreve JSON com indent 2 */
|
|
21
|
+
function writeJSON(filePath, data) {
|
|
22
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
23
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Verifica se array já tem entry com determinado campo=valor */
|
|
27
|
+
function hasEntry(arr, field, value) {
|
|
28
|
+
return Array.isArray(arr) && arr.some(e => e && e[field] === value)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Scripts content
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
const SIGNAL_SCRIPT = `#!/usr/bin/env bash
|
|
36
|
+
# trackfw attention signal — PreToolUse/BeforeTool hook
|
|
37
|
+
# Writes .trackfw-attention.json so trackfw serve board shows a banner.
|
|
38
|
+
set -euo pipefail
|
|
39
|
+
|
|
40
|
+
INPUT=$(cat)
|
|
41
|
+
[ -f "trackfw.yaml" ] || exit 0
|
|
42
|
+
|
|
43
|
+
if command -v jq &>/dev/null; then
|
|
44
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
45
|
+
MSG=$(echo "$INPUT" | jq -r '(.tool_input.question // .tool_input.command // ("Agent executing: " + (.tool_name // "unknown"))) | .[0:300]')
|
|
46
|
+
else
|
|
47
|
+
TOOL=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_name',''))" 2>/dev/null || echo "")
|
|
48
|
+
MSG=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); ti=d.get('tool_input',{}); print((ti.get('question') or ti.get('command') or 'Agent executing: '+d.get('tool_name','unknown'))[:300])" 2>/dev/null || echo "Agent needs attention")
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
ROADMAP_DIR=$(grep '^roadmap_dir:' trackfw.yaml 2>/dev/null | awk '{print $2}' | tr -d "\"'" | head -1)
|
|
52
|
+
ROADMAP_DIR=\${ROADMAP_DIR:-docs/roadmaps}
|
|
53
|
+
|
|
54
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
55
|
+
|
|
56
|
+
mkdir -p "$ROADMAP_DIR"
|
|
57
|
+
printf '{"tool":"%s","message":"%s","level":"action_required","timestamp":"%s"}\\n' \\
|
|
58
|
+
"$(echo "$TOOL" | sed 's/"/\\\\"/g')" \\
|
|
59
|
+
"$(echo "$MSG" | sed 's/"/\\\\"/g; s/$//' | tr -d '\\n')" \\
|
|
60
|
+
"$TIMESTAMP" > "$ROADMAP_DIR/.trackfw-attention.json"
|
|
61
|
+
|
|
62
|
+
exit 0
|
|
63
|
+
`
|
|
64
|
+
|
|
65
|
+
const CLEANUP_SCRIPT = `#!/usr/bin/env bash
|
|
66
|
+
# trackfw attention cleanup — PostToolUse/AfterTool hook
|
|
67
|
+
set -euo pipefail
|
|
68
|
+
|
|
69
|
+
[ -f "trackfw.yaml" ] || exit 0
|
|
70
|
+
|
|
71
|
+
ROADMAP_DIR=$(grep '^roadmap_dir:' trackfw.yaml 2>/dev/null | awk '{print $2}' | tr -d "\"'" | head -1)
|
|
72
|
+
ROADMAP_DIR=\${ROADMAP_DIR:-docs/roadmaps}
|
|
73
|
+
|
|
74
|
+
rm -f "$ROADMAP_DIR/.trackfw-attention.json"
|
|
75
|
+
exit 0
|
|
76
|
+
`
|
|
77
|
+
|
|
78
|
+
const SIGNAL_CMD = 'scripts/trackfw-attention-signal.sh'
|
|
79
|
+
const CLEANUP_CMD = 'scripts/trackfw-attention-cleanup.sh'
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// generateAttentionScripts — writes the two shell scripts to scripts/
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
function generateAttentionScripts(cfg, cwd) {
|
|
86
|
+
const root = cwd || process.cwd()
|
|
87
|
+
const scriptsDir = path.join(root, 'scripts')
|
|
88
|
+
fs.mkdirSync(scriptsDir, { recursive: true })
|
|
89
|
+
|
|
90
|
+
const signalPath = path.join(scriptsDir, 'trackfw-attention-signal.sh')
|
|
91
|
+
fs.writeFileSync(signalPath, SIGNAL_SCRIPT, { encoding: 'utf8', mode: 0o755 })
|
|
92
|
+
|
|
93
|
+
const cleanupPath = path.join(scriptsDir, 'trackfw-attention-cleanup.sh')
|
|
94
|
+
fs.writeFileSync(cleanupPath, CLEANUP_SCRIPT, { encoding: 'utf8', mode: 0o755 })
|
|
95
|
+
|
|
96
|
+
console.log(' ✓ scripts/trackfw-attention-signal.sh')
|
|
97
|
+
console.log(' ✓ scripts/trackfw-attention-cleanup.sh')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Claude Code — .claude/settings.json
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
function injectClaudeHooks(cwd) {
|
|
105
|
+
const filePath = path.join(cwd, '.claude', 'settings.json')
|
|
106
|
+
const data = readJSON(filePath)
|
|
107
|
+
|
|
108
|
+
if (!data.hooks) data.hooks = {}
|
|
109
|
+
|
|
110
|
+
// PreToolUse — AskUserQuestion
|
|
111
|
+
if (!data.hooks.PreToolUse) data.hooks.PreToolUse = []
|
|
112
|
+
let preEntry = data.hooks.PreToolUse.find(e => e && e.matcher === 'AskUserQuestion')
|
|
113
|
+
if (!preEntry) {
|
|
114
|
+
preEntry = { matcher: 'AskUserQuestion', hooks: [] }
|
|
115
|
+
data.hooks.PreToolUse.push(preEntry)
|
|
116
|
+
}
|
|
117
|
+
if (!Array.isArray(preEntry.hooks)) preEntry.hooks = []
|
|
118
|
+
if (!hasEntry(preEntry.hooks, 'command', SIGNAL_CMD)) {
|
|
119
|
+
preEntry.hooks.push({ type: 'command', command: SIGNAL_CMD })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// PostToolUse — cleanup
|
|
123
|
+
if (!data.hooks.PostToolUse) data.hooks.PostToolUse = []
|
|
124
|
+
let postEntry = data.hooks.PostToolUse.find(e => e && e.matcher === 'AskUserQuestion')
|
|
125
|
+
if (!postEntry) {
|
|
126
|
+
postEntry = { matcher: 'AskUserQuestion', hooks: [] }
|
|
127
|
+
data.hooks.PostToolUse.push(postEntry)
|
|
128
|
+
}
|
|
129
|
+
if (!Array.isArray(postEntry.hooks)) postEntry.hooks = []
|
|
130
|
+
if (!hasEntry(postEntry.hooks, 'command', CLEANUP_CMD)) {
|
|
131
|
+
postEntry.hooks.push({ type: 'command', command: CLEANUP_CMD })
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
writeJSON(filePath, data)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Codex — .codex/hooks.json
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
function injectCodexHooks(cwd) {
|
|
142
|
+
const filePath = path.join(cwd, '.codex', 'hooks.json')
|
|
143
|
+
const data = readJSON(filePath)
|
|
144
|
+
|
|
145
|
+
if (!data.PreToolUse) data.PreToolUse = []
|
|
146
|
+
if (!data.PostToolUse) data.PostToolUse = []
|
|
147
|
+
|
|
148
|
+
if (!hasEntry(data.PreToolUse, 'command', SIGNAL_CMD)) {
|
|
149
|
+
data.PreToolUse.push({ matcher: '.*', command: SIGNAL_CMD })
|
|
150
|
+
}
|
|
151
|
+
if (!hasEntry(data.PostToolUse, 'command', CLEANUP_CMD)) {
|
|
152
|
+
data.PostToolUse.push({ matcher: '.*', command: CLEANUP_CMD })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
writeJSON(filePath, data)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Gemini — .gemini/settings.json
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
function injectGeminiHooks(cwd) {
|
|
163
|
+
const filePath = path.join(cwd, '.gemini', 'settings.json')
|
|
164
|
+
const data = readJSON(filePath)
|
|
165
|
+
|
|
166
|
+
if (!data.hooks) data.hooks = {}
|
|
167
|
+
|
|
168
|
+
if (!Array.isArray(data.hooks.BeforeTool)) data.hooks.BeforeTool = []
|
|
169
|
+
if (!hasEntry(data.hooks.BeforeTool, 'command', SIGNAL_CMD)) {
|
|
170
|
+
data.hooks.BeforeTool.push({ command: SIGNAL_CMD })
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!Array.isArray(data.hooks.AfterTool)) data.hooks.AfterTool = []
|
|
174
|
+
if (!hasEntry(data.hooks.AfterTool, 'command', CLEANUP_CMD)) {
|
|
175
|
+
data.hooks.AfterTool.push({ command: CLEANUP_CMD })
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
writeJSON(filePath, data)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Kiro — .kiro/hooks/trackfw-attention.json (dedicated file, safe overwrite)
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
function injectKiroHooks(cwd) {
|
|
186
|
+
const filePath = path.join(cwd, '.kiro', 'hooks', 'trackfw-attention.json')
|
|
187
|
+
const data = {
|
|
188
|
+
hooks: [
|
|
189
|
+
{
|
|
190
|
+
name: 'trackfw-attention-signal',
|
|
191
|
+
event: 'PreToolUse',
|
|
192
|
+
matcher: { tool_name: '.*' },
|
|
193
|
+
action: { type: 'command', command: SIGNAL_CMD },
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: 'trackfw-attention-cleanup',
|
|
197
|
+
event: 'PostToolUse',
|
|
198
|
+
matcher: { tool_name: '.*' },
|
|
199
|
+
action: { type: 'command', command: CLEANUP_CMD },
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
}
|
|
203
|
+
writeJSON(filePath, data)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Copilot — .github/hooks/trackfw-attention.json (dedicated file, safe overwrite)
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
function injectCopilotHooks(cwd) {
|
|
211
|
+
const filePath = path.join(cwd, '.github', 'hooks', 'trackfw-attention.json')
|
|
212
|
+
const data = {
|
|
213
|
+
hooks: [
|
|
214
|
+
{ event: 'preToolUse', run: SIGNAL_CMD },
|
|
215
|
+
{ event: 'postToolUse', run: CLEANUP_CMD },
|
|
216
|
+
],
|
|
217
|
+
}
|
|
218
|
+
writeJSON(filePath, data)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// Cursor — .cursor/hooks.json
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
function injectCursorHooks(cwd) {
|
|
226
|
+
const filePath = path.join(cwd, '.cursor', 'hooks.json')
|
|
227
|
+
const data = readJSON(filePath)
|
|
228
|
+
|
|
229
|
+
if (!Array.isArray(data.preToolUse)) data.preToolUse = []
|
|
230
|
+
if (!hasEntry(data.preToolUse, 'command', SIGNAL_CMD)) {
|
|
231
|
+
data.preToolUse.push({ command: SIGNAL_CMD })
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!Array.isArray(data.postToolUse)) data.postToolUse = []
|
|
235
|
+
if (!hasEntry(data.postToolUse, 'command', CLEANUP_CMD)) {
|
|
236
|
+
data.postToolUse.push({ command: CLEANUP_CMD })
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
writeJSON(filePath, data)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// injectHooksDetected — public entry point
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
function injectHooksDetected(cwd) {
|
|
247
|
+
const root = cwd || process.cwd()
|
|
248
|
+
|
|
249
|
+
const detections = {
|
|
250
|
+
claude: {
|
|
251
|
+
check: () =>
|
|
252
|
+
fs.existsSync(path.join(root, '.claude')) ||
|
|
253
|
+
fs.existsSync(path.join(root, 'CLAUDE.md')),
|
|
254
|
+
fn: injectClaudeHooks,
|
|
255
|
+
},
|
|
256
|
+
codex: {
|
|
257
|
+
check: () =>
|
|
258
|
+
fs.existsSync(path.join(root, 'AGENTS.md')) ||
|
|
259
|
+
fs.existsSync(path.join(root, '.codex')),
|
|
260
|
+
fn: injectCodexHooks,
|
|
261
|
+
},
|
|
262
|
+
gemini: {
|
|
263
|
+
check: () =>
|
|
264
|
+
fs.existsSync(path.join(root, 'GEMINI.md')) ||
|
|
265
|
+
fs.existsSync(path.join(root, '.gemini')),
|
|
266
|
+
fn: injectGeminiHooks,
|
|
267
|
+
},
|
|
268
|
+
kiro: {
|
|
269
|
+
check: () => fs.existsSync(path.join(root, '.kiro')),
|
|
270
|
+
fn: injectKiroHooks,
|
|
271
|
+
},
|
|
272
|
+
copilot: {
|
|
273
|
+
check: () =>
|
|
274
|
+
fs.existsSync(path.join(root, '.github', 'copilot-instructions.md')),
|
|
275
|
+
fn: injectCopilotHooks,
|
|
276
|
+
},
|
|
277
|
+
cursor: {
|
|
278
|
+
check: () => fs.existsSync(path.join(root, '.cursor')),
|
|
279
|
+
fn: injectCursorHooks,
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
for (const [name, { check, fn }] of Object.entries(detections)) {
|
|
284
|
+
if (!check()) continue
|
|
285
|
+
try {
|
|
286
|
+
fn(root)
|
|
287
|
+
} catch (e) {
|
|
288
|
+
console.warn(` ⚠ hooks (${name}): ${e.message}`)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = {
|
|
294
|
+
generateAttentionScripts,
|
|
295
|
+
injectClaudeHooks,
|
|
296
|
+
injectCodexHooks,
|
|
297
|
+
injectGeminiHooks,
|
|
298
|
+
injectKiroHooks,
|
|
299
|
+
injectCopilotHooks,
|
|
300
|
+
injectCursorHooks,
|
|
301
|
+
injectHooksDetected,
|
|
302
|
+
}
|
package/src/generators/init.js
CHANGED
|
@@ -27,6 +27,7 @@ async function scaffold(cfg) {
|
|
|
27
27
|
|
|
28
28
|
writeTrackfwConfig(cfg)
|
|
29
29
|
generateValidateScript(cfg)
|
|
30
|
+
generateAttentionScripts(cfg)
|
|
30
31
|
generateCIWorkflow(cfg)
|
|
31
32
|
generateGitHooks(cfg)
|
|
32
33
|
generateCommitMsgHook(cfg)
|
|
@@ -116,6 +117,15 @@ trackfw validate
|
|
|
116
117
|
return base
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// scripts/trackfw-attention-signal.sh + trackfw-attention-cleanup.sh
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
function generateAttentionScripts(cfg, cwd) {
|
|
125
|
+
const { generateAttentionScripts: gen } = require('./hooks')
|
|
126
|
+
gen(cfg, cwd || process.cwd())
|
|
127
|
+
}
|
|
128
|
+
|
|
119
129
|
// ---------------------------------------------------------------------------
|
|
120
130
|
// CI workflows
|
|
121
131
|
// ---------------------------------------------------------------------------
|
|
@@ -1069,6 +1079,7 @@ module.exports = {
|
|
|
1069
1079
|
scaffold,
|
|
1070
1080
|
writeTrackfwConfig,
|
|
1071
1081
|
generateValidateScript,
|
|
1082
|
+
generateAttentionScripts,
|
|
1072
1083
|
generateCIWorkflow,
|
|
1073
1084
|
generateGitHooks,
|
|
1074
1085
|
generateCommitMsgHook,
|
package/src/validator/index.js
CHANGED
|
@@ -735,6 +735,33 @@ function validateFilenameUniqueness() {
|
|
|
735
735
|
return violations
|
|
736
736
|
}
|
|
737
737
|
|
|
738
|
+
// validateBranchHasWIPRoadmap — verifica que branch feat/fix/refactor tem ao menos um roadmap em wip/
|
|
739
|
+
function validateBranchHasWIPRoadmap() {
|
|
740
|
+
const { execSync } = require('child_process')
|
|
741
|
+
let branch
|
|
742
|
+
try {
|
|
743
|
+
branch = execSync('git symbolic-ref --short HEAD', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
|
744
|
+
} catch {
|
|
745
|
+
return [] // não é git repo ou HEAD detached
|
|
746
|
+
}
|
|
747
|
+
if (!branch.startsWith('feat/') && !branch.startsWith('fix/') && !branch.startsWith('refactor/')) {
|
|
748
|
+
return []
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const cfg = config.load()
|
|
752
|
+
const wipDirs = resolveWIPDirs(cfg)
|
|
753
|
+
let total = 0
|
|
754
|
+
for (const wipDir of wipDirs) {
|
|
755
|
+
const files = listDir(wipDir).filter(f => f.endsWith('.md'))
|
|
756
|
+
total += files.length
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (total === 0) {
|
|
760
|
+
return [`branch "${branch}" is a feat/fix/refactor branch but no roadmap is in wip/ — create governance artifacts first:\n trackfw req new "title"\n trackfw roadmap new "title"\n trackfw roadmap move <name> wip`]
|
|
761
|
+
}
|
|
762
|
+
return []
|
|
763
|
+
}
|
|
764
|
+
|
|
738
765
|
// _itemMeta: mapa de message → { rule, file } para enriquecer saída JSON.
|
|
739
766
|
// Populado em applyRule e nos pushs diretos do validateUnfiltered.
|
|
740
767
|
// Permanece em memória apenas durante a execução de uma chamada validate*.
|
|
@@ -816,6 +843,7 @@ async function validateUnfiltered() {
|
|
|
816
843
|
applyRule('ref_targets_exist', validateRefTargetsExist(), violations, warnings)
|
|
817
844
|
applyRule('folder_status', validateFolderStatusCoherence(), violations, warnings)
|
|
818
845
|
applyRule('filename_uniqueness', validateFilenameUniqueness(), violations, warnings)
|
|
846
|
+
applyRule('branch_has_wip_roadmap', validateBranchHasWIPRoadmap(), violations, warnings)
|
|
819
847
|
applyRule('blocked_by_draft_adr', validateREQsNotBlockedByDraftADRs(), violations, warnings)
|
|
820
848
|
|
|
821
849
|
// Regras configuráveis via applyRule (popula _itemMeta automaticamente)
|
|
@@ -979,6 +1007,7 @@ module.exports = {
|
|
|
979
1007
|
validateRefTargetsExist,
|
|
980
1008
|
validateFolderStatusCoherence,
|
|
981
1009
|
validateFilenameUniqueness,
|
|
1010
|
+
validateBranchHasWIPRoadmap,
|
|
982
1011
|
// novas funções ML-2B
|
|
983
1012
|
contentHasMarker,
|
|
984
1013
|
ruleSeverity,
|