trackfw 3.1.0 → 5.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 CHANGED
@@ -150,6 +150,7 @@ trackfw ship -m "feat(auth): add login flow"
150
150
  | `trackfw roadmap move <name> <state>` | Move roadmap between states |
151
151
  | `trackfw roadmap list` | List all roadmaps grouped by state |
152
152
  | `trackfw validate` | Check governance consistency (use as CI gate) |
153
+ | `trackfw barrier <roadmap> --wave <n>` | Deterministic wave-release gate — stack-agnostic, checks MLs, acceptance evidence, project-declared gates, and governance |
153
154
  | `trackfw ship -m "msg"` | Governed `git commit + push + open PR/MR` — enforces branch pattern and governance gate; resolves forge (GitHub/GitLab/Bitbucket/Azure) automatically or via `--forge`; falls back to a browser URL when the forge CLI is absent |
154
155
  | `trackfw context` | Print a structured summary of the project's governance state (REQs, Roadmaps, ADRs with counts and statuses) |
155
156
  | `trackfw serve` | Start a local governance dashboard (no cloud, no accounts) |
@@ -164,7 +165,6 @@ trackfw ship -m "feat(auth): add login flow"
164
165
  | `trackfw agents uninstall` | Remove selected owned agent deployments |
165
166
  | `trackfw skills list` | List available governance skills and deployment state |
166
167
  | `trackfw skills install/update/uninstall` | Manage selected governance skills |
167
- | `trackfw gemini`, `cursor`, `copilot`, `windsurf`, `amazonq` | Historical aliases in the Go distribution only; use `agents`/`skills` |
168
168
  | `trackfw version` | Print version |
169
169
 
170
170
  The same lifecycle contract is available from the Go/Homebrew, npm, and PyPI
@@ -330,6 +330,90 @@ $ trackfw status
330
330
 
331
331
  ---
332
332
 
333
+ ## `trackfw barrier` — deterministic wave-release gate
334
+
335
+ ```bash
336
+ trackfw barrier <roadmap> --wave <n> [--json]
337
+ ```
338
+
339
+ `trackfw barrier` is the stack-agnostic core of the wave-release gate: it never assumes a build
340
+ tool, a test runner, or a parity rule. Every check either comes from the roadmap itself (the wave's
341
+ declared gates) or from `trackfw validate` run in-process. Point it at a roadmap basename (with or
342
+ without `.md`, resolved against `wip/` then `done/`) and a wave number, and it tells you whether
343
+ that wave is ready to release.
344
+
345
+ A wave passes only when **all four** built-in checks are green:
346
+
347
+ | Check | Passes when |
348
+ |---|---|
349
+ | `mls_complete` | The wave has at least one ML and every ML is marked `**Status:** ✅` |
350
+ | `acceptance_evidence` | Every ML has a non-empty `**Critérios de aceite:**` block with no unchecked `- [ ]` line |
351
+ | `gates` | Every command declared under the wave's `**Gates da wave:**` fenced block exits 0 — a wave with no such block declares zero gates, and the barrier never invents one |
352
+ | `validate` | `trackfw validate --json` reports `violations: 0` |
353
+
354
+ ### Exit codes
355
+
356
+ | Exit | Meaning |
357
+ |---|---|
358
+ | `0` | `status: "passed"` — every check is green, the wave may release |
359
+ | `1` | `status: "blocked"` — at least one check failed; the JSON/text report says which |
360
+ | `2` | Usage/resolution error — the roadmap or the wave number could not be resolved. This is **not** `blocked`: a barrier that could not run is distinct from one that ran and failed |
361
+
362
+ ### Correcting a blocked wave
363
+
364
+ ```bash
365
+ $ trackfw barrier ROADMAP-example --wave 2
366
+ ✗ mls_complete: ML-2C: not complete (status: 🔄)
367
+ ✗ acceptance_evidence: ML-2C: 2 unmet acceptance criteria
368
+ wave 2: blocked
369
+ ```
370
+
371
+ Fix the roadmap (mark the ML `✅`, check off the remaining criteria) and rerun the exact same
372
+ command — the barrier is not a one-shot denial; a corrected wave passes on the next invocation:
373
+
374
+ ```bash
375
+ $ trackfw barrier ROADMAP-example --wave 2
376
+ ✓ mls_complete
377
+ ✓ acceptance_evidence
378
+ ✓ gates
379
+ ✓ validate
380
+ wave 2: passed
381
+ ```
382
+
383
+ ### JSON output
384
+
385
+ ```bash
386
+ trackfw barrier ROADMAP-example --wave 2 --json
387
+ ```
388
+
389
+ ```json
390
+ {
391
+ "roadmap": "ROADMAP-example.md",
392
+ "wave": 2,
393
+ "status": "blocked",
394
+ "started_at": "2026-07-29T10:30:00Z",
395
+ "finished_at": "2026-07-29T10:30:04Z",
396
+ "checks": [
397
+ { "name": "mls_complete", "status": "passed", "evidence": ["ML-2A: ✅"], "failures": [] },
398
+ { "name": "acceptance_evidence", "status": "blocked", "evidence": [], "failures": ["ML-2C: 2 unmet acceptance criteria"] },
399
+ { "name": "gates", "status": "passed", "commands": ["make quality"], "evidence": ["make quality: exit 0"], "failures": [] },
400
+ { "name": "validate", "status": "passed", "evidence": ["0 violations, 0 warnings"], "failures": [] }
401
+ ],
402
+ "failures": ["acceptance_evidence: ML-2C: 2 unmet acceptance criteria"]
403
+ }
404
+ ```
405
+
406
+ ### `trackfw barrier` vs. `/trackfw:barrier`
407
+
408
+ `trackfw barrier` is the deterministic, reproducible CLI — it never invokes agents and never
409
+ performs Git operations. The `/trackfw:barrier` slash command wraps it with the parts a binary
410
+ cannot evaluate: dispatching `code-quality`/`security` reviews, auditing the diff against scope,
411
+ and — only for `trackfw_architect`, the sole Git authority in this workflow — committing and
412
+ pushing once every check and review is green. A green CLI barrier is necessary but not sufficient
413
+ to release a wave. Full contract: `docs/cli-parity.md` → `## trackfw barrier`.
414
+
415
+ ---
416
+
333
417
  ## AI assistant integration
