trackfw 6.9.1 → 7.0.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 +0 -3
- package/package.json +1 -1
- package/src/branch/runner.js +33 -20
- package/src/changelog/index.js +114 -0
- package/src/commands/branch.js +12 -9
- package/src/commands/changelog.js +29 -0
- package/src/commands/commit.js +60 -0
- package/src/commands/discover.js +69 -0
- package/src/commands/index.js +22 -4
- package/src/commands/integrations.js +7 -0
- package/src/commands/ship.js +4 -3
- package/src/commands/thirdparty.js +384 -0
- package/src/commands/update-harness.js +3 -1
- package/src/commands/update.js +3 -1
- package/src/commit/runner.js +352 -0
- package/src/config/index.js +27 -0
- package/src/generators/hooks.js +479 -2
- package/src/generators/init.js +28 -6
- package/src/i18n/locales/en-US.json +1 -19
- package/src/i18n/locales/es-ES.json +1 -19
- package/src/i18n/locales/pt-BR.json +1 -19
- package/src/integrations/index.js +19 -2
- package/src/integrations/manager.js +15 -2
- package/src/integrations/render.js +102 -4
- package/src/lib/unknown-command.js +82 -0
- package/src/ship/runner.js +213 -26
- package/src/thirdparty/fetch.js +140 -0
- package/src/thirdparty/markers.js +207 -0
- package/src/thirdparty/provenance.js +114 -0
- package/src/thirdparty/quarantine.js +113 -0
- package/src/thirdparty/references.js +194 -0
- package/src/validator/index.js +715 -16
- package/src/commands/plugins.js +0 -184
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* commit/runner.js — Core implementation of `trackfw commit`.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors internal/commands/commit.go byte-for-byte in behavior and message text — Go is the
|
|
7
|
+
* behavioral reference (docs/cli-parity.md: "Go is the behavioral reference"). Reuses the same
|
|
8
|
+
* governance matching primitives already shared by `trackfw branch new` (../branch/runner.js) and
|
|
9
|
+
* `trackfw validate`/`trackfw ship` (../validator): validator.branchSlugMatchesRoadmap,
|
|
10
|
+
* validator.branchGovernanceOrientation, validator.branchNoMatchingRoadmapMessage,
|
|
11
|
+
* validator.normalizeBranchSlug — never duplicate this logic.
|
|
12
|
+
*
|
|
13
|
+
* Mirrors Go's stdout/stderr split exactly (internal/commands/commit.go + root.go Execute()):
|
|
14
|
+
* the primary governance message (block reason, or the housekeeping-branch warning) goes to
|
|
15
|
+
* stdout via `writeln`; the short "blocked: ..." error / usage error that Execute() prints from
|
|
16
|
+
* the returned error goes to stderr via `writeErr`.
|
|
17
|
+
*
|
|
18
|
+
* All git operations are injectable for testability — never runs a real `git commit` in unit
|
|
19
|
+
* tests.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const { spawnSync } = require('child_process')
|
|
23
|
+
const config = require('../config')
|
|
24
|
+
const validator = require('../validator')
|
|
25
|
+
|
|
26
|
+
// commitProtectedBranches lists branches where a direct `trackfw commit` is never allowed,
|
|
27
|
+
// regardless of governance state — mirrors the same hard rule `trackfw ship` already enforces.
|
|
28
|
+
// Mirrors Go's commitProtectedBranches: literal "main"/"master" only, no dynamic resolution of
|
|
29
|
+
// the repo's configured default branch.
|
|
30
|
+
const COMMIT_PROTECTED_BRANCHES = new Set(['main', 'master'])
|
|
31
|
+
|
|
32
|
+
// commitGovernedPrefixes lists the branch-type prefixes that require a matching roadmap in
|
|
33
|
+
// wip/ or done/ before a commit is allowed — the same vocabulary `trackfw branch new` and the
|
|
34
|
+
// branch_has_wip_roadmap governance rule already enforce. Mirrors Go's commitGovernedPrefixes.
|
|
35
|
+
const COMMIT_GOVERNED_PREFIXES = ['feat/', 'fix/', 'refactor/']
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* defaultExecGit runs `git <args...>` and returns { stdout, error }. Mirrors
|
|
39
|
+
* ship/runner.js's defaultExecGit (same pattern, kept local here to avoid a cross-module runtime
|
|
40
|
+
* dependency for a single helper).
|
|
41
|
+
* @param {string[]} args
|
|
42
|
+
* @returns {{ stdout: string, error: Error|null }}
|
|
43
|
+
*/
|
|
44
|
+
function defaultExecGit(args) {
|
|
45
|
+
const result = spawnSync('git', args, { encoding: 'utf8' })
|
|
46
|
+
if (result.status !== 0) {
|
|
47
|
+
const msg = (result.stderr || '').trim() || `git ${args.join(' ')} exited with ${result.status}`
|
|
48
|
+
return { stdout: '', error: new Error(msg) }
|
|
49
|
+
}
|
|
50
|
+
return { stdout: (result.stdout || '').trim(), error: null }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* defaultStagedNameStatus runs `git diff --cached --name-status` and returns its raw output.
|
|
55
|
+
* Mirrors Go's defaultStagedNameStatus (internal/commands/commit.go), which reuses the same git
|
|
56
|
+
* subcommand. Used only by buildSuggestedMessage — never touched by the normal `-m` commit flow.
|
|
57
|
+
* @param {function(string[]): {stdout: string, error: Error|null}} execGit
|
|
58
|
+
* @returns {string}
|
|
59
|
+
* @throws {Error} when git fails (e.g. not a git repo)
|
|
60
|
+
*/
|
|
61
|
+
function defaultStagedNameStatus(execGit) {
|
|
62
|
+
const result = execGit(['diff', '--cached', '--name-status'])
|
|
63
|
+
if (result.error) throw result.error
|
|
64
|
+
return result.stdout
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* defaultCurrentBranch reads the current branch via `git rev-parse --abbrev-ref HEAD` — mirrors
|
|
69
|
+
* Go's defaultCurrentBranch (internal/commands/commit.go), which reuses the same git subcommand.
|
|
70
|
+
* @param {function(string[]): {stdout: string, error: Error|null}} execGit
|
|
71
|
+
* @returns {string}
|
|
72
|
+
* @throws {Error} when git fails (e.g. not a git repo)
|
|
73
|
+
*/
|
|
74
|
+
function defaultCurrentBranch(execGit) {
|
|
75
|
+
const result = execGit(['rev-parse', '--abbrev-ref', 'HEAD'])
|
|
76
|
+
if (result.error) throw result.error
|
|
77
|
+
return result.stdout.trim()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* defaultGitCommit runs `git commit -m <message>` with inherited stdio, so Git's own output
|
|
82
|
+
* reaches the user unmodified. Returns Git's own numeric exit code (0 on success), never a
|
|
83
|
+
* hardcoded 1 — same contract as branch/runner.js's defaultGitCheckout.
|
|
84
|
+
* @param {string} message
|
|
85
|
+
* @returns {number} exit code
|
|
86
|
+
*/
|
|
87
|
+
function defaultGitCommit(message) {
|
|
88
|
+
const result = spawnSync('git', ['commit', '-m', message], { stdio: 'inherit' })
|
|
89
|
+
if (result.error) return 1
|
|
90
|
+
if (result.status === null) return 1
|
|
91
|
+
return result.status
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* commitGovernedBranchPrefix returns the matched prefix (e.g. "feat/") when branch starts with
|
|
96
|
+
* one of COMMIT_GOVERNED_PREFIXES, or null otherwise. Mirrors Go's commitGovernedBranchPrefix.
|
|
97
|
+
* @param {string} branch
|
|
98
|
+
* @returns {string|null}
|
|
99
|
+
*/
|
|
100
|
+
function commitGovernedBranchPrefix(branch) {
|
|
101
|
+
for (const prefix of COMMIT_GOVERNED_PREFIXES) {
|
|
102
|
+
if (branch.startsWith(prefix)) return prefix
|
|
103
|
+
}
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* isCommitGatedBranch returns true when branch matches feat|fix|refactor/<slug> — the branches
|
|
109
|
+
* this command requires a matching wip/done roadmap for.
|
|
110
|
+
* @param {string} branch
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
function isCommitGatedBranch(branch) {
|
|
114
|
+
return commitGovernedBranchPrefix(branch) !== null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* runCommit implements the `trackfw commit -m "<message>"` flow described in ML-2A/ML-2B of
|
|
119
|
+
* docs/roadmaps/wip/ROADMAP-2026-08-14-bloqueio-tecnico-de-comandos-git-brutos-por-subagente-via-deny-hooks-nos-7-runtimes-suportados.md.
|
|
120
|
+
*
|
|
121
|
+
* 0. Requires a non-empty message (mirrors Go's cobra RunE pre-check, before touching git).
|
|
122
|
+
* 1. Reads the current branch. On "main"/"master": always blocked.
|
|
123
|
+
* 2. On a feat/fix/refactor branch: requires a matching roadmap in wip/ or done/ — the same
|
|
124
|
+
* matching logic `trackfw branch new` already uses. No match: blocks with the same
|
|
125
|
+
* governance orientation message `trackfw validate` already prints for
|
|
126
|
+
* branch_has_wip_roadmap.
|
|
127
|
+
* 3. Any other branch shape: allowed, only logs a warning — orchestration/housekeeping
|
|
128
|
+
* branches are not gated by this rule.
|
|
129
|
+
* 4. If it passed (1)-(3): runs `git commit -m <message>`, propagating Git's own output and
|
|
130
|
+
* exit status literally.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} message commit message (-m value)
|
|
133
|
+
* @param {{ loadConfig?: function, resolveWIPDirs?: function, resolveDoneDirs?: function,
|
|
134
|
+
* matchSlug?: function(string, string[], string[]): {matched: boolean, candidates: string[]},
|
|
135
|
+
* execGit?: function(string[]): {stdout: string, error: Error|null},
|
|
136
|
+
* currentBranch?: function(): string,
|
|
137
|
+
* execGitCommit?: function(string): number, writeln?: function(string): void,
|
|
138
|
+
* writeErr?: function(string): void }} deps
|
|
139
|
+
* @returns {number} exit code (0 = success, 1 = blocked/usage error, or Git's own exit code when
|
|
140
|
+
* `git commit` fails — propagated literally, never hardcoded)
|
|
141
|
+
*/
|
|
142
|
+
function runCommit(message, deps = {}) {
|
|
143
|
+
const loadConfig = deps.loadConfig || config.load
|
|
144
|
+
const resolveWIPDirs = deps.resolveWIPDirs || validator.resolveWIPDirs
|
|
145
|
+
const resolveDoneDirs = deps.resolveDoneDirs || validator.resolveDoneDirs
|
|
146
|
+
const matchSlug = deps.matchSlug || validator.branchSlugMatchesRoadmap
|
|
147
|
+
const execGit = deps.execGit || defaultExecGit
|
|
148
|
+
const currentBranch = deps.currentBranch || (() => defaultCurrentBranch(execGit))
|
|
149
|
+
const execGitCommit = deps.execGitCommit || defaultGitCommit
|
|
150
|
+
const writeln = deps.writeln || ((s) => process.stdout.write(s + '\n'))
|
|
151
|
+
const writeErr = deps.writeErr || ((s) => process.stderr.write(s + '\n'))
|
|
152
|
+
|
|
153
|
+
if (!message || String(message).trim() === '') {
|
|
154
|
+
writeErr('commit message is required — use -m:\n trackfw commit -m "feat(<scope>): <description>"')
|
|
155
|
+
return 1
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let branch
|
|
159
|
+
try {
|
|
160
|
+
branch = currentBranch()
|
|
161
|
+
} catch (err) {
|
|
162
|
+
writeErr(`could not determine current branch (are you in a git repo?): ${err.message}`)
|
|
163
|
+
return 1
|
|
164
|
+
}
|
|
165
|
+
branch = String(branch || '').trim()
|
|
166
|
+
|
|
167
|
+
// (a) main/master: always blocked.
|
|
168
|
+
if (COMMIT_PROTECTED_BRANCHES.has(branch)) {
|
|
169
|
+
const msg = `trackfw commit: commit direto em "${branch}" não é permitido. Use 'trackfw branch new <type>/<slug>' primeiro. Ver CLAUDE.md §1.`
|
|
170
|
+
writeln(msg)
|
|
171
|
+
writeErr(`blocked: commit directly on "${branch}" is not permitted`)
|
|
172
|
+
return 1
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// (b) feat/fix/refactor: require a matching roadmap in wip/ or done/.
|
|
176
|
+
const governedPrefix = commitGovernedBranchPrefix(branch)
|
|
177
|
+
if (governedPrefix) {
|
|
178
|
+
const cfg = loadConfig()
|
|
179
|
+
const wipDirs = resolveWIPDirs(cfg)
|
|
180
|
+
const doneDirs = resolveDoneDirs(cfg)
|
|
181
|
+
const slug = branch.slice(governedPrefix.length)
|
|
182
|
+
const normalizedSlug = validator.normalizeBranchSlug(slug)
|
|
183
|
+
const { matched, candidates } = matchSlug(normalizedSlug, wipDirs, doneDirs)
|
|
184
|
+
|
|
185
|
+
if (!matched) {
|
|
186
|
+
const msg = (!candidates || candidates.length === 0)
|
|
187
|
+
? validator.branchGovernanceOrientation(branch)
|
|
188
|
+
: validator.branchNoMatchingRoadmapMessage(branch, candidates)
|
|
189
|
+
writeln(msg)
|
|
190
|
+
writeErr(`blocked: no matching roadmap in wip/ nor done/ for "${branch}"`)
|
|
191
|
+
return 1
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
// (c) branches outside the feat/fix/refactor pattern (e.g. doc/housekeeping branches):
|
|
195
|
+
// allow without requiring a roadmap, but warn.
|
|
196
|
+
writeln(`trackfw commit: branch "${branch}" does not follow feat/fix/refactor — committing without a roadmap check.`)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// (d) passed all checks: commit. Git's own stdio (inherited) already reached the user
|
|
200
|
+
// unmodified — do not reformat, duplicate, or replace this exit code with a generic 1.
|
|
201
|
+
return execGitCommit(message)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// commitCommandDirs lists the directories (across the 3 supported CLIs) where a new (status "A")
|
|
205
|
+
// file signals a new CLI command was added — used by the "feat" heuristic rule below. Mirrors
|
|
206
|
+
// Go's commitCommandDirs.
|
|
207
|
+
const COMMIT_COMMAND_DIRS = ['internal/commands/', 'npm/src/commands/', 'pypi/trackfw/commands/']
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* parseStagedNameStatus parses raw `git diff --cached --name-status` output (tab-separated
|
|
211
|
+
* "<status>\t<path>" lines) into { status, path } entries, skipping blank lines. Mirrors Go's
|
|
212
|
+
* parseStagedNameStatus.
|
|
213
|
+
* @param {string} raw
|
|
214
|
+
* @returns {{status: string, path: string}[]}
|
|
215
|
+
*/
|
|
216
|
+
function parseStagedNameStatus(raw) {
|
|
217
|
+
const files = []
|
|
218
|
+
for (const rawLine of String(raw || '').split('\n')) {
|
|
219
|
+
const line = rawLine.replace(/\r+$/, '')
|
|
220
|
+
if (line.trim() === '') continue
|
|
221
|
+
const tabIdx = line.indexOf('\t')
|
|
222
|
+
if (tabIdx === -1) continue
|
|
223
|
+
const status = line.slice(0, tabIdx).trim()
|
|
224
|
+
const path = line.slice(tabIdx + 1)
|
|
225
|
+
files.push({ status, path })
|
|
226
|
+
}
|
|
227
|
+
return files
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* isTestFile reports whether path matches one of the recognized test-file naming conventions
|
|
232
|
+
* across the 3 supported stacks: *_test.go, *.test.js, test_*.py, *_test.py. Mirrors Go's
|
|
233
|
+
* isTestFile.
|
|
234
|
+
* @param {string} path
|
|
235
|
+
* @returns {boolean}
|
|
236
|
+
*/
|
|
237
|
+
function isTestFile(path) {
|
|
238
|
+
const idx = path.lastIndexOf('/')
|
|
239
|
+
const base = idx === -1 ? path : path.slice(idx + 1)
|
|
240
|
+
if (base.endsWith('_test.go')) return true
|
|
241
|
+
if (base.endsWith('.test.js')) return true
|
|
242
|
+
if (base.startsWith('test_') && base.endsWith('.py')) return true
|
|
243
|
+
if (base.endsWith('_test.py')) return true
|
|
244
|
+
return false
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* isDocsFile reports whether path lives under docs/ or vault/, or has a .md extension. Mirrors
|
|
249
|
+
* Go's isDocsFile.
|
|
250
|
+
* @param {string} path
|
|
251
|
+
* @returns {boolean}
|
|
252
|
+
*/
|
|
253
|
+
function isDocsFile(path) {
|
|
254
|
+
if (path.startsWith('docs/') || path.startsWith('vault/')) return true
|
|
255
|
+
return path.endsWith('.md')
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* isUnderAnyDir reports whether path starts with any of the given directory prefixes. Mirrors
|
|
260
|
+
* Go's isUnderAnyDir.
|
|
261
|
+
* @param {string} path
|
|
262
|
+
* @param {string[]} dirs
|
|
263
|
+
* @returns {boolean}
|
|
264
|
+
*/
|
|
265
|
+
function isUnderAnyDir(path, dirs) {
|
|
266
|
+
return dirs.some((d) => path.startsWith(d))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* suggestedCommitType returns the Conventional Commits type suggested for a set of staged files,
|
|
271
|
+
* following the fixed-priority heuristic documented in ML-1A/ML-2A of
|
|
272
|
+
* docs/roadmaps/wip/ROADMAP-2026-08-15-trackfw-commit-sugere-mensagem-de-commit-a-partir-do-diff-staged.md
|
|
273
|
+
* (first matching rule wins — this is a deliberately simple heuristic, not an attempt at perfect
|
|
274
|
+
* classification). Mirrors Go's suggestedCommitType.
|
|
275
|
+
* 1. every staged file matches a test-file pattern -> "test"
|
|
276
|
+
* 2. every staged file is under docs/ or vault/, or has a .md extension -> "docs"
|
|
277
|
+
* 3. at least one new ("A") file lives under one of COMMIT_COMMAND_DIRS -> "feat"
|
|
278
|
+
* 4. otherwise -> "fix"
|
|
279
|
+
* @param {{status: string, path: string}[]} files
|
|
280
|
+
* @returns {string}
|
|
281
|
+
*/
|
|
282
|
+
function suggestedCommitType(files) {
|
|
283
|
+
let allTests = true
|
|
284
|
+
let allDocs = true
|
|
285
|
+
let hasNewCommandFile = false
|
|
286
|
+
|
|
287
|
+
for (const f of files) {
|
|
288
|
+
if (!isTestFile(f.path)) allTests = false
|
|
289
|
+
if (!isDocsFile(f.path)) allDocs = false
|
|
290
|
+
if (f.status === 'A' && isUnderAnyDir(f.path, COMMIT_COMMAND_DIRS)) hasNewCommandFile = true
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (allTests) return 'test'
|
|
294
|
+
if (allDocs) return 'docs'
|
|
295
|
+
if (hasNewCommandFile) return 'feat'
|
|
296
|
+
return 'fix'
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* buildSuggestedMessage implements `trackfw commit --suggest`: it reads the staged diff via
|
|
301
|
+
* deps.stagedNameStatus, classifies it with suggestedCommitType, and renders the heuristic
|
|
302
|
+
* Conventional Commits skeleton described in ML-1A/ML-2A. It never calls deps.execGitCommit — no
|
|
303
|
+
* commit ever happens as a side effect of this function. Mirrors Go's buildSuggestedMessage
|
|
304
|
+
* byte-for-byte in output template.
|
|
305
|
+
* @param {{ stagedNameStatus?: function(): string }} deps
|
|
306
|
+
* @returns {string}
|
|
307
|
+
* @throws {Error} when staged files cannot be read, or nothing is staged
|
|
308
|
+
*/
|
|
309
|
+
function buildSuggestedMessage(deps = {}) {
|
|
310
|
+
const stagedNameStatus = deps.stagedNameStatus || (() => defaultStagedNameStatus(deps.execGit || defaultExecGit))
|
|
311
|
+
|
|
312
|
+
let raw
|
|
313
|
+
try {
|
|
314
|
+
raw = stagedNameStatus()
|
|
315
|
+
} catch (err) {
|
|
316
|
+
throw new Error(`could not read staged changes (are you in a git repo?): ${err.message}`)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const files = parseStagedNameStatus(raw)
|
|
320
|
+
if (files.length === 0) {
|
|
321
|
+
throw new Error('nothing staged — `git add` files first')
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const commitType = suggestedCommitType(files)
|
|
325
|
+
|
|
326
|
+
const lines = []
|
|
327
|
+
lines.push('# Sugestão heurística — NÃO é uma mensagem pronta, revise antes de usar.')
|
|
328
|
+
lines.push(`# Tipo sugerido: ${commitType}`)
|
|
329
|
+
lines.push('')
|
|
330
|
+
lines.push(`${commitType}(<escopo>): <descrição>`)
|
|
331
|
+
lines.push('')
|
|
332
|
+
lines.push('## Arquivos staged')
|
|
333
|
+
for (const f of files) {
|
|
334
|
+
lines.push(`${f.status} ${f.path}`)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return lines.join('\n')
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
module.exports = {
|
|
341
|
+
runCommit,
|
|
342
|
+
buildSuggestedMessage,
|
|
343
|
+
defaultExecGit,
|
|
344
|
+
defaultCurrentBranch,
|
|
345
|
+
defaultGitCommit,
|
|
346
|
+
defaultStagedNameStatus,
|
|
347
|
+
isCommitGatedBranch,
|
|
348
|
+
commitGovernedBranchPrefix,
|
|
349
|
+
suggestedCommitType,
|
|
350
|
+
isTestFile,
|
|
351
|
+
isDocsFile,
|
|
352
|
+
}
|
package/src/config/index.js
CHANGED
|
@@ -48,6 +48,9 @@ function defaults() {
|
|
|
48
48
|
backend: '',
|
|
49
49
|
frontend: '',
|
|
50
50
|
pkgManager: '',
|
|
51
|
+
// ML-2A (agentes especialistas aceitam contexto de convenções) field — mirrors Go's
|
|
52
|
+
// UpdateConfig.AgentConventions: agent_conventions, free-text, multi-line (default: '').
|
|
53
|
+
agentConventions: '',
|
|
51
54
|
},
|
|
52
55
|
sync: {
|
|
53
56
|
linearApiKey: '',
|
|
@@ -258,6 +261,7 @@ function parse(content, cfg) {
|
|
|
258
261
|
if (stringVal(m, 'backend') !== undefined) cfg.update.backend = m.backend;
|
|
259
262
|
if (stringVal(m, 'frontend') !== undefined) cfg.update.frontend = m.frontend;
|
|
260
263
|
if (stringVal(m, 'pkg_manager') !== undefined) cfg.update.pkgManager = m.pkg_manager;
|
|
264
|
+
if (stringVal(m, 'agent_conventions') !== undefined) cfg.update.agentConventions = m.agent_conventions;
|
|
261
265
|
if (stringVal(m, 'linear_api_key') !== undefined) cfg.sync.linearApiKey = m.linear_api_key;
|
|
262
266
|
if (stringVal(m, 'linear_team_id') !== undefined) cfg.sync.linearTeamId = m.linear_team_id;
|
|
263
267
|
if (stringVal(m, 'jira_base_url') !== undefined) cfg.sync.jiraBaseUrl = m.jira_base_url;
|
|
@@ -299,6 +303,28 @@ function parseRulesFromContent(content) {
|
|
|
299
303
|
return cfg.rules;
|
|
300
304
|
}
|
|
301
305
|
|
|
306
|
+
// readAgentConventions reads the `agent_conventions` key directly out of <cwd>/trackfw.yaml,
|
|
307
|
+
// bypassing the load() singleton — mirrors Go's config.ReadAgentConventions, needed here because
|
|
308
|
+
// generators (init.js's injectOrUpdateRules) inject rules into agent files for a given cwd that is
|
|
309
|
+
// not necessarily the process's own working directory the singleton is cached against. The file
|
|
310
|
+
// being absent, unreadable, malformed or simply missing the key are all treated the same: return
|
|
311
|
+
// '' silently, never an error — this is a best-effort read of an optional, free-text field.
|
|
312
|
+
function readAgentConventions(cwd) {
|
|
313
|
+
let content;
|
|
314
|
+
try {
|
|
315
|
+
content = fs.readFileSync(path.join(cwd || process.cwd(), 'trackfw.yaml'), 'utf8');
|
|
316
|
+
} catch (_) {
|
|
317
|
+
return '';
|
|
318
|
+
}
|
|
319
|
+
const cfg = { rules: {}, credentialGuard: {}, update: { agentConventions: '' }, sync: {}, linkFields: {} };
|
|
320
|
+
try {
|
|
321
|
+
parse(content, cfg);
|
|
322
|
+
} catch (_) {
|
|
323
|
+
return '';
|
|
324
|
+
}
|
|
325
|
+
return cfg.update.agentConventions || '';
|
|
326
|
+
}
|
|
327
|
+
|
|
302
328
|
const NAMESPACING_FLAT = 'flat';
|
|
303
329
|
const NAMESPACING_BY_AGENT = 'by_agent';
|
|
304
330
|
|
|
@@ -308,6 +334,7 @@ module.exports = {
|
|
|
308
334
|
defaults,
|
|
309
335
|
expandPath,
|
|
310
336
|
parseRulesFromContent,
|
|
337
|
+
readAgentConventions,
|
|
311
338
|
NAMESPACING_FLAT,
|
|
312
339
|
NAMESPACING_BY_AGENT,
|
|
313
340
|
MALFORMED_CONFIG_MESSAGE,
|