trackfw 7.2.0 → 7.4.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 +67 -1
- package/package.json +1 -1
- package/src/commands/adr.js +2 -1
- package/src/commands/audit-surface.js +520 -0
- package/src/commands/barrier.js +400 -34
- package/src/commands/context.js +42 -36
- package/src/commands/discover.js +72 -21
- package/src/commands/doctor.js +37 -4
- package/src/commands/index.js +1 -0
- package/src/commands/init.js +2 -1
- package/src/commands/integrations.js +22 -4
- package/src/commands/req.js +2 -1
- package/src/commands/thirdparty.js +8 -2
- package/src/commands/update-harness.js +6 -3
- package/src/commands/update.js +147 -10
- package/src/config/index.js +82 -2
- package/src/generators/hooks.js +139 -6
- package/src/generators/init.js +173 -21
- package/src/generators/req.js +15 -53
- package/src/generators/roadmap.js +132 -28
- package/src/homedir.js +30 -0
- package/src/integrations/assets/agents/architect.md +5 -3
- package/src/integrations/assets/agents/security.md +3 -0
- package/src/integrations/doctor.js +0 -0
- package/src/integrations/doctor_remote.js +224 -0
- package/src/integrations/index.js +2 -1
- package/src/integrations/manager.js +10 -2
- package/src/integrations/render.js +39 -8
- package/src/integrations/scaffold_doctor.js +395 -0
- package/src/lib/pathfmt.js +43 -0
- package/src/lib/update-engine.js +28 -3
- package/src/serve/api_board.js +11 -10
- package/src/serve/api_chain.js +13 -17
- package/src/serve/api_metrics.js +2 -8
- package/src/validator/index.js +768 -167
package/src/commands/context.js
CHANGED
|
@@ -4,7 +4,7 @@ const { Command } = require('commander')
|
|
|
4
4
|
const fs = require('fs')
|
|
5
5
|
const path = require('path')
|
|
6
6
|
const config = require('../config')
|
|
7
|
-
const { validate } = require('../validator')
|
|
7
|
+
const { validate, resolveAgentNamespaces, resolveReqFiles } = require('../validator')
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* extractFrontmatterField — extrai valor de campo YAML dentro de bloco --- ... ---.
|
|
@@ -85,11 +85,37 @@ function collectEntries(dir, type, state) {
|
|
|
85
85
|
return entries
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* collectReqEntries — entradas de REQ para o `context`, derivadas do ponto único de leitura
|
|
90
|
+
* (resolveReqFiles). O `state` sai do nome da pasta-pai quando ela é um estado legado; em layout
|
|
91
|
+
* flat ou no canônico req_dir/<agente>/*.md não há estado (invariante D1: REQ não tem estado).
|
|
92
|
+
* @param {object} cfg
|
|
93
|
+
* @returns {Array<{type:string,file:string,status:string,state?:string}>}
|
|
94
|
+
*/
|
|
95
|
+
function collectReqEntries(cfg) {
|
|
96
|
+
const STATES = ['backlog', 'analyzing', 'wip', 'blocked', 'done', 'abandoned']
|
|
97
|
+
const entries = []
|
|
98
|
+
for (const full of resolveReqFiles(cfg)) {
|
|
99
|
+
let content = ''
|
|
100
|
+
try { content = fs.readFileSync(full, 'utf8') } catch (_) {}
|
|
101
|
+
const parent = path.basename(path.dirname(full))
|
|
102
|
+
const state = STATES.includes(parent) ? parent : ''
|
|
103
|
+
let status = extractFrontmatterField(content, 'status')
|
|
104
|
+
if (!status) status = extractInlineStatus(content)
|
|
105
|
+
if (!status) status = state || 'unknown'
|
|
106
|
+
const entry = { type: 'REQ', file: path.basename(full), status }
|
|
107
|
+
if (state) entry.state = state
|
|
108
|
+
entries.push(entry)
|
|
109
|
+
}
|
|
110
|
+
return entries
|
|
111
|
+
}
|
|
112
|
+
|
|
88
113
|
/**
|
|
89
114
|
* getContext — coleta governança e imprime em md ou json.
|
|
90
115
|
* @param {string} format - 'md' | 'json'
|
|
116
|
+
* @returns {Promise<void>}
|
|
91
117
|
*/
|
|
92
|
-
function getContext(format) {
|
|
118
|
+
async function getContext(format) {
|
|
93
119
|
const cfg = config.load()
|
|
94
120
|
|
|
95
121
|
// ADRs
|
|
@@ -98,41 +124,15 @@ function getContext(format) {
|
|
|
98
124
|
adrs.push(...collectEntries(adrDir, 'ADR'))
|
|
99
125
|
}
|
|
100
126
|
|
|
101
|
-
// REQs
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
const reqNamespacing = cfg.roadmapNamespacing || cfg.roadmap_namespacing || ''
|
|
105
|
-
if (reqNamespacing === 'by_agent') {
|
|
106
|
-
const STATES = ['backlog', 'wip', 'blocked', 'done', 'abandoned']
|
|
107
|
-
let agents = cfg.agents || []
|
|
108
|
-
if (!agents.length) {
|
|
109
|
-
try {
|
|
110
|
-
agents = fs.readdirSync(reqDir).filter(f => {
|
|
111
|
-
try { return fs.statSync(path.join(reqDir, f)).isDirectory() } catch (_) { return false }
|
|
112
|
-
})
|
|
113
|
-
} catch (_) {}
|
|
114
|
-
}
|
|
115
|
-
for (const agent of agents) {
|
|
116
|
-
for (const state of STATES) {
|
|
117
|
-
reqs.push(...collectEntries(path.join(reqDir, agent, state), 'REQ', state))
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
} else {
|
|
121
|
-
reqs.push(...collectEntries(reqDir, 'REQ'))
|
|
122
|
-
}
|
|
127
|
+
// REQs — pelo PONTO ÚNICO de leitura (ADR-2026-09-03, D3/D4). Antes, `context` montava a própria
|
|
128
|
+
// árvore (flat, ou <agente>/<estado>/ em by_agent) e não enxergava o layout canônico <agente>/*.md.
|
|
129
|
+
const reqs = collectReqEntries(cfg)
|
|
123
130
|
|
|
124
131
|
// Roadmaps
|
|
125
132
|
const roadmaps = []
|
|
126
133
|
const states = ['wip', 'backlog', 'blocked', 'done', 'abandoned']
|
|
127
134
|
if (cfg.roadmapNamespacing === 'by_agent') {
|
|
128
|
-
|
|
129
|
-
if (agents.length === 0) {
|
|
130
|
-
try {
|
|
131
|
-
agents = fs.readdirSync(cfg.roadmapDir).filter(f => {
|
|
132
|
-
try { return fs.statSync(path.join(cfg.roadmapDir, f)).isDirectory() } catch (_) { return false }
|
|
133
|
-
})
|
|
134
|
-
} catch (_) { agents = [] }
|
|
135
|
-
}
|
|
135
|
+
const agents = resolveAgentNamespaces(cfg, cfg.roadmapDir)
|
|
136
136
|
for (const agent of agents) {
|
|
137
137
|
for (const state of states) {
|
|
138
138
|
const dir = path.join(cfg.roadmapDir, agent, state)
|
|
@@ -147,7 +147,7 @@ function getContext(format) {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
// Validate
|
|
150
|
-
const { violations, warnings } = validate()
|
|
150
|
+
const { violations, warnings } = await validate()
|
|
151
151
|
|
|
152
152
|
// Score
|
|
153
153
|
let score = 0
|
|
@@ -202,8 +202,14 @@ module.exports = (function () {
|
|
|
202
202
|
cmd
|
|
203
203
|
.description('Print governance context for LLM consumption')
|
|
204
204
|
.option('--format <fmt>', 'Output format: md or json', 'md')
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
205
|
+
// Retorna a Promise: o bin roda parseAsync() e só assim uma rejeição chega
|
|
206
|
+
// ao .catch(reportFatalError) com exit code 1 em vez de virar unhandled
|
|
207
|
+
// rejection (ML-1A, ROADMAP-2026-09-02-context-do-cli-node-aguarda-validate).
|
|
208
|
+
.action((opts) => getContext(opts.format))
|
|
208
209
|
return cmd
|
|
209
210
|
})()
|
|
211
|
+
|
|
212
|
+
// Exposto para teste: o teste de context em by_agent duplicava a lógica de coleta e, por isso,
|
|
213
|
+
// continuava verde mesmo quando a produção mudava (asserção sobre uma CÓPIA). Anexar a função ao
|
|
214
|
+
// Command exportado é o menor caminho para o teste exercitar o código de produção de fato.
|
|
215
|
+
module.exports.collectReqEntries = collectReqEntries
|
package/src/commands/discover.js
CHANGED
|
@@ -302,24 +302,72 @@ function installLefthook(rootDir) {
|
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
305
|
+
// DISCOVER_GITHUB_ACTIONS_WORKFLOW_PATH is the canonical relative path of the second,
|
|
306
|
+
// independent CI workflow trackfw writes — the one `trackfw discover --init` generates
|
|
307
|
+
// via installGates, distinct from GITHUB_ACTIONS_WORKFLOW_PATH (trackfw-gate.yml,
|
|
308
|
+
// written by init/update). Both files can coexist in the same project
|
|
309
|
+
// (ADR-2026-08-28). Exported so scaffold_doctor.js can compare against it by path.
|
|
310
|
+
const DISCOVER_GITHUB_ACTIONS_WORKFLOW_PATH = '.github/workflows/trackfw-validate.yml';
|
|
311
|
+
|
|
312
|
+
// buildDiscoverGitHubActionsWorkflowContent returns the template content trackfw writes
|
|
313
|
+
// to DISCOVER_GITHUB_ACTIONS_WORKFLOW_PATH. Mirrors
|
|
314
|
+
// generators.BuildDiscoverGitHubActionsWorkflowContent in
|
|
315
|
+
// internal/generators/scaffold_doctor.go (Go, canonical source of truth) byte-for-byte
|
|
316
|
+
// for the same version.
|
|
317
|
+
//
|
|
318
|
+
// NOT version-independent (ADR-2026-08-28, REQ-2026-08-28 AC6/AC7): the `go install
|
|
319
|
+
// .../cmd/trackfw@vX.Y.Z` step pins the second install mechanism (`go install ...@latest`)
|
|
320
|
+
// to the version of npm/package.json — the version of the CLI that generated/updated the
|
|
321
|
+
// project — mirroring the install.sh pin already applied to
|
|
322
|
+
// buildGitHubActionsWorkflowContent (trackfw-gate.yml) in generators/init.js. Never
|
|
323
|
+
// hardcoded: read lazily to avoid a circular require at module load time.
|
|
324
|
+
//
|
|
325
|
+
// Job id is `governance-go-install` (ML-1A, ROADMAP-2026-09-01) — was `governance`,
|
|
326
|
+
// colliding with buildGitHubActionsWorkflowContent's job id (trackfw-gate.yml,
|
|
327
|
+
// generators/init.js). See that function's doc comment for the full rationale; named
|
|
328
|
+
// after the install mechanism (`go install`), not the workflow file.
|
|
329
|
+
function buildDiscoverGitHubActionsWorkflowContent() {
|
|
330
|
+
const { version } = require('../../package.json');
|
|
331
|
+
return `name: trackfw validate
|
|
311
332
|
on: [push, pull_request]
|
|
312
333
|
jobs:
|
|
313
|
-
governance:
|
|
334
|
+
governance-go-install:
|
|
314
335
|
runs-on: ubuntu-latest
|
|
315
336
|
steps:
|
|
316
337
|
- uses: actions/checkout@v4
|
|
317
338
|
- uses: actions/setup-go@v5
|
|
318
339
|
with:
|
|
319
340
|
go-version: "1.22"
|
|
320
|
-
- run: go install github.com/kgsaran/trackfw/cmd/trackfw@
|
|
341
|
+
- run: go install github.com/kgsaran/trackfw/cmd/trackfw@v${version}
|
|
321
342
|
- run: trackfw validate
|
|
322
343
|
`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function writeCIWorkflow(rootDir) {
|
|
347
|
+
const workflowsDir = path.join(rootDir, '.github', 'workflows');
|
|
348
|
+
if (!isDir(workflowsDir)) fs.mkdirSync(workflowsDir, { recursive: true });
|
|
349
|
+
const dest = path.join(workflowsDir, 'trackfw-validate.yml');
|
|
350
|
+
// Uses fs.lstatSync directly, NOT the isFile helper (fs.statSync, follows
|
|
351
|
+
// symlinks): a DANGLING symlink at dest resolves to "does not exist" under
|
|
352
|
+
// fs.statSync, so the idempotency guard below would not fire, and
|
|
353
|
+
// fs.writeFileSync would then follow the link and CREATE the workflow
|
|
354
|
+
// template at whatever path outside the project the symlink points to. A
|
|
355
|
+
// symlink here — live or dangling — is treated as "already present" so
|
|
356
|
+
// this function never writes through it; it refuses loudly instead of
|
|
357
|
+
// silently creating a file somewhere the caller never asked for.
|
|
358
|
+
let info
|
|
359
|
+
try {
|
|
360
|
+
info = fs.lstatSync(dest)
|
|
361
|
+
} catch (e) {
|
|
362
|
+
info = null
|
|
363
|
+
}
|
|
364
|
+
if (info) {
|
|
365
|
+
if (info.isSymbolicLink()) {
|
|
366
|
+
console.error(`aviso: ${DISCOVER_GITHUB_ACTIONS_WORKFLOW_PATH} é um symlink; trackfw discover não escreve através de symlinks — arquivo não foi tocado`)
|
|
367
|
+
}
|
|
368
|
+
return; // idempotente — não sobrescreve (nem segue o link)
|
|
369
|
+
}
|
|
370
|
+
const content = buildDiscoverGitHubActionsWorkflowContent();
|
|
323
371
|
fs.writeFileSync(dest, content, 'utf8');
|
|
324
372
|
}
|
|
325
373
|
|
|
@@ -551,19 +599,20 @@ function writeCIWorkflowForce(rootDir) {
|
|
|
551
599
|
const workflowsDir = path.join(rootDir, '.github', 'workflows');
|
|
552
600
|
if (!isDir(workflowsDir)) fs.mkdirSync(workflowsDir, { recursive: true });
|
|
553
601
|
const dest = path.join(workflowsDir, 'trackfw-validate.yml');
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
602
|
+
// Same symlink guard as writeCIWorkflow above: "force" means "overwrite
|
|
603
|
+
// an existing REGULAR file unconditionally", not "follow whatever this
|
|
604
|
+
// path resolves to". A symlink here — live or dangling — is refused, not
|
|
605
|
+
// followed.
|
|
606
|
+
try {
|
|
607
|
+
const info = fs.lstatSync(dest)
|
|
608
|
+
if (info.isSymbolicLink()) {
|
|
609
|
+
console.error(`aviso: ${DISCOVER_GITHUB_ACTIONS_WORKFLOW_PATH} é um symlink; trackfw discover não escreve através de symlinks — arquivo não foi tocado`)
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
} catch (e) {
|
|
613
|
+
// does not exist — fine, this is the "force create" case
|
|
614
|
+
}
|
|
615
|
+
const content = buildDiscoverGitHubActionsWorkflowContent();
|
|
567
616
|
fs.writeFileSync(dest, content, 'utf8');
|
|
568
617
|
}
|
|
569
618
|
|
|
@@ -575,3 +624,5 @@ module.exports.writeValidateScript = writeValidateScript;
|
|
|
575
624
|
module.exports.writeCIWorkflow = writeCIWorkflow;
|
|
576
625
|
module.exports.writeCIWorkflowForce = writeCIWorkflowForce;
|
|
577
626
|
module.exports.detectTestFramework = detectTestFramework;
|
|
627
|
+
module.exports.buildDiscoverGitHubActionsWorkflowContent = buildDiscoverGitHubActionsWorkflowContent;
|
|
628
|
+
module.exports.DISCOVER_GITHUB_ACTIONS_WORKFLOW_PATH = DISCOVER_GITHUB_ACTIONS_WORKFLOW_PATH;
|
package/src/commands/doctor.js
CHANGED
|
@@ -1,17 +1,38 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { Command } = require('commander')
|
|
4
|
-
const {
|
|
4
|
+
const {
|
|
5
|
+
runDoctor,
|
|
6
|
+
UNREGISTERED_WRITE,
|
|
7
|
+
HAND_MODIFIED,
|
|
8
|
+
UNKNOWN_CONTENT,
|
|
9
|
+
REQUIRED_STATUS_CHECKS_MISSING,
|
|
10
|
+
ENFORCE_ADMINS_DISABLED,
|
|
11
|
+
HOOKS_PATH_NEUTRALIZED,
|
|
12
|
+
NOT_EVALUATED,
|
|
13
|
+
} = require('../integrations/doctor')
|
|
14
|
+
const { runScaffoldDoctor, SCAFFOLD_DIVERGENT, SCAFFOLD_MISSING, SCAFFOLD_WRONG_MODE } = require('../integrations/scaffold_doctor')
|
|
15
|
+
const { runDoctorRemote } = require('../integrations/doctor_remote')
|
|
5
16
|
const projectConfig = require('../config')
|
|
6
17
|
|
|
7
18
|
function printReport(findings) {
|
|
8
19
|
if (findings.length === 0) {
|
|
9
|
-
return 'trackfw doctor: no mismatches found -- disk matches the manifest for every catalog-managed artifact.'
|
|
20
|
+
return 'trackfw doctor: no mismatches found -- disk matches the manifest for every catalog-managed artifact and all scaffold templates.'
|
|
10
21
|
}
|
|
11
22
|
const unregistered = findings.filter(finding => finding.finding === UNREGISTERED_WRITE).length
|
|
12
23
|
const handModified = findings.filter(finding => finding.finding === HAND_MODIFIED).length
|
|
13
24
|
const unknownContent = findings.filter(finding => finding.finding === UNKNOWN_CONTENT).length
|
|
14
|
-
const
|
|
25
|
+
const scaffoldDivergent = findings.filter(finding => finding.finding === SCAFFOLD_DIVERGENT).length
|
|
26
|
+
const scaffoldMissing = findings.filter(finding => finding.finding === SCAFFOLD_MISSING).length
|
|
27
|
+
const scaffoldWrongMode = findings.filter(finding => finding.finding === SCAFFOLD_WRONG_MODE).length
|
|
28
|
+
const requiredChecksMissing = findings.filter(finding => finding.finding === REQUIRED_STATUS_CHECKS_MISSING).length
|
|
29
|
+
const enforceAdminsDisabled = findings.filter(finding => finding.finding === ENFORCE_ADMINS_DISABLED).length
|
|
30
|
+
const hooksPathNeutralized = findings.filter(finding => finding.finding === HOOKS_PATH_NEUTRALIZED).length
|
|
31
|
+
const notEvaluated = findings.filter(finding => finding.finding === NOT_EVALUATED).length
|
|
32
|
+
const lines = [
|
|
33
|
+
`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, ${requiredChecksMissing} required-status-checks-missing, ${enforceAdminsDisabled} enforce-admins-disabled, ${hooksPathNeutralized} hooks-path-neutralized, ${notEvaluated} not-evaluated`,
|
|
34
|
+
'',
|
|
35
|
+
]
|
|
15
36
|
for (const finding of findings) {
|
|
16
37
|
lines.push(`[${finding.finding}] ${finding.destination}`, ` remedy: ${finding.remedy}`, '')
|
|
17
38
|
}
|
|
@@ -21,8 +42,20 @@ function printReport(findings) {
|
|
|
21
42
|
const cmd = new Command('doctor')
|
|
22
43
|
cmd.description('Detect artifacts on disk missing from the manifest, distinguishing hand-modified artifacts from unknown content')
|
|
23
44
|
cmd.option('--json', 'Emit findings as a JSON array instead of the text report')
|
|
45
|
+
cmd.option('--remote', 'Also check GitHub branch protection (required_status_checks, enforce_admins) and local core.hooksPath neutralization. Requires network + a GitHub credential with admin access; absence of either is reported as not-evaluated, never as a pass (ADR-2026-09-02).')
|
|
24
46
|
cmd.action(async options => {
|
|
25
|
-
const
|
|
47
|
+
const projectRoot = process.cwd()
|
|
48
|
+
const catalogFindings = runDoctor({ agentModels: projectConfig.load().agentModels || {} })
|
|
49
|
+
// Scaffold coverage (ADR-2026-08-27): compare scaffold artifacts on disk against
|
|
50
|
+
// the templates the current binary would generate, using the project's own
|
|
51
|
+
// trackfw.yaml. No manifest entry is written or read (AC3).
|
|
52
|
+
const scaffoldFindings = runScaffoldDoctor(projectRoot)
|
|
53
|
+
let findings = [...catalogFindings, ...scaffoldFindings]
|
|
54
|
+
// Remote modality (ADR-2026-09-02, ML-3A): opt-in only — never runs without --remote, so
|
|
55
|
+
// doctor's offline default is unchanged.
|
|
56
|
+
if (options.remote) {
|
|
57
|
+
findings = [...findings, ...runDoctorRemote({ configForge: projectConfig.load().forge || '', repoDir: projectRoot })]
|
|
58
|
+
}
|
|
26
59
|
if (options.json) {
|
|
27
60
|
process.stdout.write(`${JSON.stringify(findings, null, 2)}\n`)
|
|
28
61
|
return
|
package/src/commands/index.js
CHANGED
|
@@ -57,6 +57,7 @@ function createProgram() {
|
|
|
57
57
|
program.addCommand(require('./commit'))
|
|
58
58
|
program.addCommand(require('./push'))
|
|
59
59
|
program.addCommand(require('./doctor'))
|
|
60
|
+
program.addCommand(require('./audit-surface'))
|
|
60
61
|
|
|
61
62
|
const { createServeCommand } = require('./serve')
|
|
62
63
|
program.addCommand(createServeCommand())
|
package/src/commands/init.js
CHANGED
|
@@ -5,6 +5,7 @@ const identityStore = require('../identity')
|
|
|
5
5
|
const identityWizard = require('./identity-wizard')
|
|
6
6
|
const { resolveIdentityPreset, identityFileExists } = identityWizard
|
|
7
7
|
const { resolveScope } = require('./integrations')
|
|
8
|
+
const { homedir } = require('../homedir')
|
|
8
9
|
|
|
9
10
|
const cmd = new Command('init')
|
|
10
11
|
cmd.description(t('init.description'))
|
|
@@ -16,7 +17,7 @@ cmd.action(async (options, command) => {
|
|
|
16
17
|
const path = require('path')
|
|
17
18
|
const generators = require('../generators/init')
|
|
18
19
|
|
|
19
|
-
const home =
|
|
20
|
+
const home = homedir()
|
|
20
21
|
|
|
21
22
|
// A validação e persistência da flag acontecem incondicionalmente, acima
|
|
22
23
|
// do early-return não-TTY abaixo — é isso que faz um --identity-preset
|
|
@@ -10,6 +10,7 @@ const identityWizard = require('./identity-wizard')
|
|
|
10
10
|
const { t } = require('../i18n')
|
|
11
11
|
const { createThirdPartyCommand } = require('./thirdparty')
|
|
12
12
|
const configModule = require('../config')
|
|
13
|
+
const { homedir } = require('../homedir')
|
|
13
14
|
|
|
14
15
|
const csv = value => String(value).split(',').map(entry => entry.trim()).filter(Boolean)
|
|
15
16
|
const collect = (value, previous) => previous.concat(value)
|
|
@@ -179,7 +180,7 @@ function createLifecycleCommand(kind) {
|
|
|
179
180
|
let presetChanged = false
|
|
180
181
|
if (mutation && kind === 'agents') {
|
|
181
182
|
presetChanged = cmd.getOptionValueSource('identityPreset') === 'cli'
|
|
182
|
-
if (presetChanged) identityWizard.applyIdentityPresetFlag(
|
|
183
|
+
if (presetChanged) identityWizard.applyIdentityPresetFlag(homedir(), options.identityPreset, operation)
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
if (mutation && (!options.targets || !options.targets.length)) {
|
|
@@ -197,7 +198,7 @@ function createLifecycleCommand(kind) {
|
|
|
197
198
|
// planos abaixo. Espelha
|
|
198
199
|
// internal/commands/integrations_flags.go:executeIntegrationMutation.
|
|
199
200
|
if (mutation && kind === 'agents' && !presetChanged) {
|
|
200
|
-
const homeRoot =
|
|
201
|
+
const homeRoot = homedir()
|
|
201
202
|
const identityExists = identityWizard.identityFileExists(homeRoot)
|
|
202
203
|
const isTTY = Boolean(process.stdin.isTTY)
|
|
203
204
|
if (identityWizard.shouldPromptIdentity(kind, isTTY, identityExists, forceIdentity)) {
|
|
@@ -221,7 +222,9 @@ function createLifecycleCommand(kind) {
|
|
|
221
222
|
}
|
|
222
223
|
}
|
|
223
224
|
|
|
224
|
-
|
|
225
|
+
const { models: resolvedModels, warning: modelsWarning } = configModule.resolveAgentModels(options.scope, homedir(), process.cwd())
|
|
226
|
+
if (modelsWarning) process.stderr.write(modelsWarning + '\n')
|
|
227
|
+
options.agentModels = resolvedModels
|
|
225
228
|
const output = execute(kind, operation, options)
|
|
226
229
|
console.log(options.json ? JSON.stringify(output) : human(output))
|
|
227
230
|
})
|
|
@@ -250,7 +253,22 @@ function createAgentModelsCommand() {
|
|
|
250
253
|
const cmd = new Command('models')
|
|
251
254
|
.description('Show the resolved model each agent uses per target')
|
|
252
255
|
cmd.action(() => {
|
|
253
|
-
|
|
256
|
+
// AC5 + AC11: read agent_models from the global config (~/.trackfw/trackfw.yaml),
|
|
257
|
+
// not from the cwd singleton. Show origin before the table.
|
|
258
|
+
const { models: agentModels, source: modelsSource } = configModule.loadGlobalAgentModels(homedir(), process.cwd())
|
|
259
|
+
|
|
260
|
+
// Source line (AC5): show origin before the table; advisory to stderr when not resolved.
|
|
261
|
+
const sourceLines = {
|
|
262
|
+
global: 'source: ~/.trackfw/trackfw.yaml',
|
|
263
|
+
none: 'source: não configurado',
|
|
264
|
+
project_only: 'source: trackfw.yaml do projeto (não vale para escopo global)',
|
|
265
|
+
global_malformed: 'source: arquivo global malformado',
|
|
266
|
+
}
|
|
267
|
+
const sourceLine = sourceLines[modelsSource] || sourceLines['none']
|
|
268
|
+
process.stdout.write(sourceLine + '\n')
|
|
269
|
+
if (modelsSource === 'none') process.stderr.write(configModule.GLOBAL_AGENT_MODELS_NONE_MESSAGE + '\n')
|
|
270
|
+
else if (modelsSource === 'project_only') process.stderr.write(configModule.GLOBAL_AGENT_MODELS_PROJECT_ONLY_MESSAGE + '\n')
|
|
271
|
+
else if (modelsSource === 'global_malformed') process.stderr.write(configModule.MALFORMED_GLOBAL_CONFIG_MESSAGE + '\n')
|
|
254
272
|
|
|
255
273
|
// Warnings: emit once per suspect tier, sorted, to stderr.
|
|
256
274
|
const suspectTiers = Object.keys(agentModels)
|
package/src/commands/req.js
CHANGED
|
@@ -4,6 +4,7 @@ const os = require('os')
|
|
|
4
4
|
const { Command } = require('commander')
|
|
5
5
|
const { listREQs, moveREQ } = require('../generators/req')
|
|
6
6
|
const { t } = require('../i18n')
|
|
7
|
+
const { homedir } = require('../homedir')
|
|
7
8
|
|
|
8
9
|
const cmd = new Command('req')
|
|
9
10
|
cmd.description(t('req.description'))
|
|
@@ -38,7 +39,7 @@ cmd.command('new <title>')
|
|
|
38
39
|
default: 'local',
|
|
39
40
|
})
|
|
40
41
|
const adrDir = adrScope === 'global'
|
|
41
|
-
? path.join(
|
|
42
|
+
? path.join(homedir(), '.trackfw', 'adr')
|
|
42
43
|
: require('../config').load().adrDirs[0]
|
|
43
44
|
|
|
44
45
|
// Perguntas dinâmicas por probe
|
|
@@ -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')
|
|
@@ -16,6 +19,7 @@ const { checkMarkers, checksum: sha256hex } = require('../thirdparty/markers')
|
|
|
16
19
|
const { quarantinePath, newQuarantineEntry, writeQuarantine, readQuarantine, decodeContent } = require('../thirdparty/quarantine')
|
|
17
20
|
const { loadProvenance, verifyApproval, upsertProvenanceEntry } = require('../thirdparty/provenance')
|
|
18
21
|
const { upsertThirdPartyReference, normalizeThirdPartyContent, resolveThirdPartySkillDestination } = require('../thirdparty/references')
|
|
22
|
+
const { homedir } = require('../homedir')
|
|
19
23
|
|
|
20
24
|
// api is the module.exports object itself, referenced internally via
|
|
21
25
|
// `api.thirdPartyFetch(...)` so tests can substitute the network fetch by
|
|
@@ -186,13 +190,15 @@ async function executeThirdPartyInstall(kind, options) {
|
|
|
186
190
|
// write happens (including the skill file below) — failing everything up
|
|
187
191
|
// front avoids leaving a partial state (skill file written, no reference
|
|
188
192
|
// injected) on a precondition failure.
|
|
193
|
+
const { models: resolvedAgentModels, warning: agentModelsWarning } = configModule.resolveAgentModels(scope, homedir(), projectRoot)
|
|
194
|
+
if (agentModelsWarning) process.stderr.write(agentModelsWarning + '\n')
|
|
189
195
|
let identityConfig
|
|
190
196
|
if (options.applyTo && options.applyTo.length) {
|
|
191
197
|
identityConfig = identityStore.load(manager.roots.global)
|
|
192
198
|
for (const agentID of options.applyTo) {
|
|
193
199
|
if (!catalogItems('agents').some(item => item.id === agentID)) throw new Error(`unknown agent item "${agentID}"`)
|
|
194
200
|
for (const rt of resolvedTargets) {
|
|
195
|
-
const agentPlans = buildPlans('agents', { targets: [rt.targetID], items: [agentID], scope, identity: identityConfig, projectRoot })
|
|
201
|
+
const agentPlans = buildPlans('agents', { targets: [rt.targetID], items: [agentID], scope, identity: identityConfig, projectRoot, agentModels: resolvedAgentModels })
|
|
196
202
|
if (!agentPlans.length) throw new Error(`target ${rt.targetID} has no supported agents surface for item "${agentID}"`)
|
|
197
203
|
const inspection = manager.inspect([agentPlans[0]])[0]
|
|
198
204
|
// ADR imprecision found and resolved here (reported, not silently
|
|
@@ -333,7 +339,7 @@ async function executeThirdPartyInstall(kind, options) {
|
|
|
333
339
|
// immediately — not only on the next `agents update`. buildPlans
|
|
334
340
|
// picks up the registry entry just written via
|
|
335
341
|
// applyThirdPartyReferences (../integrations/index.js).
|
|
336
|
-
const agentPlans = buildPlans('agents', { targets: [rt.targetID], items: [agentID], scope, identity: identityConfig, projectRoot })
|
|
342
|
+
const agentPlans = buildPlans('agents', { targets: [rt.targetID], items: [agentID], scope, identity: identityConfig, projectRoot, agentModels: resolvedAgentModels })
|
|
337
343
|
manager.update(agentPlans, {})
|
|
338
344
|
}
|
|
339
345
|
}
|
|
@@ -8,6 +8,7 @@ const projectConfig = require('../config')
|
|
|
8
8
|
const { catalog, buildPlans, IntegrationManager, globalGroupPath } = require('../integrations')
|
|
9
9
|
const { tildeify, validateTargets, buildDocument, humanReport, silenceConsole } = require('../lib/update-engine')
|
|
10
10
|
const { mergeClaudeHookArray, mergeSimpleCommandArray, mergeCopilotHookArray, generateGlobalCredentialGuardScript, generateGlobalGitBranchGuardScript } = require('../generators/hooks')
|
|
11
|
+
const { homedir } = require('../homedir')
|
|
11
12
|
|
|
12
13
|
// `trackfw update harness` is the global counterpart to `trackfw update` —
|
|
13
14
|
// see docs/cli-parity.md, "`trackfw update` vs `trackfw update harness`".
|
|
@@ -40,7 +41,7 @@ const { mergeClaudeHookArray, mergeSimpleCommandArray, mergeCopilotHookArray, ge
|
|
|
40
41
|
|
|
41
42
|
// claudeSkillContent — mirrors generators/init.js:installSkillsForce
|
|
42
43
|
// (npm/src/generators/init.js:1242) byte-for-byte. Duplicated here, not
|
|
43
|
-
// imported, because installSkillsForce() bakes
|
|
44
|
+
// imported, because installSkillsForce() bakes homedir() internally with
|
|
44
45
|
// an unconditional overwrite and no dry-run mode — it cannot be reused
|
|
45
46
|
// without either mutating on --dry-run or forking its signature. Reported
|
|
46
47
|
// as a follow-up cleanup opportunity in the ML-6C handoff report.
|
|
@@ -758,7 +759,9 @@ function catalogBundleTarget(toolId, kind, homeRoot, identityConfig, { dryRun, i
|
|
|
758
759
|
let displayPath = `~/.${toolId}`
|
|
759
760
|
try {
|
|
760
761
|
displayPath = globalGroupPath(toolId, kind)
|
|
761
|
-
const
|
|
762
|
+
const { models: harnessAgentModels, warning: harnessWarnMsg } = projectConfig.resolveAgentModels('global', homeRoot, process.cwd())
|
|
763
|
+
if (harnessWarnMsg) process.stderr.write(harnessWarnMsg + '\n')
|
|
764
|
+
const plans = buildPlans(kind, { targets: [toolId], scope: 'global', identity: identityConfig, agentModels: harnessAgentModels })
|
|
762
765
|
if (!plans.length) return { id, state: 'missing', path: displayPath }
|
|
763
766
|
|
|
764
767
|
const manager = new IntegrationManager({ homeRoot })
|
|
@@ -878,7 +881,7 @@ function run(options) {
|
|
|
878
881
|
process.exit(1)
|
|
879
882
|
}
|
|
880
883
|
|
|
881
|
-
const homeRoot =
|
|
884
|
+
const homeRoot = homedir()
|
|
882
885
|
const dryRun = Boolean(options.dryRun)
|
|
883
886
|
const installMissing = Boolean(options.installMissing)
|
|
884
887
|
|