trackfw 7.1.0 → 7.3.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/package.json +1 -1
- package/src/commands/audit-surface.js +520 -0
- package/src/commands/barrier.js +107 -8
- package/src/commands/commit.js +7 -3
- package/src/commands/doctor.js +17 -3
- package/src/commands/index.js +3 -0
- package/src/commands/integrations.js +80 -1
- package/src/commands/push.js +51 -0
- package/src/commands/release.js +47 -0
- package/src/commands/ship.js +10 -2
- package/src/commands/thirdparty.js +7 -2
- package/src/commands/update-harness.js +4 -1
- package/src/commands/update.js +21 -2
- package/src/config/index.js +96 -2
- package/src/generators/hooks.js +194 -13
- package/src/generators/init.js +84 -16
- package/src/generators/roadmap.js +53 -2
- package/src/integrations/assets/agents/architect.md +17 -3
- package/src/integrations/assets/agents/security.md +3 -0
- package/src/integrations/doctor.js +0 -0
- package/src/integrations/index.js +1 -1
- package/src/integrations/render.js +116 -2
- package/src/integrations/scaffold_doctor.js +366 -0
- package/src/lib/update-engine.js +13 -1
- package/src/push/runner.js +223 -0
- package/src/release/runner.js +474 -0
- package/src/ship/runner.js +177 -21
- package/src/validator/index.js +270 -13
package/src/commands/barrier.js
CHANGED
|
@@ -48,17 +48,20 @@ function resolveRoadmapFile(cfg, roadmapArg) {
|
|
|
48
48
|
// recognised as a wave heading (neither valid nor malformed).
|
|
49
49
|
const WAVE_SCAN_RE = /^## Wave (\S+) /
|
|
50
50
|
|
|
51
|
-
// Grammar: <integer>[-<suffix>] where integer >=
|
|
52
|
-
// Valid: 1, 2, 2-bis, 2-hotfix, 10-a2.
|
|
53
|
-
//
|
|
51
|
+
// Grammar: <integer>[-<suffix>] where integer >= 0 and suffix is [a-z0-9]+ (lowercase).
|
|
52
|
+
// Valid: 0, 1, 2, 2-bis, 2-hotfix, 10-a2. 0 is the Wave 0 threat-model convention
|
|
53
|
+
// (docs/cli-parity.md § "Wave label grammar").
|
|
54
|
+
// Invalid: X, 2-BIS, -bis, 2-, 2-bis-ter.
|
|
54
55
|
const WAVE_LABEL_RE = /^(\d+)(?:-([a-z0-9]+))?$/
|
|
55
56
|
|
|
56
57
|
// isValidWaveLabel returns true iff token matches the grammar above.
|
|
57
58
|
// Exported so unit tests can assert the full table of valid/invalid examples.
|
|
59
|
+
// Shared by both guard points (the --wave flag validation and the heading
|
|
60
|
+
// pre-pass in findWave) — a single function, unlike Go's two duplicated checks.
|
|
58
61
|
function isValidWaveLabel(token) {
|
|
59
62
|
const m = WAVE_LABEL_RE.exec(String(token))
|
|
60
63
|
if (!m) return false
|
|
61
|
-
return parseInt(m[1], 10) >=
|
|
64
|
+
return parseInt(m[1], 10) >= 0
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
// ────────────────────────────────────────────────────────────────────────────
|
|
@@ -224,7 +227,96 @@ function evalAcceptanceEvidence(mls) {
|
|
|
224
227
|
return { name: 'acceptance_evidence', status, evidence, failures }
|
|
225
228
|
}
|
|
226
229
|
|
|
227
|
-
|
|
230
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
231
|
+
// Roadmap trust check (AC11, AC12 — docs/cli-parity.md § Trust and --trust-local-gates)
|
|
232
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
// roadmapTrustForGates determines whether the gates declared in a roadmap can
|
|
235
|
+
// be trusted for execution without --trust-local-gates.
|
|
236
|
+
//
|
|
237
|
+
// Decision (AC4, AC11): the discriminant is git — a roadmap whose content
|
|
238
|
+
// differs from origin/main, or that is absent from origin/main, is untrusted.
|
|
239
|
+
//
|
|
240
|
+
// Returns { trusted: true } or { trusted: false, failureMsg: '...' }.
|
|
241
|
+
// Fail-open when not in a git repo, origin/main not resolvable, or any git
|
|
242
|
+
// error other than "path absent from origin/main". See docs/cli-parity.md.
|
|
243
|
+
function roadmapTrustForGates(roadmapPath) {
|
|
244
|
+
const roadmapDir = path.dirname(roadmapPath)
|
|
245
|
+
|
|
246
|
+
// Step 1: check if we are inside a git repository.
|
|
247
|
+
const revParse = spawnSync('git', ['rev-parse', '--git-dir'], {
|
|
248
|
+
cwd: roadmapDir,
|
|
249
|
+
encoding: 'utf8',
|
|
250
|
+
stdio: 'pipe',
|
|
251
|
+
})
|
|
252
|
+
if (revParse.status !== 0) {
|
|
253
|
+
// Not a git repo → fail-open.
|
|
254
|
+
return { trusted: true }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Step 2: get the repository toplevel.
|
|
258
|
+
const topLevel = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
259
|
+
cwd: roadmapDir,
|
|
260
|
+
encoding: 'utf8',
|
|
261
|
+
stdio: 'pipe',
|
|
262
|
+
})
|
|
263
|
+
if (topLevel.status !== 0) {
|
|
264
|
+
return { trusted: true }
|
|
265
|
+
}
|
|
266
|
+
const repoRoot = topLevel.stdout.trim()
|
|
267
|
+
|
|
268
|
+
// Step 3: compute repo-relative path (always forward slashes for git).
|
|
269
|
+
const absRoadmap = path.resolve(roadmapPath)
|
|
270
|
+
const relPath = path.relative(repoRoot, absRoadmap).split(path.sep).join('/')
|
|
271
|
+
|
|
272
|
+
// Step 4: retrieve the file at origin/main.
|
|
273
|
+
const show = spawnSync('git', ['show', `origin/main:${relPath}`], {
|
|
274
|
+
cwd: repoRoot,
|
|
275
|
+
encoding: 'utf8',
|
|
276
|
+
stdio: 'pipe',
|
|
277
|
+
})
|
|
278
|
+
if (show.status !== 0) {
|
|
279
|
+
// If the path specifically does not exist in origin/main → untrusted.
|
|
280
|
+
const stderr = show.stderr || ''
|
|
281
|
+
if (stderr.includes('does not exist in') || stderr.includes('exists on disk, but not in')) {
|
|
282
|
+
return {
|
|
283
|
+
trusted: false,
|
|
284
|
+
failureMsg: 'gates not evaluated: roadmap is not committed in origin/main — pass --trust-local-gates to evaluate local gates',
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Other failures (no remote, not fetched) → fail-open.
|
|
288
|
+
return { trusted: true }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Step 5: compare content byte-for-byte.
|
|
292
|
+
let localContent
|
|
293
|
+
try {
|
|
294
|
+
localContent = fs.readFileSync(roadmapPath, 'utf8')
|
|
295
|
+
} catch (_) {
|
|
296
|
+
return { trusted: true }
|
|
297
|
+
}
|
|
298
|
+
if (show.stdout !== localContent) {
|
|
299
|
+
return {
|
|
300
|
+
trusted: false,
|
|
301
|
+
failureMsg: 'gates not evaluated: roadmap content differs from origin/main — pass --trust-local-gates to evaluate local gates',
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return { trusted: true }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function evalGates(commands, cwd, trustResult = { trusted: true }) {
|
|
308
|
+
// trustResult: { trusted: true } | { trusted: false, failureMsg: string }
|
|
309
|
+
if (!trustResult.trusted) {
|
|
310
|
+
// Roadmap is not trusted: do not execute gates (AC3, AC14).
|
|
311
|
+
// Report as not_evaluated — distinct from passed and blocked (AC6).
|
|
312
|
+
return {
|
|
313
|
+
name: 'gates',
|
|
314
|
+
status: 'not_evaluated',
|
|
315
|
+
commands,
|
|
316
|
+
evidence: [],
|
|
317
|
+
failures: [trustResult.failureMsg],
|
|
318
|
+
}
|
|
319
|
+
}
|
|
228
320
|
const evidence = []
|
|
229
321
|
const failures = []
|
|
230
322
|
for (const command of commands) {
|
|
@@ -303,7 +395,7 @@ function printTextReport(doc) {
|
|
|
303
395
|
// Command
|
|
304
396
|
// ────────────────────────────────────────────────────────────────────────────
|
|
305
397
|
|
|
306
|
-
async function runBarrier(roadmapArg, waveOption, jsonOutput) {
|
|
398
|
+
async function runBarrier(roadmapArg, waveOption, jsonOutput, trustLocalGates) {
|
|
307
399
|
if (waveOption === undefined || waveOption === null || String(waveOption).trim() === '') {
|
|
308
400
|
throw new UsageError('--wave is required')
|
|
309
401
|
}
|
|
@@ -322,10 +414,15 @@ async function runBarrier(roadmapArg, waveOption, jsonOutput) {
|
|
|
322
414
|
const mls = findMLs(lines, wave.startLine, wave.endLine)
|
|
323
415
|
const gates = parseGates(lines, wave.startLine, wave.endLine)
|
|
324
416
|
|
|
417
|
+
// Determine trust for gate execution (AC11, AC12).
|
|
418
|
+
const trustResult = trustLocalGates
|
|
419
|
+
? { trusted: true }
|
|
420
|
+
: roadmapTrustForGates(resolved.path)
|
|
421
|
+
|
|
325
422
|
const checks = []
|
|
326
423
|
checks.push(evalMlsComplete(mls, waveLabel))
|
|
327
424
|
checks.push(evalAcceptanceEvidence(mls))
|
|
328
|
-
checks.push(evalGates(gates.commands, process.cwd()))
|
|
425
|
+
checks.push(evalGates(gates.commands, process.cwd(), trustResult))
|
|
329
426
|
checks.push(await evalValidate())
|
|
330
427
|
const finishedAt = new Date()
|
|
331
428
|
|
|
@@ -351,9 +448,10 @@ function createBarrierCommand() {
|
|
|
351
448
|
.argument('<roadmap>', 'Roadmap basename, with or without .md')
|
|
352
449
|
.option('--wave <label>', 'Wave label to evaluate (e.g. 1, 2-bis, 2-hotfix)')
|
|
353
450
|
.option('--json', 'Emit the result document as JSON instead of a text report')
|
|
451
|
+
.option('--trust-local-gates', 'Trust the local roadmap content for gate execution without comparing to origin/main (used by the /trackfw:barrier slash command for WIP roadmaps)')
|
|
354
452
|
.action(async (roadmapArg, options) => {
|
|
355
453
|
try {
|
|
356
|
-
const exitCode = await runBarrier(roadmapArg, options.wave, !!options.json)
|
|
454
|
+
const exitCode = await runBarrier(roadmapArg, options.wave, !!options.json, !!options.trustLocalGates)
|
|
357
455
|
process.exit(exitCode)
|
|
358
456
|
} catch (err) {
|
|
359
457
|
if (err instanceof UsageError) {
|
|
@@ -378,6 +476,7 @@ module.exports.findMLs = findMLs
|
|
|
378
476
|
module.exports.mlCompletionStatus = mlCompletionStatus
|
|
379
477
|
module.exports.mlAcceptanceEvidence = mlAcceptanceEvidence
|
|
380
478
|
module.exports.parseGates = parseGates
|
|
479
|
+
module.exports.roadmapTrustForGates = roadmapTrustForGates
|
|
381
480
|
module.exports.evalMlsComplete = evalMlsComplete
|
|
382
481
|
module.exports.evalAcceptanceEvidence = evalAcceptanceEvidence
|
|
383
482
|
module.exports.evalGates = evalGates
|
package/src/commands/commit.js
CHANGED
|
@@ -12,9 +12,13 @@ function createCommitCommand() {
|
|
|
12
12
|
const cmd = new Command('commit')
|
|
13
13
|
cmd
|
|
14
14
|
.description(
|
|
15
|
-
"trackfw commit
|
|
16
|
-
|
|
17
|
-
'
|
|
15
|
+
"trackfw commit commits staged changes directly, but blocks the commit before it\n" +
|
|
16
|
+
'happens when governance is missing, instead of letting it land and only catching it later.\n\n' +
|
|
17
|
+
'Compositional vocabulary:\n' +
|
|
18
|
+
' trackfw commit -m "..." commits\n' +
|
|
19
|
+
' trackfw push pushes\n' +
|
|
20
|
+
' trackfw ship -m "..." commit + push + PR (composition)\n\n' +
|
|
21
|
+
'Behavioral steps:\n\n' +
|
|
18
22
|
" 1. On 'main'/'master': always blocked — commit directly on the default branch is never\n" +
|
|
19
23
|
' permitted.\n' +
|
|
20
24
|
' 2. On a feat/fix/refactor branch: requires a roadmap matching the branch slug already in\n' +
|
package/src/commands/doctor.js
CHANGED
|
@@ -2,15 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
const { Command } = require('commander')
|
|
4
4
|
const { runDoctor, UNREGISTERED_WRITE, HAND_MODIFIED, UNKNOWN_CONTENT } = require('../integrations/doctor')
|
|
5
|
+
const { runScaffoldDoctor, SCAFFOLD_DIVERGENT, SCAFFOLD_MISSING, SCAFFOLD_WRONG_MODE } = require('../integrations/scaffold_doctor')
|
|
6
|
+
const projectConfig = require('../config')
|
|
5
7
|
|
|
6
8
|
function printReport(findings) {
|
|
7
9
|
if (findings.length === 0) {
|
|
8
|
-
return 'trackfw doctor: no mismatches found -- disk matches the manifest for every catalog-managed artifact.'
|
|
10
|
+
return 'trackfw doctor: no mismatches found -- disk matches the manifest for every catalog-managed artifact and all scaffold templates.'
|
|
9
11
|
}
|
|
10
12
|
const unregistered = findings.filter(finding => finding.finding === UNREGISTERED_WRITE).length
|
|
11
13
|
const handModified = findings.filter(finding => finding.finding === HAND_MODIFIED).length
|
|
12
14
|
const unknownContent = findings.filter(finding => finding.finding === UNKNOWN_CONTENT).length
|
|
13
|
-
const
|
|
15
|
+
const scaffoldDivergent = findings.filter(finding => finding.finding === SCAFFOLD_DIVERGENT).length
|
|
16
|
+
const scaffoldMissing = findings.filter(finding => finding.finding === SCAFFOLD_MISSING).length
|
|
17
|
+
const scaffoldWrongMode = findings.filter(finding => finding.finding === SCAFFOLD_WRONG_MODE).length
|
|
18
|
+
const lines = [
|
|
19
|
+
`trackfw doctor: ${findings.length} finding(s) -- ${unregistered} unregistered-write, ${handModified} hand-modified, ${unknownContent} unknown-content, ${scaffoldDivergent} scaffold-divergent, ${scaffoldMissing} scaffold-missing, ${scaffoldWrongMode} scaffold-wrong-mode`,
|
|
20
|
+
'',
|
|
21
|
+
]
|
|
14
22
|
for (const finding of findings) {
|
|
15
23
|
lines.push(`[${finding.finding}] ${finding.destination}`, ` remedy: ${finding.remedy}`, '')
|
|
16
24
|
}
|
|
@@ -21,7 +29,13 @@ const cmd = new Command('doctor')
|
|
|
21
29
|
cmd.description('Detect artifacts on disk missing from the manifest, distinguishing hand-modified artifacts from unknown content')
|
|
22
30
|
cmd.option('--json', 'Emit findings as a JSON array instead of the text report')
|
|
23
31
|
cmd.action(async options => {
|
|
24
|
-
const
|
|
32
|
+
const projectRoot = process.cwd()
|
|
33
|
+
const catalogFindings = runDoctor({ agentModels: projectConfig.load().agentModels || {} })
|
|
34
|
+
// Scaffold coverage (ADR-2026-08-27): compare scaffold artifacts on disk against
|
|
35
|
+
// the templates the current binary would generate, using the project's own
|
|
36
|
+
// trackfw.yaml. No manifest entry is written or read (AC3).
|
|
37
|
+
const scaffoldFindings = runScaffoldDoctor(projectRoot)
|
|
38
|
+
const findings = [...catalogFindings, ...scaffoldFindings]
|
|
25
39
|
if (options.json) {
|
|
26
40
|
process.stdout.write(`${JSON.stringify(findings, null, 2)}\n`)
|
|
27
41
|
return
|
package/src/commands/index.js
CHANGED
|
@@ -51,10 +51,13 @@ function createProgram() {
|
|
|
51
51
|
program.addCommand(require('./skills'))
|
|
52
52
|
program.addCommand(require('./note'))
|
|
53
53
|
program.addCommand(require('./ship'))
|
|
54
|
+
program.addCommand(require('./release'))
|
|
54
55
|
program.addCommand(require('./barrier'))
|
|
55
56
|
program.addCommand(require('./branch'))
|
|
56
57
|
program.addCommand(require('./commit'))
|
|
58
|
+
program.addCommand(require('./push'))
|
|
57
59
|
program.addCommand(require('./doctor'))
|
|
60
|
+
program.addCommand(require('./audit-surface'))
|
|
58
61
|
|
|
59
62
|
const { createServeCommand } = require('./serve')
|
|
60
63
|
program.addCommand(createServeCommand())
|
|
@@ -3,10 +3,13 @@
|
|
|
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)
|
|
@@ -124,6 +127,10 @@ function createLifecycleCommand(kind) {
|
|
|
124
127
|
// internal/commands/integrations_flags.go registering `third-party` in
|
|
125
128
|
// newIntegrationsLifecycleCmd.
|
|
126
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())
|
|
127
134
|
for (const operation of ['list', 'install', 'uninstall', 'update']) {
|
|
128
135
|
const mutation = operation !== 'list'
|
|
129
136
|
const command = new Command(operation)
|
|
@@ -214,6 +221,9 @@ function createLifecycleCommand(kind) {
|
|
|
214
221
|
}
|
|
215
222
|
}
|
|
216
223
|
|
|
224
|
+
const { models: resolvedModels, warning: modelsWarning } = configModule.resolveAgentModels(options.scope, os.homedir(), process.cwd())
|
|
225
|
+
if (modelsWarning) process.stderr.write(modelsWarning + '\n')
|
|
226
|
+
options.agentModels = resolvedModels
|
|
217
227
|
const output = execute(kind, operation, options)
|
|
218
228
|
console.log(options.json ? JSON.stringify(output) : human(output))
|
|
219
229
|
})
|
|
@@ -222,4 +232,73 @@ function createLifecycleCommand(kind) {
|
|
|
222
232
|
return root
|
|
223
233
|
}
|
|
224
234
|
|
|
225
|
-
|
|
235
|
+
// createAgentModelsCommand returns the "models" subcommand of "trackfw agents".
|
|
236
|
+
// Lists, for each catalog agent × target pair, the model identifier that
|
|
237
|
+
// install/update would write to the generated artifact. Mirrors
|
|
238
|
+
// internal/commands/agents_models.go:newAgentModelsCmd and
|
|
239
|
+
// pypi/trackfw/integrations/command.py:_run_models.
|
|
240
|
+
//
|
|
241
|
+
// Column widths must match Go and Python for byte-identical output.
|
|
242
|
+
const MODELS_AGENT_WIDTH = 14
|
|
243
|
+
const MODELS_TIER_WIDTH = 8
|
|
244
|
+
const MODELS_TARGET_WIDTH = 12
|
|
245
|
+
const MODELS_NA = '—'
|
|
246
|
+
|
|
247
|
+
function pad(s, width) {
|
|
248
|
+
return String(s).padEnd(width)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function createAgentModelsCommand() {
|
|
252
|
+
const cmd = new Command('models')
|
|
253
|
+
.description('Show the resolved model each agent uses per target')
|
|
254
|
+
cmd.action(() => {
|
|
255
|
+
// AC5 + AC11: read agent_models from the global config (~/.trackfw/trackfw.yaml),
|
|
256
|
+
// not from the cwd singleton. Show origin before the table.
|
|
257
|
+
const { models: agentModels, source: modelsSource } = configModule.loadGlobalAgentModels(os.homedir(), process.cwd())
|
|
258
|
+
|
|
259
|
+
// Source line (AC5): show origin before the table; advisory to stderr when not resolved.
|
|
260
|
+
const sourceLines = {
|
|
261
|
+
global: 'source: ~/.trackfw/trackfw.yaml',
|
|
262
|
+
none: 'source: não configurado',
|
|
263
|
+
project_only: 'source: trackfw.yaml do projeto (não vale para escopo global)',
|
|
264
|
+
global_malformed: 'source: arquivo global malformado',
|
|
265
|
+
}
|
|
266
|
+
const sourceLine = sourceLines[modelsSource] || sourceLines['none']
|
|
267
|
+
process.stdout.write(sourceLine + '\n')
|
|
268
|
+
if (modelsSource === 'none') process.stderr.write(configModule.GLOBAL_AGENT_MODELS_NONE_MESSAGE + '\n')
|
|
269
|
+
else if (modelsSource === 'project_only') process.stderr.write(configModule.GLOBAL_AGENT_MODELS_PROJECT_ONLY_MESSAGE + '\n')
|
|
270
|
+
else if (modelsSource === 'global_malformed') process.stderr.write(configModule.MALFORMED_GLOBAL_CONFIG_MESSAGE + '\n')
|
|
271
|
+
|
|
272
|
+
// Warnings: emit once per suspect tier, sorted, to stderr.
|
|
273
|
+
const suspectTiers = Object.keys(agentModels)
|
|
274
|
+
.filter(tier => looksLikeSuspectModelValue(agentModels[tier]))
|
|
275
|
+
.sort()
|
|
276
|
+
for (const tier of suspectTiers) {
|
|
277
|
+
process.stderr.write(
|
|
278
|
+
`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`
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const lines = []
|
|
283
|
+
lines.push(`${pad('AGENT', MODELS_AGENT_WIDTH)} ${pad('TIER', MODELS_TIER_WIDTH)} ${pad('TARGET', MODELS_TARGET_WIDTH)} RESOLVED`)
|
|
284
|
+
|
|
285
|
+
for (const agent of catalog.agents) {
|
|
286
|
+
const source = readAsset(agent)
|
|
287
|
+
const parts = markdownParts(source)
|
|
288
|
+
const tier = parts.model || 'sonnet'
|
|
289
|
+
|
|
290
|
+
for (const target of catalog.targets) {
|
|
291
|
+
const surface = surfaceFor(target, null, 'agents')
|
|
292
|
+
const representation = surface.capabilities.agents.representation
|
|
293
|
+
const { resolved, present } = resolveAgentModel(tier, representation, target.id, agentModels)
|
|
294
|
+
const display = present ? resolved : MODELS_NA
|
|
295
|
+
lines.push(`${pad(agent.id, MODELS_AGENT_WIDTH)} ${pad(tier, MODELS_TIER_WIDTH)} ${pad(target.id, MODELS_TARGET_WIDTH)} ${display}`)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
console.log(lines.join('\n'))
|
|
300
|
+
})
|
|
301
|
+
return cmd
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
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/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 || '',
|
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
// `trackfw skills third-party` (D1). See:
|
|
7
7
|
// docs/adr/ADR-2026-08-15-gate-de-duas-fases-para-artefatos-de-terceiro-quarentena-parecer-vinculado-por-checksum-e-deteccao-por-proveniencia-versionada.md
|
|
8
8
|
|
|
9
|
+
const os = require('os')
|
|
10
|
+
|
|
9
11
|
const { Command } = require('commander')
|
|
10
12
|
|
|
13
|
+
const configModule = require('../config')
|
|
11
14
|
const identityStore = require('../identity')
|
|
12
15
|
const { items: catalogItems } = require('../integrations/catalog')
|
|
13
16
|
const { buildPlans, IntegrationManager } = require('../integrations')
|
|
@@ -186,13 +189,15 @@ async function executeThirdPartyInstall(kind, options) {
|
|
|
186
189
|
// write happens (including the skill file below) — failing everything up
|
|
187
190
|
// front avoids leaving a partial state (skill file written, no reference
|
|
188
191
|
// injected) on a precondition failure.
|
|
192
|
+
const { models: resolvedAgentModels, warning: agentModelsWarning } = configModule.resolveAgentModels(scope, os.homedir(), projectRoot)
|
|
193
|
+
if (agentModelsWarning) process.stderr.write(agentModelsWarning + '\n')
|
|
189
194
|
let identityConfig
|
|
190
195
|
if (options.applyTo && options.applyTo.length) {
|
|
191
196
|
identityConfig = identityStore.load(manager.roots.global)
|
|
192
197
|
for (const agentID of options.applyTo) {
|
|
193
198
|
if (!catalogItems('agents').some(item => item.id === agentID)) throw new Error(`unknown agent item "${agentID}"`)
|
|
194
199
|
for (const rt of resolvedTargets) {
|
|
195
|
-
const agentPlans = buildPlans('agents', { targets: [rt.targetID], items: [agentID], scope, identity: identityConfig, projectRoot })
|
|
200
|
+
const agentPlans = buildPlans('agents', { targets: [rt.targetID], items: [agentID], scope, identity: identityConfig, projectRoot, agentModels: resolvedAgentModels })
|
|
196
201
|
if (!agentPlans.length) throw new Error(`target ${rt.targetID} has no supported agents surface for item "${agentID}"`)
|
|
197
202
|
const inspection = manager.inspect([agentPlans[0]])[0]
|
|
198
203
|
// ADR imprecision found and resolved here (reported, not silently
|
|
@@ -333,7 +338,7 @@ async function executeThirdPartyInstall(kind, options) {
|
|
|
333
338
|
// immediately — not only on the next `agents update`. buildPlans
|
|
334
339
|
// picks up the registry entry just written via
|
|
335
340
|
// applyThirdPartyReferences (../integrations/index.js).
|
|
336
|
-
const agentPlans = buildPlans('agents', { targets: [rt.targetID], items: [agentID], scope, identity: identityConfig, projectRoot })
|
|
341
|
+
const agentPlans = buildPlans('agents', { targets: [rt.targetID], items: [agentID], scope, identity: identityConfig, projectRoot, agentModels: resolvedAgentModels })
|
|
337
342
|
manager.update(agentPlans, {})
|
|
338
343
|
}
|
|
339
344
|
}
|
|
@@ -4,6 +4,7 @@ const fs = require('fs')
|
|
|
4
4
|
const os = require('os')
|
|
5
5
|
const path = require('path')
|
|
6
6
|
const identityStore = require('../identity')
|
|
7
|
+
const projectConfig = require('../config')
|
|
7
8
|
const { catalog, buildPlans, IntegrationManager, globalGroupPath } = require('../integrations')
|
|
8
9
|
const { tildeify, validateTargets, buildDocument, humanReport, silenceConsole } = require('../lib/update-engine')
|
|
9
10
|
const { mergeClaudeHookArray, mergeSimpleCommandArray, mergeCopilotHookArray, generateGlobalCredentialGuardScript, generateGlobalGitBranchGuardScript } = require('../generators/hooks')
|
|
@@ -757,7 +758,9 @@ function catalogBundleTarget(toolId, kind, homeRoot, identityConfig, { dryRun, i
|
|
|
757
758
|
let displayPath = `~/.${toolId}`
|
|
758
759
|
try {
|
|
759
760
|
displayPath = globalGroupPath(toolId, kind)
|
|
760
|
-
const
|
|
761
|
+
const { models: harnessAgentModels, warning: harnessWarnMsg } = projectConfig.resolveAgentModels('global', homeRoot, process.cwd())
|
|
762
|
+
if (harnessWarnMsg) process.stderr.write(harnessWarnMsg + '\n')
|
|
763
|
+
const plans = buildPlans(kind, { targets: [toolId], scope: 'global', identity: identityConfig, agentModels: harnessAgentModels })
|
|
761
764
|
if (!plans.length) return { id, state: 'missing', path: displayPath }
|
|
762
765
|
|
|
763
766
|
const manager = new IntegrationManager({ homeRoot })
|
package/src/commands/update.js
CHANGED
|
@@ -151,7 +151,7 @@ function codexProjectAgentsTarget(cwd, identityConfig, { dryRun, installMissing
|
|
|
151
151
|
const manager = new IntegrationManager({ projectRoot: cwd });
|
|
152
152
|
let wroteAny = false;
|
|
153
153
|
for (const kind of ['agents', 'skills']) {
|
|
154
|
-
const plans = buildPlans(kind, { targets: ['codex'], scope: 'project', identity: identityConfig });
|
|
154
|
+
const plans = buildPlans(kind, { targets: ['codex'], scope: 'project', identity: identityConfig, agentModels: projectConfig.load(cwd).agentModels || {} });
|
|
155
155
|
const statuses = manager.inspect(plans);
|
|
156
156
|
const toWrite = plans.filter((_, index) => {
|
|
157
157
|
const state = statuses[index].state;
|
|
@@ -200,6 +200,10 @@ function buildProjectTargets(cwd, cfg, identityConfig, { dryRun, installMissing
|
|
|
200
200
|
path: 'CLAUDE.md, AGENTS.md, GEMINI.md, .github/copilot-instructions.md, .windsurfrules, .amazonq/developer/guidelines.md, .cursor/rules/trackfw.mdc',
|
|
201
201
|
root: cwd,
|
|
202
202
|
relPaths: ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md', '.github/copilot-instructions.md', '.windsurfrules', '.amazonq/developer/guidelines.md', '.cursor/rules/trackfw.mdc'],
|
|
203
|
+
// Gap E: injectRulesDetected → readAgentConventions reads trackfw.yaml from root;
|
|
204
|
+
// without it in the dry-run sandbox, agent_conventions is silently omitted from
|
|
205
|
+
// CLAUDE.md, producing a hash that diverges from the real run.
|
|
206
|
+
seeds: ['trackfw.yaml'],
|
|
203
207
|
apply: (root) => generators.injectRulesDetected(root),
|
|
204
208
|
dryRun,
|
|
205
209
|
installMissing,
|
|
@@ -207,7 +211,7 @@ function buildProjectTargets(cwd, cfg, identityConfig, { dryRun, installMissing
|
|
|
207
211
|
|
|
208
212
|
if (include('agent-hooks')) targets.push(runFileTarget({
|
|
209
213
|
id: 'agent-hooks',
|
|
210
|
-
path: '.claude/settings.json, .codex/hooks.json, .gemini/settings.json, .kiro/hooks/trackfw-attention.json, .github/hooks/trackfw-attention.json, .cursor/hooks.json, scripts/trackfw-attention-*.sh, scripts/trackfw-credential-guard.sh, scripts/trackfw-git-branch-guard.sh',
|
|
214
|
+
path: '.claude/settings.json, .codex/hooks.json, .gemini/settings.json, .kiro/hooks/trackfw-attention.json, .github/hooks/trackfw-attention.json, .cursor/hooks.json, scripts/trackfw-attention-*.sh, scripts/trackfw-credential-guard.sh, scripts/trackfw-git-branch-guard.sh, .windsurf/hooks.json, .amazonq/cli-agents/q_cli_default.json',
|
|
211
215
|
root: cwd,
|
|
212
216
|
relPaths: [
|
|
213
217
|
'.claude/settings.json',
|
|
@@ -220,6 +224,21 @@ function buildProjectTargets(cwd, cfg, identityConfig, { dryRun, installMissing
|
|
|
220
224
|
'scripts/trackfw-attention-cleanup.sh',
|
|
221
225
|
'scripts/trackfw-credential-guard.sh',
|
|
222
226
|
'scripts/trackfw-git-branch-guard.sh',
|
|
227
|
+
'.windsurf/hooks.json', // Gap A: injectWindsurfHooks writes this
|
|
228
|
+
'.amazonq/cli-agents/q_cli_default.json', // Gap B: injectAmazonQHooks writes this
|
|
229
|
+
],
|
|
230
|
+
// Gap C: injectHooksDetected checks these files to decide which hooks to
|
|
231
|
+
// inject. Without them in the dry-run sandbox, hooks for installed agents
|
|
232
|
+
// (windsurf, copilot, etc.) are silently omitted — dry-run lies by omission.
|
|
233
|
+
// Gap E: trackfw.yaml may be read by hooks generators for config.
|
|
234
|
+
seeds: [
|
|
235
|
+
'trackfw.yaml',
|
|
236
|
+
'CLAUDE.md',
|
|
237
|
+
'AGENTS.md',
|
|
238
|
+
'GEMINI.md',
|
|
239
|
+
'.github/copilot-instructions.md',
|
|
240
|
+
'.windsurfrules',
|
|
241
|
+
'.amazonq/developer/guidelines.md',
|
|
223
242
|
],
|
|
224
243
|
apply: (root) => {
|
|
225
244
|
hooksGen.injectHooksDetected(root);
|