trackfw 7.1.0 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/commit.js +7 -3
- package/src/commands/doctor.js +2 -1
- package/src/commands/index.js +2 -0
- package/src/commands/integrations.js +63 -1
- package/src/commands/push.js +51 -0
- package/src/commands/release.js +47 -0
- package/src/commands/ship.js +10 -2
- package/src/commands/update-harness.js +2 -1
- package/src/commands/update.js +1 -1
- package/src/config/index.js +17 -2
- package/src/generators/hooks.js +176 -13
- package/src/generators/init.js +12 -0
- package/src/integrations/assets/agents/architect.md +12 -0
- package/src/integrations/doctor.js +0 -0
- package/src/integrations/index.js +1 -1
- package/src/integrations/render.js +116 -2
- package/src/push/runner.js +223 -0
- package/src/release/runner.js +474 -0
- package/src/ship/runner.js +177 -21
- package/src/validator/index.js +270 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackfw",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.2.0",
|
|
4
4
|
"description": "Governance CLI for AI-native software delivery and AI coding agents. Enforces traceability: ADR → REQ → ROADMAP → kanban. Native support for Codex, Claude Code, Gemini CLI, Cursor, Copilot, Windsurf, Amazon Q, OpenCode, and Kiro.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
package/src/commands/commit.js
CHANGED
|
@@ -12,9 +12,13 @@ function createCommitCommand() {
|
|
|
12
12
|
const cmd = new Command('commit')
|
|
13
13
|
cmd
|
|
14
14
|
.description(
|
|
15
|
-
"trackfw commit
|
|
16
|
-
|
|
17
|
-
'
|
|
15
|
+
"trackfw commit commits staged changes directly, but blocks the commit before it\n" +
|
|
16
|
+
'happens when governance is missing, instead of letting it land and only catching it later.\n\n' +
|
|
17
|
+
'Compositional vocabulary:\n' +
|
|
18
|
+
' trackfw commit -m "..." commits\n' +
|
|
19
|
+
' trackfw push pushes\n' +
|
|
20
|
+
' trackfw ship -m "..." commit + push + PR (composition)\n\n' +
|
|
21
|
+
'Behavioral steps:\n\n' +
|
|
18
22
|
" 1. On 'main'/'master': always blocked — commit directly on the default branch is never\n" +
|
|
19
23
|
' permitted.\n' +
|
|
20
24
|
' 2. On a feat/fix/refactor branch: requires a roadmap matching the branch slug already in\n' +
|
package/src/commands/doctor.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { Command } = require('commander')
|
|
4
4
|
const { runDoctor, UNREGISTERED_WRITE, HAND_MODIFIED, UNKNOWN_CONTENT } = require('../integrations/doctor')
|
|
5
|
+
const projectConfig = require('../config')
|
|
5
6
|
|
|
6
7
|
function printReport(findings) {
|
|
7
8
|
if (findings.length === 0) {
|
|
@@ -21,7 +22,7 @@ const cmd = new Command('doctor')
|
|
|
21
22
|
cmd.description('Detect artifacts on disk missing from the manifest, distinguishing hand-modified artifacts from unknown content')
|
|
22
23
|
cmd.option('--json', 'Emit findings as a JSON array instead of the text report')
|
|
23
24
|
cmd.action(async options => {
|
|
24
|
-
const findings = runDoctor({})
|
|
25
|
+
const findings = runDoctor({ agentModels: projectConfig.load().agentModels || {} })
|
|
25
26
|
if (options.json) {
|
|
26
27
|
process.stdout.write(`${JSON.stringify(findings, null, 2)}\n`)
|
|
27
28
|
return
|
package/src/commands/index.js
CHANGED
|
@@ -51,9 +51,11 @@ function createProgram() {
|
|
|
51
51
|
program.addCommand(require('./skills'))
|
|
52
52
|
program.addCommand(require('./note'))
|
|
53
53
|
program.addCommand(require('./ship'))
|
|
54
|
+
program.addCommand(require('./release'))
|
|
54
55
|
program.addCommand(require('./barrier'))
|
|
55
56
|
program.addCommand(require('./branch'))
|
|
56
57
|
program.addCommand(require('./commit'))
|
|
58
|
+
program.addCommand(require('./push'))
|
|
57
59
|
program.addCommand(require('./doctor'))
|
|
58
60
|
|
|
59
61
|
const { createServeCommand } = require('./serve')
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
const os = require('node:os')
|
|
4
4
|
const { Command } = require('commander')
|
|
5
5
|
const { catalog, execute, parseSurfaces, buildPlans } = require('../integrations')
|
|
6
|
+
const { surfaceFor, readAsset } = require('../integrations/catalog')
|
|
7
|
+
const { markdownParts, resolveAgentModel, looksLikeSuspectModelValue } = require('../integrations/render')
|
|
6
8
|
const identityStore = require('../identity')
|
|
7
9
|
const identityWizard = require('./identity-wizard')
|
|
8
10
|
const { t } = require('../i18n')
|
|
9
11
|
const { createThirdPartyCommand } = require('./thirdparty')
|
|
12
|
+
const configModule = require('../config')
|
|
10
13
|
|
|
11
14
|
const csv = value => String(value).split(',').map(entry => entry.trim()).filter(Boolean)
|
|
12
15
|
const collect = (value, previous) => previous.concat(value)
|
|
@@ -124,6 +127,10 @@ function createLifecycleCommand(kind) {
|
|
|
124
127
|
// internal/commands/integrations_flags.go registering `third-party` in
|
|
125
128
|
// newIntegrationsLifecycleCmd.
|
|
126
129
|
root.addCommand(createThirdPartyCommand(kind))
|
|
130
|
+
// "models" is agents-only: skills have no model field, and surfacing this
|
|
131
|
+
// under `trackfw skills models` would mislead users. Mirrors the identity
|
|
132
|
+
// flag gate and the Go/Python implementations.
|
|
133
|
+
if (kind === 'agents') root.addCommand(createAgentModelsCommand())
|
|
127
134
|
for (const operation of ['list', 'install', 'uninstall', 'update']) {
|
|
128
135
|
const mutation = operation !== 'list'
|
|
129
136
|
const command = new Command(operation)
|
|
@@ -214,6 +221,7 @@ function createLifecycleCommand(kind) {
|
|
|
214
221
|
}
|
|
215
222
|
}
|
|
216
223
|
|
|
224
|
+
options.agentModels = configModule.load().agentModels || {}
|
|
217
225
|
const output = execute(kind, operation, options)
|
|
218
226
|
console.log(options.json ? JSON.stringify(output) : human(output))
|
|
219
227
|
})
|
|
@@ -222,4 +230,58 @@ function createLifecycleCommand(kind) {
|
|
|
222
230
|
return root
|
|
223
231
|
}
|
|
224
232
|
|
|
225
|
-
|
|
233
|
+
// createAgentModelsCommand returns the "models" subcommand of "trackfw agents".
|
|
234
|
+
// Lists, for each catalog agent × target pair, the model identifier that
|
|
235
|
+
// install/update would write to the generated artifact. Mirrors
|
|
236
|
+
// internal/commands/agents_models.go:newAgentModelsCmd and
|
|
237
|
+
// pypi/trackfw/integrations/command.py:_run_models.
|
|
238
|
+
//
|
|
239
|
+
// Column widths must match Go and Python for byte-identical output.
|
|
240
|
+
const MODELS_AGENT_WIDTH = 14
|
|
241
|
+
const MODELS_TIER_WIDTH = 8
|
|
242
|
+
const MODELS_TARGET_WIDTH = 12
|
|
243
|
+
const MODELS_NA = '—'
|
|
244
|
+
|
|
245
|
+
function pad(s, width) {
|
|
246
|
+
return String(s).padEnd(width)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function createAgentModelsCommand() {
|
|
250
|
+
const cmd = new Command('models')
|
|
251
|
+
.description('Show the resolved model each agent uses per target')
|
|
252
|
+
cmd.action(() => {
|
|
253
|
+
const agentModels = configModule.load().agentModels || {}
|
|
254
|
+
|
|
255
|
+
// Warnings: emit once per suspect tier, sorted, to stderr.
|
|
256
|
+
const suspectTiers = Object.keys(agentModels)
|
|
257
|
+
.filter(tier => looksLikeSuspectModelValue(agentModels[tier]))
|
|
258
|
+
.sort()
|
|
259
|
+
for (const tier of suspectTiers) {
|
|
260
|
+
process.stderr.write(
|
|
261
|
+
`WARN: agent_models.${tier} = ${JSON.stringify(agentModels[tier])} — not a version string and not a claude- model ID; will be written literally and may produce an invalid model identifier\n`
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const lines = []
|
|
266
|
+
lines.push(`${pad('AGENT', MODELS_AGENT_WIDTH)} ${pad('TIER', MODELS_TIER_WIDTH)} ${pad('TARGET', MODELS_TARGET_WIDTH)} RESOLVED`)
|
|
267
|
+
|
|
268
|
+
for (const agent of catalog.agents) {
|
|
269
|
+
const source = readAsset(agent)
|
|
270
|
+
const parts = markdownParts(source)
|
|
271
|
+
const tier = parts.model || 'sonnet'
|
|
272
|
+
|
|
273
|
+
for (const target of catalog.targets) {
|
|
274
|
+
const surface = surfaceFor(target, null, 'agents')
|
|
275
|
+
const representation = surface.capabilities.agents.representation
|
|
276
|
+
const { resolved, present } = resolveAgentModel(tier, representation, target.id, agentModels)
|
|
277
|
+
const display = present ? resolved : MODELS_NA
|
|
278
|
+
lines.push(`${pad(agent.id, MODELS_AGENT_WIDTH)} ${pad(tier, MODELS_TIER_WIDTH)} ${pad(target.id, MODELS_TARGET_WIDTH)} ${display}`)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
console.log(lines.join('\n'))
|
|
283
|
+
})
|
|
284
|
+
return cmd
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = { createLifecycleCommand, createAgentModelsCommand, csv, human, promptSelection, promptAmbiguousSurfaces, resolveScope }
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander')
|
|
4
|
+
const { runPush } = require('../push/runner')
|
|
5
|
+
const config = require('../config')
|
|
6
|
+
|
|
7
|
+
function createPushCommand() {
|
|
8
|
+
const cmd = new Command('push')
|
|
9
|
+
cmd
|
|
10
|
+
.description(
|
|
11
|
+
'trackfw push pushes already-created commits without committing and without opening a PR/MR.\n\n' +
|
|
12
|
+
' 1. Validates branch name — must match feat|fix|refactor|chore|docs/<slug>\n' +
|
|
13
|
+
' 2. Validates governance — REQ + roadmap in wip/ must exist for feat/fix/refactor branches\n' +
|
|
14
|
+
' (hard gate: not affected by lenient mode or per-rule severity); chore/docs branches skip\n' +
|
|
15
|
+
' this check, mirroring \'trackfw commit\'\n' +
|
|
16
|
+
' 3. Detects pending squash-merges in other branches (advisory only)\n' +
|
|
17
|
+
' 4. Pushes to origin (adds -u if no upstream is configured yet)\n\n' +
|
|
18
|
+
'push never commits and never opens a PR/MR.\n' +
|
|
19
|
+
'Does not accept -m. If you have not committed yet, run \'trackfw commit -m "..."\' first.\n\n' +
|
|
20
|
+
'Compositional vocabulary:\n' +
|
|
21
|
+
' trackfw commit -m "..." commits\n' +
|
|
22
|
+
' trackfw push pushes\n' +
|
|
23
|
+
' trackfw ship -m "..." commit + push + PR (composition)\n\n' +
|
|
24
|
+
"--force-with-lease pushes with 'git push --force-with-lease' instead of a plain push — for\n" +
|
|
25
|
+
'the post-rebase case, where a plain push is rejected. It only runs when the branch already\n' +
|
|
26
|
+
"has an open PR/MR (verified via the resolved forge CLI): the safe path is always to open the\n" +
|
|
27
|
+
'PR first.'
|
|
28
|
+
)
|
|
29
|
+
.option('--dry-run', 'Print what would be done without executing write commands', false)
|
|
30
|
+
.option('--force-with-lease', 'Governed force-push (git push --force-with-lease); requires an open PR/MR on this branch', false)
|
|
31
|
+
.action((options) => {
|
|
32
|
+
const cfg = config.load()
|
|
33
|
+
const exitCode = runPush(
|
|
34
|
+
{
|
|
35
|
+
dryRun: options.dryRun || false,
|
|
36
|
+
forceWithLease: options.forceWithLease || false,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
configForge: cfg.forge || '',
|
|
40
|
+
repoDir: '.',
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
if (exitCode !== 0) {
|
|
44
|
+
process.exit(exitCode)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return cmd
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = createPushCommand()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander')
|
|
4
|
+
const { runReleaseTag } = require('../release/runner')
|
|
5
|
+
const config = require('../config')
|
|
6
|
+
|
|
7
|
+
function createReleaseCommand() {
|
|
8
|
+
const cmd = new Command('release')
|
|
9
|
+
cmd.description('Governed release operations')
|
|
10
|
+
|
|
11
|
+
const tagCmd = new Command('tag')
|
|
12
|
+
tagCmd
|
|
13
|
+
.description(
|
|
14
|
+
'Create and publish an annotated release tag.\n\n' +
|
|
15
|
+
"It exists because 'trackfw ship' only pushes branches — tag is not a branch operation,\n" +
|
|
16
|
+
"and ship's governance gate (\"REQ + roadmap in wip/\") does not apply to release. See\n" +
|
|
17
|
+
'ADR-2026-08-19-caminho-governado-para-push-forcado-e-tag-de-release.md.\n\n' +
|
|
18
|
+
'Every precondition below refuses with a message naming what to fix — this command\n' +
|
|
19
|
+
'never guesses:\n' +
|
|
20
|
+
' 1. Working tree must be clean.\n' +
|
|
21
|
+
' 2. The default branch (main/master), if checked out locally, must be up to date with origin.\n' +
|
|
22
|
+
' 3. The 4 version files must all match <version> exactly.\n' +
|
|
23
|
+
' 4. CHANGELOG.md must have a "## [<version>] - YYYY-MM-DD" section.\n' +
|
|
24
|
+
' 5. The tag must not already exist, locally or on origin.\n' +
|
|
25
|
+
' 6. The GitHub CLI (gh) must be available and authenticated — release tag currently\n' +
|
|
26
|
+
' only supports GitHub; other forges are refused with instructions to push the tag\n' +
|
|
27
|
+
' manually.\n\n' +
|
|
28
|
+
'On success, it publishes the tag via two GitHub API calls (POST git/tags then POST\n' +
|
|
29
|
+
'git/refs), preserving the annotation.'
|
|
30
|
+
)
|
|
31
|
+
.argument('<version>', 'Version to tag (with or without a leading "v")')
|
|
32
|
+
.action((version) => {
|
|
33
|
+
const cfg = config.load()
|
|
34
|
+
const exitCode = runReleaseTag(version, {
|
|
35
|
+
configForge: cfg.forge || '',
|
|
36
|
+
repoDir: '.',
|
|
37
|
+
})
|
|
38
|
+
if (exitCode !== 0) {
|
|
39
|
+
process.exit(exitCode)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
cmd.addCommand(tagCmd)
|
|
44
|
+
return cmd
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = createReleaseCommand()
|
package/src/commands/ship.js
CHANGED
|
@@ -15,16 +15,23 @@ function createShipCommand() {
|
|
|
15
15
|
" this check, mirroring 'trackfw commit'\n" +
|
|
16
16
|
' 3. Detects pending squash-merges in other branches (advisory only)\n' +
|
|
17
17
|
' 4. Reviews what is staged (git status --short + git diff --cached --stat)\n' +
|
|
18
|
-
' 5. Commits with Conventional Commits format (-m is required
|
|
18
|
+
' 5. Commits with Conventional Commits format (-m is required, unless nothing is staged\n' +
|
|
19
|
+
' and --force-with-lease is set — see below)\n' +
|
|
19
20
|
' 6. Pushes to origin (adds -u if no upstream is configured yet)\n' +
|
|
20
21
|
' 7. Opens PR/MR via the resolved forge CLI (or prints URL if CLI is absent)\n\n' +
|
|
21
22
|
"Stage your files explicitly before running ship.\n" +
|
|
22
|
-
"This command never executes 'git add .' or 'git add -A'
|
|
23
|
+
"This command never executes 'git add .' or 'git add -A'.\n\n" +
|
|
24
|
+
"--force-with-lease pushes with 'git push --force-with-lease' instead of a plain push —\n" +
|
|
25
|
+
"for the post-rebase case, where a plain push is rejected. It only runs when the branch\n" +
|
|
26
|
+
"already has an open PR/MR (verified via the resolved forge CLI): the safe path is\n" +
|
|
27
|
+
"always to open the PR first. When nothing is staged, it pushes existing commits\n" +
|
|
28
|
+
"without requiring -m."
|
|
23
29
|
)
|
|
24
30
|
.option('-m, --message <msg>', 'Commit message (Conventional Commits format required)')
|
|
25
31
|
.option('--dry-run', 'Print what would be done without executing write commands', false)
|
|
26
32
|
.option('--no-pr', 'Skip PR/MR creation after push')
|
|
27
33
|
.option('--forge <forge>', 'Override forge detection (github, gitlab, bitbucket, azure)', '')
|
|
34
|
+
.option('--force-with-lease', 'Governed force-push (git push --force-with-lease); requires an open PR/MR on this branch', false)
|
|
28
35
|
.action((options) => {
|
|
29
36
|
const cfg = config.load()
|
|
30
37
|
const exitCode = runShip(
|
|
@@ -33,6 +40,7 @@ function createShipCommand() {
|
|
|
33
40
|
dryRun: options.dryRun || false,
|
|
34
41
|
noPR: options.pr === false,
|
|
35
42
|
forge: options.forge || '',
|
|
43
|
+
forceWithLease: options.forceWithLease || false,
|
|
36
44
|
},
|
|
37
45
|
{
|
|
38
46
|
configForge: cfg.forge || '',
|
|
@@ -4,6 +4,7 @@ const fs = require('fs')
|
|
|
4
4
|
const os = require('os')
|
|
5
5
|
const path = require('path')
|
|
6
6
|
const identityStore = require('../identity')
|
|
7
|
+
const projectConfig = require('../config')
|
|
7
8
|
const { catalog, buildPlans, IntegrationManager, globalGroupPath } = require('../integrations')
|
|
8
9
|
const { tildeify, validateTargets, buildDocument, humanReport, silenceConsole } = require('../lib/update-engine')
|
|
9
10
|
const { mergeClaudeHookArray, mergeSimpleCommandArray, mergeCopilotHookArray, generateGlobalCredentialGuardScript, generateGlobalGitBranchGuardScript } = require('../generators/hooks')
|
|
@@ -757,7 +758,7 @@ function catalogBundleTarget(toolId, kind, homeRoot, identityConfig, { dryRun, i
|
|
|
757
758
|
let displayPath = `~/.${toolId}`
|
|
758
759
|
try {
|
|
759
760
|
displayPath = globalGroupPath(toolId, kind)
|
|
760
|
-
const plans = buildPlans(kind, { targets: [toolId], scope: 'global', identity: identityConfig })
|
|
761
|
+
const plans = buildPlans(kind, { targets: [toolId], scope: 'global', identity: identityConfig, agentModels: projectConfig.load().agentModels || {} })
|
|
761
762
|
if (!plans.length) return { id, state: 'missing', path: displayPath }
|
|
762
763
|
|
|
763
764
|
const manager = new IntegrationManager({ homeRoot })
|
package/src/commands/update.js
CHANGED
|
@@ -151,7 +151,7 @@ function codexProjectAgentsTarget(cwd, identityConfig, { dryRun, installMissing
|
|
|
151
151
|
const manager = new IntegrationManager({ projectRoot: cwd });
|
|
152
152
|
let wroteAny = false;
|
|
153
153
|
for (const kind of ['agents', 'skills']) {
|
|
154
|
-
const plans = buildPlans(kind, { targets: ['codex'], scope: 'project', identity: identityConfig });
|
|
154
|
+
const plans = buildPlans(kind, { targets: ['codex'], scope: 'project', identity: identityConfig, agentModels: projectConfig.load(cwd).agentModels || {} });
|
|
155
155
|
const statuses = manager.inspect(plans);
|
|
156
156
|
const toWrite = plans.filter((_, index) => {
|
|
157
157
|
const state = statuses[index].state;
|
package/src/config/index.js
CHANGED
|
@@ -65,6 +65,10 @@ function defaults() {
|
|
|
65
65
|
credentialGuard: {
|
|
66
66
|
mode: 'warn',
|
|
67
67
|
},
|
|
68
|
+
// agent_models field — see ADR-2026-08-21-versao-do-modelo-por-tier-com-composicao-por-alvo.md.
|
|
69
|
+
// Maps tier name (e.g. "opus", "sonnet") to version string (e.g. "5", "4.6").
|
|
70
|
+
// Absent or empty map → behavior identical to today (tier alias used verbatim).
|
|
71
|
+
agentModels: {},
|
|
68
72
|
rules: {
|
|
69
73
|
wip_has_req: 'error',
|
|
70
74
|
wip_acceptance: 'error',
|
|
@@ -278,6 +282,17 @@ function parse(content, cfg) {
|
|
|
278
282
|
if (mode === 'warn' || mode === 'block') cfg.credentialGuard.mode = mode;
|
|
279
283
|
}
|
|
280
284
|
|
|
285
|
+
// agent_models field — flat mapping from tier name to version string.
|
|
286
|
+
// See ADR-2026-08-21-versao-do-modelo-por-tier-com-composicao-por-alvo.md.
|
|
287
|
+
// An absent, malformed, or empty block leaves agentModels as the empty object from defaults(),
|
|
288
|
+
// preserving identical behavior to today. A key with an empty string value is stored as-is
|
|
289
|
+
// (the render layer treats empty string as "no pin" — fall back to tier alias).
|
|
290
|
+
if (m.agent_models !== undefined && typeof m.agent_models === 'object' && !Array.isArray(m.agent_models)) {
|
|
291
|
+
for (const [k, v] of Object.entries(m.agent_models)) {
|
|
292
|
+
if (typeof v === 'string') cfg.agentModels[k] = v;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
281
296
|
return false;
|
|
282
297
|
}
|
|
283
298
|
|
|
@@ -298,7 +313,7 @@ function parseRulesFromContent(content) {
|
|
|
298
313
|
// never creates them) — so this cannot be a bare { rules: {} } literal or parse() throws on any
|
|
299
314
|
// content that sets one of those nested keys. rules starts empty (not seeded from defaults()),
|
|
300
315
|
// deliberately: the caller wants exactly what `rules:` in content declares, nothing else.
|
|
301
|
-
const cfg = { rules: {}, credentialGuard: {}, update: {}, sync: {}, linkFields: {} };
|
|
316
|
+
const cfg = { rules: {}, credentialGuard: {}, update: {}, sync: {}, linkFields: {}, agentModels: {} };
|
|
302
317
|
parse(content, cfg);
|
|
303
318
|
return cfg.rules;
|
|
304
319
|
}
|
|
@@ -316,7 +331,7 @@ function readAgentConventions(cwd) {
|
|
|
316
331
|
} catch (_) {
|
|
317
332
|
return '';
|
|
318
333
|
}
|
|
319
|
-
const cfg = { rules: {}, credentialGuard: {}, update: { agentConventions: '' }, sync: {}, linkFields: {} };
|
|
334
|
+
const cfg = { rules: {}, credentialGuard: {}, update: { agentConventions: '' }, sync: {}, linkFields: {}, agentModels: {} };
|
|
320
335
|
try {
|
|
321
336
|
parse(content, cfg);
|
|
322
337
|
} catch (_) {
|
package/src/generators/hooks.js
CHANGED
|
@@ -769,6 +769,24 @@ match_subcommand() {
|
|
|
769
769
|
;;
|
|
770
770
|
esac
|
|
771
771
|
done
|
|
772
|
+
# git checkout -- <path> | git checkout . descarta alterações não commitadas do
|
|
773
|
+
# caminho indicado, de forma irreversível, no worktree compartilhado — bloqueia
|
|
774
|
+
# quando '--' aparece em qualquer posição (forma explícita de pathspec) ou quando
|
|
775
|
+
# '.' aparece como token isolado. 'git checkout <branch>' sem nenhum dos dois
|
|
776
|
+
# segue liberado por decisão (distinguir branch de caminho sem '--' é ambíguo, e
|
|
777
|
+
# adivinhar produziria falso-positivo).
|
|
778
|
+
checkout_path=0
|
|
779
|
+
for tok2 in "$@"; do
|
|
780
|
+
case "$tok2" in
|
|
781
|
+
--|.)
|
|
782
|
+
checkout_path=1
|
|
783
|
+
;;
|
|
784
|
+
esac
|
|
785
|
+
done
|
|
786
|
+
if [ "$checkout_path" = "1" ]; then
|
|
787
|
+
echo "checkout-path"
|
|
788
|
+
return 0
|
|
789
|
+
fi
|
|
772
790
|
;;
|
|
773
791
|
switch)
|
|
774
792
|
for tok2 in "$@"; do
|
|
@@ -780,6 +798,87 @@ match_subcommand() {
|
|
|
780
798
|
esac
|
|
781
799
|
done
|
|
782
800
|
;;
|
|
801
|
+
stash)
|
|
802
|
+
# git stash: liberado só para leitura (list/show) — bloqueia a forma bare
|
|
803
|
+
# (equivale a "push"), push, save, clear e drop. Decisão de KG: bloquear a
|
|
804
|
+
# classe inteira, não só os literais medidos (ver REQ). Repositório com um único
|
|
805
|
+
# worktree compartilhado entre subagentes paralelos — um stash de um agente
|
|
806
|
+
# remove as alterações não commitadas de todos os outros.
|
|
807
|
+
stash_sub="\${1:-}"
|
|
808
|
+
case "$stash_sub" in
|
|
809
|
+
list|show)
|
|
810
|
+
;;
|
|
811
|
+
*)
|
|
812
|
+
echo "stash"
|
|
813
|
+
return 0
|
|
814
|
+
;;
|
|
815
|
+
esac
|
|
816
|
+
;;
|
|
817
|
+
reset)
|
|
818
|
+
# Só --hard bloqueia, em qualquer posição de token — --soft/--mixed (inclusive
|
|
819
|
+
# sem flag, que é --mixed implícito) seguem liberados: --soft é o contorno
|
|
820
|
+
# padrão para reempurrar trabalho staged via ` + "`" + `trackfw ship -m "..."` + "`" + ` (ainda falta commitar após --soft).
|
|
821
|
+
for tok2 in "$@"; do
|
|
822
|
+
case "$tok2" in
|
|
823
|
+
--hard)
|
|
824
|
+
echo "reset-hard"
|
|
825
|
+
return 0
|
|
826
|
+
;;
|
|
827
|
+
esac
|
|
828
|
+
done
|
|
829
|
+
;;
|
|
830
|
+
clean)
|
|
831
|
+
# Bloqueia qualquer forma com force (-f, -fd, -fx, --force) ou -x/-X, EXCETO
|
|
832
|
+
# quando -n/--dry-run também está presente (dry-run nunca apaga nada).
|
|
833
|
+
clean_dry=0
|
|
834
|
+
clean_force=0
|
|
835
|
+
for tok2 in "$@"; do
|
|
836
|
+
case "$tok2" in
|
|
837
|
+
-n|--dry-run)
|
|
838
|
+
clean_dry=1
|
|
839
|
+
;;
|
|
840
|
+
-f*|--force|--force=*|-x|-X)
|
|
841
|
+
clean_force=1
|
|
842
|
+
;;
|
|
843
|
+
esac
|
|
844
|
+
done
|
|
845
|
+
if [ "$clean_dry" != "1" ] && [ "$clean_force" = "1" ]; then
|
|
846
|
+
echo "clean-force"
|
|
847
|
+
return 0
|
|
848
|
+
fi
|
|
849
|
+
;;
|
|
850
|
+
restore)
|
|
851
|
+
# git restore --staged SOZINHO nunca toca o working tree (mexe só no
|
|
852
|
+
# index), então segue liberado mesmo com path. Mas --worktree/-W (com ou
|
|
853
|
+
# sem --staged junto) SEMPRE afeta o working tree — inclusive
|
|
854
|
+
# "--staged --worktree", que restaura os dois — então bloqueia sempre que
|
|
855
|
+
# --worktree/-W aparecer, e também no caso padrão (sem --staged em
|
|
856
|
+
# nenhuma forma) com um argumento posicional (o path).
|
|
857
|
+
restore_staged=0
|
|
858
|
+
restore_worktree=0
|
|
859
|
+
restore_positional=0
|
|
860
|
+
for tok2 in "$@"; do
|
|
861
|
+
case "$tok2" in
|
|
862
|
+
--staged)
|
|
863
|
+
restore_staged=1
|
|
864
|
+
;;
|
|
865
|
+
--worktree|-W)
|
|
866
|
+
restore_worktree=1
|
|
867
|
+
;;
|
|
868
|
+
-*)
|
|
869
|
+
;;
|
|
870
|
+
*)
|
|
871
|
+
restore_positional=1
|
|
872
|
+
;;
|
|
873
|
+
esac
|
|
874
|
+
done
|
|
875
|
+
if [ "$restore_positional" = "1" ]; then
|
|
876
|
+
if [ "$restore_worktree" = "1" ] || [ "$restore_staged" != "1" ]; then
|
|
877
|
+
echo "restore-path"
|
|
878
|
+
return 0
|
|
879
|
+
fi
|
|
880
|
+
fi
|
|
881
|
+
;;
|
|
783
882
|
branch)
|
|
784
883
|
# git branch é majoritariamente leitura (sem args, -a, -r, -l, --list, -v/-vv,
|
|
785
884
|
# --show-current, --contains, --no-contains, --merged, --no-merged, --sort=,
|
|
@@ -835,8 +934,44 @@ match_subcommand() {
|
|
|
835
934
|
;;
|
|
836
935
|
esac
|
|
837
936
|
done
|
|
937
|
+
elif [ "\${1:-}" = "remove" ]; then
|
|
938
|
+
# git worktree remove SEM -f/--force já recusa sozinho quando há alteração não
|
|
939
|
+
# commitada no worktree indicado — só a forma com force é irreversível o bastante
|
|
940
|
+
# para bloquear aqui.
|
|
941
|
+
shift
|
|
942
|
+
for tok2 in "$@"; do
|
|
943
|
+
case "$tok2" in
|
|
944
|
+
-f|--force)
|
|
945
|
+
echo "worktree-remove-force"
|
|
946
|
+
return 0
|
|
947
|
+
;;
|
|
948
|
+
esac
|
|
949
|
+
done
|
|
838
950
|
fi
|
|
839
951
|
;;
|
|
952
|
+
update-ref)
|
|
953
|
+
# git update-ref reescreve um ref (inclusive refs/remotes/origin/*) sem tocar o
|
|
954
|
+
# objeto apontado nem exigir push — foi o mecanismo que tornou alcançável o exploit
|
|
955
|
+
# descrito no ADR-2026-08-19-caminho-governado-para-push-forcado-e-tag-de-release.md
|
|
956
|
+
# (Emenda 1): forjar origin/<base> localmente para desviar o commit-alvo de trackfw
|
|
957
|
+
# release tag. Sem forma de leitura equivalente a bloquear seletivamente — a
|
|
958
|
+
# subcommand inteira é escrita — bloqueia sempre, sem exceção de token.
|
|
959
|
+
echo "update-ref"
|
|
960
|
+
return 0
|
|
961
|
+
;;
|
|
962
|
+
rm)
|
|
963
|
+
# git rm -f/--force apaga do working tree e do index de forma irreversível, mesma
|
|
964
|
+
# classe de git clean -f/git reset --hard já bloqueados acima — sem exceção para
|
|
965
|
+
# --cached (destrancar do index sem -f já segue liberado por não precisar de force).
|
|
966
|
+
for tok2 in "$@"; do
|
|
967
|
+
case "$tok2" in
|
|
968
|
+
-f*|--force|--force=*)
|
|
969
|
+
echo "rm-force"
|
|
970
|
+
return 0
|
|
971
|
+
;;
|
|
972
|
+
esac
|
|
973
|
+
done
|
|
974
|
+
;;
|
|
840
975
|
esac
|
|
841
976
|
done <<EOF
|
|
842
977
|
$normalized
|
|
@@ -848,22 +983,46 @@ SUBCOMMAND=$(match_subcommand "$CMD_RAW") || exit 0
|
|
|
848
983
|
|
|
849
984
|
case "$SUBCOMMAND" in
|
|
850
985
|
checkout-b)
|
|
851
|
-
REASON="trackfw: git checkout -b bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_BACKTICK + `. Ver CLAUDE.md §1."
|
|
986
|
+
REASON="trackfw: git checkout -b bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_BACKTICK + `. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
852
987
|
;;
|
|
853
988
|
switch-c)
|
|
854
|
-
REASON="trackfw: git switch -c bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_BACKTICK + `. Ver CLAUDE.md §1."
|
|
989
|
+
REASON="trackfw: git switch -c bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_BACKTICK + `. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
855
990
|
;;
|
|
856
991
|
branch-create)
|
|
857
|
-
REASON="trackfw: git branch bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_BACKTICK + `. Ver CLAUDE.md §1."
|
|
992
|
+
REASON="trackfw: git branch bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_BACKTICK + `. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
858
993
|
;;
|
|
859
994
|
worktree-add-b)
|
|
860
|
-
REASON="trackfw: git worktree add -b bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_BACKTICK + `. Ver CLAUDE.md §1."
|
|
995
|
+
REASON="trackfw: git worktree add -b bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_BACKTICK + `. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
861
996
|
;;
|
|
862
997
|
commit)
|
|
863
|
-
REASON="trackfw: git commit bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw commit -m '<mensagem>'` + GBG_BACKTICK + `. Ver CLAUDE.md §1."
|
|
998
|
+
REASON="trackfw: git commit bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw commit -m '<mensagem>'` + GBG_BACKTICK + `. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
864
999
|
;;
|
|
865
1000
|
push)
|
|
866
|
-
REASON="trackfw: git push bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw ship` + GBG_BACKTICK +
|
|
1001
|
+
REASON="trackfw: git push bruto bloqueado. Use ` + GBG_BACKTICK + `trackfw push` + GBG_BACKTICK + ` (para empurrar commits já criados), ` + GBG_BACKTICK + `trackfw ship` + GBG_BACKTICK + ` (para commit+push+PR em uma etapa) ou ` + GBG_BACKTICK + `trackfw release tag` + GBG_BACKTICK + ` (para publicar uma tag de release). Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1002
|
+
;;
|
|
1003
|
+
stash)
|
|
1004
|
+
REASON="trackfw: git stash bruto bloqueado — worktree compartilhado entre subagentes, um stash remove as alterações não commitadas de todos os outros. ` + GBG_BACKTICK + `git stash list` + GBG_BACKTICK + `/` + GBG_BACKTICK + `git stash show` + GBG_BACKTICK + ` seguem liberados; para guardar trabalho em progresso, use uma branch própria via ` + GBG_BACKTICK + `trackfw branch new` + GBG_BACKTICK + ` e commit nela. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1005
|
+
;;
|
|
1006
|
+
reset-hard)
|
|
1007
|
+
REASON="trackfw: git reset --hard bruto bloqueado — descarta de forma irreversível as alterações não commitadas de todo o worktree compartilhado. ` + GBG_BACKTICK + `git reset --soft` + GBG_BACKTICK + `/` + GBG_BACKTICK + `--mixed` + GBG_BACKTICK + ` seguem liberados (ex.: ` + GBG_BACKTICK + `git reset --soft HEAD~1` + GBG_BACKTICK + ` é o caminho padrão; use ` + GBG_BACKTICK + `trackfw ship -m "..."` + GBG_BACKTICK + ` para commitar e empurrar). Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1008
|
+
;;
|
|
1009
|
+
clean-force)
|
|
1010
|
+
REASON="trackfw: git clean -f/-x bruto bloqueado — apaga arquivos não rastreados do worktree compartilhado, de forma irreversível. ` + GBG_BACKTICK + `git clean -n` + GBG_BACKTICK + `/` + GBG_BACKTICK + `--dry-run` + GBG_BACKTICK + ` segue liberado para revisar antes o que seria apagado. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1011
|
+
;;
|
|
1012
|
+
restore-path)
|
|
1013
|
+
REASON="trackfw: git restore <path> bruto bloqueado — descarta de forma irreversível as alterações não commitadas do caminho indicado. ` + GBG_BACKTICK + `git restore --staged` + GBG_BACKTICK + ` (não toca o working tree) segue liberado; para descartar de fato, confirme antes com o usuário. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1014
|
+
;;
|
|
1015
|
+
checkout-path)
|
|
1016
|
+
REASON="trackfw: git checkout -- <path>/git checkout . bruto bloqueado — descarta de forma irreversível as alterações não commitadas do caminho indicado. ` + GBG_BACKTICK + `git checkout <branch>` + GBG_BACKTICK + `/` + GBG_BACKTICK + `git switch <branch>` + GBG_BACKTICK + ` seguem liberados; para descartar de fato, confirme antes com o usuário. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1017
|
+
;;
|
|
1018
|
+
update-ref)
|
|
1019
|
+
REASON="trackfw: git update-ref bruto bloqueado — reescreve um ref (inclusive refs/remotes/origin/*) sem tocar o objeto apontado nem exigir push, o que permite forjar o commit-alvo que ` + GBG_BACKTICK + `trackfw release tag` + GBG_BACKTICK + ` publicaria. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1020
|
+
;;
|
|
1021
|
+
worktree-remove-force)
|
|
1022
|
+
REASON="trackfw: git worktree remove -f/--force bruto bloqueado — remove um worktree e descarta de forma irreversível qualquer alteração não commitada nele. ` + GBG_BACKTICK + `git worktree remove` + GBG_BACKTICK + ` sem force segue liberado (recusa sozinho quando há algo não commitado). Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
1023
|
+
;;
|
|
1024
|
+
rm-force)
|
|
1025
|
+
REASON="trackfw: git rm -f/--force bruto bloqueado — apaga arquivos do working tree e do index de forma irreversível, mesma classe de ` + GBG_BACKTICK + `git clean -f` + GBG_BACKTICK + `/` + GBG_BACKTICK + `git reset --hard` + GBG_BACKTICK + ` já bloqueados. Nada antes deste comando foi executado (comando composto é bloqueado por inteiro). Ver CLAUDE.md §1."
|
|
867
1026
|
;;
|
|
868
1027
|
*)
|
|
869
1028
|
exit 0
|
|
@@ -1965,7 +2124,17 @@ function injectWindsurfHooks(cwd) {
|
|
|
1965
2124
|
// written on first creation so the default agent keeps today's unrestricted
|
|
1966
2125
|
// tool access (this fix does not narrow what any agent can do, only where
|
|
1967
2126
|
// the deny wiring lives). Mirrors Go's InjectAmazonQHooks
|
|
1968
|
-
// (internal/generators/agentfiles.go) field-for-field
|
|
2127
|
+
// (internal/generators/agentfiles.go) field-for-field -- Go is the
|
|
2128
|
+
// canonical set (ROADMAP-2026-08-20, ML-1A-bis): only `name`, `description`
|
|
2129
|
+
// and `tools` are written on first creation. `prompt`/`mcpServers`/
|
|
2130
|
+
// `toolAliases`/`allowedTools`/`resources`/`useLegacyMcpJson` were written
|
|
2131
|
+
// here until ML-1A-bis and are now deliberately NOT written -- an extra
|
|
2132
|
+
// field the real schema doesn't expect risks failing validation, whereas an
|
|
2133
|
+
// absent optional field usually doesn't (asymmetry-of-risk decision, not a
|
|
2134
|
+
// verification against the live Amazon Q schema -- see docs/cli-parity.md
|
|
2135
|
+
// for the recorded limit). Pre-existing occurrences of the dropped fields
|
|
2136
|
+
// in a user's file are left untouched (never removed, only no longer
|
|
2137
|
+
// created).
|
|
1969
2138
|
//
|
|
1970
2139
|
// Native custom-agent toolset restriction (REQ acceptance criterion): still
|
|
1971
2140
|
// NOT implemented here -- this ML only wires the guard/deny fields on the
|
|
@@ -1982,13 +2151,7 @@ function injectAmazonQHooks(cwd) {
|
|
|
1982
2151
|
const defaults = {
|
|
1983
2152
|
name: 'q_cli_default',
|
|
1984
2153
|
description: 'trackfw-managed default agent — wires the git branch guard hook/denylist. See docs/cli-parity.md.',
|
|
1985
|
-
prompt: null,
|
|
1986
|
-
mcpServers: {},
|
|
1987
2154
|
tools: ['*'],
|
|
1988
|
-
toolAliases: {},
|
|
1989
|
-
allowedTools: [],
|
|
1990
|
-
resources: [],
|
|
1991
|
-
useLegacyMcpJson: false,
|
|
1992
2155
|
}
|
|
1993
2156
|
for (const [k, v] of Object.entries(defaults)) {
|
|
1994
2157
|
if (!Object.prototype.hasOwnProperty.call(data, k)) data[k] = v
|
package/src/generators/init.js
CHANGED
|
@@ -723,6 +723,18 @@ function generateClaudeMD(cfg) {
|
|
|
723
723
|
content += 'Decide low-risk details autonomously following existing project conventions, '
|
|
724
724
|
content += 'and record autonomous decisions in the commit message.\n'
|
|
725
725
|
|
|
726
|
+
content += '\n## Architect responses\n\n'
|
|
727
|
+
content += 'Default: what changed · what was decided · what is needed from you. Three to five lines.\n\n'
|
|
728
|
+
content += 'Scale up only on these three triggers, and only on them: a **blocker** that stops the next wave; '
|
|
729
|
+
content += 'a **pending user decision** that cannot be inferred from context; '
|
|
730
|
+
content += 'an **error the architect made** that cannot be self-corrected.\n\n'
|
|
731
|
+
content += 'Never cut, even when short: measured evidence (command and result), barrier verdict, decision taken and why. '
|
|
732
|
+
content += 'A response that buries a blocker in paragraph seven produced the same effect as not reporting it.\n\n'
|
|
733
|
+
content += 'Cut: restating what an executor already reported, re-explaining reasoning already given, '
|
|
734
|
+
content += 'recapping state that has not changed, closing praise. '
|
|
735
|
+
content += 'Tables and code blocks only when they replace prose, never when they add to it.\n\n'
|
|
736
|
+
content += 'Depth is on demand from the user.\n'
|
|
737
|
+
|
|
726
738
|
injectOrUpdateRules('CLAUDE.md', content, '.')
|
|
727
739
|
console.log(' ✓ CLAUDE.md')
|
|
728
740
|
}
|
|
@@ -44,4 +44,16 @@ Before releasing the next wave, verify each acceptance criterion yourself: read
|
|
|
44
44
|
## Mission
|
|
45
45
|
Map the existing architecture and traceability chain before proposing changes. Record material decisions as ADRs, produce decision-complete plans, and delegate implementation to the appropriate specialist. Do not implement product code.
|
|
46
46
|
|
|
47
|
+
## Response length
|
|
48
|
+
|
|
49
|
+
Default: what changed · what I decided · what I need from you. Three to five lines.
|
|
50
|
+
|
|
51
|
+
Scale up only on these three triggers, and only on them: a **blocker** that stops the next wave; a **pending user decision** that cannot be inferred from context; an **error I made** that cannot be self-corrected.
|
|
52
|
+
|
|
53
|
+
Never cut, even when short: measured evidence (command and result), barrier verdict, decision taken and why. A response that buries a blocker in paragraph seven produced the same effect as not reporting it.
|
|
54
|
+
|
|
55
|
+
Cut: restating what an executor already reported, re-explaining reasoning already given, recapping state that has not changed, closing praise. Tables and code blocks only when they replace prose, never when they add to it.
|
|
56
|
+
|
|
57
|
+
Depth is on demand from the user.
|
|
58
|
+
|
|
47
59
|
— Architect, Principal Software Architect
|
|
Binary file
|