trackfw 7.3.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/barrier.js +293 -26
- package/src/commands/context.js +42 -36
- package/src/commands/discover.js +72 -21
- package/src/commands/doctor.js +23 -3
- package/src/commands/init.js +2 -1
- package/src/commands/integrations.js +5 -4
- package/src/commands/req.js +2 -1
- package/src/commands/thirdparty.js +2 -1
- package/src/commands/update-harness.js +3 -2
- package/src/commands/update.js +127 -9
- package/src/config/index.js +4 -3
- package/src/generators/hooks.js +121 -6
- package/src/generators/init.js +103 -7
- package/src/generators/req.js +15 -53
- package/src/generators/roadmap.js +81 -28
- package/src/homedir.js +30 -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 +29 -0
- package/src/lib/pathfmt.js +43 -0
- package/src/lib/update-engine.js +15 -2
- 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/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,8 +1,18 @@
|
|
|
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')
|
|
5
14
|
const { runScaffoldDoctor, SCAFFOLD_DIVERGENT, SCAFFOLD_MISSING, SCAFFOLD_WRONG_MODE } = require('../integrations/scaffold_doctor')
|
|
15
|
+
const { runDoctorRemote } = require('../integrations/doctor_remote')
|
|
6
16
|
const projectConfig = require('../config')
|
|
7
17
|
|
|
8
18
|
function printReport(findings) {
|
|
@@ -15,8 +25,12 @@ function printReport(findings) {
|
|
|
15
25
|
const scaffoldDivergent = findings.filter(finding => finding.finding === SCAFFOLD_DIVERGENT).length
|
|
16
26
|
const scaffoldMissing = findings.filter(finding => finding.finding === SCAFFOLD_MISSING).length
|
|
17
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
|
|
18
32
|
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`,
|
|
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`,
|
|
20
34
|
'',
|
|
21
35
|
]
|
|
22
36
|
for (const finding of findings) {
|
|
@@ -28,6 +42,7 @@ function printReport(findings) {
|
|
|
28
42
|
const cmd = new Command('doctor')
|
|
29
43
|
cmd.description('Detect artifacts on disk missing from the manifest, distinguishing hand-modified artifacts from unknown content')
|
|
30
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).')
|
|
31
46
|
cmd.action(async options => {
|
|
32
47
|
const projectRoot = process.cwd()
|
|
33
48
|
const catalogFindings = runDoctor({ agentModels: projectConfig.load().agentModels || {} })
|
|
@@ -35,7 +50,12 @@ cmd.action(async options => {
|
|
|
35
50
|
// the templates the current binary would generate, using the project's own
|
|
36
51
|
// trackfw.yaml. No manifest entry is written or read (AC3).
|
|
37
52
|
const scaffoldFindings = runScaffoldDoctor(projectRoot)
|
|
38
|
-
|
|
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
|
+
}
|
|
39
59
|
if (options.json) {
|
|
40
60
|
process.stdout.write(`${JSON.stringify(findings, null, 2)}\n`)
|
|
41
61
|
return
|
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,7 @@ function createLifecycleCommand(kind) {
|
|
|
221
222
|
}
|
|
222
223
|
}
|
|
223
224
|
|
|
224
|
-
const { models: resolvedModels, warning: modelsWarning } = configModule.resolveAgentModels(options.scope,
|
|
225
|
+
const { models: resolvedModels, warning: modelsWarning } = configModule.resolveAgentModels(options.scope, homedir(), process.cwd())
|
|
225
226
|
if (modelsWarning) process.stderr.write(modelsWarning + '\n')
|
|
226
227
|
options.agentModels = resolvedModels
|
|
227
228
|
const output = execute(kind, operation, options)
|
|
@@ -254,7 +255,7 @@ function createAgentModelsCommand() {
|
|
|
254
255
|
cmd.action(() => {
|
|
255
256
|
// AC5 + AC11: read agent_models from the global config (~/.trackfw/trackfw.yaml),
|
|
256
257
|
// not from the cwd singleton. Show origin before the table.
|
|
257
|
-
const { models: agentModels, source: modelsSource } = configModule.loadGlobalAgentModels(
|
|
258
|
+
const { models: agentModels, source: modelsSource } = configModule.loadGlobalAgentModels(homedir(), process.cwd())
|
|
258
259
|
|
|
259
260
|
// Source line (AC5): show origin before the table; advisory to stderr when not resolved.
|
|
260
261
|
const sourceLines = {
|
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
|
|
@@ -19,6 +19,7 @@ const { checkMarkers, checksum: sha256hex } = require('../thirdparty/markers')
|
|
|
19
19
|
const { quarantinePath, newQuarantineEntry, writeQuarantine, readQuarantine, decodeContent } = require('../thirdparty/quarantine')
|
|
20
20
|
const { loadProvenance, verifyApproval, upsertProvenanceEntry } = require('../thirdparty/provenance')
|
|
21
21
|
const { upsertThirdPartyReference, normalizeThirdPartyContent, resolveThirdPartySkillDestination } = require('../thirdparty/references')
|
|
22
|
+
const { homedir } = require('../homedir')
|
|
22
23
|
|
|
23
24
|
// api is the module.exports object itself, referenced internally via
|
|
24
25
|
// `api.thirdPartyFetch(...)` so tests can substitute the network fetch by
|
|
@@ -189,7 +190,7 @@ async function executeThirdPartyInstall(kind, options) {
|
|
|
189
190
|
// write happens (including the skill file below) — failing everything up
|
|
190
191
|
// front avoids leaving a partial state (skill file written, no reference
|
|
191
192
|
// injected) on a precondition failure.
|
|
192
|
-
const { models: resolvedAgentModels, warning: agentModelsWarning } = configModule.resolveAgentModels(scope,
|
|
193
|
+
const { models: resolvedAgentModels, warning: agentModelsWarning } = configModule.resolveAgentModels(scope, homedir(), projectRoot)
|
|
193
194
|
if (agentModelsWarning) process.stderr.write(agentModelsWarning + '\n')
|
|
194
195
|
let identityConfig
|
|
195
196
|
if (options.applyTo && options.applyTo.length) {
|
|
@@ -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.
|
|
@@ -880,7 +881,7 @@ function run(options) {
|
|
|
880
881
|
process.exit(1)
|
|
881
882
|
}
|
|
882
883
|
|
|
883
|
-
const homeRoot =
|
|
884
|
+
const homeRoot = homedir()
|
|
884
885
|
const dryRun = Boolean(options.dryRun)
|
|
885
886
|
const installMissing = Boolean(options.installMissing)
|
|
886
887
|
|
package/src/commands/update.js
CHANGED
|
@@ -7,6 +7,7 @@ const path = require('path');
|
|
|
7
7
|
const identityStore = require('../identity');
|
|
8
8
|
const projectConfig = require('../config');
|
|
9
9
|
const { runFileTarget, validateTargets, buildDocument, humanReport, silenceConsole } = require('../lib/update-engine');
|
|
10
|
+
const { homedir } = require('../homedir')
|
|
10
11
|
|
|
11
12
|
// `trackfw update` is project-scoped only — see docs/cli-parity.md,
|
|
12
13
|
// "`trackfw update` vs `trackfw update harness`". It must NEVER touch global
|
|
@@ -37,7 +38,7 @@ function loadUpdateConfig(rootDir) {
|
|
|
37
38
|
// (line-based text splice, not a YAML parse+reserialize) to preserve 100% of
|
|
38
39
|
// the user's existing trackfw.yaml content (comments, key order, formatting).
|
|
39
40
|
function ensureGlobalAdrDirRegistered(cwd) {
|
|
40
|
-
const home =
|
|
41
|
+
const home = homedir();
|
|
41
42
|
const globalDir = path.join(home, '.trackfw', 'adr');
|
|
42
43
|
if (!fs.existsSync(globalDir)) return;
|
|
43
44
|
|
|
@@ -168,10 +169,74 @@ function codexProjectAgentsTarget(cwd, identityConfig, { dryRun, installMissing
|
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
// discoverWorkflowPresent — reports whether
|
|
173
|
+
// .github/workflows/trackfw-validate.yml (written by `trackfw discover
|
|
174
|
+
// --init`, DISCOVER_GITHUB_ACTIONS_WORKFLOW_PATH in discover.js) already
|
|
175
|
+
// exists under cwd AS A REGULAR FILE. Used both to decide whether
|
|
176
|
+
// `ci-workflow` is declared (AC17(c), REQ-2026-08-28) and, inside its
|
|
177
|
+
// apply(), to decide whether to refresh it — existence is checked against
|
|
178
|
+
// the REAL cwd, never the --dry-run sandbox, mirroring how cfg itself is
|
|
179
|
+
// read before the sandbox is built.
|
|
180
|
+
//
|
|
181
|
+
// Uses fs.lstatSync, NOT fs.existsSync: fs.existsSync follows symlinks, so a
|
|
182
|
+
// symlink at this path — even a live one pointing outside the project —
|
|
183
|
+
// would report "present" purely because its target resolves, pulling
|
|
184
|
+
// `ci-workflow` into the declared target set on the strength of a link this
|
|
185
|
+
// command does not own. Symlinks are therefore treated as NOT present here:
|
|
186
|
+
// `update` will not declare/manage a target on their account, and
|
|
187
|
+
// refreshDiscoverGitHubActionsWorkflowIfPresent below refuses to write
|
|
188
|
+
// through them regardless.
|
|
189
|
+
function discoverWorkflowPresent(cwd) {
|
|
190
|
+
const { DISCOVER_GITHUB_ACTIONS_WORKFLOW_PATH } = require('./discover')
|
|
191
|
+
const dest = path.join(cwd, DISCOVER_GITHUB_ACTIONS_WORKFLOW_PATH)
|
|
192
|
+
let info
|
|
193
|
+
try {
|
|
194
|
+
info = fs.lstatSync(dest)
|
|
195
|
+
} catch (e) {
|
|
196
|
+
return false
|
|
197
|
+
}
|
|
198
|
+
return !info.isSymbolicLink()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// refreshDiscoverGitHubActionsWorkflowIfPresent — refreshes
|
|
202
|
+
// .github/workflows/trackfw-validate.yml ONLY when it already exists under
|
|
203
|
+
// root as a REGULAR FILE — `update` never creates this file (AC17(b)):
|
|
204
|
+
// ownership of the install decision belongs to `trackfw discover --init`,
|
|
205
|
+
// not `update`. Writes the SAME builder scaffold doctor compares against
|
|
206
|
+
// (buildDiscoverGitHubActionsWorkflowContent, discover.js) so what `update`
|
|
207
|
+
// writes and what `doctor` expects can never drift apart by construction.
|
|
208
|
+
//
|
|
209
|
+
// Uses fs.lstatSync to decide presence, not fs.existsSync: this path is the
|
|
210
|
+
// most sensitive one `update` can write to (it controls what runs in CI for
|
|
211
|
+
// anyone who checks the project out), so if it is a symlink — live or
|
|
212
|
+
// dangling — this function refuses to write through it. Refusing is loud
|
|
213
|
+
// (stderr), never silent, so "update didn't refresh my workflow" stays
|
|
214
|
+
// diagnosable instead of a silent no-op.
|
|
215
|
+
function refreshDiscoverGitHubActionsWorkflowIfPresent(root) {
|
|
216
|
+
const { DISCOVER_GITHUB_ACTIONS_WORKFLOW_PATH, buildDiscoverGitHubActionsWorkflowContent } = require('./discover')
|
|
217
|
+
const dest = path.join(root, DISCOVER_GITHUB_ACTIONS_WORKFLOW_PATH)
|
|
218
|
+
let info
|
|
219
|
+
try {
|
|
220
|
+
info = fs.lstatSync(dest)
|
|
221
|
+
} catch (e) {
|
|
222
|
+
return // not installed — update never creates it (AC17(b))
|
|
223
|
+
}
|
|
224
|
+
if (info.isSymbolicLink()) {
|
|
225
|
+
console.error(`aviso: ${DISCOVER_GITHUB_ACTIONS_WORKFLOW_PATH} é um symlink; trackfw update não escreve através de symlinks — arquivo não foi tocado`)
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
fs.writeFileSync(dest, buildDiscoverGitHubActionsWorkflowContent(), 'utf8')
|
|
229
|
+
}
|
|
230
|
+
|
|
171
231
|
// PROJECT_TARGET_IDS — the fixed declared order for `trackfw update`
|
|
172
232
|
// targets. `ci-workflow` and `git-hooks` only appear when the project's
|
|
173
|
-
// trackfw.yaml opted into a CI system / hook framework —
|
|
174
|
-
//
|
|
233
|
+
// trackfw.yaml opted into a CI system / hook framework, OR — for
|
|
234
|
+
// `ci-workflow` only — when trackfw-validate.yml already exists on disk
|
|
235
|
+
// (AC17(c)) — see ambiguity note in the ML-6C handoff report about
|
|
236
|
+
// config-conditional target lists. This constant is the full declared
|
|
237
|
+
// surface (used to validate --targets); the config/disk-conditional
|
|
238
|
+
// inclusion itself lives in buildProjectTargets' `include('ci-workflow')`
|
|
239
|
+
// call below.
|
|
175
240
|
const PROJECT_TARGET_IDS = [
|
|
176
241
|
'agent-rules',
|
|
177
242
|
'agent-hooks',
|
|
@@ -189,7 +254,6 @@ const PROJECT_TARGET_IDS = [
|
|
|
189
254
|
// filtering afterwards would still have written every unrequested target.
|
|
190
255
|
function buildProjectTargets(cwd, cfg, identityConfig, { dryRun, installMissing }, wanted) {
|
|
191
256
|
const generators = require('../generators/init');
|
|
192
|
-
const discover = require('./discover');
|
|
193
257
|
const hooksGen = require('../generators/hooks');
|
|
194
258
|
const include = (id) => !wanted || wanted.includes(id);
|
|
195
259
|
|
|
@@ -274,16 +338,70 @@ function buildProjectTargets(cwd, cfg, identityConfig, { dryRun, installMissing
|
|
|
274
338
|
installMissing,
|
|
275
339
|
}));
|
|
276
340
|
|
|
277
|
-
|
|
341
|
+
// ci-workflow mirrors Go's internal/generators/update.go:1925 target: it manages
|
|
342
|
+
// the SAME pair of files Go manages — .github/workflows/trackfw-gate.yml (the
|
|
343
|
+
// generated, version-pinned governance gate, ADR-2026-08-28) and
|
|
344
|
+
// .gitlab-ci-trackfw.yml — AND, since ML-2G (AC17), ALSO
|
|
345
|
+
// .github/workflows/trackfw-validate.yml, the different file written by
|
|
346
|
+
// discover.js (own template, `go install …@latest`, version-pinned to the
|
|
347
|
+
// generating binary). That third file is refreshed-only-if-present
|
|
348
|
+
// (AC17(a)/(b)): `update` never installs it, it only keeps an
|
|
349
|
+
// already-discover-installed copy in sync with what the current binary
|
|
350
|
+
// would generate, using discover.js's own builder so `update` and `doctor`
|
|
351
|
+
// can never drift apart by construction.
|
|
352
|
+
// Writing directly with the byte-identical builders (instead of calling
|
|
353
|
+
// generateCIWorkflow/generateGitHubActionsWorkflow from generators/init.js) is
|
|
354
|
+
// deliberate: those functions hardcode paths relative to process.cwd() with no
|
|
355
|
+
// root parameter, which would silently escape the dry-run tmp sandbox that
|
|
356
|
+
// runFileTarget builds for every other target here.
|
|
357
|
+
//
|
|
358
|
+
// AC17(c): the target is included when cfg.ci opts into github-actions/
|
|
359
|
+
// gitlab-ci (as before) OR when trackfw-validate.yml already exists on
|
|
360
|
+
// disk — otherwise a `ci: none` project that ran `discover` would have
|
|
361
|
+
// that file outside any command's management.
|
|
362
|
+
const ciWorkflowConfigOptIn = cfg.ci === 'github-actions' || cfg.ci === 'github_actions' || cfg.ci === 'gitlab-ci';
|
|
363
|
+
if (include('ci-workflow') && (ciWorkflowConfigOptIn || discoverWorkflowPresent(cwd))) {
|
|
364
|
+
const isGitLab = cfg.ci === 'gitlab-ci';
|
|
365
|
+
const { DISCOVER_GITHUB_ACTIONS_WORKFLOW_PATH } = require('./discover');
|
|
278
366
|
targets.push(runFileTarget({
|
|
279
367
|
id: 'ci-workflow',
|
|
280
|
-
path:
|
|
368
|
+
path: `.github/workflows/trackfw-gate.yml, .gitlab-ci-trackfw.yml, ${DISCOVER_GITHUB_ACTIONS_WORKFLOW_PATH}`,
|
|
281
369
|
root: cwd,
|
|
282
|
-
relPaths: ['.github/workflows/trackfw-
|
|
283
|
-
apply: (root) =>
|
|
370
|
+
relPaths: ['.github/workflows/trackfw-gate.yml', '.gitlab-ci-trackfw.yml', DISCOVER_GITHUB_ACTIONS_WORKFLOW_PATH],
|
|
371
|
+
apply: (root) => {
|
|
372
|
+
if (isGitLab) {
|
|
373
|
+
fs.writeFileSync(path.join(root, '.gitlab-ci-trackfw.yml'), generators.buildGitLabCIWorkflowContent(cfg), 'utf8');
|
|
374
|
+
} else if (cfg.ci === 'github-actions' || cfg.ci === 'github_actions') {
|
|
375
|
+
fs.mkdirSync(path.join(root, '.github/workflows'), { recursive: true });
|
|
376
|
+
fs.writeFileSync(path.join(root, '.github/workflows/trackfw-gate.yml'), generators.buildGitHubActionsWorkflowContent(cfg), 'utf8');
|
|
377
|
+
}
|
|
378
|
+
refreshDiscoverGitHubActionsWorkflowIfPresent(root);
|
|
379
|
+
},
|
|
284
380
|
dryRun,
|
|
285
381
|
installMissing,
|
|
286
382
|
}));
|
|
383
|
+
} else if (include('ci-workflow')) {
|
|
384
|
+
// ML-3E (audit of ML-3D): the branch above is skipped exactly when
|
|
385
|
+
// `ci-workflow` isn't config-opted-in AND discoverWorkflowPresent(cwd)
|
|
386
|
+
// is false — which is also true when the file at
|
|
387
|
+
// DISCOVER_GITHUB_ACTIONS_WORKFLOW_PATH is a symlink (live or dangling),
|
|
388
|
+
// since discoverWorkflowPresent() deliberately treats symlinks as NOT
|
|
389
|
+
// present (AC17(c) comment above). Without this call, a `ci: none`
|
|
390
|
+
// project with a discover-installed symlink went completely silent:
|
|
391
|
+
// no target declared, no write, no warning — the exact failure mode
|
|
392
|
+
// this REQ exists to close (ML-3D's own stderr warning only fired from
|
|
393
|
+
// inside the branch above, i.e. only in cases where it was already not
|
|
394
|
+
// needed). refreshDiscoverGitHubActionsWorkflowIfPresent(cwd) re-checks
|
|
395
|
+
// the real cwd independently: it is a no-op when the file is absent
|
|
396
|
+
// (AC17(b) — update never creates it) and prints the SAME stderr
|
|
397
|
+
// warning Go/Python emit — byte-identical, see
|
|
398
|
+
// internal/generators/update.go:1899 and
|
|
399
|
+
// pypi/trackfw/commands/update.py:221-225 — when it is a symlink. No
|
|
400
|
+
// regular file ever reaches this branch (that case is always declared
|
|
401
|
+
// above via discoverWorkflowPresent(cwd) === true), so this can never
|
|
402
|
+
// duplicate the write/warning already performed by the branch above,
|
|
403
|
+
// and never writes through a symlink itself.
|
|
404
|
+
refreshDiscoverGitHubActionsWorkflowIfPresent(cwd);
|
|
287
405
|
}
|
|
288
406
|
|
|
289
407
|
if (include('git-hooks') && (cfg.hooks === 'husky' || cfg.hooks === 'lefthook')) {
|
|
@@ -359,7 +477,7 @@ cmd.action((mode, options) => {
|
|
|
359
477
|
// A identidade é carregada uma única vez, fora de qualquer try/catch —
|
|
360
478
|
// um identity.json corrompido deve abortar o comando inteiro, nunca cair
|
|
361
479
|
// silenciosamente para os nomes neutros default.
|
|
362
|
-
const identityConfig = identityStore.load(
|
|
480
|
+
const identityConfig = identityStore.load(homedir());
|
|
363
481
|
|
|
364
482
|
// With --json, stdout must carry only the result document — apply()
|
|
365
483
|
// functions log human progress lines as a side effect; silence them so a
|
package/src/config/index.js
CHANGED
|
@@ -4,14 +4,15 @@ const fs = require('fs');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { parseDocument, isScalar, isAlias, isSeq, isMap } = require('yaml');
|
|
7
|
+
const { homedir } = require('../homedir')
|
|
7
8
|
|
|
8
9
|
function expandPath(filePath) {
|
|
9
10
|
if (!filePath || typeof filePath !== 'string') return filePath;
|
|
10
11
|
if (filePath === '~') {
|
|
11
|
-
return
|
|
12
|
+
return homedir();
|
|
12
13
|
}
|
|
13
14
|
if (filePath.startsWith('~/') || filePath.startsWith('~\\')) {
|
|
14
|
-
return path.join(
|
|
15
|
+
return path.join(homedir(), filePath.slice(2));
|
|
15
16
|
}
|
|
16
17
|
return filePath;
|
|
17
18
|
}
|
|
@@ -369,7 +370,7 @@ function _cwdAgentModelsSource(cwd) {
|
|
|
369
370
|
}
|
|
370
371
|
|
|
371
372
|
// loadGlobalAgentModels reads agent_models from ~/.trackfw/trackfw.yaml, bypassing the load()
|
|
372
|
-
// singleton. homeDir is
|
|
373
|
+
// singleton. homeDir is homedir(); cwd is process.cwd() (used only for AC14 diagnostic).
|
|
373
374
|
// Returns { models: {}, source } where source is one of:
|
|
374
375
|
// 'global' — agent_models resolved from ~/.trackfw/trackfw.yaml
|
|
375
376
|
// 'none' — global config absent or has no agent_models; cwd also has none
|