trackfw 7.6.0 → 8.0.0-rc2

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.
Files changed (124) hide show
  1. package/README.md +28 -11
  2. package/bin/trackfw.js +81 -0
  3. package/package.json +14 -8
  4. package/bin/trackfw +0 -41
  5. package/src/branch/prune.js +0 -450
  6. package/src/branch/runner.js +0 -153
  7. package/src/changelog/index.js +0 -114
  8. package/src/commands/adr.js +0 -56
  9. package/src/commands/agents.js +0 -3
  10. package/src/commands/audit-surface.js +0 -520
  11. package/src/commands/barrier.js +0 -822
  12. package/src/commands/baseline.js +0 -13
  13. package/src/commands/branch.js +0 -98
  14. package/src/commands/changelog.js +0 -29
  15. package/src/commands/commit.js +0 -66
  16. package/src/commands/configure.js +0 -106
  17. package/src/commands/context.js +0 -215
  18. package/src/commands/discover.js +0 -628
  19. package/src/commands/doctor.js +0 -66
  20. package/src/commands/help.js +0 -358
  21. package/src/commands/identity-wizard.js +0 -258
  22. package/src/commands/index.js +0 -68
  23. package/src/commands/init.js +0 -296
  24. package/src/commands/integrations.js +0 -305
  25. package/src/commands/log.js +0 -31
  26. package/src/commands/metrics.js +0 -242
  27. package/src/commands/note.js +0 -24
  28. package/src/commands/push.js +0 -51
  29. package/src/commands/release.js +0 -47
  30. package/src/commands/req.js +0 -98
  31. package/src/commands/roadmap.js +0 -44
  32. package/src/commands/serve.js +0 -345
  33. package/src/commands/ship.js +0 -58
  34. package/src/commands/skills.js +0 -3
  35. package/src/commands/status.js +0 -12
  36. package/src/commands/sync.js +0 -348
  37. package/src/commands/thirdparty.js +0 -390
  38. package/src/commands/update-harness.js +0 -1002
  39. package/src/commands/update.js +0 -500
  40. package/src/commands/validate.js +0 -57
  41. package/src/commands/version.js +0 -10
  42. package/src/commit/runner.js +0 -352
  43. package/src/config/index.js +0 -457
  44. package/src/forge/adapter.js +0 -189
  45. package/src/forge/resolve.js +0 -191
  46. package/src/generators/adr.js +0 -198
  47. package/src/generators/codex.js +0 -79
  48. package/src/generators/date.js +0 -23
  49. package/src/generators/hooks.js +0 -2414
  50. package/src/generators/init.js +0 -1563
  51. package/src/generators/note.js +0 -154
  52. package/src/generators/req.js +0 -436
  53. package/src/generators/roadmap.js +0 -841
  54. package/src/homedir.js +0 -30
  55. package/src/i18n/index.js +0 -53
  56. package/src/i18n/locales/en-US.json +0 -124
  57. package/src/i18n/locales/es-ES.json +0 -124
  58. package/src/i18n/locales/pt-BR.json +0 -124
  59. package/src/identity/config.js +0 -134
  60. package/src/identity/index.js +0 -19
  61. package/src/identity/preset.js +0 -219
  62. package/src/identity/slug.js +0 -62
  63. package/src/integrations/assets/agents/architect.md +0 -61
  64. package/src/integrations/assets/agents/backend.md +0 -41
  65. package/src/integrations/assets/agents/code-quality.md +0 -48
  66. package/src/integrations/assets/agents/data.md +0 -41
  67. package/src/integrations/assets/agents/dba.md +0 -41
  68. package/src/integrations/assets/agents/frontend.md +0 -41
  69. package/src/integrations/assets/agents/iac.md +0 -46
  70. package/src/integrations/assets/agents/infra.md +0 -43
  71. package/src/integrations/assets/agents/qa.md +0 -41
  72. package/src/integrations/assets/agents/security.md +0 -51
  73. package/src/integrations/assets/agents/tooling.md +0 -47
  74. package/src/integrations/assets/agents/ux.md +0 -48
  75. package/src/integrations/assets/catalog.json +0 -48
  76. package/src/integrations/assets/skills/architecture.md +0 -66
  77. package/src/integrations/assets/skills/backend.md +0 -67
  78. package/src/integrations/assets/skills/code-quality.md +0 -78
  79. package/src/integrations/assets/skills/data.md +0 -75
  80. package/src/integrations/assets/skills/dba.md +0 -77
  81. package/src/integrations/assets/skills/frontend.md +0 -70
  82. package/src/integrations/assets/skills/governance.md +0 -6
  83. package/src/integrations/assets/skills/iac.md +0 -90
  84. package/src/integrations/assets/skills/implement.md +0 -6
  85. package/src/integrations/assets/skills/infra.md +0 -62
  86. package/src/integrations/assets/skills/plan.md +0 -6
  87. package/src/integrations/assets/skills/qa.md +0 -63
  88. package/src/integrations/assets/skills/release.md +0 -6
  89. package/src/integrations/assets/skills/review.md +0 -6
  90. package/src/integrations/assets/skills/security.md +0 -74
  91. package/src/integrations/assets/skills/tooling.md +0 -80
  92. package/src/integrations/assets/skills/ux.md +0 -70
  93. package/src/integrations/catalog.js +0 -58
  94. package/src/integrations/doctor.js +0 -0
  95. package/src/integrations/doctor_remote.js +0 -224
  96. package/src/integrations/index.js +0 -171
  97. package/src/integrations/legacy.js +0 -35
  98. package/src/integrations/manager.js +0 -438
  99. package/src/integrations/register-agent.js +0 -106
  100. package/src/integrations/render.js +0 -563
  101. package/src/integrations/scaffold_doctor.js +0 -395
  102. package/src/lib/fatal-error.js +0 -64
  103. package/src/lib/pathfmt.js +0 -43
  104. package/src/lib/unknown-command.js +0 -82
  105. package/src/lib/update-engine.js +0 -242
  106. package/src/push/runner.js +0 -226
  107. package/src/release/runner.js +0 -474
  108. package/src/serve/api_attention.js +0 -24
  109. package/src/serve/api_board.js +0 -105
  110. package/src/serve/api_chain.js +0 -246
  111. package/src/serve/api_file.js +0 -117
  112. package/src/serve/api_metrics.js +0 -139
  113. package/src/serve/static/app.js +0 -1151
  114. package/src/serve/static/index.html +0 -338
  115. package/src/serve/static/style.css +0 -297
  116. package/src/ship/runner.js +0 -775
  117. package/src/thirdparty/fetch.js +0 -140
  118. package/src/thirdparty/markers.js +0 -207
  119. package/src/thirdparty/provenance.js +0 -114
  120. package/src/thirdparty/quarantine.js +0 -113
  121. package/src/thirdparty/references.js +0 -194
  122. package/src/validator/git-exec.js +0 -72
  123. package/src/validator/index.js +0 -4086
  124. package/src/validator/traceid.js +0 -155
