trackfw 7.3.0 → 7.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 +67 -1
- package/package.json +1 -1
- package/src/commands/adr.js +2 -1
- package/src/commands/barrier.js +293 -26
- package/src/commands/context.js +42 -36
- package/src/commands/discover.js +72 -21
- package/src/commands/doctor.js +23 -3
- package/src/commands/init.js +2 -1
- package/src/commands/integrations.js +5 -4
- package/src/commands/req.js +2 -1
- package/src/commands/thirdparty.js +2 -1
- package/src/commands/update-harness.js +3 -2
- package/src/commands/update.js +127 -9
- package/src/config/index.js +4 -3
- package/src/generators/hooks.js +121 -6
- package/src/generators/init.js +103 -7
- package/src/generators/req.js +15 -53
- package/src/generators/roadmap.js +81 -28
- package/src/homedir.js +30 -0
- package/src/integrations/doctor.js +0 -0
- package/src/integrations/doctor_remote.js +224 -0
- package/src/integrations/index.js +2 -1
- package/src/integrations/manager.js +10 -2
- package/src/integrations/render.js +39 -8
- package/src/integrations/scaffold_doctor.js +29 -0
- package/src/lib/pathfmt.js +43 -0
- package/src/lib/update-engine.js +15 -2
- package/src/serve/api_board.js +11 -10
- package/src/serve/api_chain.js +13 -17
- package/src/serve/api_metrics.js +2 -8
- package/src/validator/index.js +768 -167
package/src/generators/hooks.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const path = require('path')
|
|
5
5
|
const os = require('os')
|
|
6
|
+
const { homedir } = require('../homedir')
|
|
6
7
|
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// Helpers
|
|
@@ -143,8 +144,8 @@ if command -v jq &>/dev/null; then
|
|
|
143
144
|
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
144
145
|
MSG=$(echo "$INPUT" | jq -r '(.tool_input.question // .tool_input.command // "Agent is executing: \\(.tool_name // "unknown")") | .[0:300]')
|
|
145
146
|
else
|
|
146
|
-
TOOL=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_name',''))" 2>/dev/null || echo "")
|
|
147
|
-
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 is executing: '+d.get('tool_name','unknown'))[:300])" 2>/dev/null || echo "Agent needs attention")
|
|
147
|
+
TOOL=$(echo "$INPUT" | PYTHONIOENCODING=utf-8 python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_name',''))" 2>/dev/null || echo "")
|
|
148
|
+
MSG=$(echo "$INPUT" | PYTHONIOENCODING=utf-8 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 is executing: '+d.get('tool_name','unknown'))[:300])" 2>/dev/null || echo "Agent needs attention")
|
|
148
149
|
fi
|
|
149
150
|
|
|
150
151
|
ROADMAP_DIR=$(grep '^roadmap_dir:' trackfw.yaml 2>/dev/null | head -1 | sed 's/^roadmap_dir:[[:space:]]*//; s/[[:space:]]*#.*$//' | tr -d '"' | tr -d "'" || true)
|
|
@@ -1257,14 +1258,14 @@ const GBG_CMD_WINDSURF = 'bash scripts/trackfw-git-branch-guard.sh'
|
|
|
1257
1258
|
|
|
1258
1259
|
/** Mirrors Go's globalCredentialGuardScriptPath. */
|
|
1259
1260
|
function globalCredentialGuardScriptPath() {
|
|
1260
|
-
const home =
|
|
1261
|
+
const home = homedir()
|
|
1261
1262
|
if (!home) return null
|
|
1262
1263
|
return path.join(home, '.trackfw', 'scripts', 'trackfw-credential-guard.sh')
|
|
1263
1264
|
}
|
|
1264
1265
|
|
|
1265
1266
|
/** Reads+parses JSON at $HOME/<...relParts>; returns null on any failure (fail-open). */
|
|
1266
1267
|
function readGlobalHookJSON(...relParts) {
|
|
1267
|
-
const home =
|
|
1268
|
+
const home = homedir()
|
|
1268
1269
|
if (!home) return null
|
|
1269
1270
|
try {
|
|
1270
1271
|
const raw = fs.readFileSync(path.join(home, ...relParts), 'utf8')
|
|
@@ -1290,9 +1291,121 @@ function readGlobalHookJSON(...relParts) {
|
|
|
1290
1291
|
* (normalizeGuardPath) and pypi/trackfw/generators/hooks.py
|
|
1291
1292
|
* (_normalize_guard_path). Never call with anything other than a script
|
|
1292
1293
|
* path -- it is not a general string normalizer.
|
|
1294
|
+
*
|
|
1295
|
+
* ROADMAP-2026-09-03 ML-7B -- Windows separator canonicalization, gated on
|
|
1296
|
+
* anchoring, NOT a blanket "\\" -> "/" translate. On POSIX "\\" is a legal
|
|
1297
|
+
* filename byte, so translating it unconditionally would make two
|
|
1298
|
+
* genuinely different paths (one with a literal backslash in a segment
|
|
1299
|
+
* name, one with an extra path separator there) compare equal -- the exact
|
|
1300
|
+
* dangerous loosening this function's own doc comment warns against, and
|
|
1301
|
+
* the risk this ML was told to treat explicitly. The decision, per input
|
|
1302
|
+
* shape (mirrors the UNC/drive-letter arms of
|
|
1303
|
+
* internal/validator/pathIsAnchoredForHookConfig (Go) / index.js's
|
|
1304
|
+
* pathIsAnchoredForHookConfig, reimplemented locally -- not imported,
|
|
1305
|
+
* different question, and this module must not import the validator):
|
|
1306
|
+
*
|
|
1307
|
+
* - "C:\Users\x\guard.sh" / "C:/Users/x/guard.sh" (ASCII drive letter,
|
|
1308
|
+
* ":", then "\" or "/") -- CANONICALIZED: every "\" is translated to
|
|
1309
|
+
* "/" before the collapse below runs, so both forms land on the same
|
|
1310
|
+
* "C:/Users/x/guard.sh". This is the case ML-7A measured as the actual
|
|
1311
|
+
* trigger (a path.join-computed Windows path vs. a hand-concatenated
|
|
1312
|
+
* one).
|
|
1313
|
+
* - "\\servidor\share\guard.sh" (valid UNC: non-empty SERVER not "." or
|
|
1314
|
+
* "..", followed by a non-empty SHARE not itself starting with "\") --
|
|
1315
|
+
* UNCHANGED, byte-for-byte, including its backslashes. Translating it
|
|
1316
|
+
* would collapse "\\server\share" into "//server/share" and then this
|
|
1317
|
+
* function's own "//" -> "/" collapse would eat the second slash,
|
|
1318
|
+
* producing "/server/share/..." -- indistinguishable from a
|
|
1319
|
+
* single-leading-backslash, drive-root-relative path
|
|
1320
|
+
* ("\server\share\..." means something else on Windows) or from a
|
|
1321
|
+
* same-named POSIX path. That collision is precisely a false "already
|
|
1322
|
+
* installed": a network-share guard would dedup-match a local one.
|
|
1323
|
+
* Left untouched, a UNC command never cross-matches a non-UNC one -- no
|
|
1324
|
+
* new equality is introduced.
|
|
1325
|
+
* - "//servidor/share/guard.sh" (the POSIX-typed equivalent of UNC) --
|
|
1326
|
+
* UNCHANGED behavior from before this ML: it does not start with "\",
|
|
1327
|
+
* so it never enters the new branch above; it still collapses via the
|
|
1328
|
+
* existing "//" -> "/" rule below, same as any other POSIX path. Not
|
|
1329
|
+
* unified with the "\\servidor\share\..." form above (pre-existing
|
|
1330
|
+
* asymmetry, out of scope for this ML).
|
|
1331
|
+
* - "\\" and "\\x" alone (no SHARE segment) -- NOT valid UNC by the
|
|
1332
|
+
* predicate above, so they fall through unchanged: no drive letter, no
|
|
1333
|
+
* valid UNC, no translation.
|
|
1334
|
+
* - "C:foo" (drive-relative, no separator after ":") and homoglyph/
|
|
1335
|
+
* zero-width-prefixed strings (e.g. "\uFF43:\...", "\u200bC:\...") --
|
|
1336
|
+
* do NOT match the ASCII-only, position-0 drive-letter check, so no
|
|
1337
|
+
* translation happens. Same anti-spoofing posture as the validator's
|
|
1338
|
+
* drive-letter check.
|
|
1339
|
+
*
|
|
1340
|
+
* Known residual, documented not fixed (hades-tf ML-7B barrier review,
|
|
1341
|
+
* 2026-09-05 parecer): three real Windows-with-"\" shapes have no drive
|
|
1342
|
+
* letter at position 0, so hasWindowsDriveLetterPrefix's gate leaves them
|
|
1343
|
+
* uncanonicalized -- the same pre-ML-7A defect survives for them. Direction
|
|
1344
|
+
* is always TIGHTENS (possible duplicate hook entry), never loosens (the
|
|
1345
|
+
* guard is never silently skipped) -- same safe direction as every other
|
|
1346
|
+
* case above:
|
|
1347
|
+
*
|
|
1348
|
+
* - "\\?\C:\Users\x\guard.sh" (the Win32 long-path prefix; a real form
|
|
1349
|
+
* Windows/long-path APIs produce automatically, not a hypothetical).
|
|
1350
|
+
* - A relative path containing "\" (e.g. "guard\scripts\hook.sh"). Low
|
|
1351
|
+
* practical risk: today both sides of the real comparison always come
|
|
1352
|
+
* from path.join with an absolute home, so a relative command should
|
|
1353
|
+
* not reach this function -- but nothing in the comparator itself
|
|
1354
|
+
* prevents it.
|
|
1355
|
+
* - A home resolved via a network-profile UNC path (e.g.
|
|
1356
|
+
* "\\fileserver\homes\kg\.trackfw\scripts\..."). It never enters the
|
|
1357
|
+
* drive-letter branch and is intentionally left untouched (see the UNC
|
|
1358
|
+
* bullet above), so the original defect persists for both UNC
|
|
1359
|
+
* spellings, not just between UNC and drive-letter forms.
|
|
1360
|
+
*
|
|
1361
|
+
* Not fixed here on purpose: closing any of the three would touch the
|
|
1362
|
+
* drive-letter gate this barrier just approved as conservative, trading a
|
|
1363
|
+
* duplicate-entry nuisance for the more expensive failure mode (collapsing
|
|
1364
|
+
* genuinely different paths into a false "already installed"). Do not treat
|
|
1365
|
+
* rediscovering these three as a new finding.
|
|
1366
|
+
*/
|
|
1367
|
+
/**
|
|
1368
|
+
* Reports whether p begins with a Windows UNC prefix ("\\server\share...")
|
|
1369
|
+
* with a non-empty SERVER segment (not "." or "..") followed by a non-empty
|
|
1370
|
+
* SHARE segment that does not itself start with another backslash. Mirrors
|
|
1371
|
+
* the UNC arm of index.js's pathIsAnchoredForHookConfig -- reimplemented
|
|
1372
|
+
* here (not imported: different question, and this module must not import
|
|
1373
|
+
* the validator). "\\", "\\x" (no share segment) and "\\.\x" / "\\..\evil"
|
|
1374
|
+
* (server "." or "..") are NOT valid UNC. Currently unused by
|
|
1375
|
+
* normalizeGuardPath itself (a valid-UNC string never has a drive-letter
|
|
1376
|
+
* prefix, so the two checks are already mutually exclusive by construction)
|
|
1377
|
+
* -- kept as a named, tested predicate so the "UNC stays untouched"
|
|
1378
|
+
* decision above is verifiable by name, not just by absence of a call, same
|
|
1379
|
+
* as Go's hasValidUNCPrefix (internal/generators/agentfiles.go) and
|
|
1380
|
+
* Python's _has_valid_unc_prefix (pypi/trackfw/generators/hooks.py).
|
|
1381
|
+
* EXPORTED (unlike Go's package-private equivalent) only because
|
|
1382
|
+
* npm/tests/guard_path_normalize.test.js reaches it via
|
|
1383
|
+
* require('../src/generators/hooks') destructuring -- there is no
|
|
1384
|
+
* production caller. Do not build new logic against this export assuming
|
|
1385
|
+
* UNC gets active handling; it does not (see the doc comment above).
|
|
1293
1386
|
*/
|
|
1387
|
+
function hasValidUNCPrefix(p) {
|
|
1388
|
+
if (p.length < 2 || p[0] !== '\\' || p[1] !== '\\') return false
|
|
1389
|
+
const rest = p.slice(2)
|
|
1390
|
+
const cut = rest.indexOf('\\')
|
|
1391
|
+
if (cut === -1) return false
|
|
1392
|
+
const server = rest.slice(0, cut)
|
|
1393
|
+
const share = rest.slice(cut + 1)
|
|
1394
|
+
return server !== '' && server !== '.' && server !== '..' && share !== '' && share[0] !== '\\'
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function hasWindowsDriveLetterPrefix(p) {
|
|
1398
|
+
if (p.length < 3) return false
|
|
1399
|
+
const c = p.charCodeAt(0)
|
|
1400
|
+
const isASCIILetter = (c >= 97 && c <= 122) || (c >= 65 && c <= 90)
|
|
1401
|
+
return isASCIILetter && p[1] === ':' && (p[2] === '\\' || p[2] === '/')
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1294
1404
|
function normalizeGuardPath(p) {
|
|
1295
1405
|
if (!p) return p
|
|
1406
|
+
if (hasWindowsDriveLetterPrefix(p)) {
|
|
1407
|
+
p = p.split('\\').join('/')
|
|
1408
|
+
}
|
|
1296
1409
|
let out = ''
|
|
1297
1410
|
let prevSlash = false
|
|
1298
1411
|
for (const ch of p) {
|
|
@@ -1410,7 +1523,7 @@ function globalCredentialGuardInstalledCopilot() {
|
|
|
1410
1523
|
* explicit instruction for Kiro.
|
|
1411
1524
|
*/
|
|
1412
1525
|
function globalCredentialGuardInstalledKiro() {
|
|
1413
|
-
const home =
|
|
1526
|
+
const home = homedir()
|
|
1414
1527
|
if (!home) return false
|
|
1415
1528
|
try {
|
|
1416
1529
|
const stat = fs.statSync(path.join(home, '.kiro', 'hooks', 'trackfw-credential-guard.json'))
|
|
@@ -1437,7 +1550,7 @@ function globalCredentialGuardInstalledKiro() {
|
|
|
1437
1550
|
|
|
1438
1551
|
/** Mirrors Go's globalGitBranchGuardScriptPath. */
|
|
1439
1552
|
function globalGitBranchGuardScriptPath() {
|
|
1440
|
-
const home =
|
|
1553
|
+
const home = homedir()
|
|
1441
1554
|
if (!home) return null
|
|
1442
1555
|
return path.join(home, '.trackfw', 'scripts', 'trackfw-git-branch-guard.sh')
|
|
1443
1556
|
}
|
|
@@ -2264,6 +2377,8 @@ module.exports = {
|
|
|
2264
2377
|
mergeCopilotHookArray,
|
|
2265
2378
|
normalizeGuardPath,
|
|
2266
2379
|
samePathCommand,
|
|
2380
|
+
hasValidUNCPrefix,
|
|
2381
|
+
hasWindowsDriveLetterPrefix,
|
|
2267
2382
|
// Template constants — exported for scaffold doctor (ADR-2026-08-27).
|
|
2268
2383
|
// These are the single source of truth for each script's content; the
|
|
2269
2384
|
// scaffold doctor compares on-disk files against these exact bytes, using
|
package/src/generators/init.js
CHANGED
|
@@ -4,6 +4,17 @@ const fs = require('fs')
|
|
|
4
4
|
const path = require('path')
|
|
5
5
|
const os = require('os')
|
|
6
6
|
const { readAgentConventions } = require('../config/index.js')
|
|
7
|
+
const { homedir } = require('../homedir');
|
|
8
|
+
|
|
9
|
+
// PACKAGE_VERSION — the version of the trackfw binary generating the CI
|
|
10
|
+
// workflow templates. Read once from package.json (never a literal), so
|
|
11
|
+
// the pin written into the workflow always tracks the generator that
|
|
12
|
+
// wrote it (ADR-2026-08-28: gate de CI nasce pinado na versão que o
|
|
13
|
+
// gerou). Reused by buildGitHubActionsWorkflowContent /
|
|
14
|
+
// buildGitLabCIWorkflowContent AND by scaffold doctor's comparison
|
|
15
|
+
// (which calls the same functions), so drift between write and compare
|
|
16
|
+
// paths is structurally impossible.
|
|
17
|
+
const { version: PACKAGE_VERSION } = require('../../package.json')
|
|
7
18
|
|
|
8
19
|
const GOV_DIRS = [
|
|
9
20
|
'docs/adr',
|
|
@@ -31,6 +42,7 @@ async function scaffold(cfg, cwd) {
|
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
generateVaultIndex(root)
|
|
45
|
+
generateGitAttributes(root)
|
|
34
46
|
writeTrackfwConfig(cfg, root)
|
|
35
47
|
generateValidateScript(cfg, root)
|
|
36
48
|
generateAttentionScripts(cfg, root)
|
|
@@ -45,6 +57,60 @@ async function scaffold(cfg, cwd) {
|
|
|
45
57
|
injectHooksDetected(root)
|
|
46
58
|
}
|
|
47
59
|
|
|
60
|
+
// gitAttributesRuleTarget — primeiro campo da regra mantida por este gerador.
|
|
61
|
+
// Casa o BASENAME (padrão sem barra) de propósito: `roadmap_dir` e `req_dir` são
|
|
62
|
+
// configuráveis por projeto (trackfw.yaml) e ambos carregam um `.trackfw-log`,
|
|
63
|
+
// então caminho fixo nasceria quebrado em quem configurou diretório diferente.
|
|
64
|
+
const GITATTRIBUTES_RULE_TARGET = '.trackfw-log'
|
|
65
|
+
|
|
66
|
+
// GITATTRIBUTES_BLOCK — byte-idêntico ao de internal/generators/scaffold.go e
|
|
67
|
+
// pypi/trackfw/generators/init_gen.py (regra dura de paridade).
|
|
68
|
+
const GITATTRIBUTES_BLOCK = `# trackfw: .trackfw-log is append-only — every write lands on the last line, so
|
|
69
|
+
# two parallel branches conflict on every merge. merge=union keeps the lines
|
|
70
|
+
# from both sides (chronological order is not guaranteed). The pattern has no
|
|
71
|
+
# slash, so it matches the file in any directory — roadmap_dir and req_dir both
|
|
72
|
+
# carry one, and both are configurable per project.
|
|
73
|
+
.trackfw-log merge=union
|
|
74
|
+
`
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* hasGitAttributesRule — verdadeiro quando ALGUMA linha não-comentário tem
|
|
78
|
+
* `.trackfw-log` como primeiro campo. Predicado sobre o CAMPO, não sobre a
|
|
79
|
+
* string literal da linha inteira: espaçamento diferente ou outro atributo já
|
|
80
|
+
* são "a regra existe", e reescrever por cima sobrescreveria decisão do projeto.
|
|
81
|
+
* @param {string} content
|
|
82
|
+
*/
|
|
83
|
+
function hasGitAttributesRule(content) {
|
|
84
|
+
for (const line of content.split('\n')) {
|
|
85
|
+
const trimmed = line.trim()
|
|
86
|
+
if (trimmed === '' || trimmed.startsWith('#')) continue
|
|
87
|
+
if (trimmed.split(/\s+/)[0] === GITATTRIBUTES_RULE_TARGET) return true
|
|
88
|
+
}
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* generateGitAttributes — garante `merge=union` para o `.trackfw-log` no
|
|
94
|
+
* `.gitattributes` da raiz. Três ramos, todos idempotentes: ausente → cria;
|
|
95
|
+
* existe sem a regra → APPEND (nunca sobrescreve); existe com a regra → no-op.
|
|
96
|
+
* @param {string} root
|
|
97
|
+
*/
|
|
98
|
+
function generateGitAttributes(root) {
|
|
99
|
+
const filePath = path.join(root || process.cwd(), '.gitattributes')
|
|
100
|
+
if (!fs.existsSync(filePath)) {
|
|
101
|
+
fs.writeFileSync(filePath, GITATTRIBUTES_BLOCK, 'utf8')
|
|
102
|
+
console.log(' ✓ .gitattributes')
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
const existing = fs.readFileSync(filePath, 'utf8')
|
|
106
|
+
if (hasGitAttributesRule(existing)) return
|
|
107
|
+
// Arquivo preexistente sem newline final: emendar direto grudaria a primeira
|
|
108
|
+
// linha do bloco na última linha do projeto, corrompendo a config em silêncio.
|
|
109
|
+
const prefix = existing !== '' && !existing.endsWith('\n') ? existing + '\n' : existing
|
|
110
|
+
fs.writeFileSync(filePath, prefix + GITATTRIBUTES_BLOCK, 'utf8')
|
|
111
|
+
console.log(' ✓ .gitattributes')
|
|
112
|
+
}
|
|
113
|
+
|
|
48
114
|
/**
|
|
49
115
|
* generateVaultIndex — cria vault/notes/index.md se ainda não existir.
|
|
50
116
|
* @param {string} root
|
|
@@ -209,7 +275,18 @@ function generateCIWorkflow(cfg) {
|
|
|
209
275
|
// workflow generator would write. Exported for scaffold doctor (ADR-2026-08-27): the
|
|
210
276
|
// comparison uses the same function the write path uses, so drift is structurally impossible.
|
|
211
277
|
// The cfg parameter is accepted for API symmetry with Go's buildGitHubActionsWorkflowContent
|
|
212
|
-
// (which also ignores cfg for now — the template is
|
|
278
|
+
// (which also ignores cfg for now — the template is cfg-independent but, since
|
|
279
|
+
// ADR-2026-08-28, NOT version-independent: it pins TRACKFW_VERSION to the version of the
|
|
280
|
+
// trackfw binary that generated it, read from package.json — never a literal).
|
|
281
|
+
//
|
|
282
|
+
// Job id is `governance-install-script` (ML-1A, ROADMAP-2026-09-01): was `governance`,
|
|
283
|
+
// colliding with the discover-installed sibling workflow's job id (trackfw-validate.yml,
|
|
284
|
+
// buildDiscoverGitHubActionsWorkflowContent in commands/discover.js) — both previously
|
|
285
|
+
// `governance`, producing two identically-named check-runs on any PR of a project with
|
|
286
|
+
// both workflows installed, which makes required_status_checks ambiguous (GitHub
|
|
287
|
+
// matches by name). Named after the install mechanism (install.sh here, `go install`
|
|
288
|
+
// there) rather than the workflow file, since that's what a required_status_checks
|
|
289
|
+
// reader needs to tell the two apart without opening the YAML.
|
|
213
290
|
function buildGitHubActionsWorkflowContent(_cfg) {
|
|
214
291
|
return `name: trackfw-gate
|
|
215
292
|
on:
|
|
@@ -217,8 +294,11 @@ on:
|
|
|
217
294
|
branches: [main]
|
|
218
295
|
|
|
219
296
|
jobs:
|
|
220
|
-
governance:
|
|
297
|
+
governance-install-script:
|
|
221
298
|
runs-on: ubuntu-latest
|
|
299
|
+
timeout-minutes: 10
|
|
300
|
+
env:
|
|
301
|
+
TRACKFW_VERSION: "${PACKAGE_VERSION}"
|
|
222
302
|
steps:
|
|
223
303
|
- uses: actions/checkout@v4
|
|
224
304
|
|
|
@@ -232,11 +312,15 @@ jobs:
|
|
|
232
312
|
}
|
|
233
313
|
|
|
234
314
|
// buildGitLabCIWorkflowContent mirrors buildGitHubActionsWorkflowContent for GitLab CI.
|
|
315
|
+
// Same version-pin rationale as above — see buildGitHubActionsWorkflowContent.
|
|
235
316
|
function buildGitLabCIWorkflowContent(_cfg) {
|
|
236
317
|
return `# trackfw governance gate
|
|
237
318
|
trackfw-gate:
|
|
238
319
|
stage: test
|
|
239
320
|
image: alpine:latest
|
|
321
|
+
timeout: 10 minutes
|
|
322
|
+
variables:
|
|
323
|
+
TRACKFW_VERSION: "${PACKAGE_VERSION}"
|
|
240
324
|
before_script:
|
|
241
325
|
- apk add --no-cache curl
|
|
242
326
|
- curl -sSfL https://github.com/kgsaran/trackfw/releases/latest/download/install.sh | sh
|
|
@@ -333,9 +417,12 @@ function generateCommitMsgHook(cfg) {
|
|
|
333
417
|
// ---------------------------------------------------------------------------
|
|
334
418
|
|
|
335
419
|
function generatePomXml(cfg) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
420
|
+
// Usa o toSlug compartilhado em vez de uma variante inline: a versao anterior nao
|
|
421
|
+
// fazia NFKD e PERDIA a letra acentuada — "Cafe App" (com acento) virava "caf-app"
|
|
422
|
+
// aqui e "cafe-app" no Go, que usa o toSlug dele. Ver a secao
|
|
423
|
+
// "Artifact slug contract" em docs/cli-parity.md.
|
|
424
|
+
const { toSlug } = require('./adr')
|
|
425
|
+
const slug = cfg.projectName ? toSlug(cfg.projectName) : 'my-app'
|
|
339
426
|
const name = cfg.projectName || 'My App'
|
|
340
427
|
const content = `<?xml version="1.0" encoding="UTF-8"?>
|
|
341
428
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
|
@@ -789,6 +876,13 @@ function backendCommands(cfg) {
|
|
|
789
876
|
// (missing roadmap.md, implement.md, barrier.md) undetected.
|
|
790
877
|
// ---------------------------------------------------------------------------
|
|
791
878
|
|
|
879
|
+
// CLAUDE_COMMANDS "trackfw não está instalado" blurbs (AC13) are deliberately
|
|
880
|
+
// OUT of the version pin introduced by ADR-2026-08-28. This text is shown when
|
|
881
|
+
// the `trackfw` command fails because it is not installed — recommending the
|
|
882
|
+
// exact old version that generated the project would be worse UX than
|
|
883
|
+
// pointing at `releases/latest`, since the reason to (re)install is usually
|
|
884
|
+
// being outdated. Do not pin these on a future pass without a product
|
|
885
|
+
// decision; see ROADMAP-2026-08-28 Wave 0 §3 (AC13 row).
|
|
792
886
|
const CLAUDE_COMMANDS = {
|
|
793
887
|
'adr.md': `Execute o seguinte comando bash: \`trackfw adr new "$ARGUMENTS"\`
|
|
794
888
|
|
|
@@ -1304,7 +1398,7 @@ async function installAmazonQ(cwd = process.cwd()) {
|
|
|
1304
1398
|
async function installIntegrationTarget(target, cwd = process.cwd(), scope = 'project', { onSkip } = {}) {
|
|
1305
1399
|
const { execute, buildPlans } = require('../integrations')
|
|
1306
1400
|
const { resolveAgentModels } = require('../config')
|
|
1307
|
-
const { models: agentModels, warning: agentModelsWarning } = resolveAgentModels(scope,
|
|
1401
|
+
const { models: agentModels, warning: agentModelsWarning } = resolveAgentModels(scope, homedir(), cwd)
|
|
1308
1402
|
if (agentModelsWarning) process.stderr.write(agentModelsWarning + '\n')
|
|
1309
1403
|
const roots = { projectRoot: cwd }
|
|
1310
1404
|
const options = { targets: [target], scope, onSkip, agentModels }
|
|
@@ -1340,7 +1434,7 @@ function generateClaudeCommandsForce(rootDir) {
|
|
|
1340
1434
|
* installSkillsForce — sobrescreve ~/.claude/skills/trackfw/SKILL.md sempre.
|
|
1341
1435
|
*/
|
|
1342
1436
|
function installSkillsForce() {
|
|
1343
|
-
const skillDir = path.join(
|
|
1437
|
+
const skillDir = path.join(homedir(), '.claude', 'skills', 'trackfw')
|
|
1344
1438
|
fs.mkdirSync(skillDir, { recursive: true })
|
|
1345
1439
|
|
|
1346
1440
|
const content = `---
|
|
@@ -1418,6 +1512,8 @@ module.exports = {
|
|
|
1418
1512
|
generateClaudeCommands,
|
|
1419
1513
|
generateClaudeCommandsForce,
|
|
1420
1514
|
generateVaultIndex,
|
|
1515
|
+
generateGitAttributes,
|
|
1516
|
+
GITATTRIBUTES_BLOCK,
|
|
1421
1517
|
installSkillsForce,
|
|
1422
1518
|
installAgents,
|
|
1423
1519
|
installGemini,
|
package/src/generators/req.js
CHANGED
|
@@ -4,66 +4,21 @@ const path = require('path')
|
|
|
4
4
|
const { localDateISO } = require('./date')
|
|
5
5
|
const roadmapGen = require('./roadmap')
|
|
6
6
|
const config = require('../config')
|
|
7
|
+
const { resolveAgentNamespaces, resolveReqFiles, reqWriteDir } = require('../validator')
|
|
8
|
+
const { normalizeCRLF } = require('../integrations/render')
|
|
7
9
|
|
|
8
10
|
const VALID_STATES = roadmapGen.VALID_STATES
|
|
9
11
|
const STATE_ORDER = roadmapGen.STATE_ORDER
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
|
-
* listREQFiles —
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* Os três conjuntos não são mutuamente exclusivos — concatena todos.
|
|
14
|
+
* listREQFiles — NÃO reimplementa a descoberta: delega ao ponto único de leitura do validador
|
|
15
|
+
* (resolveReqFiles — ADR-2026-09-03, D3/D4), o mesmo consumido pelas regras de validate. Duas
|
|
16
|
+
* noções de layout no mesmo runtime foram a causa do defeito da REQ-2026-08-30.
|
|
16
17
|
* @param {object} cfg — config completo (ver npm/src/config)
|
|
17
|
-
* @returns {string[]} paths completos,
|
|
18
|
+
* @returns {string[]} paths completos, união dos 4 layouts, deduplicados e ordenados
|
|
18
19
|
*/
|
|
19
20
|
function listREQFiles(cfg) {
|
|
20
|
-
|
|
21
|
-
const files = []
|
|
22
|
-
|
|
23
|
-
// (a) flat legado — reqDir/*.md
|
|
24
|
-
try {
|
|
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
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return files
|
|
21
|
+
return resolveReqFiles(cfg)
|
|
67
22
|
}
|
|
68
23
|
|
|
69
24
|
/**
|
|
@@ -114,7 +69,11 @@ function parseREQStatus(filepath) {
|
|
|
114
69
|
return 'unknown'
|
|
115
70
|
}
|
|
116
71
|
|
|
72
|
+
// D1/D3 (ADR-2026-09-04-parser-de-frontmatter-tolera-crlf-na-fronteira-de-entrada, ML-5B):
|
|
73
|
+
// normaliza CRLF -> LF via normalizeCRLF na própria entrada, antes de qualquer casamento de
|
|
74
|
+
// "---\n" — reaproveita a única implementação Node, sem criar uma segunda cópia.
|
|
117
75
|
function rewriteREQStatus(source, status) {
|
|
76
|
+
source = normalizeCRLF(String(source))
|
|
118
77
|
if (!source.startsWith('---\n')) return { content: source, changed: false }
|
|
119
78
|
const end = source.slice(4).indexOf('\n---')
|
|
120
79
|
if (end < 0) return { content: source, changed: false }
|
|
@@ -286,7 +245,10 @@ function toSlug(s) {
|
|
|
286
245
|
* @returns {Promise<void>}
|
|
287
246
|
*/
|
|
288
247
|
async function newREQ(content) {
|
|
289
|
-
|
|
248
|
+
// Ponto único de decisão de caminho de ESCRITA (ADR-2026-09-03, D2/D4): by_agent grava no
|
|
249
|
+
// canônico req_dir/<agente>/; flat grava em req_dir/ — o mesmo ponto que alimenta a união de leitura.
|
|
250
|
+
const cfg = require('../config').load()
|
|
251
|
+
const reqDir = reqWriteDir(cfg) || cfg.reqDir
|
|
290
252
|
fs.mkdirSync(reqDir, { recursive: true })
|
|
291
253
|
|
|
292
254
|
const slug = toSlug(content.title)
|