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 +85 -1
- package/package.json +1 -1
- package/src/commands/barrier.js +386 -0
- package/src/commands/help.js +99 -9
- package/src/commands/index.js +1 -0
- package/src/commands/init.js +10 -2
- package/src/commands/integrations.js +9 -0
- package/src/commands/update-harness.js +208 -0
- package/src/commands/update.js +206 -72
- package/src/generators/init.js +126 -38
- package/src/generators/roadmap.js +125 -0
- package/src/integrations/assets/agents/architect.md +4 -1
- package/src/integrations/assets/agents/backend.md +3 -3
- package/src/integrations/assets/agents/code-quality.md +3 -0
- package/src/integrations/assets/agents/data.md +3 -3
- package/src/integrations/assets/agents/dba.md +3 -3
- package/src/integrations/assets/agents/frontend.md +3 -3
- package/src/integrations/assets/agents/iac.md +3 -3
- package/src/integrations/assets/agents/infra.md +3 -3
- package/src/integrations/assets/agents/qa.md +3 -3
- package/src/integrations/assets/agents/security.md +3 -0
- package/src/integrations/assets/agents/tooling.md +3 -3
- package/src/integrations/assets/agents/ux.md +3 -0
- package/src/integrations/catalog.js +25 -1
- package/src/integrations/index.js +21 -3
- package/src/integrations/manager.js +34 -5
- package/src/lib/update-engine.js +217 -0
- package/src/validator/index.js +2 -0
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
|
+
"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
|
package/src/commands/help.js
CHANGED
|
@@ -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
|
|
252
|
-
cmd.argument('[
|
|
253
|
-
cmd.action((
|
|
254
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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.
|
|
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
|
package/src/commands/index.js
CHANGED
|
@@ -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())
|
package/src/commands/init.js
CHANGED
|
@@ -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 || []))
|
|
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
|
})
|