@@ -1,13 +0,0 @@
1
- 'use strict'
2
- const { Command } = require('commander')
3
- const { validateUnfiltered, saveBaseline } = require('../validator')
4
-
5
- const cmd = new Command('baseline')
6
- cmd.description('Grava snapshot das violations atuais em .trackfw-baseline.json')
7
- cmd.action(async () => {
8
- const { violations, warnings } = await validateUnfiltered()
9
- saveBaseline(violations, warnings)
10
- console.log(`Baseline gravado: ${violations.length} violations, ${warnings.length} warnings`)
11
- })
12
-
13
- module.exports = cmd
@@ -1,98 +0,0 @@
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
- const { runBranchPrune } = require('../branch/prune')
11
-
12
- function createBranchCommand() {
13
- const cmd = new Command('branch')
14
- cmd.description('Manage governed feature branches')
15
-
16
- const newCmd = new Command('new')
17
- newCmd
18
- .description(
19
- 'Create a feat/fix/refactor/chore/docs branch; feat/fix/refactor gated on a matching REQ + roadmap already in wip/ or done/\n\n' +
20
- 'trackfw branch new moves the branch_has_wip_roadmap governance gate (already enforced\n' +
21
- "by 'trackfw validate' and 'trackfw ship') to before branch creation, instead of after:\n\n" +
22
- ' 1. Validates <type> is one of feat, fix, refactor, chore, docs and <slug> is non-empty.\n' +
23
- ' 2. For feat, fix, refactor: checks whether a roadmap in wip/ or done/ matches the given\n' +
24
- " slug — the exact matching logic 'trackfw validate' already uses (normalized slug,\n" +
25
- " filename contains match). Without a match: blocks — 'git checkout -b' is never\n" +
26
- " executed — and prints the same governance orientation message 'trackfw validate'\n" +
27
- " already prints for this rule.\n" +
28
- " 3. For chore, docs: housekeeping types already treated as roadmap-exempt by 'trackfw ship'\n" +
29
- " and 'trackfw commit' — the branch is created without the roadmap gate.\n" +
30
- " 4. With a match (or for chore/docs): runs 'git checkout -b <type>/<slug>', propagating\n" +
31
- " Git's own output and exit status literally.\n\n" +
32
- 'Create the governance artifacts first if this blocks you:\n' +
33
- ' trackfw req new "title" # flat project\n' +
34
- ' trackfw req new --agent <agent> "title" # by_agent project with 2+ agents\n' +
35
- ' trackfw roadmap new "title" # flat project\n' +
36
- ' trackfw roadmap new --agent <agent> "title" # by_agent project with 2+ agents\n' +
37
- ' trackfw roadmap move <name> wip'
38
- )
39
- .argument('<spec>', '<type>/<slug>, type in feat, fix, refactor, chore, docs')
40
- .option('--dry-run', 'Report whether the branch would be created or blocked, without executing git', false)
41
- .action((spec, options) => {
42
- const exitCode = runBranchNew(spec, !!options.dryRun, {})
43
- if (exitCode !== 0) {
44
- process.exit(exitCode)
45
- }
46
- })
47
-
48
- cmd.addCommand(newCmd)
49
-
50
- const pruneCmd = new Command('prune')
51
- pruneCmd
52
- .description('Report (and, with --apply, delete) local branches already integrated into origin/main')
53
- .addHelpText(
54
- 'after',
55
- '\n' +
56
- 'trackfw branch prune automates the "one active branch at a time" check documented in\n' +
57
- 'CLAUDE.md §1 — it does not remove human judgment from every case: a branch whose only\n' +
58
- 'remaining divergence is doc/config files is flagged for manual review, never deleted\n' +
59
- 'automatically.\n\n' +
60
- "A best-effort 'git fetch origin --prune' runs first. Failure (offline, no remote) is\n" +
61
- 'non-blocking: a warning is printed and evaluation proceeds against the local origin/main\n' +
62
- 'ref, whatever its state. A stale origin/main only ever makes the result MORE conservative —\n' +
63
- 'it can miss a branch that was in fact integrated since the last fetch (reporting it kept\n' +
64
- 'when a fresh fetch would show it deletable), but it never reports one as deletable that a\n' +
65
- 'fresh fetch would show as pending.\n\n' +
66
- "Decides integration with the touched-files heuristic, NOT git's own ancestry check (which\n" +
67
- 'always refuses squash-merged branches) and NOT a naive bidirectional diff against origin/main\n' +
68
- '(which false-positives on a branch that is merged but stale, once main has advanced further):\n\n' +
69
- ' mb = git merge-base origin/main <branch>\n' +
70
- ' touched = git diff --name-only mb <branch> (what the branch touched)\n' +
71
- ' diverg = git diff --name-only origin/main <branch> -- touched (what still differs there)\n\n' +
72
- 'touched empty -> integrated (safe to delete)\n' +
73
- 'diverg empty -> integrated (safe to delete) -- squash-merged, stale, main advanced since\n' +
74
- 'diverg doc/config only -> flagged for review (kept; probable housekeeping, confirm and delete manually)\n' +
75
- 'otherwise -> kept, with the diverging files named\n\n' +
76
- 'Every local branch is reported, always, with its decision and reason. The current branch, any\n' +
77
- 'branch checked out in another worktree, and the default branch (main) are always kept and\n' +
78
- 'never evaluated for deletion. Without origin/main resolvable at all (offline with no prior\n' +
79
- 'fetch ever having run, or no remote configured), the whole command refuses and deletes\n' +
80
- 'nothing.\n\n' +
81
- '--dry-run is the default: without --apply, nothing is ever deleted, even the clearly\n' +
82
- "integrated. Deletion tries 'git branch -d' first — confirming the integration via git's own\n" +
83
- "ancestry check too, when possible — and falls back to 'git branch -D' only when -d refuses,\n" +
84
- 'the expected case for squash-merged branches, which never have fast-forward ancestry with main.'
85
- )
86
- .option('--apply', 'Actually delete branches decided as integrated (default: report only, delete nothing)', false)
87
- .action((options) => {
88
- const exitCode = runBranchPrune(!!options.apply, {})
89
- if (exitCode !== 0) {
90
- process.exit(exitCode)
91
- }
92
- })
93
-
94
- cmd.addCommand(pruneCmd)
95
- return cmd
96
- }
97
-
98
- module.exports = createBranchCommand()
@@ -1,29 +0,0 @@
1
- 'use strict'
2
- const { Command } = require('commander')
3
- const { read, parseSections, firstSection, findVersion, formatSection } = require('../changelog')
4
-
5
- const cmd = new Command('changelog')
6
- cmd.description('Show entries from CHANGELOG.md')
7
- // enablePositionalOptions escopa a flag local "--version" a este subcomando,
8
- // evitando que o root a intercepte como a flag global "trackfw --version".
9
- cmd.enablePositionalOptions()
10
- cmd.option('--version <x.y.z>', 'Show a specific version section')
11
- cmd.option('--all', 'Show the entire CHANGELOG.md')
12
- cmd.action(async (opts) => {
13
- try {
14
- const root = process.cwd()
15
- const content = read(root)
16
- if (opts.all) {
17
- process.stdout.write(content)
18
- return
19
- }
20
- const sections = parseSections(content)
21
- const section = opts.version ? findVersion(sections, opts.version) : firstSection(sections)
22
- process.stdout.write(formatSection(section))
23
- } catch (err) {
24
- console.error(`Error: ${err.message}`)
25
- process.exitCode = 1
26
- }
27
- })
28
-
29
- module.exports = cmd
@@ -1,66 +0,0 @@
1
- 'use strict'
2
-
3
- // trackfw commit -m "<message>"
4
- //
5
- // CLI wiring for the `trackfw commit` command. The testable logic lives in
6
- // ../commit/runner.js (mirrors the branch.js / branch/runner.js split already used in this CLI).
7
-
8
- const { Command } = require('commander')
9
- const { runCommit, buildSuggestedMessage } = require('../commit/runner')
10
-
11
- function createCommitCommand() {
12
- const cmd = new Command('commit')
13
- cmd
14
- .description(
15
- "trackfw commit commits staged changes directly, but blocks the commit before it\n" +
16
- 'happens when governance is missing, instead of letting it land and only catching it later.\n\n' +
17
- 'Compositional vocabulary:\n' +
18
- ' trackfw commit -m "..." commits\n' +
19
- ' trackfw push pushes\n' +
20
- ' trackfw ship -m "..." commit + push + PR (composition)\n\n' +
21
- 'Behavioral steps:\n\n' +
22
- " 1. On 'main'/'master': always blocked — commit directly on the default branch is never\n" +
23
- ' permitted.\n' +
24
- ' 2. On a feat/fix/refactor branch: requires a roadmap matching the branch slug already in\n' +
25
- " wip/ or done/ — the exact matching logic 'trackfw branch new' and 'trackfw validate'\n" +
26
- ' already use. Without a match, blocks with the same governance orientation message.\n' +
27
- ' 3. On any other branch (e.g. doc/housekeeping branches): allowed without requiring a\n' +
28
- ' roadmap — a warning is logged, but the commit proceeds.\n' +
29
- " 4. When allowed: runs 'git commit -m <message>', propagating Git's own output and exit\n" +
30
- ' status literally.\n\n' +
31
- "'--suggest' takes a completely separate path: it prints a heuristic Conventional Commits\n" +
32
- "skeleton built from 'git diff --cached --name-status' (type + staged file list) and exits\n" +
33
- 'without ever committing — no LLM call, just a structural heuristic. It is not a\n' +
34
- 'ready-to-use message; review and edit before using it with -m. When \'--suggest\' is set,\n' +
35
- "'-m' (if also passed) is ignored and no commit ever happens.\n\n" +
36
- 'Create the governance artifacts first if this blocks you:\n' +
37
- ' trackfw req new "title" # flat project\n' +
38
- ' trackfw req new --agent <agent> "title" # by_agent project with 2+ agents\n' +
39
- ' trackfw roadmap new "title" # flat project\n' +
40
- ' trackfw roadmap new --agent <agent> "title" # by_agent project with 2+ agents\n' +
41
- ' trackfw roadmap move <name> wip'
42
- )
43
- .option('-m, --message <msg>', 'Commit message (required)')
44
- .option('--suggest', 'Print a heuristic Conventional Commits message skeleton from staged files and exit without committing (ignores -m)')
45
- .action((options) => {
46
- if (options.suggest) {
47
- try {
48
- const suggestion = buildSuggestedMessage({})
49
- process.stdout.write(suggestion + '\n')
50
- return
51
- } catch (err) {
52
- process.stderr.write(err.message + '\n')
53
- process.exit(1)
54
- }
55
- }
56
-
57
- const exitCode = runCommit(options.message || '', {})
58
- if (exitCode !== 0) {
59
- process.exit(exitCode)
60
- }
61
- })
62
-
63
- return cmd
64
- }
65
-
66
- module.exports = createCommitCommand()
@@ -1,106 +0,0 @@
1
- 'use strict'
2
- const { Command } = require('commander')
3
- const readline = require('readline')
4
- const fs = require('fs')
5
- const path = require('path')
6
-
7
- const DEFAULTS = {
8
- adr_dirs: 'docs/adr',
9
- req_dir: 'docs/req',
10
- roadmap_dir: 'docs/roadmaps',
11
- wip_limit: '1',
12
- link_req: 'REQ:'
13
- }
14
-
15
- /**
16
- * Pergunta ao usuário via readline e retorna a resposta como Promise<string>.
17
- * Se o usuário pressionar Enter sem digitar nada, retorna o valor padrão.
18
- * @param {readline.Interface} rl
19
- * @param {string} question
20
- * @param {string} defaultValue
21
- * @returns {Promise<string>}
22
- */
23
- function ask(rl, question, defaultValue) {
24
- return new Promise((resolve) => {
25
- rl.question(question, (answer) => {
26
- resolve(answer.trim() || defaultValue)
27
- })
28
- })
29
- }
30
-
31
- /**
32
- * Executa o wizard interativo e grava trackfw.yaml.
33
- * Exportada separadamente para facilitar testes.
34
- * @param {readline.Interface} [rl] — opcional; se não fornecido, cria um novo
35
- * @param {string} [cwd] — diretório de trabalho (padrão: process.cwd())
36
- * @returns {Promise<{ fieldsWritten: number, filePath: string }>}
37
- */
38
- async function runConfigure(rl, cwd) {
39
- const dir = cwd || process.cwd()
40
- const yamlPath = path.join(dir, 'trackfw.yaml')
41
- let ownRl = false
42
-
43
- if (!rl) {
44
- rl = readline.createInterface({ input: process.stdin, output: process.stdout })
45
- ownRl = true
46
- }
47
-
48
- try {
49
- // Verificar se já existe e perguntar se recria
50
- if (fs.existsSync(yamlPath)) {
51
- const answer = await ask(rl, 'trackfw.yaml já existe. Recriar do zero? (s/N) ', 'N')
52
- if (answer.toLowerCase() !== 's') {
53
- console.log('Operação cancelada.')
54
- return { fieldsWritten: 0, filePath: yamlPath }
55
- }
56
- }
57
-
58
- // Coletar campos via prompts
59
- const adrDirs = await ask(rl, `ADR dirs [${DEFAULTS.adr_dirs}]: `, DEFAULTS.adr_dirs)
60
- const reqDir = await ask(rl, `REQ dir [${DEFAULTS.req_dir}]: `, DEFAULTS.req_dir)
61
- const roadmapDir = await ask(rl, `Roadmap dir [${DEFAULTS.roadmap_dir}]: `, DEFAULTS.roadmap_dir)
62
- const wipLimit = await ask(rl, `WIP limit [${DEFAULTS.wip_limit}]: `, DEFAULTS.wip_limit)
63
- const linkReq = await ask(rl, `Marcador de REQ [${DEFAULTS.link_req}]: `, DEFAULTS.link_req)
64
-
65
- // Montar campos customizados (somente diferenças dos defaults)
66
- const custom = {}
67
- if (adrDirs !== DEFAULTS.adr_dirs) custom.adr_dirs = adrDirs
68
- if (reqDir !== DEFAULTS.req_dir) custom.req_dir = reqDir
69
- if (roadmapDir !== DEFAULTS.roadmap_dir) custom.roadmap_dir = roadmapDir
70
- if (wipLimit !== DEFAULTS.wip_limit) custom.wip_limit = wipLimit
71
- if (linkReq !== DEFAULTS.link_req) custom['link_fields.req'] = linkReq
72
-
73
- // Gerar conteúdo do YAML
74
- let content = '# trackfw.yaml — gerado por trackfw configure\n'
75
- const fieldsWritten = Object.keys(custom).length
76
-
77
- if (fieldsWritten > 0) {
78
- for (const [key, value] of Object.entries(custom)) {
79
- if (key === 'adr_dirs') {
80
- content += `adr_dirs:\n - ${value}\n`
81
- } else if (key === 'link_fields.req') {
82
- content += `link_fields:\n req:\n - "${value}"\n`
83
- } else {
84
- content += `${key}: ${value}\n`
85
- }
86
- }
87
- }
88
-
89
- fs.writeFileSync(yamlPath, content, 'utf8')
90
- console.log(`trackfw.yaml gravado com ${fieldsWritten} campos customizados`)
91
-
92
- return { fieldsWritten, filePath: yamlPath }
93
- } finally {
94
- if (ownRl) rl.close()
95
- }
96
- }
97
-
98
- const cmd = new Command('configure')
99
- cmd.description('Wizard interativo para criar/atualizar trackfw.yaml')
100
- cmd.action(async () => {
101
- await runConfigure()
102
- })
103
-
104
- module.exports = cmd
105
- module.exports.runConfigure = runConfigure
106
- module.exports.DEFAULTS = DEFAULTS
@@ -1,215 +0,0 @@
1
- 'use strict'
2
-
3
- const { Command } = require('commander')
4
- const fs = require('fs')
5
- const path = require('path')
6
- const config = require('../config')
7
- const { validate, resolveAgentNamespaces, resolveReqFiles } = require('../validator')
8
-
9
- /**
10
- * extractFrontmatterField — extrai valor de campo YAML dentro de bloco --- ... ---.
11
- * Retorna string vazia se não encontrado ou valor '""'.
12
- * @param {string} content
13
- * @param {string} field
14
- * @returns {string}
15
- */
16
- function extractFrontmatterField(content, field) {
17
- const lines = content.split('\n')
18
- let started = false
19
- let inFrontmatter = false
20
- for (const line of lines) {
21
- const trimmed = line.trim()
22
- if (trimmed === '---') {
23
- if (!started) {
24
- started = true
25
- inFrontmatter = true
26
- continue
27
- }
28
- break // segundo --- fecha o bloco
29
- }
30
- if (!inFrontmatter) break
31
- const key = field + ':'
32
- if (trimmed.startsWith(key)) {
33
- let val = trimmed.slice(key.length).trim()
34
- val = val.replace(/^["']|["']$/g, '') // remover aspas
35
- return val
36
- }
37
- }
38
- return ''
39
- }
40
-
41
- /**
42
- * extractInlineStatus — extrai status da linha "| Status: ..." do markdown.
43
- * @param {string} content
44
- * @returns {string}
45
- */
46
- function extractInlineStatus(content) {
47
- for (const line of content.split('\n')) {
48
- const idx = line.indexOf('| Status: ')
49
- if (idx >= 0) {
50
- let rest = line.slice(idx + '| Status: '.length)
51
- const pipeIdx = rest.indexOf(' |')
52
- if (pipeIdx >= 0) rest = rest.slice(0, pipeIdx)
53
- rest = rest.replace(/[\s>|]+$/, '').trim()
54
- return rest || 'unknown'
55
- }
56
- }
57
- return 'unknown'
58
- }
59
-
60
- /**
61
- * collectEntries — lê diretório e retorna lista de entradas com type, file, status, state.
62
- * @param {string} dir
63
- * @param {string} type - 'ADR' | 'REQ' | 'ROADMAP'
64
- * @param {string} [state] - estado kanban (somente ROADMAP)
65
- * @returns {Array<{type: string, file: string, status: string, state?: string}>}
66
- */
67
- function collectEntries(dir, type, state) {
68
- const entries = []
69
- let files = []
70
- try {
71
- files = fs.readdirSync(dir).filter(f => f.endsWith('.md') && !fs.statSync(path.join(dir, f)).isDirectory())
72
- } catch (_) {
73
- return entries
74
- }
75
- for (const file of files) {
76
- let content = ''
77
- try { content = fs.readFileSync(path.join(dir, file), 'utf8') } catch (_) {}
78
- let status = extractFrontmatterField(content, 'status')
79
- if (!status) status = extractInlineStatus(content)
80
- if (!status) status = state || 'unknown'
81
- const entry = { type, file, status }
82
- if (state) entry.state = state
83
- entries.push(entry)
84
- }
85
- return entries
86
- }
87
-
88
- /**
89
- * collectReqEntries — entradas de REQ para o `context`, derivadas do ponto único de leitura
90
- * (resolveReqFiles). O `state` sai do nome da pasta-pai quando ela é um estado legado; em layout
91
- * flat ou no canônico req_dir/<agente>/*.md não há estado (invariante D1: REQ não tem estado).
92
- * @param {object} cfg
93
- * @returns {Array<{type:string,file:string,status:string,state?:string}>}
94
- */
95
- function collectReqEntries(cfg) {
96
- const STATES = ['backlog', 'analyzing', 'wip', 'blocked', 'done', 'abandoned']
97
- const entries = []
98
- for (const full of resolveReqFiles(cfg)) {
99
- let content = ''
100
- try { content = fs.readFileSync(full, 'utf8') } catch (_) {}
101
- const parent = path.basename(path.dirname(full))
102
- const state = STATES.includes(parent) ? parent : ''
103
- let status = extractFrontmatterField(content, 'status')
104
- if (!status) status = extractInlineStatus(content)
105
- if (!status) status = state || 'unknown'
106
- const entry = { type: 'REQ', file: path.basename(full), status }
107
- if (state) entry.state = state
108
- entries.push(entry)
109
- }
110
- return entries
111
- }
112
-
113
- /**
114
- * getContext — coleta governança e imprime em md ou json.
115
- * @param {string} format - 'md' | 'json'
116
- * @returns {Promise<void>}
117
- */
118
- async function getContext(format) {
119
- const cfg = config.load()
120
-
121
- // ADRs
122
- const adrs = []
123
- for (const adrDir of (cfg.adrDirs || ['docs/adr'])) {
124
- adrs.push(...collectEntries(adrDir, 'ADR'))
125
- }
126
-
127
- // REQs — pelo PONTO ÚNICO de leitura (ADR-2026-09-03, D3/D4). Antes, `context` montava a própria
128
- // árvore (flat, ou <agente>/<estado>/ em by_agent) e não enxergava o layout canônico <agente>/*.md.
129
- const reqs = collectReqEntries(cfg)
130
-
131
- // Roadmaps
132
- const roadmaps = []
133
- const states = ['wip', 'backlog', 'blocked', 'done', 'abandoned']
134
- if (cfg.roadmapNamespacing === 'by_agent') {
135
- const agents = resolveAgentNamespaces(cfg, cfg.roadmapDir)
136
- for (const agent of agents) {
137
- for (const state of states) {
138
- const dir = path.join(cfg.roadmapDir, agent, state)
139
- roadmaps.push(...collectEntries(dir, 'ROADMAP', state))
140
- }
141
- }
142
- } else {
143
- for (const state of states) {
144
- const dir = path.join(cfg.roadmapDir, state)
145
- roadmaps.push(...collectEntries(dir, 'ROADMAP', state))
146
- }
147
- }
148
-
149
- // Validate
150
- const { violations, warnings } = await validate()
151
-
152
- // Score
153
- let score = 0
154
- if (adrs.length > 0) score += 20
155
- if (reqs.length > 0) score += 20
156
- if (roadmaps.length > 0) score += 20
157
- if (violations.length === 0) score += 40
158
-
159
- if (format === 'json') {
160
- console.log(JSON.stringify({ score, violations, warnings, adrs, reqs, roadmaps }, null, 2))
161
- return
162
- }
163
-
164
- // Markdown
165
- console.log('# trackfw governance context\n')
166
- console.log(`**Governance score:** ${score}/100\n`)
167
-
168
- console.log(`## ADRs (${adrs.length})`)
169
- if (adrs.length === 0) {
170
- console.log('- (none)')
171
- } else {
172
- for (const a of adrs) console.log(`- ${a.file} [${a.status}]`)
173
- }
174
-
175
- console.log(`\n## REQs (${reqs.length})`)
176
- if (reqs.length === 0) {
177
- console.log('- (none)')
178
- } else {
179
- for (const r of reqs) console.log(`- ${r.file} [${r.status}]`)
180
- }
181
-
182
- console.log(`\n## Roadmaps (${roadmaps.length})`)
183
- if (roadmaps.length === 0) {
184
- console.log('- (none)')
185
- } else {
186
- for (const r of roadmaps) console.log(`- ${r.file} [${r.state}]`)
187
- }
188
-
189
- if (violations.length > 0) {
190
- console.log(`\n## Violations (${violations.length})`)
191
- for (const v of violations) console.log(`- ${v}`)
192
- }
193
-
194
- if (warnings.length > 0) {
195
- console.log(`\n## Warnings (${warnings.length})`)
196
- for (const w of warnings) console.log(`- ${w}`)
197
- }
198
- }
199
-
200
- module.exports = (function () {
201
- const cmd = new Command('context')
202
- cmd
203
- .description('Print governance context for LLM consumption')
204
- .option('--format <fmt>', 'Output format: md or json', 'md')
205
- // Retorna a Promise: o bin roda parseAsync() e só assim uma rejeição chega
206
- // ao .catch(reportFatalError) com exit code 1 em vez de virar unhandled
207
- // rejection (ML-1A, ROADMAP-2026-09-02-context-do-cli-node-aguarda-validate).
208
- .action((opts) => getContext(opts.format))
209
- return cmd
210
- })()
211
-
212
- // Exposto para teste: o teste de context em by_agent duplicava a lógica de coleta e, por isso,
213
- // continuava verde mesmo quando a produção mudava (asserção sobre uma CÓPIA). Anexar a função ao
214
- // Command exportado é o menor caminho para o teste exercitar o código de produção de fato.
215
- module.exports.collectReqEntries = collectReqEntries