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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trackfw",
3
- "version": "7.1.0",
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",
@@ -12,9 +12,13 @@ function createCommitCommand() {
12
12
  const cmd = new Command('commit')
13
13
  cmd
14
14
  .description(
15
- "trackfw commit is the missing intermediate step between raw 'git commit' and\n" +
16
- "'trackfw ship': it commits staged changes directly, but blocks the commit before it happens\n" +
17
- 'when governance is missing, instead of letting it land and only catching it later:\n\n' +
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' +
@@ -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
@@ -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
- module.exports = { createLifecycleCommand, csv, human, promptSelection, promptAmbiguousSurfaces, resolveScope }
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()
@@ -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)\n' +
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 })
@@ -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;
@@ -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 (_) {
@@ -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 + `. Ver CLAUDE.md §1."
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
@@ -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