334
418
 
335
419
  `trackfw init` can install initial AI integrations. The `agents` and `skills`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trackfw",
3
- "version": "3.1.0",
3
+ "version": "5.0.0",
4
4
  "description": "Governance CLI for AI-native software delivery and AI coding agents. Enforces traceability: ADR → REQ → ROADMAP → kanban. Native support for Codex, Claude Code, Gemini CLI, Cursor, Copilot, Windsurf, and Amazon Q.",
5
5
  "keywords": [
6
6
  "cli",
@@ -0,0 +1,386 @@
1
+ 'use strict'
2
+
3
+ // trackfw barrier <roadmap> --wave <n> [--json]
4
+ //
5
+ // Deterministic core of the wave-release barrier. Stack-agnostic: every executable
6
+ // check comes from the roadmap itself (see docs/cli-parity.md → `## trackfw barrier`).
7
+ // This command never assumes a build tool, a test runner or a parity rule, and it
8
+ // never performs git operations or invokes specialist agents — that orchestration
9
+ // lives in the `/trackfw:barrier` slash command.
10
+
11
+ const { Command } = require('commander')
12
+ const fs = require('fs')
13
+ const path = require('path')
14
+ const { spawnSync } = require('child_process')
15
+ const config = require('../config')
16
+ const validator = require('../validator')
17
+
18
+ // UsageError signals a resolution/parsing failure that must map to exit code 2 —
19
+ // distinct from a "blocked" (exit 1) evaluation result.
20
+ class UsageError extends Error {}
21
+
22
+ // ────────────────────────────────────────────────────────────────────────────
23
+ // Roadmap resolution
24
+ // ────────────────────────────────────────────────────────────────────────────
25
+
26
+ // resolveRoadmapFile finds <roadmapArg> (basename with or without .md) under
27
+ // wip/ then done/ (both flat and by_agent layouts). Throws UsageError naming the
28
+ // roadmap exactly as the user typed it (no .md normalization) when not found —
29
+ // pinned literally by docs/cli-parity.md (`## trackfw barrier`).
30
+ function resolveRoadmapFile(cfg, roadmapArg) {
31
+ const base = roadmapArg.endsWith('.md') ? roadmapArg : `${roadmapArg}.md`
32
+ const dirs = [...validator.resolveWIPDirs(cfg), ...validator.resolveDoneDirs(cfg)]
33
+ for (const dir of dirs) {
34
+ const candidate = path.join(config.expandPath(dir), base)
35
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
36
+ return { path: candidate, basename: base }
37
+ }
38
+ }
39
+ throw new UsageError(`roadmap "${roadmapArg}" not found in wip/ nor done/ under ${cfg.roadmapDir}`)
40
+ }
41
+
42
+ // ────────────────────────────────────────────────────────────────────────────
43
+ // Wave label grammar (pinned by docs/cli-parity.md, "Wave label grammar" section)
44
+ // ────────────────────────────────────────────────────────────────────────────
45
+
46
+ // Scan regex (mirrors Go's waveHeadingRe): catches any "## Wave <token> " line.
47
+ // The trailing space is part of rule 1 — a heading whose label touches EOL is not
48
+ // recognised as a wave heading (neither valid nor malformed).
49
+ const WAVE_SCAN_RE = /^## Wave (\S+) /
50
+
51
+ // Grammar: <integer>[-<suffix>] where integer >= 1 and suffix is [a-z0-9]+ (lowercase).
52
+ // Valid: 1, 2, 2-bis, 2-hotfix, 10-a2.
53
+ // Invalid: X, 2-BIS, -bis, 2-, 2-bis-ter, 0.
54
+ const WAVE_LABEL_RE = /^(\d+)(?:-([a-z0-9]+))?$/
55
+
56
+ // isValidWaveLabel returns true iff token matches the grammar above.
57
+ // Exported so unit tests can assert the full table of valid/invalid examples.
58
+ function isValidWaveLabel(token) {
59
+ const m = WAVE_LABEL_RE.exec(String(token))
60
+ if (!m) return false
61
+ return parseInt(m[1], 10) >= 1
62
+ }
63
+
64
+ // ────────────────────────────────────────────────────────────────────────────
65
+ // Roadmap parsing rules (string-level, see docs/cli-parity.md)
66
+ // ────────────────────────────────────────────────────────────────────────────
67
+
68
+ // findWave locates the `## Wave <label> ` heading matching waveLabel and returns its
69
+ // line range [startLine, endLine) within `lines`. Performs a full pre-pass over all
70
+ // wave headings (mirroring Go's parseWaves): throws UsageError for the first malformed
71
+ // heading regardless of position — this ensures a typo like `## Wave X — ...` aborts
72
+ // the entire document (ADR decision 16, non-vacuity). Throws UsageError naming the
73
+ // wave label and the resolved roadmap basename when not found (pinned literally by
74
+ // docs/cli-parity.md). roadmapBasename is optional so unit tests that only exercise
75
+ // parsing can omit it; the CLI path always supplies it.
76
+ function findWave(lines, waveLabel, roadmapBasename) {
77
+ // Pre-pass: validate ALL wave headings and collect them.
78
+ // Abort immediately on the first malformed heading — this is intentional and
79
+ // mirrors Go's parseWaves() full-scan approach (ADR decision 16).
80
+ const waves = []
81
+ for (let i = 0; i < lines.length; i++) {
82
+ const attempt = WAVE_SCAN_RE.exec(lines[i])
83
+ if (!attempt) continue
84
+ const token = attempt[1]
85
+ if (!isValidWaveLabel(token)) {
86
+ throw new UsageError(`malformed wave heading at line ${i + 1}: "${token}" is not a valid wave label`)
87
+ }
88
+ let endLine = lines.length
89
+ for (let j = i + 1; j < lines.length; j++) {
90
+ if (/^## /.test(lines[j])) { endLine = j; break }
91
+ }
92
+ waves.push({ label: token, startLine: i, endLine })
93
+ }
94
+
95
+ // Find the requested wave by exact label match (no prefix/fuzzy matching).
96
+ const wave = waves.find(w => w.label === String(waveLabel))
97
+ if (!wave) {
98
+ throw new UsageError(`wave ${waveLabel} not found in roadmap "${roadmapBasename}"`)
99
+ }
100
+ return { startLine: wave.startLine, endLine: wave.endLine }
101
+ }
102
+
103
+ // findMLs locates every `### ML-` heading inside [startLine, endLine) and returns
104
+ // { id, lines } for each, where `lines` is the ML body (heading excluded).
105
+ function findMLs(lines, startLine, endLine) {
106
+ const headings = []
107
+ for (let i = startLine; i < endLine; i++) {
108
+ if (/^### ML-/.test(lines[i])) {
109
+ const m = /^### (\S+)/.exec(lines[i])
110
+ headings.push({ id: m[1], headingLine: i })
111
+ }
112
+ }
113
+ return headings.map((h, idx) => {
114
+ let end = endLine
115
+ for (let j = h.headingLine + 1; j < endLine; j++) {
116
+ if (/^### /.test(lines[j]) || /^## /.test(lines[j])) { end = j; break }
117
+ }
118
+ return { id: h.id, lines: lines.slice(h.headingLine + 1, end) }
119
+ })
120
+ }
121
+
122
+ // mlCompletionStatus applies rule 3 — completion is a `**Status:**` line whose
123
+ // remainder contains ✅. Returns { complete, marker } where marker is "missing"
124
+ // when no `**Status:**` line exists at all.
125
+ function mlCompletionStatus(mlLines) {
126
+ for (const raw of mlLines) {
127
+ const line = raw.trim()
128
+ const m = /^\*\*Status:\*\*(.*)$/.exec(line)
129
+ if (m) {
130
+ const marker = m[1].trim()
131
+ return { complete: marker.includes('✅'), marker: marker.length > 0 ? marker : 'missing' }
132
+ }
133
+ }
134
+ return { complete: false, marker: 'missing' }
135
+ }
136
+
137
+ // mlAcceptanceEvidence applies rule 4. Returns { hasBlock, total, unmet }.
138
+ function mlAcceptanceEvidence(mlLines) {
139
+ let blockStart = -1
140
+ for (let i = 0; i < mlLines.length; i++) {
141
+ if (/^\*\*Crit[ée]rios de aceite:\*\*/.test(mlLines[i].trim())) { blockStart = i; break }
142
+ }
143
+ if (blockStart === -1) return { hasBlock: false, total: 0, unmet: 0 }
144
+
145
+ let blockEnd = mlLines.length
146
+ for (let j = blockStart + 1; j < mlLines.length; j++) {
147
+ if (/^\*\*/.test(mlLines[j].trim())) { blockEnd = j; break }
148
+ }
149
+
150
+ const blockLines = mlLines.slice(blockStart + 1, blockEnd)
151
+ .map(l => l.trim())
152
+ .filter(l => l.length > 0)
153
+
154
+ const criteria = blockLines.filter(l => /^- \[/.test(l))
155
+ if (criteria.length === 0) return { hasBlock: false, total: 0, unmet: 0 }
156
+
157
+ const unmet = criteria.filter(l => /^- \[ \]/.test(l))
158
+ return { hasBlock: true, total: criteria.length, unmet: unmet.length }
159
+ }
160
+
161
+ // parseGates applies rule 5. Returns { commands }. Throws UsageError naming the
162
+ // offending line number for an unterminated fence.
163
+ function parseGates(lines, startLine, endLine) {
164
+ let markerLine = -1
165
+ for (let i = startLine; i < endLine; i++) {
166
+ if (lines[i].trim() === '**Gates da wave:**') { markerLine = i; break }
167
+ }
168
+ if (markerLine === -1) return { commands: [] }
169
+
170
+ let i = markerLine + 1
171
+ while (i < endLine && lines[i].trim() === '') i++
172
+ if (i >= endLine || lines[i].trim() !== '```bash') {
173
+ throw new UsageError(`malformed gates block at line ${markerLine + 1}: expected a \`\`\`bash fence`)
174
+ }
175
+
176
+ const fenceStart = i
177
+ i++
178
+ const commands = []
179
+ let closed = false
180
+ for (; i < endLine; i++) {
181
+ const trimmed = lines[i].trim()
182
+ if (trimmed === '```') { closed = true; break }
183
+ if (trimmed.length === 0 || trimmed.startsWith('#')) continue
184
+ commands.push(trimmed)
185
+ }
186
+ if (!closed) {
187
+ throw new UsageError(`unterminated gates fence starting at line ${fenceStart + 1}`)
188
+ }
189
+ return { commands }
190
+ }
191
+
192
+ // ────────────────────────────────────────────────────────────────────────────
193
+ // Checks (fixed evaluation order: mls_complete, acceptance_evidence, gates, validate)
194
+ // ────────────────────────────────────────────────────────────────────────────
195
+
196
+ function evalMlsComplete(mls, waveLabel) {
197
+ const evidence = []
198
+ const failures = []
199
+ for (const ml of mls) {
200
+ const { complete, marker } = mlCompletionStatus(ml.lines)
201
+ if (complete) evidence.push(`${ml.id}: ✅`)
202
+ else failures.push(`${ml.id}: not complete (status: ${marker})`)
203
+ }
204
+ const status = mls.length > 0 && failures.length === 0 ? 'passed' : 'blocked'
205
+ // Pinned literally by docs/cli-parity.md ("Wave contains zero MLs" case).
206
+ if (mls.length === 0) failures.push(`wave ${waveLabel}: no ML found`)
207
+ return { name: 'mls_complete', status, evidence, failures }
208
+ }
209
+
210
+ function evalAcceptanceEvidence(mls) {
211
+ const evidence = []
212
+ const failures = []
213
+ for (const ml of mls) {
214
+ const ev = mlAcceptanceEvidence(ml.lines)
215
+ if (!ev.hasBlock) {
216
+ failures.push(`${ml.id}: no acceptance block`)
217
+ } else if (ev.unmet > 0) {
218
+ failures.push(`${ml.id}: ${ev.unmet} unmet acceptance criteria`)
219
+ } else {
220
+ evidence.push(`${ml.id}: ${ev.total} criteria met`)
221
+ }
222
+ }
223
+ const status = failures.length === 0 ? 'passed' : 'blocked'
224
+ return { name: 'acceptance_evidence', status, evidence, failures }
225
+ }
226
+
227
+ function evalGates(commands, cwd) {
228
+ const evidence = []
229
+ const failures = []
230
+ for (const command of commands) {
231
+ const result = spawnSync(command, {
232
+ shell: true,
233
+ cwd,
234
+ encoding: 'utf8',
235
+ stdio: 'pipe',
236
+ })
237
+ const code = result.status === null || result.status === undefined ? 1 : result.status
238
+ if (code === 0) evidence.push(`${command}: exit 0`)
239
+ else failures.push(`${command}: exit ${code}`)
240
+ }
241
+ const status = failures.length === 0 ? 'passed' : 'blocked'
242
+ return { name: 'gates', status, commands, evidence, failures }
243
+ }
244
+
245
+ async function evalValidate() {
246
+ const { violations, warnings } = await validator.validate()
247
+ const summary = `${violations.length} violations, ${warnings.length} warnings`
248
+ if (violations.length === 0) {
249
+ return { name: 'validate', status: 'passed', evidence: [summary], failures: [] }
250
+ }
251
+ return { name: 'validate', status: 'blocked', evidence: [], failures: [summary] }
252
+ }
253
+
254
+ // ────────────────────────────────────────────────────────────────────────────
255
+ // Result document
256
+ // ────────────────────────────────────────────────────────────────────────────
257
+
258
+ function isoSeconds(date) {
259
+ return date.toISOString().replace(/\.\d{3}Z$/, 'Z')
260
+ }
261
+
262
+ function buildDoc(roadmapBasename, waveNumber, checks, startedAt, finishedAt) {
263
+ const status = checks.every(c => c.status === 'passed') ? 'passed' : 'blocked'
264
+ const failures = []
265
+ for (const check of checks) {
266
+ for (const f of check.failures) failures.push(`${check.name}: ${f}`)
267
+ }
268
+ return {
269
+ roadmap: roadmapBasename,
270
+ wave: waveNumber,
271
+ status,
272
+ started_at: isoSeconds(startedAt),
273
+ finished_at: isoSeconds(finishedAt),
274
+ checks: checks.map(c => {
275
+ const doc = { name: c.name, status: c.status, evidence: c.evidence, failures: c.failures }
276
+ if (c.name === 'gates') {
277
+ return { name: c.name, status: c.status, commands: c.commands, evidence: c.evidence, failures: c.failures }
278
+ }
279
+ return doc
280
+ }),
281
+ failures,
282
+ }
283
+ }
284
+
285
+ function printTextReport(doc) {
286
+ console.log(`Roadmap: ${doc.roadmap}`)
287
+ console.log(`Wave: ${doc.wave}`)
288
+ console.log(`Status: ${doc.status}`)
289
+ console.log('')
290
+ for (const check of doc.checks) {
291
+ console.log(`[${check.status}] ${check.name}`)
292
+ for (const e of check.evidence) console.log(` ✓ ${e}`)
293
+ for (const f of check.failures) console.log(` ✗ ${f}`)
294
+ }
295
+ if (doc.failures.length > 0) {
296
+ console.log('')
297
+ console.log('Failures:')
298
+ for (const f of doc.failures) console.log(` • ${f}`)
299
+ }
300
+ }
301
+
302
+ // ────────────────────────────────────────────────────────────────────────────
303
+ // Command
304
+ // ────────────────────────────────────────────────────────────────────────────
305
+
306
+ async function runBarrier(roadmapArg, waveOption, jsonOutput) {
307
+ if (waveOption === undefined || waveOption === null || String(waveOption).trim() === '') {
308
+ throw new UsageError('--wave is required')
309
+ }
310
+ const waveLabel = String(waveOption).trim()
311
+ if (!isValidWaveLabel(waveLabel)) {
312
+ throw new UsageError(`invalid --wave "${waveOption}" — not a valid wave label`)
313
+ }
314
+
315
+ const cfg = config.load()
316
+ const resolved = resolveRoadmapFile(cfg, roadmapArg)
317
+ const content = fs.readFileSync(resolved.path, 'utf8')
318
+ const lines = content.split('\n')
319
+
320
+ const startedAt = new Date()
321
+ const wave = findWave(lines, waveLabel, resolved.basename)
322
+ const mls = findMLs(lines, wave.startLine, wave.endLine)
323
+ const gates = parseGates(lines, wave.startLine, wave.endLine)
324
+
325
+ const checks = []
326
+ checks.push(evalMlsComplete(mls, waveLabel))
327
+ checks.push(evalAcceptanceEvidence(mls))
328
+ checks.push(evalGates(gates.commands, process.cwd()))
329
+ checks.push(await evalValidate())
330
+ const finishedAt = new Date()
331
+
332
+ const doc = buildDoc(resolved.basename, waveLabel, checks, startedAt, finishedAt)
333
+
334
+ if (jsonOutput) {
335
+ console.log(JSON.stringify(doc, null, 2))
336
+ } else {
337
+ printTextReport(doc)
338
+ }
339
+
340
+ return doc.status === 'passed' ? 0 : 1
341
+ }
342
+
343
+ function createBarrierCommand() {
344
+ const cmd = new Command('barrier')
345
+ cmd
346
+ .description(
347
+ 'Deterministic core of the wave-release barrier: evaluates ML completion, ' +
348
+ 'acceptance evidence, wave gates and governance validation for a single wave ' +
349
+ 'declared in a roadmap.'
350
+ )
351
+ .argument('<roadmap>', 'Roadmap basename, with or without .md')
352
+ .option('--wave <label>', 'Wave label to evaluate (e.g. 1, 2-bis, 2-hotfix)')
353
+ .option('--json', 'Emit the result document as JSON instead of a text report')
354
+ .action(async (roadmapArg, options) => {
355
+ try {
356
+ const exitCode = await runBarrier(roadmapArg, options.wave, !!options.json)
357
+ process.exit(exitCode)
358
+ } catch (err) {
359
+ if (err instanceof UsageError) {
360
+ process.stderr.write(`trackfw barrier: ${err.message}\n`)
361
+ process.exit(2)
362
+ return
363
+ }
364
+ process.stderr.write(`trackfw barrier: unexpected error: ${err && err.message ? err.message : err}\n`)
365
+ process.exit(2)
366
+ }
367
+ })
368
+
369
+ return cmd
370
+ }
371
+
372
+ module.exports = createBarrierCommand()
373
+ module.exports.UsageError = UsageError
374
+ module.exports.isValidWaveLabel = isValidWaveLabel
375
+ module.exports.resolveRoadmapFile = resolveRoadmapFile
376
+ module.exports.findWave = findWave
377
+ module.exports.findMLs = findMLs
378
+ module.exports.mlCompletionStatus = mlCompletionStatus
379
+ module.exports.mlAcceptanceEvidence = mlAcceptanceEvidence
380
+ module.exports.parseGates = parseGates
381
+ module.exports.evalMlsComplete = evalMlsComplete
382
+ module.exports.evalAcceptanceEvidence = evalAcceptanceEvidence
383
+ module.exports.evalGates = evalGates
384
+ module.exports.evalValidate = evalValidate
385
+ module.exports.buildDoc = buildDoc
386
+ module.exports.runBarrier = runBarrier
@@ -247,22 +247,112 @@ function describeKey(key) {
247
247
  ].join('\n')
248
248
  }
249
249
 
250
+ /**
251
+ * Retorna a listagem de comandos disponíveis a partir do programa raiz.
252
+ * @param {import('commander').Command} root
253
+ * @returns {string}
254
+ */
255
+ function listCommands(root) {
256
+ const visible = root.commands.filter((sub) => !sub._hidden)
257
+ const nameWidth = Math.max(...visible.map((sub) => sub.name().length)) + 2
258
+ const lines = ['Comandos disponíveis:']
259
+ for (const sub of visible) {
260
+ lines.push(` ${sub.name().padEnd(nameWidth)}${sub.description()}`)
261
+ }
262
+ return lines.join('\n')
263
+ }
264
+
265
+ /**
266
+ * Distância de edição (Levenshtein) entre duas strings.
267
+ * @param {string} a
268
+ * @param {string} b
269
+ * @returns {number}
270
+ */
271
+ function levenshtein(a, b) {
272
+ const la = a.length
273
+ const lb = b.length
274
+ let prev = new Array(lb + 1)
275
+ let curr = new Array(lb + 1)
276
+ for (let j = 0; j <= lb; j++) prev[j] = j
277
+ for (let i = 1; i <= la; i++) {
278
+ curr[0] = i
279
+ for (let j = 1; j <= lb; j++) {
280
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1
281
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost)
282
+ }
283
+ const tmp = prev
284
+ prev = curr
285
+ curr = tmp
286
+ }
287
+ return prev[lb]
288
+ }
289
+
290
+ /**
291
+ * Retorna o comando ou chave de configuração conhecida mais próxima de
292
+ * topic (distância de edição <= 3), ou null se nenhuma for próxima o
293
+ * suficiente.
294
+ * @param {string} topic
295
+ * @param {import('commander').Command} root
296
+ * @returns {string|null}
297
+ */
298
+ function suggestTopic(topic, root) {
299
+ const threshold = 3
300
+ const candidates = [
301
+ ...root.commands.filter((sub) => !sub._hidden).map((sub) => sub.name()),
302
+ ...Object.keys(configDocs)
303
+ ]
304
+ let best = null
305
+ let bestDist = threshold + 1
306
+ for (const candidate of candidates) {
307
+ const dist = levenshtein(topic, candidate)
308
+ if (dist < bestDist) {
309
+ bestDist = dist
310
+ best = candidate
311
+ }
312
+ }
313
+ return bestDist <= threshold ? best : null
314
+ }
315
+
250
316
  const cmd = new Command('help')
251
- cmd.description('Exibe documentação das keys configuráveis do trackfw.yaml')
252
- cmd.argument('[key]', 'key específica para detalhar')
253
- cmd.action((key) => {
254
- if (!key) {
317
+ cmd.description('Exibe ajuda de comandos e documentação das chaves de configuração do trackfw.yaml')
318
+ cmd.argument('[topic]', 'comando ou chave de configuração para detalhar')
319
+ cmd.action(function (topic, options, thisCommand) {
320
+ const self = thisCommand || this
321
+ const root = self.parent || self
322
+
323
+ if (!topic) {
324
+ console.log(listCommands(root))
325
+ console.log()
255
326
  console.log(listKeys())
256
327
  return
257
328
  }
258
- const output = describeKey(key)
259
- if (!output) {
260
- console.error(`chave desconhecida: ${key}`)
261
- process.exit(1)
329
+
330
+ // 1) comando conhecido → ajuda do comando.
331
+ const sub = root.commands.find((c) => c.name() === topic)
332
+ if (sub) {
333
+ console.log(sub.helpInformation().trimEnd())
334
+ return
335
+ }
336
+
337
+ // 2) chave de configuração conhecida → documentação da chave.
338
+ const output = describeKey(topic)
339
+ if (output) {
340
+ console.log(output)
341
+ return
342
+ }
343
+
344
+ // 3) assunto desconhecido → erro com sugestão útil.
345
+ let msg = `assunto desconhecido: ${topic}`
346
+ const suggestion = suggestTopic(topic, root)
347
+ if (suggestion) {
348
+ msg += `\nVocê quis dizer: ${suggestion}?`
262
349
  }
263
- console.log(output)
350
+ console.error(msg)
351
+ process.exitCode = 1
264
352
  })
265
353
 
266
354
  module.exports = cmd
267
355
  module.exports.listKeys = listKeys
268
356
  module.exports.describeKey = describeKey
357
+ module.exports.listCommands = listCommands
358
+ module.exports.suggestTopic = suggestTopic
@@ -31,6 +31,7 @@ function createProgram() {
31
31
  program.addCommand(require('./skills'))
32
32
  program.addCommand(require('./note'))
33
33
  program.addCommand(require('./ship'))
34
+ program.addCommand(require('./barrier'))
34
35
 
35
36
  const { createServeCommand } = require('./serve')
36
37
  program.addCommand(createServeCommand())
@@ -61,9 +61,12 @@ cmd.action(async (options, command) => {
61
61
  const supported = new Set(['claude', 'codex', 'gemini', 'antigravity', 'cursor', 'copilot', 'windsurf', 'amazonq', 'kiro'])
62
62
  // Sem TTY, o escopo nunca é perguntado: default `global` (ADR D1/D4).
63
63
  const scope = await resolveScope({}, { interactive: false })
64
+ const makeOnSkip = () => (_destination, reason) => {
65
+ process.stderr.write(`${reason}\n`)
66
+ }
64
67
  for (const tool of aiTools) {
65
68
  if (!supported.has(tool)) throw new Error(`Unsupported AI tool: ${tool}`)
66
- await generators.installIntegrationTarget(tool, process.cwd(), scope)
69
+ await generators.installIntegrationTarget(tool, process.cwd(), scope, { onSkip: makeOnSkip() })
67
70
  }
68
71
  console.log(`\n${t('init.success')}`)
69
72
  require('../generators/init').printArchitectNextSteps(process.cwd())
@@ -277,7 +280,12 @@ cmd.action(async (options, command) => {
277
280
  const cfg = { projectName, projectType, frontend, backend, backendFramework, pkgManager, hooks, ci, forge: forgeValue || '', requireReqInCommit }
278
281
  await generators.scaffold(cfg)
279
282
 
280
- for (const tool of (aiTools || [])) await generators.installIntegrationTarget(tool, process.cwd(), scope)
283
+ for (const tool of (aiTools || [])) {
284
+ const onSkip = (_destination, reason) => {
285
+ process.stderr.write(`${reason}\n`)
286
+ }
287
+ await generators.installIntegrationTarget(tool, process.cwd(), scope, { onSkip })
288
+ }
281
289
 
282
290
  console.log(`\n${t('init.success')}`)
283
291
  require('../generators/init').printArchitectNextSteps(process.cwd())
@@ -183,6 +183,15 @@ function createLifecycleCommand(kind) {
183
183
  // operações de mutação (install/update/uninstall) e fora de --json.
184
184
  if (mutation) printResolvedDestinations(kind, options)
185
185
 
186
+ // Ligar onSkip: aviso em stderr por artefato outdated+owned pulado pelo
187
+ // preflight de install (contrato: docs/cli-parity.md, seção "install
188
+ // sobre artefato gerenciado desatualizado — skip, não erro fatal").
189
+ if (mutation) {
190
+ options.onSkip = (_destination, reason) => {
191
+ process.stderr.write(`${reason}\n`)
192
+ }
193
+ }
194
+
186
195
  const output = execute(kind, operation, options)
187
196
  console.log(options.json ? JSON.stringify(output) : human(output))
188
197
  })