trackfw 3.0.0 → 4.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 +94 -3
- package/package.json +1 -1
- package/src/commands/barrier.js +358 -0
- package/src/commands/discover.js +18 -0
- package/src/commands/help.js +99 -9
- package/src/commands/index.js +3 -0
- package/src/commands/init.js +40 -2
- package/src/commands/log.js +2 -1
- package/src/commands/note.js +24 -0
- package/src/commands/req.js +7 -1
- package/src/commands/roadmap.js +4 -3
- package/src/commands/ship.js +49 -0
- package/src/commands/update-harness.js +208 -0
- package/src/commands/update.js +206 -72
- package/src/config/index.js +5 -2
- package/src/forge/adapter.js +189 -0
- package/src/forge/resolve.js +191 -0
- package/src/generators/adr.js +13 -4
- package/src/generators/date.js +23 -0
- package/src/generators/init.js +229 -39
- package/src/generators/note.js +154 -0
- package/src/generators/req.js +88 -5
- package/src/generators/roadmap.js +96 -20
- package/src/i18n/locales/en-US.json +2 -1
- package/src/i18n/locales/es-ES.json +2 -1
- package/src/i18n/locales/pt-BR.json +2 -1
- package/src/identity/preset.js +22 -0
- package/src/integrations/assets/agents/architect.md +35 -0
- package/src/integrations/assets/agents/backend.md +32 -0
- package/src/integrations/assets/agents/code-quality.md +32 -0
- package/src/integrations/assets/agents/data.md +32 -0
- package/src/integrations/assets/agents/dba.md +32 -0
- package/src/integrations/assets/agents/frontend.md +32 -0
- package/src/integrations/assets/agents/iac.md +46 -0
- package/src/integrations/assets/agents/infra.md +34 -0
- package/src/integrations/assets/agents/qa.md +32 -0
- package/src/integrations/assets/agents/security.md +32 -0
- package/src/integrations/assets/agents/tooling.md +47 -0
- package/src/integrations/assets/agents/ux.md +32 -0
- package/src/integrations/assets/catalog.json +16 -2
- package/src/integrations/assets/skills/architecture.md +66 -0
- package/src/integrations/assets/skills/backend.md +67 -0
- package/src/integrations/assets/skills/code-quality.md +78 -0
- package/src/integrations/assets/skills/data.md +75 -0
- package/src/integrations/assets/skills/dba.md +77 -0
- package/src/integrations/assets/skills/frontend.md +70 -0
- package/src/integrations/assets/skills/iac.md +90 -0
- package/src/integrations/assets/skills/infra.md +62 -0
- package/src/integrations/assets/skills/qa.md +63 -0
- package/src/integrations/assets/skills/security.md +74 -0
- package/src/integrations/assets/skills/tooling.md +80 -0
- package/src/integrations/assets/skills/ux.md +70 -0
- package/src/integrations/catalog.js +25 -1
- package/src/integrations/index.js +20 -2
- package/src/integrations/render.js +57 -6
- package/src/lib/update-engine.js +217 -0
- package/src/ship/runner.js +454 -0
- package/src/validator/index.js +323 -111
- package/src/validator/traceid.js +1 -1
package/README.md
CHANGED
|
@@ -128,6 +128,10 @@ trackfw validate
|
|
|
128
128
|
|
|
129
129
|
# 6. See what is in flight
|
|
130
130
|
trackfw status
|
|
131
|
+
|
|
132
|
+
# 7. Governed commit + push + open PR/MR (feat/fix/refactor branches only)
|
|
133
|
+
git add -p # stage your changes explicitly
|
|
134
|
+
trackfw ship -m "feat(auth): add login flow"
|
|
131
135
|
```
|
|
132
136
|
|
|
133
137
|
---
|
|
@@ -146,6 +150,8 @@ trackfw status
|
|
|
146
150
|
| `trackfw roadmap move <name> <state>` | Move roadmap between states |
|
|
147
151
|
| `trackfw roadmap list` | List all roadmaps grouped by state |
|
|
148
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 |
|
|
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 |
|
|
149
155
|
| `trackfw context` | Print a structured summary of the project's governance state (REQs, Roadmaps, ADRs with counts and statuses) |
|
|
150
156
|
| `trackfw serve` | Start a local governance dashboard (no cloud, no accounts) |
|
|
151
157
|
| `trackfw status` | Show wip, blocked, REQs waiting on ADRs |
|
|
@@ -159,7 +165,6 @@ trackfw status
|
|
|
159
165
|
| `trackfw agents uninstall` | Remove selected owned agent deployments |
|
|
160
166
|
| `trackfw skills list` | List available governance skills and deployment state |
|
|
161
167
|
| `trackfw skills install/update/uninstall` | Manage selected governance skills |
|
|
162
|
-
| `trackfw gemini`, `cursor`, `copilot`, `windsurf`, `amazonq` | Historical aliases in the Go distribution only; use `agents`/`skills` |
|
|
163
168
|
| `trackfw version` | Print version |
|
|
164
169
|
|
|
165
170
|
The same lifecycle contract is available from the Go/Homebrew, npm, and PyPI
|
|
@@ -325,6 +330,90 @@ $ trackfw status
|
|
|
325
330
|
|
|
326
331
|
---
|
|
327
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
|
+
|
|
328
417
|
## AI assistant integration
|
|
329
418
|
|
|
330
419
|
`trackfw init` can install initial AI integrations. The `agents` and `skills`
|
|
@@ -372,14 +461,16 @@ operation could delete artifacts from the user's home directory. `list` never
|
|
|
372
461
|
prompts and always assumes `global` unless `--scope` is given, so it reports
|
|
373
462
|
the same destinations `install` actually wrote to.
|
|
374
463
|
|
|
375
|
-
The
|
|
464
|
+
The **12 roles** installed for each tool: **architect · backend · frontend · qa · infra · security · code-quality · dba · ux · data · iac · tooling**
|
|
465
|
+
|
|
466
|
+
The **17 skills** cover governance process (governance, implement, plan, release, review) and technical specialties (backend-skill, code-quality-skill, data-skill, dba-skill, frontend-skill, iac-skill, infra-skill, qa-skill, security-skill, tooling-skill, ux-skill, vault-skill).
|
|
376
467
|
|
|
377
468
|
---
|
|
378
469
|
|
|
379
470
|
## Agent identity — give your agents a name
|
|
380
471
|
|
|
381
472
|
By default the agents are functional and impersonal: `trackfw-architect`,
|
|
382
|
-
`trackfw-backend`, … Agent identity lets you name all
|
|
473
|
+
`trackfw-backend`, … Agent identity lets you name all twelve, pick how they
|
|
383
474
|
address you, and call them by name.
|
|
384
475
|
|
|
385
476
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackfw",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.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,358 @@
|
|
|
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
|
+
// Roadmap parsing rules (string-level, see docs/cli-parity.md)
|
|
44
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
// findWave locates the `## Wave <n> ` heading matching waveNumber and returns its
|
|
47
|
+
// line range [startLine, endLine) within `lines`. Throws UsageError naming the wave
|
|
48
|
+
// number and the resolved roadmap basename when not found (pinned literally by
|
|
49
|
+
// docs/cli-parity.md), or naming the offending line number for a malformed heading.
|
|
50
|
+
// roadmapBasename is optional so existing unit tests that only exercise parsing can
|
|
51
|
+
// omit it; the CLI path always supplies it.
|
|
52
|
+
function findWave(lines, waveNumber, roadmapBasename) {
|
|
53
|
+
let startLine = -1
|
|
54
|
+
for (let i = 0; i < lines.length; i++) {
|
|
55
|
+
const line = lines[i]
|
|
56
|
+
const attempt = /^## Wave (\S+)/.exec(line)
|
|
57
|
+
if (!attempt) continue
|
|
58
|
+
if (!/^\d+$/.test(attempt[1])) {
|
|
59
|
+
throw new UsageError(`malformed wave heading at line ${i + 1}: "${line}"`)
|
|
60
|
+
}
|
|
61
|
+
if (parseInt(attempt[1], 10) === waveNumber) {
|
|
62
|
+
startLine = i
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (startLine === -1) {
|
|
67
|
+
throw new UsageError(`wave ${waveNumber} not found in roadmap "${roadmapBasename}"`)
|
|
68
|
+
}
|
|
69
|
+
let endLine = lines.length
|
|
70
|
+
for (let j = startLine + 1; j < lines.length; j++) {
|
|
71
|
+
if (/^## /.test(lines[j])) { endLine = j; break }
|
|
72
|
+
}
|
|
73
|
+
return { startLine, endLine }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// findMLs locates every `### ML-` heading inside [startLine, endLine) and returns
|
|
77
|
+
// { id, lines } for each, where `lines` is the ML body (heading excluded).
|
|
78
|
+
function findMLs(lines, startLine, endLine) {
|
|
79
|
+
const headings = []
|
|
80
|
+
for (let i = startLine; i < endLine; i++) {
|
|
81
|
+
if (/^### ML-/.test(lines[i])) {
|
|
82
|
+
const m = /^### (\S+)/.exec(lines[i])
|
|
83
|
+
headings.push({ id: m[1], headingLine: i })
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return headings.map((h, idx) => {
|
|
87
|
+
let end = endLine
|
|
88
|
+
for (let j = h.headingLine + 1; j < endLine; j++) {
|
|
89
|
+
if (/^### /.test(lines[j]) || /^## /.test(lines[j])) { end = j; break }
|
|
90
|
+
}
|
|
91
|
+
return { id: h.id, lines: lines.slice(h.headingLine + 1, end) }
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// mlCompletionStatus applies rule 3 — completion is a `**Status:**` line whose
|
|
96
|
+
// remainder contains ✅. Returns { complete, marker } where marker is "missing"
|
|
97
|
+
// when no `**Status:**` line exists at all.
|
|
98
|
+
function mlCompletionStatus(mlLines) {
|
|
99
|
+
for (const raw of mlLines) {
|
|
100
|
+
const line = raw.trim()
|
|
101
|
+
const m = /^\*\*Status:\*\*(.*)$/.exec(line)
|
|
102
|
+
if (m) {
|
|
103
|
+
const marker = m[1].trim()
|
|
104
|
+
return { complete: marker.includes('✅'), marker: marker.length > 0 ? marker : 'missing' }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { complete: false, marker: 'missing' }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// mlAcceptanceEvidence applies rule 4. Returns { hasBlock, total, unmet }.
|
|
111
|
+
function mlAcceptanceEvidence(mlLines) {
|
|
112
|
+
let blockStart = -1
|
|
113
|
+
for (let i = 0; i < mlLines.length; i++) {
|
|
114
|
+
if (/^\*\*Crit[ée]rios de aceite:\*\*/.test(mlLines[i].trim())) { blockStart = i; break }
|
|
115
|
+
}
|
|
116
|
+
if (blockStart === -1) return { hasBlock: false, total: 0, unmet: 0 }
|
|
117
|
+
|
|
118
|
+
let blockEnd = mlLines.length
|
|
119
|
+
for (let j = blockStart + 1; j < mlLines.length; j++) {
|
|
120
|
+
if (/^\*\*/.test(mlLines[j].trim())) { blockEnd = j; break }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const blockLines = mlLines.slice(blockStart + 1, blockEnd)
|
|
124
|
+
.map(l => l.trim())
|
|
125
|
+
.filter(l => l.length > 0)
|
|
126
|
+
|
|
127
|
+
const criteria = blockLines.filter(l => /^- \[/.test(l))
|
|
128
|
+
if (criteria.length === 0) return { hasBlock: false, total: 0, unmet: 0 }
|
|
129
|
+
|
|
130
|
+
const unmet = criteria.filter(l => /^- \[ \]/.test(l))
|
|
131
|
+
return { hasBlock: true, total: criteria.length, unmet: unmet.length }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// parseGates applies rule 5. Returns { commands }. Throws UsageError naming the
|
|
135
|
+
// offending line number for an unterminated fence.
|
|
136
|
+
function parseGates(lines, startLine, endLine) {
|
|
137
|
+
let markerLine = -1
|
|
138
|
+
for (let i = startLine; i < endLine; i++) {
|
|
139
|
+
if (lines[i].trim() === '**Gates da wave:**') { markerLine = i; break }
|
|
140
|
+
}
|
|
141
|
+
if (markerLine === -1) return { commands: [] }
|
|
142
|
+
|
|
143
|
+
let i = markerLine + 1
|
|
144
|
+
while (i < endLine && lines[i].trim() === '') i++
|
|
145
|
+
if (i >= endLine || lines[i].trim() !== '```bash') {
|
|
146
|
+
throw new UsageError(`malformed gates block at line ${markerLine + 1}: expected a \`\`\`bash fence`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const fenceStart = i
|
|
150
|
+
i++
|
|
151
|
+
const commands = []
|
|
152
|
+
let closed = false
|
|
153
|
+
for (; i < endLine; i++) {
|
|
154
|
+
const trimmed = lines[i].trim()
|
|
155
|
+
if (trimmed === '```') { closed = true; break }
|
|
156
|
+
if (trimmed.length === 0 || trimmed.startsWith('#')) continue
|
|
157
|
+
commands.push(trimmed)
|
|
158
|
+
}
|
|
159
|
+
if (!closed) {
|
|
160
|
+
throw new UsageError(`unterminated gates fence starting at line ${fenceStart + 1}`)
|
|
161
|
+
}
|
|
162
|
+
return { commands }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
166
|
+
// Checks (fixed evaluation order: mls_complete, acceptance_evidence, gates, validate)
|
|
167
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
function evalMlsComplete(mls, waveNumber) {
|
|
170
|
+
const evidence = []
|
|
171
|
+
const failures = []
|
|
172
|
+
for (const ml of mls) {
|
|
173
|
+
const { complete, marker } = mlCompletionStatus(ml.lines)
|
|
174
|
+
if (complete) evidence.push(`${ml.id}: ✅`)
|
|
175
|
+
else failures.push(`${ml.id}: not complete (status: ${marker})`)
|
|
176
|
+
}
|
|
177
|
+
const status = mls.length > 0 && failures.length === 0 ? 'passed' : 'blocked'
|
|
178
|
+
// Pinned literally by docs/cli-parity.md ("Wave contains zero MLs" case).
|
|
179
|
+
if (mls.length === 0) failures.push(`wave ${waveNumber}: no ML found`)
|
|
180
|
+
return { name: 'mls_complete', status, evidence, failures }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function evalAcceptanceEvidence(mls) {
|
|
184
|
+
const evidence = []
|
|
185
|
+
const failures = []
|
|
186
|
+
for (const ml of mls) {
|
|
187
|
+
const ev = mlAcceptanceEvidence(ml.lines)
|
|
188
|
+
if (!ev.hasBlock) {
|
|
189
|
+
failures.push(`${ml.id}: no acceptance block`)
|
|
190
|
+
} else if (ev.unmet > 0) {
|
|
191
|
+
failures.push(`${ml.id}: ${ev.unmet} unmet acceptance criteria`)
|
|
192
|
+
} else {
|
|
193
|
+
evidence.push(`${ml.id}: ${ev.total} criteria met`)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const status = failures.length === 0 ? 'passed' : 'blocked'
|
|
197
|
+
return { name: 'acceptance_evidence', status, evidence, failures }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function evalGates(commands, cwd) {
|
|
201
|
+
const evidence = []
|
|
202
|
+
const failures = []
|
|
203
|
+
for (const command of commands) {
|
|
204
|
+
const result = spawnSync(command, {
|
|
205
|
+
shell: true,
|
|
206
|
+
cwd,
|
|
207
|
+
encoding: 'utf8',
|
|
208
|
+
stdio: 'pipe',
|
|
209
|
+
})
|
|
210
|
+
const code = result.status === null || result.status === undefined ? 1 : result.status
|
|
211
|
+
if (code === 0) evidence.push(`${command}: exit 0`)
|
|
212
|
+
else failures.push(`${command}: exit ${code}`)
|
|
213
|
+
}
|
|
214
|
+
const status = failures.length === 0 ? 'passed' : 'blocked'
|
|
215
|
+
return { name: 'gates', status, commands, evidence, failures }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function evalValidate() {
|
|
219
|
+
const { violations, warnings } = await validator.validate()
|
|
220
|
+
const summary = `${violations.length} violations, ${warnings.length} warnings`
|
|
221
|
+
if (violations.length === 0) {
|
|
222
|
+
return { name: 'validate', status: 'passed', evidence: [summary], failures: [] }
|
|
223
|
+
}
|
|
224
|
+
return { name: 'validate', status: 'blocked', evidence: [], failures: [summary] }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
228
|
+
// Result document
|
|
229
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
function isoSeconds(date) {
|
|
232
|
+
return date.toISOString().replace(/\.\d{3}Z$/, 'Z')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function buildDoc(roadmapBasename, waveNumber, checks, startedAt, finishedAt) {
|
|
236
|
+
const status = checks.every(c => c.status === 'passed') ? 'passed' : 'blocked'
|
|
237
|
+
const failures = []
|
|
238
|
+
for (const check of checks) {
|
|
239
|
+
for (const f of check.failures) failures.push(`${check.name}: ${f}`)
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
roadmap: roadmapBasename,
|
|
243
|
+
wave: waveNumber,
|
|
244
|
+
status,
|
|
245
|
+
started_at: isoSeconds(startedAt),
|
|
246
|
+
finished_at: isoSeconds(finishedAt),
|
|
247
|
+
checks: checks.map(c => {
|
|
248
|
+
const doc = { name: c.name, status: c.status, evidence: c.evidence, failures: c.failures }
|
|
249
|
+
if (c.name === 'gates') {
|
|
250
|
+
return { name: c.name, status: c.status, commands: c.commands, evidence: c.evidence, failures: c.failures }
|
|
251
|
+
}
|
|
252
|
+
return doc
|
|
253
|
+
}),
|
|
254
|
+
failures,
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function printTextReport(doc) {
|
|
259
|
+
console.log(`Roadmap: ${doc.roadmap}`)
|
|
260
|
+
console.log(`Wave: ${doc.wave}`)
|
|
261
|
+
console.log(`Status: ${doc.status}`)
|
|
262
|
+
console.log('')
|
|
263
|
+
for (const check of doc.checks) {
|
|
264
|
+
console.log(`[${check.status}] ${check.name}`)
|
|
265
|
+
for (const e of check.evidence) console.log(` ✓ ${e}`)
|
|
266
|
+
for (const f of check.failures) console.log(` ✗ ${f}`)
|
|
267
|
+
}
|
|
268
|
+
if (doc.failures.length > 0) {
|
|
269
|
+
console.log('')
|
|
270
|
+
console.log('Failures:')
|
|
271
|
+
for (const f of doc.failures) console.log(` • ${f}`)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
276
|
+
// Command
|
|
277
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
async function runBarrier(roadmapArg, waveOption, jsonOutput) {
|
|
280
|
+
if (waveOption === undefined || waveOption === null || String(waveOption).trim() === '') {
|
|
281
|
+
throw new UsageError('--wave is required')
|
|
282
|
+
}
|
|
283
|
+
if (!/^\d+$/.test(String(waveOption).trim()) || parseInt(waveOption, 10) < 1) {
|
|
284
|
+
throw new UsageError(`invalid --wave value: "${waveOption}" (must be an integer >= 1)`)
|
|
285
|
+
}
|
|
286
|
+
const waveNumber = parseInt(waveOption, 10)
|
|
287
|
+
|
|
288
|
+
const cfg = config.load()
|
|
289
|
+
const resolved = resolveRoadmapFile(cfg, roadmapArg)
|
|
290
|
+
const content = fs.readFileSync(resolved.path, 'utf8')
|
|
291
|
+
const lines = content.split('\n')
|
|
292
|
+
|
|
293
|
+
const startedAt = new Date()
|
|
294
|
+
const wave = findWave(lines, waveNumber, resolved.basename)
|
|
295
|
+
const mls = findMLs(lines, wave.startLine, wave.endLine)
|
|
296
|
+
const gates = parseGates(lines, wave.startLine, wave.endLine)
|
|
297
|
+
|
|
298
|
+
const checks = []
|
|
299
|
+
checks.push(evalMlsComplete(mls, waveNumber))
|
|
300
|
+
checks.push(evalAcceptanceEvidence(mls))
|
|
301
|
+
checks.push(evalGates(gates.commands, process.cwd()))
|
|
302
|
+
checks.push(await evalValidate())
|
|
303
|
+
const finishedAt = new Date()
|
|
304
|
+
|
|
305
|
+
const doc = buildDoc(resolved.basename, waveNumber, checks, startedAt, finishedAt)
|
|
306
|
+
|
|
307
|
+
if (jsonOutput) {
|
|
308
|
+
console.log(JSON.stringify(doc, null, 2))
|
|
309
|
+
} else {
|
|
310
|
+
printTextReport(doc)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return doc.status === 'passed' ? 0 : 1
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function createBarrierCommand() {
|
|
317
|
+
const cmd = new Command('barrier')
|
|
318
|
+
cmd
|
|
319
|
+
.description(
|
|
320
|
+
'Deterministic core of the wave-release barrier: evaluates ML completion, ' +
|
|
321
|
+
'acceptance evidence, wave gates and governance validation for a single wave ' +
|
|
322
|
+
'declared in a roadmap.'
|
|
323
|
+
)
|
|
324
|
+
.argument('<roadmap>', 'Roadmap basename, with or without .md')
|
|
325
|
+
.option('--wave <n>', 'Wave number to evaluate (integer >= 1)')
|
|
326
|
+
.option('--json', 'Emit the result document as JSON instead of a text report')
|
|
327
|
+
.action(async (roadmapArg, options) => {
|
|
328
|
+
try {
|
|
329
|
+
const exitCode = await runBarrier(roadmapArg, options.wave, !!options.json)
|
|
330
|
+
process.exit(exitCode)
|
|
331
|
+
} catch (err) {
|
|
332
|
+
if (err instanceof UsageError) {
|
|
333
|
+
process.stderr.write(`trackfw barrier: ${err.message}\n`)
|
|
334
|
+
process.exit(2)
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
process.stderr.write(`trackfw barrier: unexpected error: ${err && err.message ? err.message : err}\n`)
|
|
338
|
+
process.exit(2)
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
return cmd
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = createBarrierCommand()
|
|
346
|
+
module.exports.UsageError = UsageError
|
|
347
|
+
module.exports.resolveRoadmapFile = resolveRoadmapFile
|
|
348
|
+
module.exports.findWave = findWave
|
|
349
|
+
module.exports.findMLs = findMLs
|
|
350
|
+
module.exports.mlCompletionStatus = mlCompletionStatus
|
|
351
|
+
module.exports.mlAcceptanceEvidence = mlAcceptanceEvidence
|
|
352
|
+
module.exports.parseGates = parseGates
|
|
353
|
+
module.exports.evalMlsComplete = evalMlsComplete
|
|
354
|
+
module.exports.evalAcceptanceEvidence = evalAcceptanceEvidence
|
|
355
|
+
module.exports.evalGates = evalGates
|
|
356
|
+
module.exports.evalValidate = evalValidate
|
|
357
|
+
module.exports.buildDoc = buildDoc
|
|
358
|
+
module.exports.runBarrier = runBarrier
|
package/src/commands/discover.js
CHANGED
|
@@ -4,6 +4,7 @@ const { Command } = require('commander');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { execSync } = require('child_process');
|
|
7
|
+
const { resolve: resolveForge } = require('../forge/resolve');
|
|
7
8
|
|
|
8
9
|
function scan(rootDir) {
|
|
9
10
|
const r = {
|
|
@@ -20,6 +21,7 @@ function scan(rootDir) {
|
|
|
20
21
|
governanceScore: 0,
|
|
21
22
|
hookFramework: 'none',
|
|
22
23
|
ciSystem: 'none',
|
|
24
|
+
forge: '',
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
// trackfw.yaml
|
|
@@ -108,6 +110,18 @@ function scan(rootDir) {
|
|
|
108
110
|
r.ciSystem = 'none';
|
|
109
111
|
}
|
|
110
112
|
|
|
113
|
+
// Forge detection — reuse forge/resolve.js (no duplicate parse).
|
|
114
|
+
// gitRemoteURL returns '' on any error; CI detection is filesystem-based.
|
|
115
|
+
try {
|
|
116
|
+
const { resolveFromRepo } = require('../forge/resolve');
|
|
117
|
+
const res = resolveFromRepo('', '', rootDir);
|
|
118
|
+
if (res.source !== 'none') {
|
|
119
|
+
r.forge = res.forge;
|
|
120
|
+
}
|
|
121
|
+
} catch (_) {
|
|
122
|
+
// forge detection is best-effort; never block scan on it
|
|
123
|
+
}
|
|
124
|
+
|
|
111
125
|
r.governanceScore = calcScore(r);
|
|
112
126
|
return r;
|
|
113
127
|
}
|
|
@@ -146,6 +160,10 @@ function generateYAML(r) {
|
|
|
146
160
|
out += `hooks: ${r.hookFramework}\n`;
|
|
147
161
|
out += `ci: ${r.ciSystem}\n`;
|
|
148
162
|
|
|
163
|
+
if (r.forge) {
|
|
164
|
+
out += `forge: ${r.forge}\n`;
|
|
165
|
+
}
|
|
166
|
+
|
|
149
167
|
return out;
|
|
150
168
|
}
|
|
151
169
|
|
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
|
@@ -29,6 +29,9 @@ function createProgram() {
|
|
|
29
29
|
program.addCommand(require('./version'))
|
|
30
30
|
program.addCommand(require('./agents'))
|
|
31
31
|
program.addCommand(require('./skills'))
|
|
32
|
+
program.addCommand(require('./note'))
|
|
33
|
+
program.addCommand(require('./ship'))
|
|
34
|
+
program.addCommand(require('./barrier'))
|
|
32
35
|
|
|
33
36
|
const { createServeCommand } = require('./serve')
|
|
34
37
|
program.addCommand(createServeCommand())
|