trackfw 7.2.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/doctor.js +16 -3
- package/src/commands/index.js +1 -0
- package/src/commands/integrations.js +19 -2
- package/src/commands/thirdparty.js +7 -2
- package/src/commands/update-harness.js +3 -1
- package/src/commands/update.js +20 -1
- package/src/config/index.js +79 -0
- package/src/generators/hooks.js +18 -0
- package/src/generators/init.js +72 -16
- package/src/generators/roadmap.js +53 -2
- package/src/integrations/assets/agents/architect.md +5 -3
- package/src/integrations/assets/agents/security.md +3 -0
- package/src/integrations/scaffold_doctor.js +366 -0
- package/src/lib/update-engine.js +13 -1
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/doctor.js
CHANGED
|
@@ -2,16 +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')
|
|
5
6
|
const projectConfig = require('../config')
|
|
6
7
|
|
|
7
8
|
function printReport(findings) {
|
|
8
9
|
if (findings.length === 0) {
|
|
9
|
-
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.'
|
|
10
11
|
}
|
|
11
12
|
const unregistered = findings.filter(finding => finding.finding === UNREGISTERED_WRITE).length
|
|
12
13
|
const handModified = findings.filter(finding => finding.finding === HAND_MODIFIED).length
|
|
13
14
|
const unknownContent = findings.filter(finding => finding.finding === UNKNOWN_CONTENT).length
|
|
14
|
-
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
|
+
]
|
|
15
22
|
for (const finding of findings) {
|
|
16
23
|
lines.push(`[${finding.finding}] ${finding.destination}`, ` remedy: ${finding.remedy}`, '')
|
|
17
24
|
}
|
|
@@ -22,7 +29,13 @@ const cmd = new Command('doctor')
|
|
|
22
29
|
cmd.description('Detect artifacts on disk missing from the manifest, distinguishing hand-modified artifacts from unknown content')
|
|
23
30
|
cmd.option('--json', 'Emit findings as a JSON array instead of the text report')
|
|
24
31
|
cmd.action(async options => {
|
|
25
|
-
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]
|
|
26
39
|
if (options.json) {
|
|
27
40
|
process.stdout.write(`${JSON.stringify(findings, null, 2)}\n`)
|
|
28
41
|
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())
|
|
@@ -221,7 +221,9 @@ function createLifecycleCommand(kind) {
|
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
|
|
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
|
|
225
227
|
const output = execute(kind, operation, options)
|
|
226
228
|
console.log(options.json ? JSON.stringify(output) : human(output))
|
|
227
229
|
})
|
|
@@ -250,7 +252,22 @@ function createAgentModelsCommand() {
|
|
|
250
252
|
const cmd = new Command('models')
|
|
251
253
|
.description('Show the resolved model each agent uses per target')
|
|
252
254
|
cmd.action(() => {
|
|
253
|
-
|
|
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')
|
|
254
271
|
|
|
255
272
|
// Warnings: emit once per suspect tier, sorted, to stderr.
|
|
256
273
|
const suspectTiers = Object.keys(agentModels)
|
|
@@ -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
|
}
|
|
@@ -758,7 +758,9 @@ function catalogBundleTarget(toolId, kind, homeRoot, identityConfig, { dryRun, i
|
|
|
758
758
|
let displayPath = `~/.${toolId}`
|
|
759
759
|
try {
|
|
760
760
|
displayPath = globalGroupPath(toolId, kind)
|
|
761
|
-
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 })
|
|
762
764
|
if (!plans.length) return { id, state: 'missing', path: displayPath }
|
|
763
765
|
|
|
764
766
|
const manager = new IntegrationManager({ homeRoot })
|
package/src/commands/update.js
CHANGED
|
@@ -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);
|
package/src/config/index.js
CHANGED
|
@@ -86,6 +86,21 @@ function defaults() {
|
|
|
86
86
|
|
|
87
87
|
let _instance = null;
|
|
88
88
|
|
|
89
|
+
// MALFORMED_GLOBAL_CONFIG_MESSAGE is written to stderr (non-fatal) when ~/.trackfw/trackfw.yaml
|
|
90
|
+
// exists but fails to parse as valid YAML. Must be byte-identical to Go's MalformedGlobalConfigMessage
|
|
91
|
+
// and Python's MALFORMED_GLOBAL_CONFIG_MESSAGE — ADR-2026-08-23.
|
|
92
|
+
const MALFORMED_GLOBAL_CONFIG_MESSAGE = 'trackfw: aviso: "~/.trackfw/trackfw.yaml" tem YAML malformado — config global de modelo ignorada; usando tier canônico.';
|
|
93
|
+
|
|
94
|
+
// GLOBAL_AGENT_MODELS_NONE_MESSAGE is emitted to stderr when agent_models is not configured
|
|
95
|
+
// in the global config and not found in cwd's trackfw.yaml either.
|
|
96
|
+
// Must be byte-identical to Go's GlobalAgentModelsNoneMessage and Python's equivalent.
|
|
97
|
+
const GLOBAL_AGENT_MODELS_NONE_MESSAGE = 'trackfw: agents global: agent_models não configurado em ~/.trackfw/trackfw.yaml — usando tier canônico. Configure em ~/.trackfw/trackfw.yaml para pinar versões.';
|
|
98
|
+
|
|
99
|
+
// GLOBAL_AGENT_MODELS_PROJECT_ONLY_MESSAGE is emitted when agent_models is found in the
|
|
100
|
+
// project's trackfw.yaml but not in the global config (AC4/AC14). Value NOT applied.
|
|
101
|
+
// Must be byte-identical to Go's GlobalAgentModelsProjectOnlyMessage and Python's equivalent.
|
|
102
|
+
const GLOBAL_AGENT_MODELS_PROJECT_ONLY_MESSAGE = 'trackfw: agents global: agent_models configurado em trackfw.yaml do projeto mas não vale para escopo global. Mova a chave para ~/.trackfw/trackfw.yaml.';
|
|
103
|
+
|
|
89
104
|
// MALFORMED_CONFIG_MESSAGE is written to stderr, verbatim, when trackfw.yaml exists but fails to
|
|
90
105
|
// parse as YAML. Kept identical, character-for-character, to Go's MalformedConfigMessage and
|
|
91
106
|
// Python's MALFORMED_CONFIG_MESSAGE — see the comment on parse() below for why the text is
|
|
@@ -340,6 +355,65 @@ function readAgentConventions(cwd) {
|
|
|
340
355
|
return cfg.update.agentConventions || '';
|
|
341
356
|
}
|
|
342
357
|
|
|
358
|
+
// _cwdAgentModelsSource returns 'project_only' if cwd's trackfw.yaml has agent_models configured,
|
|
359
|
+
// 'none' otherwise. Used for the AC14 diagnostic in loadGlobalAgentModels.
|
|
360
|
+
function _cwdAgentModelsSource(cwd) {
|
|
361
|
+
if (!cwd) return 'none';
|
|
362
|
+
try {
|
|
363
|
+
const cwdContent = fs.readFileSync(path.join(cwd, 'trackfw.yaml'), 'utf8');
|
|
364
|
+
const cwdCfg = { rules: {}, credentialGuard: {}, update: {}, sync: {}, linkFields: {}, agentModels: {} };
|
|
365
|
+
parse(cwdContent, cwdCfg);
|
|
366
|
+
if (cwdCfg.agentModels && Object.keys(cwdCfg.agentModels).length > 0) return 'project_only';
|
|
367
|
+
} catch (_) {}
|
|
368
|
+
return 'none';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// loadGlobalAgentModels reads agent_models from ~/.trackfw/trackfw.yaml, bypassing the load()
|
|
372
|
+
// singleton. homeDir is os.homedir(); cwd is process.cwd() (used only for AC14 diagnostic).
|
|
373
|
+
// Returns { models: {}, source } where source is one of:
|
|
374
|
+
// 'global' — agent_models resolved from ~/.trackfw/trackfw.yaml
|
|
375
|
+
// 'none' — global config absent or has no agent_models; cwd also has none
|
|
376
|
+
// 'project_only' — agent_models in cwd's trackfw.yaml but not global (AC14 trigger)
|
|
377
|
+
// 'global_malformed' — ~/.trackfw/trackfw.yaml exists but has invalid YAML (AC12 trigger)
|
|
378
|
+
// Never calls process.exit. Pattern mirrors readAgentConventions above.
|
|
379
|
+
function loadGlobalAgentModels(homeDir, cwd) {
|
|
380
|
+
const globalPath = path.join(homeDir, '.trackfw', 'trackfw.yaml');
|
|
381
|
+
let data;
|
|
382
|
+
try {
|
|
383
|
+
data = fs.readFileSync(globalPath, 'utf8');
|
|
384
|
+
} catch (_) {
|
|
385
|
+
return { models: {}, source: _cwdAgentModelsSource(cwd) };
|
|
386
|
+
}
|
|
387
|
+
// Validate YAML (non-fatal for global config, per AC12).
|
|
388
|
+
const globalCfg = { rules: {}, credentialGuard: {}, update: {}, sync: {}, linkFields: {}, agentModels: {} };
|
|
389
|
+
const malformed = parse(data, globalCfg);
|
|
390
|
+
if (malformed) {
|
|
391
|
+
return { models: {}, source: 'global_malformed' };
|
|
392
|
+
}
|
|
393
|
+
if (globalCfg.agentModels && Object.keys(globalCfg.agentModels).length > 0) {
|
|
394
|
+
return { models: globalCfg.agentModels, source: 'global' };
|
|
395
|
+
}
|
|
396
|
+
// Global file exists but has no agent_models → check cwd (AC14).
|
|
397
|
+
return { models: {}, source: _cwdAgentModelsSource(cwd) };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// resolveAgentModels returns { models, warning } for the given scope.
|
|
401
|
+
// For global scope, reads from ~/.trackfw/trackfw.yaml via loadGlobalAgentModels.
|
|
402
|
+
// For project scope, reads from the cwd's trackfw.yaml via load().
|
|
403
|
+
// warning is a non-empty string when the caller should emit an advisory to stderr.
|
|
404
|
+
// resolveAgentModels never writes to stderr itself.
|
|
405
|
+
function resolveAgentModels(scope, homeDir, cwd) {
|
|
406
|
+
if (scope !== 'global') {
|
|
407
|
+
return { models: load().agentModels || {}, warning: '' };
|
|
408
|
+
}
|
|
409
|
+
const { models, source } = loadGlobalAgentModels(homeDir, cwd);
|
|
410
|
+
let warning = '';
|
|
411
|
+
if (source === 'none') warning = GLOBAL_AGENT_MODELS_NONE_MESSAGE;
|
|
412
|
+
else if (source === 'project_only') warning = GLOBAL_AGENT_MODELS_PROJECT_ONLY_MESSAGE;
|
|
413
|
+
else if (source === 'global_malformed') warning = MALFORMED_GLOBAL_CONFIG_MESSAGE;
|
|
414
|
+
return { models, warning };
|
|
415
|
+
}
|
|
416
|
+
|
|
343
417
|
const NAMESPACING_FLAT = 'flat';
|
|
344
418
|
const NAMESPACING_BY_AGENT = 'by_agent';
|
|
345
419
|
|
|
@@ -350,7 +424,12 @@ module.exports = {
|
|
|
350
424
|
expandPath,
|
|
351
425
|
parseRulesFromContent,
|
|
352
426
|
readAgentConventions,
|
|
427
|
+
loadGlobalAgentModels,
|
|
428
|
+
resolveAgentModels,
|
|
353
429
|
NAMESPACING_FLAT,
|
|
354
430
|
NAMESPACING_BY_AGENT,
|
|
355
431
|
MALFORMED_CONFIG_MESSAGE,
|
|
432
|
+
MALFORMED_GLOBAL_CONFIG_MESSAGE,
|
|
433
|
+
GLOBAL_AGENT_MODELS_NONE_MESSAGE,
|
|
434
|
+
GLOBAL_AGENT_MODELS_PROJECT_ONLY_MESSAGE,
|
|
356
435
|
};
|
package/src/generators/hooks.js
CHANGED
|
@@ -1047,6 +1047,8 @@ function generateGitBranchGuardScript(cwd) {
|
|
|
1047
1047
|
|
|
1048
1048
|
const scriptPath = path.join(scriptsDir, 'trackfw-git-branch-guard.sh')
|
|
1049
1049
|
fs.writeFileSync(scriptPath, GIT_BRANCH_GUARD_SCRIPT, { encoding: 'utf8', mode: 0o755 })
|
|
1050
|
+
// AC9: chmodSync restores execute bit on existing files.
|
|
1051
|
+
fs.chmodSync(scriptPath, 0o755)
|
|
1050
1052
|
|
|
1051
1053
|
console.log(' ✓ scripts/trackfw-git-branch-guard.sh')
|
|
1052
1054
|
}
|
|
@@ -1084,6 +1086,8 @@ function generateCredentialGuardScript(cwd) {
|
|
|
1084
1086
|
|
|
1085
1087
|
const scriptPath = path.join(scriptsDir, 'trackfw-credential-guard.sh')
|
|
1086
1088
|
fs.writeFileSync(scriptPath, CREDENTIAL_GUARD_SCRIPT, { encoding: 'utf8', mode: 0o755 })
|
|
1089
|
+
// AC9: chmodSync restores execute bit on existing files.
|
|
1090
|
+
fs.chmodSync(scriptPath, 0o755)
|
|
1087
1091
|
|
|
1088
1092
|
console.log(' ✓ scripts/trackfw-credential-guard.sh')
|
|
1089
1093
|
}
|
|
@@ -1489,9 +1493,14 @@ function generateAttentionScripts(cfg, cwd) {
|
|
|
1489
1493
|
|
|
1490
1494
|
const signalPath = path.join(scriptsDir, 'trackfw-attention-signal.sh')
|
|
1491
1495
|
fs.writeFileSync(signalPath, SIGNAL_SCRIPT, { encoding: 'utf8', mode: 0o755 })
|
|
1496
|
+
// AC9 (REQ-2026-08-28): chmodSync restores the execute bit on existing files
|
|
1497
|
+
// where writeFileSync's {mode} option has no effect (applies only on O_CREAT).
|
|
1498
|
+
fs.chmodSync(signalPath, 0o755)
|
|
1492
1499
|
|
|
1493
1500
|
const cleanupPath = path.join(scriptsDir, 'trackfw-attention-cleanup.sh')
|
|
1494
1501
|
fs.writeFileSync(cleanupPath, CLEANUP_SCRIPT, { encoding: 'utf8', mode: 0o755 })
|
|
1502
|
+
// AC9: same rationale as signalPath above.
|
|
1503
|
+
fs.chmodSync(cleanupPath, 0o755)
|
|
1495
1504
|
|
|
1496
1505
|
console.log(' ✓ scripts/trackfw-attention-signal.sh')
|
|
1497
1506
|
console.log(' ✓ scripts/trackfw-attention-cleanup.sh')
|
|
@@ -2255,4 +2264,13 @@ module.exports = {
|
|
|
2255
2264
|
mergeCopilotHookArray,
|
|
2256
2265
|
normalizeGuardPath,
|
|
2257
2266
|
samePathCommand,
|
|
2267
|
+
// Template constants — exported for scaffold doctor (ADR-2026-08-27).
|
|
2268
|
+
// These are the single source of truth for each script's content; the
|
|
2269
|
+
// scaffold doctor compares on-disk files against these exact bytes, using
|
|
2270
|
+
// the same constants the write path uses, so drift between the comparison
|
|
2271
|
+
// and the generator is structurally impossible.
|
|
2272
|
+
SIGNAL_SCRIPT,
|
|
2273
|
+
CLEANUP_SCRIPT,
|
|
2274
|
+
CREDENTIAL_GUARD_SCRIPT,
|
|
2275
|
+
GIT_BRANCH_GUARD_SCRIPT,
|
|
2258
2276
|
}
|