trackfw 7.0.0 → 7.2.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/bin/trackfw +38 -1
- package/package.json +1 -1
- package/src/branch/prune.js +450 -0
- package/src/commands/branch.js +47 -0
- package/src/commands/commit.js +7 -3
- package/src/commands/discover.js +6 -0
- package/src/commands/doctor.js +33 -0
- package/src/commands/index.js +3 -0
- package/src/commands/integrations.js +79 -2
- package/src/commands/push.js +51 -0
- package/src/commands/release.js +47 -0
- package/src/commands/serve.js +52 -4
- package/src/commands/ship.js +10 -2
- package/src/commands/update-harness.js +319 -12
- package/src/commands/update.js +1 -1
- package/src/config/index.js +17 -2
- package/src/generators/hooks.js +650 -45
- package/src/generators/init.js +12 -0
- package/src/i18n/locales/en-US.json +0 -3
- package/src/i18n/locales/es-ES.json +0 -3
- package/src/i18n/locales/pt-BR.json +0 -3
- package/src/integrations/assets/agents/architect.md +12 -0
- package/src/integrations/doctor.js +0 -0
- package/src/integrations/index.js +1 -1
- package/src/integrations/manager.js +118 -17
- package/src/integrations/render.js +116 -2
- package/src/lib/fatal-error.js +64 -0
- package/src/push/runner.js +223 -0
- package/src/release/runner.js +474 -0
- package/src/ship/runner.js +226 -93
- package/src/validator/index.js +686 -95
|
@@ -3,14 +3,32 @@
|
|
|
3
3
|
const os = require('node:os')
|
|
4
4
|
const { Command } = require('commander')
|
|
5
5
|
const { catalog, execute, parseSurfaces, buildPlans } = require('../integrations')
|
|
6
|
+
const { surfaceFor, readAsset } = require('../integrations/catalog')
|
|
7
|
+
const { markdownParts, resolveAgentModel, looksLikeSuspectModelValue } = require('../integrations/render')
|
|
6
8
|
const identityStore = require('../identity')
|
|
7
9
|
const identityWizard = require('./identity-wizard')
|
|
8
10
|
const { t } = require('../i18n')
|
|
9
11
|
const { createThirdPartyCommand } = require('./thirdparty')
|
|
12
|
+
const configModule = require('../config')
|
|
10
13
|
|
|
11
14
|
const csv = value => String(value).split(',').map(entry => entry.trim()).filter(Boolean)
|
|
12
15
|
const collect = (value, previous) => previous.concat(value)
|
|
13
16
|
|
|
17
|
+
// forceHelp returns the --force help text for a mutation subcommand. The
|
|
18
|
+
// three operations grant --force different powers, and a single shared
|
|
19
|
+
// string previously overstated update/uninstall's reach while never
|
|
20
|
+
// mentioning install's ability to adopt unmanaged bytes — that ambiguity is
|
|
21
|
+
// what sent a user straight into the "Unmanaged artifact does not match a
|
|
22
|
+
// trackfw template" error on `update --force` (see unmanagedArtifactError in
|
|
23
|
+
// src/integrations/manager.js for the matching remediation). Mirrors
|
|
24
|
+
// internal/commands/integrations_flags.go:forceHelp and
|
|
25
|
+
// pypi/trackfw/integrations/command.py.
|
|
26
|
+
function forceHelp(operation) {
|
|
27
|
+
if (operation === 'install') return 'Replace a modified managed artifact, or adopt/overwrite an unmanaged file already on disk'
|
|
28
|
+
if (operation === 'uninstall') return 'Remove a modified managed artifact'
|
|
29
|
+
return "Replace a modified managed artifact; never adopts unmanaged bytes — use 'install --force' for that"
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
function human(result) {
|
|
15
33
|
const lines = [`Available ${result.kind} (catalog ${result.catalog_version}):`]
|
|
16
34
|
for (const item of result.items) lines.push(` ${item.id.padEnd(14)} ${item.name} — ${item.description}`)
|
|
@@ -109,6 +127,10 @@ function createLifecycleCommand(kind) {
|
|
|
109
127
|
// internal/commands/integrations_flags.go registering `third-party` in
|
|
110
128
|
// newIntegrationsLifecycleCmd.
|
|
111
129
|
root.addCommand(createThirdPartyCommand(kind))
|
|
130
|
+
// "models" is agents-only: skills have no model field, and surfacing this
|
|
131
|
+
// under `trackfw skills models` would mislead users. Mirrors the identity
|
|
132
|
+
// flag gate and the Go/Python implementations.
|
|
133
|
+
if (kind === 'agents') root.addCommand(createAgentModelsCommand())
|
|
112
134
|
for (const operation of ['list', 'install', 'uninstall', 'update']) {
|
|
113
135
|
const mutation = operation !== 'list'
|
|
114
136
|
const command = new Command(operation)
|
|
@@ -117,7 +139,7 @@ function createLifecycleCommand(kind) {
|
|
|
117
139
|
.option('--scope <scope>', 'Installation scope: project or global (default: global; asks interactively)')
|
|
118
140
|
.option('--surface <target=surface>', 'Surface selection (repeatable)', collect, [])
|
|
119
141
|
.option('--json', 'Print deterministic JSON')
|
|
120
|
-
.option('--force',
|
|
142
|
+
.option('--force', forceHelp(operation))
|
|
121
143
|
|
|
122
144
|
// Flags de identidade são exclusivas de agents (ADR D5): skills não têm
|
|
123
145
|
// identidade, e createLifecycleCommand é compartilhado entre `agents` e
|
|
@@ -199,6 +221,7 @@ function createLifecycleCommand(kind) {
|
|
|
199
221
|
}
|
|
200
222
|
}
|
|
201
223
|
|
|
224
|
+
options.agentModels = configModule.load().agentModels || {}
|
|
202
225
|
const output = execute(kind, operation, options)
|
|
203
226
|
console.log(options.json ? JSON.stringify(output) : human(output))
|
|
204
227
|
})
|
|
@@ -207,4 +230,58 @@ function createLifecycleCommand(kind) {
|
|
|
207
230
|
return root
|
|
208
231
|
}
|
|
209
232
|
|
|
210
|
-
|
|
233
|
+
// createAgentModelsCommand returns the "models" subcommand of "trackfw agents".
|
|
234
|
+
// Lists, for each catalog agent × target pair, the model identifier that
|
|
235
|
+
// install/update would write to the generated artifact. Mirrors
|
|
236
|
+
// internal/commands/agents_models.go:newAgentModelsCmd and
|
|
237
|
+
// pypi/trackfw/integrations/command.py:_run_models.
|
|
238
|
+
//
|
|
239
|
+
// Column widths must match Go and Python for byte-identical output.
|
|
240
|
+
const MODELS_AGENT_WIDTH = 14
|
|
241
|
+
const MODELS_TIER_WIDTH = 8
|
|
242
|
+
const MODELS_TARGET_WIDTH = 12
|
|
243
|
+
const MODELS_NA = '—'
|
|
244
|
+
|
|
245
|
+
function pad(s, width) {
|
|
246
|
+
return String(s).padEnd(width)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function createAgentModelsCommand() {
|
|
250
|
+
const cmd = new Command('models')
|
|
251
|
+
.description('Show the resolved model each agent uses per target')
|
|
252
|
+
cmd.action(() => {
|
|
253
|
+
const agentModels = configModule.load().agentModels || {}
|
|
254
|
+
|
|
255
|
+
// Warnings: emit once per suspect tier, sorted, to stderr.
|
|
256
|
+
const suspectTiers = Object.keys(agentModels)
|
|
257
|
+
.filter(tier => looksLikeSuspectModelValue(agentModels[tier]))
|
|
258
|
+
.sort()
|
|
259
|
+
for (const tier of suspectTiers) {
|
|
260
|
+
process.stderr.write(
|
|
261
|
+
`WARN: agent_models.${tier} = ${JSON.stringify(agentModels[tier])} — not a version string and not a claude- model ID; will be written literally and may produce an invalid model identifier\n`
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const lines = []
|
|
266
|
+
lines.push(`${pad('AGENT', MODELS_AGENT_WIDTH)} ${pad('TIER', MODELS_TIER_WIDTH)} ${pad('TARGET', MODELS_TARGET_WIDTH)} RESOLVED`)
|
|
267
|
+
|
|
268
|
+
for (const agent of catalog.agents) {
|
|
269
|
+
const source = readAsset(agent)
|
|
270
|
+
const parts = markdownParts(source)
|
|
271
|
+
const tier = parts.model || 'sonnet'
|
|
272
|
+
|
|
273
|
+
for (const target of catalog.targets) {
|
|
274
|
+
const surface = surfaceFor(target, null, 'agents')
|
|
275
|
+
const representation = surface.capabilities.agents.representation
|
|
276
|
+
const { resolved, present } = resolveAgentModel(tier, representation, target.id, agentModels)
|
|
277
|
+
const display = present ? resolved : MODELS_NA
|
|
278
|
+
lines.push(`${pad(agent.id, MODELS_AGENT_WIDTH)} ${pad(tier, MODELS_TIER_WIDTH)} ${pad(target.id, MODELS_TARGET_WIDTH)} ${display}`)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
console.log(lines.join('\n'))
|
|
283
|
+
})
|
|
284
|
+
return cmd
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = { createLifecycleCommand, createAgentModelsCommand, csv, human, promptSelection, promptAmbiguousSurfaces, resolveScope }
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander')
|
|
4
|
+
const { runPush } = require('../push/runner')
|
|
5
|
+
const config = require('../config')
|
|
6
|
+
|
|
7
|
+
function createPushCommand() {
|
|
8
|
+
const cmd = new Command('push')
|
|
9
|
+
cmd
|
|
10
|
+
.description(
|
|
11
|
+
'trackfw push pushes already-created commits without committing and without opening a PR/MR.\n\n' +
|
|
12
|
+
' 1. Validates branch name — must match feat|fix|refactor|chore|docs/<slug>\n' +
|
|
13
|
+
' 2. Validates governance — REQ + roadmap in wip/ must exist for feat/fix/refactor branches\n' +
|
|
14
|
+
' (hard gate: not affected by lenient mode or per-rule severity); chore/docs branches skip\n' +
|
|
15
|
+
' this check, mirroring \'trackfw commit\'\n' +
|
|
16
|
+
' 3. Detects pending squash-merges in other branches (advisory only)\n' +
|
|
17
|
+
' 4. Pushes to origin (adds -u if no upstream is configured yet)\n\n' +
|
|
18
|
+
'push never commits and never opens a PR/MR.\n' +
|
|
19
|
+
'Does not accept -m. If you have not committed yet, run \'trackfw commit -m "..."\' first.\n\n' +
|
|
20
|
+
'Compositional vocabulary:\n' +
|
|
21
|
+
' trackfw commit -m "..." commits\n' +
|
|
22
|
+
' trackfw push pushes\n' +
|
|
23
|
+
' trackfw ship -m "..." commit + push + PR (composition)\n\n' +
|
|
24
|
+
"--force-with-lease pushes with 'git push --force-with-lease' instead of a plain push — for\n" +
|
|
25
|
+
'the post-rebase case, where a plain push is rejected. It only runs when the branch already\n' +
|
|
26
|
+
"has an open PR/MR (verified via the resolved forge CLI): the safe path is always to open the\n" +
|
|
27
|
+
'PR first.'
|
|
28
|
+
)
|
|
29
|
+
.option('--dry-run', 'Print what would be done without executing write commands', false)
|
|
30
|
+
.option('--force-with-lease', 'Governed force-push (git push --force-with-lease); requires an open PR/MR on this branch', false)
|
|
31
|
+
.action((options) => {
|
|
32
|
+
const cfg = config.load()
|
|
33
|
+
const exitCode = runPush(
|
|
34
|
+
{
|
|
35
|
+
dryRun: options.dryRun || false,
|
|
36
|
+
forceWithLease: options.forceWithLease || false,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
configForge: cfg.forge || '',
|
|
40
|
+
repoDir: '.',
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
if (exitCode !== 0) {
|
|
44
|
+
process.exit(exitCode)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return cmd
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = createPushCommand()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander')
|
|
4
|
+
const { runReleaseTag } = require('../release/runner')
|
|
5
|
+
const config = require('../config')
|
|
6
|
+
|
|
7
|
+
function createReleaseCommand() {
|
|
8
|
+
const cmd = new Command('release')
|
|
9
|
+
cmd.description('Governed release operations')
|
|
10
|
+
|
|
11
|
+
const tagCmd = new Command('tag')
|
|
12
|
+
tagCmd
|
|
13
|
+
.description(
|
|
14
|
+
'Create and publish an annotated release tag.\n\n' +
|
|
15
|
+
"It exists because 'trackfw ship' only pushes branches — tag is not a branch operation,\n" +
|
|
16
|
+
"and ship's governance gate (\"REQ + roadmap in wip/\") does not apply to release. See\n" +
|
|
17
|
+
'ADR-2026-08-19-caminho-governado-para-push-forcado-e-tag-de-release.md.\n\n' +
|
|
18
|
+
'Every precondition below refuses with a message naming what to fix — this command\n' +
|
|
19
|
+
'never guesses:\n' +
|
|
20
|
+
' 1. Working tree must be clean.\n' +
|
|
21
|
+
' 2. The default branch (main/master), if checked out locally, must be up to date with origin.\n' +
|
|
22
|
+
' 3. The 4 version files must all match <version> exactly.\n' +
|
|
23
|
+
' 4. CHANGELOG.md must have a "## [<version>] - YYYY-MM-DD" section.\n' +
|
|
24
|
+
' 5. The tag must not already exist, locally or on origin.\n' +
|
|
25
|
+
' 6. The GitHub CLI (gh) must be available and authenticated — release tag currently\n' +
|
|
26
|
+
' only supports GitHub; other forges are refused with instructions to push the tag\n' +
|
|
27
|
+
' manually.\n\n' +
|
|
28
|
+
'On success, it publishes the tag via two GitHub API calls (POST git/tags then POST\n' +
|
|
29
|
+
'git/refs), preserving the annotation.'
|
|
30
|
+
)
|
|
31
|
+
.argument('<version>', 'Version to tag (with or without a leading "v")')
|
|
32
|
+
.action((version) => {
|
|
33
|
+
const cfg = config.load()
|
|
34
|
+
const exitCode = runReleaseTag(version, {
|
|
35
|
+
configForge: cfg.forge || '',
|
|
36
|
+
repoDir: '.',
|
|
37
|
+
})
|
|
38
|
+
if (exitCode !== 0) {
|
|
39
|
+
process.exit(exitCode)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
cmd.addCommand(tagCmd)
|
|
44
|
+
return cmd
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = createReleaseCommand()
|
package/src/commands/serve.js
CHANGED
|
@@ -13,6 +13,48 @@ const { handleAttention } = require('../serve/api_attention')
|
|
|
13
13
|
|
|
14
14
|
const STATIC_DIR = path.join(__dirname, '..', 'serve', 'static')
|
|
15
15
|
|
|
16
|
+
// Aviso pinado, byte-idêntico entre os 3 runtimes (Go, Node.js, Python) — ver
|
|
17
|
+
// docs/cli-parity.md "`trackfw serve` — endereço de escuta, `--host` e
|
|
18
|
+
// aviso de exposição". Emitido quando --host resolve para uma interface
|
|
19
|
+
// diferente de loopback.
|
|
20
|
+
function exposureWarning(host, port) {
|
|
21
|
+
return `WARNING: trackfw serve is binding to ${host}:${port} — the governance chain (ADRs, REQs, roadmaps) will be readable without authentication by any device that can reach it.`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Texto de --help pinado, byte-idêntico entre os 3 runtimes.
|
|
25
|
+
const SERVE_HOST_FLAG_HELP = 'Host to bind to (loopback only by default; use 0.0.0.0 to expose on the network)'
|
|
26
|
+
|
|
27
|
+
// Espelha internal/serve/serve.go IsLoopbackHost — 'localhost' ou IP loopback
|
|
28
|
+
// (IPv4 127.0.0.0/8 inteiro, ou IPv6 ::1), igual a net.IP.IsLoopback() do Go
|
|
29
|
+
// e ipaddress.ip_address(...).is_loopback do Python.
|
|
30
|
+
function isLoopbackHost(host) {
|
|
31
|
+
const net = require('net')
|
|
32
|
+
if (host === 'localhost') return true
|
|
33
|
+
if (net.isIPv4(host)) {
|
|
34
|
+
return host.split('.')[0] === '127'
|
|
35
|
+
}
|
|
36
|
+
if (net.isIPv6(host)) {
|
|
37
|
+
return host === '::1' || host === '0:0:0:0:0:0:0:1'
|
|
38
|
+
}
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Espelha internal/serve/serve.go DisplayURL — URL a imprimir e a abrir no
|
|
43
|
+
// browser. 'localhost' é mantido só para 'localhost' ou IPv4 loopback
|
|
44
|
+
// (127.0.0.0/8), para não mudar a saída do caso comum; hosts IPv6 usam
|
|
45
|
+
// colchetes; qualquer outro host é impresso como está.
|
|
46
|
+
function displayUrl(host, port) {
|
|
47
|
+
const net = require('net')
|
|
48
|
+
if (host === 'localhost') return `http://localhost:${port}`
|
|
49
|
+
if (net.isIPv4(host)) {
|
|
50
|
+
return host.split('.')[0] === '127' ? `http://localhost:${port}` : `http://${host}:${port}`
|
|
51
|
+
}
|
|
52
|
+
if (net.isIPv6(host)) {
|
|
53
|
+
return `http://[${host}]:${port}`
|
|
54
|
+
}
|
|
55
|
+
return `http://${host}:${port}`
|
|
56
|
+
}
|
|
57
|
+
|
|
16
58
|
// Mapa de extensão → Content-Type
|
|
17
59
|
const MIME = {
|
|
18
60
|
'.html': 'text/html; charset=utf-8',
|
|
@@ -141,19 +183,25 @@ function createServeCommand() {
|
|
|
141
183
|
cmd
|
|
142
184
|
.description('Inicia o servidor HTTP do trackfw dashboard (kanban + chain + metrics)')
|
|
143
185
|
.option('--port <port>', 'Porta do servidor', '8080')
|
|
186
|
+
.option('--host <host>', SERVE_HOST_FLAG_HELP, '127.0.0.1')
|
|
144
187
|
.option('--no-open', 'Não abrir o browser automaticamente')
|
|
145
188
|
.action((opts) => {
|
|
146
189
|
const cfg = config.load()
|
|
147
190
|
const port = parseInt(opts.port, 10) || 8080
|
|
191
|
+
const host = opts.host
|
|
148
192
|
|
|
149
193
|
const server = createServer(cfg, port)
|
|
150
194
|
|
|
151
|
-
|
|
152
|
-
console.
|
|
195
|
+
if (!isLoopbackHost(host)) {
|
|
196
|
+
console.error(exposureWarning(host, port))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
server.listen(port, host, () => {
|
|
200
|
+
const url = displayUrl(host, port)
|
|
201
|
+
console.log(`trackfw serve: ${url}`)
|
|
153
202
|
|
|
154
203
|
if (opts.open !== false) {
|
|
155
204
|
// Tentar abrir o browser
|
|
156
|
-
const url = `http://localhost:${port}`
|
|
157
205
|
const { exec } = require('child_process')
|
|
158
206
|
const platform = process.platform
|
|
159
207
|
let openCmd
|
|
@@ -187,4 +235,4 @@ function createServeCommand() {
|
|
|
187
235
|
return cmd
|
|
188
236
|
}
|
|
189
237
|
|
|
190
|
-
module.exports = { createServeCommand, createServer }
|
|
238
|
+
module.exports = { createServeCommand, createServer, isLoopbackHost, displayUrl }
|
package/src/commands/ship.js
CHANGED
|
@@ -15,16 +15,23 @@ function createShipCommand() {
|
|
|
15
15
|
" this check, mirroring 'trackfw commit'\n" +
|
|
16
16
|
' 3. Detects pending squash-merges in other branches (advisory only)\n' +
|
|
17
17
|
' 4. Reviews what is staged (git status --short + git diff --cached --stat)\n' +
|
|
18
|
-
' 5. Commits with Conventional Commits format (-m is required
|
|
18
|
+
' 5. Commits with Conventional Commits format (-m is required, unless nothing is staged\n' +
|
|
19
|
+
' and --force-with-lease is set — see below)\n' +
|
|
19
20
|
' 6. Pushes to origin (adds -u if no upstream is configured yet)\n' +
|
|
20
21
|
' 7. Opens PR/MR via the resolved forge CLI (or prints URL if CLI is absent)\n\n' +
|
|
21
22
|
"Stage your files explicitly before running ship.\n" +
|
|
22
|
-
"This command never executes 'git add .' or 'git add -A'
|
|
23
|
+
"This command never executes 'git add .' or 'git add -A'.\n\n" +
|
|
24
|
+
"--force-with-lease pushes with 'git push --force-with-lease' instead of a plain push —\n" +
|
|
25
|
+
"for the post-rebase case, where a plain push is rejected. It only runs when the branch\n" +
|
|
26
|
+
"already has an open PR/MR (verified via the resolved forge CLI): the safe path is\n" +
|
|
27
|
+
"always to open the PR first. When nothing is staged, it pushes existing commits\n" +
|
|
28
|
+
"without requiring -m."
|
|
23
29
|
)
|
|
24
30
|
.option('-m, --message <msg>', 'Commit message (Conventional Commits format required)')
|
|
25
31
|
.option('--dry-run', 'Print what would be done without executing write commands', false)
|
|
26
32
|
.option('--no-pr', 'Skip PR/MR creation after push')
|
|
27
33
|
.option('--forge <forge>', 'Override forge detection (github, gitlab, bitbucket, azure)', '')
|
|
34
|
+
.option('--force-with-lease', 'Governed force-push (git push --force-with-lease); requires an open PR/MR on this branch', false)
|
|
28
35
|
.action((options) => {
|
|
29
36
|
const cfg = config.load()
|
|
30
37
|
const exitCode = runShip(
|
|
@@ -33,6 +40,7 @@ function createShipCommand() {
|
|
|
33
40
|
dryRun: options.dryRun || false,
|
|
34
41
|
noPR: options.pr === false,
|
|
35
42
|
forge: options.forge || '',
|
|
43
|
+
forceWithLease: options.forceWithLease || false,
|
|
36
44
|
},
|
|
37
45
|
{
|
|
38
46
|
configForge: cfg.forge || '',
|