trackfw 2.15.0 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -367,6 +367,179 @@ The 10 roles installed for each tool: **architect · backend · frontend · qa
367
367
 
368
368
  ---
369
369
 
370
+ ## Agent identity — give your agents a name
371
+
372
+ By default the agents are functional and impersonal: `trackfw-architect`,
373
+ `trackfw-backend`, … Agent identity lets you name all ten, pick how they
374
+ address you, and call them by name.
375
+
376
+ ```bash
377
+ # Non-interactive: pick a themed preset
378
+ trackfw init --identity-preset greek
379
+
380
+ # Or answer the wizard, which also offers "name them one by one"
381
+ trackfw init
382
+ ```
383
+
384
+ `--identity-preset` accepts ten themed presets plus two opt-outs:
385
+
386
+ `greek` · `norse` · `potter` · `thrones` · `chaves` · `pioneers` · `starwars` · `tolkien` · `turma` · `egyptian` · `neutral` · `none`
387
+
388
+ `neutral` and `none` write nothing and keep the current behavior.
389
+
390
+ Sample mapping (three of the ten presets):
391
+
392
+ | Agent | `greek` | `pioneers` | `tolkien` |
393
+ |---|---|---|---|
394
+ | architect | Zeus | Turing | Gandalf |
395
+ | backend | Apolo | Ritchie | Aragorn |
396
+ | frontend | Afrodite | Berners-Lee | Arwen |
397
+ | qa | Ártemis | Hamilton | Legolas |
398
+ | infra | Ares | Torvalds | Gimli |
399
+ | security | Hades | Diffie | Boromir |
400
+ | dba | Poseidon | Codd | Elrond |
401
+ | ux | Atena | Norman | Galadriel |
402
+ | code-quality | Hefesto | Knuth | Faramir |
403
+ | data | Métis | Hopper | Bilbo |
404
+
405
+ The remaining presets are `norse` (Odin, Thor, Freya…), `potter` (Dumbledore,
406
+ Snape, Luna…), `thrones` (Tyrion, Jon, Arya…), `chaves` (Girafales, Madruga,
407
+ Chiquinha…), `starwars` (Yoda, Han, Leia…), `turma` (Franjinha, Cebolinha,
408
+ Magali…), and `egyptian` (Thoth, Rá, Ísis…).
409
+
410
+ ### Custom mode and your nickname
411
+
412
+ The interactive wizard also offers **name them one by one**: you type all ten
413
+ display names yourself. Each entry is validated as you go — an invalid name is
414
+ rejected with an inline error, never silently corrected, and two names that
415
+ resolve to the same identifier are rejected too.
416
+
417
+ The wizard then asks for an optional **nickname for you**, which is how the
418
+ agents will address you.
419
+
420
+ ### `agents install` also runs the wizard
421
+
422
+ `trackfw init` is not the only entry point: `trackfw agents install`, the
423
+ natural path in a project that is already governed, offers the same wizard.
424
+ The rule is identical across the three CLIs — it appears **only** when
425
+ **all** of the following hold:
426
+
427
+ - the command is `agents` (never `skills`: skills have no identity);
428
+ - stdin is a TTY (a non-interactive run never blocks on a prompt);
429
+ - and either no `~/.trackfw/identity.json` exists yet, or `--identity` was
430
+ passed to force reconfiguration.
431
+
432
+ With an identity already configured and no `--identity`, the command asks
433
+ nothing — it prints `identity: N custom agent(s)` and installs directly.
434
+
435
+ ```bash
436
+ # First run on this machine: offers the wizard, then installs
437
+ trackfw agents install --targets claude
438
+
439
+ # Identity already configured: no prompt, installs directly
440
+ trackfw agents install --targets claude
441
+
442
+ # Force reconfiguration
443
+ trackfw agents install --targets claude --identity
444
+
445
+ # Non-interactive, same semantics as init --identity-preset
446
+ trackfw agents install --targets claude --identity-preset chaves
447
+ ```
448
+
449
+ Two new flags exist only on `agents install` (`skills install` never
450
+ registers them): `--identity` (bool, forces reconfiguration even if a file
451
+ already exists) and `--identity-preset <preset>` (same ten themed presets
452
+ plus `neutral` and `none`; an invalid value errors out listing the valid
453
+ ones).
454
+
455
+ In **name them one by one** mode, each field is now labeled by the agent's
456
+ specialty, taken from the catalog, never by its technical id:
457
+
458
+ ```
459
+ Architect — Architecture, ADRs and governed coordination
460
+ > _
461
+ ```
462
+
463
+ Before anything is written to disk — for a themed preset **or** for custom
464
+ names — a confirmation screen lists all ten `specialty → name` pairs plus
465
+ your nickname:
466
+
467
+ ```
468
+ ── Confirmation ──────────────────────────────
469
+ Architecture, ADRs and governed coordination → Girafales
470
+ Backend APIs, domain logic and integrations → Madruga
471
+ ...
472
+ What we'll call you: chefe
473
+
474
+ ? Confirm?
475
+ ```
476
+
477
+ Answering no returns to preset selection; nothing is written until you
478
+ confirm.
479
+
480
+ Everything is stored in a single global file, shared by the Go, npm, and PyPI
481
+ distributions:
482
+
483
+ ```json
484
+ // ~/.trackfw/identity.json
485
+ {
486
+ "schema_version": 1,
487
+ "user_nickname": "Kleber",
488
+ "agents": {
489
+ "architect": { "display_name": "Zeus", "slug": "zeus" }
490
+ }
491
+ }
492
+ ```
493
+
494
+ The generated artifact — still installed at the unchanged path
495
+ `~/.claude/agents/trackfw-architect.md`:
496
+
497
+ ```markdown
498
+ ---
499
+ name: zeus-tf
500
+ description: Zeus — Principal software architect for system design, ADRs and governed multi-agent coordination.
501
+ model: opus
502
+ ---
503
+
504
+ Você é Zeus. Trate o usuário como Kleber.
505
+
506
+ # Architect
507
+ ...
508
+ ```
509
+
510
+ ### Why the `-tf` suffix
511
+
512
+ The `name` always ends in `-tf`. Two agents sharing the same `name` in the same
513
+ directory make Claude Code load *"only one of them, chosen by filesystem read
514
+ order rather than a documented precedence"* — a silent, non-deterministic
515
+ shadowing that the user cannot detect. If you already keep a personal `zeus.md`
516
+ agent, `zeus-tf` guarantees both survive.
517
+
518
+ The suffix belongs to the technical identifier only. It never appears in how
519
+ the agent presents itself: the `description` and the body both say **Zeus**.
520
+
521
+ ### How to invoke it
522
+
523
+ | You type | What happens |
524
+ |---|---|
525
+ | `@agent-zeus-tf` | Works — explicit mention resolves against `name` |
526
+ | "chame o Zeus" / "ask Zeus to…" | Works — natural-language routing reads `description` |
527
+ | "quem é você?" | Answers "Sou Zeus" — the body is loaded after selection |
528
+
529
+ ### Cost and non-regression
530
+
531
+ The agent **never reads the configuration at runtime**. Identity is
532
+ materialized into the artifact at install time, so the per-interaction cost is
533
+ essentially zero: the `description` is substituted rather than extended, and
534
+ the body grows by tens of tokens that are loaded only after the agent has
535
+ already been selected. No tool call, no file read, no permanent instruction.
536
+
537
+ Without `~/.trackfw/identity.json`, the generated artifacts are **byte for byte
538
+ identical** to the current ones in all three CLIs. The feature is opt-in and
539
+ regresses nothing.
540
+
541
+ ---
542
+
370
543
  ## `trackfw init` — stack-aware scaffolding
371
544
 
372
545
  ```
@@ -377,6 +550,7 @@ The 10 roles installed for each tool: **architect · backend · frontend · qa
377
550
  ? Git hooks? husky / lefthook / none
378
551
  ? CI system? GitHub Actions / GitLab CI / none
379
552
  ? Which AI assistants? Claude / Codex / Gemini / Antigravity / Cursor / Copilot / Windsurf / Amazon Q / Kiro
553
+ ? Agent identity? Greek / Norse / Potter / Thrones / Chaves / Pioneers / Star Wars / Tolkien / Turma / Egyptian / Name them one by one / Neutral
380
554
  ```
381
555
 
382
556
  The governance structure (`docs/adr/`, `docs/req/`, `docs/roadmaps/`) is always identical — stack-agnostic. The generated hooks, workflows, and AI integrations adapt to your answers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trackfw",
3
- "version": "2.15.0",
3
+ "version": "2.16.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, and Amazon Q.",
5
5
  "keywords": [
6
6
  "cli",
@@ -0,0 +1,258 @@
1
+ 'use strict'
2
+
3
+ const fs = require('node:fs')
4
+ const path = require('node:path')
5
+
6
+ const identityStore = require('../identity')
7
+ const { t } = require('../i18n')
8
+
9
+ // identityFileExists reporta se ~/.trackfw/identity.json já existe, sem
10
+ // depender de identityStore.load (que retorna config vazia para "ausente").
11
+ // Espelha internal/commands/init.go:identityFileExists.
12
+ function identityFileExists(home) {
13
+ return fs.existsSync(path.join(home, '.trackfw', 'identity.json'))
14
+ }
15
+
16
+ // resolveIdentityPreset traduz o valor da flag --identity-preset para uma
17
+ // Config a persistir. "none" e "neutral" significam "não gravar nada" — o
18
+ // chamador não deve criar ~/.trackfw/identity.json para esses valores. Um
19
+ // valor desconhecido é sempre um erro, listando os valores aceitos. Espelha
20
+ // internal/commands/init.go:resolveIdentityPreset.
21
+ function resolveIdentityPreset(value) {
22
+ if (value === 'none' || value === 'neutral') return { cfg: null, shouldSave: false }
23
+ let cfg
24
+ try {
25
+ cfg = identityStore.preset(value)
26
+ } catch {
27
+ const valid = ['none', 'neutral', ...identityStore.presetNames()]
28
+ throw new Error(`identity-preset invalido "${value}" (validos: ${valid.join(', ')})`)
29
+ }
30
+ return { cfg, shouldSave: true }
31
+ }
32
+
33
+ // applyIdentityPresetFlag resolve e persiste --identity-preset para
34
+ // `agents install|update|uninstall`, reusando resolveIdentityPreset para que
35
+ // ambos os comandos aceitem exatamente os mesmos nomes de preset e rejeitem
36
+ // valores inválidos com o mesmo formato de erro. Espelha
37
+ // internal/commands/integrations_flags.go:applyIdentityPresetFlag.
38
+ function applyIdentityPresetFlag(home, presetValue, operation) {
39
+ const { cfg, shouldSave } = resolveIdentityPreset(presetValue)
40
+ if (!shouldSave) return
41
+ try {
42
+ identityStore.validate(cfg, identityStore.knownAgentIds())
43
+ } catch (err) {
44
+ throw new Error(`${operation}: identidade invalida: ${err.message}`)
45
+ }
46
+ try {
47
+ identityStore.save(home, cfg)
48
+ } catch (err) {
49
+ throw new Error(`${operation}: falha ao gravar identidade: ${err.message}`)
50
+ }
51
+ }
52
+
53
+ // shouldPromptIdentity decide se `trackfw agents install` (e as operações
54
+ // update/uninstall, que compartilham o mesmo fluxo) deve mostrar o wizard
55
+ // interativo de identidade. Todas as condições devem valer simultaneamente
56
+ // (ADR D2):
57
+ //
58
+ // 1. kind é "agents" — skills não têm identidade e nunca devem perguntar.
59
+ // 2. stdin é um TTY — nunca bloquear uma execução não-interativa.
60
+ // 3. ou não há identidade configurada ainda, ou o usuário pediu
61
+ // explicitamente para reconfigurá-la via --identity.
62
+ //
63
+ // Com uma identidade já configurada e sem a flag --identity, o wizard NÃO
64
+ // deve reaparecer — um wizard que aparece a cada install vira incentivo a
65
+ // automatizar o "pular", o que esvazia a feature. Espelha
66
+ // internal/commands/identity_wizard.go:shouldPromptIdentity.
67
+ function shouldPromptIdentity(kind, isTTY, identityExists, forceFlag) {
68
+ if (kind !== 'agents') return false
69
+ if (!isTTY) return false
70
+ return !identityExists || Boolean(forceFlag)
71
+ }
72
+
73
+ // identityPresetOptions é a lista fixa de presets temáticos de identidade
74
+ // oferecidos pelo wizard, compartilhada literalmente entre init e agents
75
+ // install para que nenhum consumidor divirja do outro. Espelha
76
+ // internal/commands/identity_wizard.go:identityPresetOptions.
77
+ function identityPresetOptions() {
78
+ return [
79
+ { name: 'Panteão grego (Zeus, Apolo, Afrodite...)', value: 'greek' },
80
+ { name: 'Mitologia nórdica (Odin, Thor, Freya...)', value: 'norse' },
81
+ { name: 'Pioneiros da computação (Turing, Codd, Knuth...)', value: 'pioneers' },
82
+ { name: 'Harry Potter (Dumbledore, Snape, Luna...)', value: 'potter' },
83
+ { name: 'Game of Thrones (Tyrion, Jon, Arya...)', value: 'thrones' },
84
+ { name: 'Senhor dos Anéis (Gandalf, Aragorn, Arwen...)', value: 'tolkien' },
85
+ { name: 'Star Wars (Yoda, Leia, Vader...)', value: 'starwars' },
86
+ { name: 'Chaves (Girafales, Madruga, Chiquinha...)', value: 'chaves' },
87
+ { name: 'Turma da Mônica (Franjinha, Cebolinha, Mônica...)', value: 'turma' },
88
+ { name: 'Panteão egípcio (Thoth, Ísis, Anúbis...)', value: 'egyptian' },
89
+ { name: 'Personalizar um a um', value: 'custom' },
90
+ { name: 'Nomes neutros (padrão)', value: 'neutral' },
91
+ ]
92
+ }
93
+
94
+ // catalogAgentItem busca o item "agents" do catálogo pelo id. Todo id vindo
95
+ // de identity.knownAgentIds() deve existir no catálogo embedado — se não
96
+ // existir, é erro de programação: falha alto em vez de rotular
97
+ // silenciosamente errado. Espelha o panic em
98
+ // internal/commands/identity_wizard.go:buildCustomIdentityGroup.
99
+ function catalogAgentItem(catalog, id) {
100
+ const found = (catalog.agents || []).find(entry => entry.id === id)
101
+ if (!found) {
102
+ throw new Error(`identity wizard: agent id "${id}" from identity.knownAgentIds() has no entry in the agents catalog`)
103
+ }
104
+ return found
105
+ }
106
+
107
+ // buildIdentityConfig constrói a Config em memória para o caminho escolhido
108
+ // no wizard, sem tocar em disco. Espelha
109
+ // internal/commands/identity_wizard.go:buildIdentityConfig.
110
+ function buildIdentityConfig(identitySelect, knownAgentIds, customDisplayNames, userNickname) {
111
+ let cfg
112
+ if (identitySelect === 'custom') {
113
+ const agents = {}
114
+ knownAgentIds.forEach((id, index) => {
115
+ let slug
116
+ try {
117
+ slug = identityStore.slugify(customDisplayNames[index])
118
+ } catch (err) {
119
+ throw new Error(`identidade customizada invalida para "${id}": ${err.message}`)
120
+ }
121
+ agents[id] = { display_name: customDisplayNames[index], slug }
122
+ })
123
+ cfg = { agents }
124
+ } else {
125
+ cfg = identityStore.preset(identitySelect)
126
+ }
127
+ cfg.user_nickname = userNickname
128
+ return cfg
129
+ }
130
+
131
+ // promptCustomNames pede um nome de exibição para cada agente conhecido,
132
+ // rotulado com a especialidade do catálogo (Item.name + Item.description) —
133
+ // nunca com o id técnico cru (ADR D4). Espelha
134
+ // internal/commands/identity_wizard.go:buildCustomIdentityGroup.
135
+ async function promptCustomNames(catalog, knownAgentIds, prompts) {
136
+ const { input } = prompts
137
+ const helperText = t('init.prompt.identityCustomName')
138
+ const customDisplayNames = []
139
+ for (const id of knownAgentIds) {
140
+ const item = catalogAgentItem(catalog, id)
141
+ const label = `${item.name} — ${item.description}`
142
+ // eslint-disable-next-line no-await-in-loop
143
+ const value = await input({
144
+ message: `${label}\n ${helperText}`,
145
+ validate: candidate => {
146
+ let slug
147
+ try {
148
+ slug = identityStore.slugify(candidate)
149
+ } catch (err) {
150
+ return err.message
151
+ }
152
+ for (let index = 0; index < customDisplayNames.length; index += 1) {
153
+ let otherSlug
154
+ try {
155
+ otherSlug = identityStore.slugify(customDisplayNames[index])
156
+ } catch {
157
+ continue
158
+ }
159
+ if (otherSlug === slug) return `slug "${slug}" duplicado com o agente "${knownAgentIds[index]}"`
160
+ }
161
+ return true
162
+ },
163
+ })
164
+ customDisplayNames.push(value)
165
+ }
166
+ return customDisplayNames
167
+ }
168
+
169
+ // confirmIdentitySelection exibe o mapeamento especialidade → nome de
170
+ // exibição (ADR D3) e pede confirmação. O mapeamento é impresso com
171
+ // console.log simples (não por um campo de prompt) para que o alinhamento
172
+ // de colunas siga exatamente o exemplo do ADR. Espelha
173
+ // internal/commands/identity_wizard.go:confirmIdentitySelection.
174
+ async function confirmIdentitySelection(catalog, knownAgentIds, cfg, prompts) {
175
+ const { confirm } = prompts
176
+ const nicknameLabel = t('identity.wizard.nicknameRowLabel')
177
+ let labelWidth = nicknameLabel.length
178
+ const labels = knownAgentIds.map(id => {
179
+ const item = catalogAgentItem(catalog, id)
180
+ if (item.description.length > labelWidth) labelWidth = item.description.length
181
+ return item.description
182
+ })
183
+
184
+ console.log()
185
+ console.log(t('identity.wizard.confirmHeader'))
186
+ knownAgentIds.forEach((id, index) => {
187
+ const agent = cfg.agents[id]
188
+ console.log(` ${labels[index].padEnd(labelWidth)} → ${agent.display_name}`)
189
+ })
190
+ if (cfg.user_nickname) {
191
+ console.log(` ${nicknameLabel.padEnd(labelWidth)} ${cfg.user_nickname}`)
192
+ }
193
+ console.log()
194
+
195
+ return confirm({ message: t('identity.wizard.confirmQuestion') })
196
+ }
197
+
198
+ // runIdentityWizard é o único wizard interativo de identidade, consumido
199
+ // tanto por `trackfw init` quanto por `trackfw agents install`
200
+ // (ADR-2026-07-25-wizard-unificado-de-identidade-no-agents-install, D1).
201
+ //
202
+ // Mostra, em ordem: seleção de preset/custom, nomes livres em texto (só no
203
+ // modo custom), o apelido do usuário e uma tela de confirmação (D3, D6). Se
204
+ // o usuário recusar a confirmação, o wizard volta à seleção de preset sem
205
+ // persistir nada — nunca retorna tendo gravado uma config parcial ou
206
+ // recusada em disco.
207
+ //
208
+ // Retorna [cfg, persisted]. "neutral" (ou deixar o select vazio) é o
209
+ // caminho "não gravar nada" e retorna [{}, false].
210
+ async function runIdentityWizard(catalog, home, prompts = require('@inquirer/prompts')) {
211
+ const { select, input } = prompts
212
+ const knownAgentIds = identityStore.knownAgentIds()
213
+
214
+ const titleIdentityPreset = t('init.prompt.identityPreset')
215
+ const titleIdentityNickname = t('init.prompt.identityNickname')
216
+
217
+ for (;;) {
218
+ const identitySelect = await select({
219
+ message: titleIdentityPreset,
220
+ choices: identityPresetOptions(),
221
+ })
222
+
223
+ if (identitySelect === '' || identitySelect === 'neutral') {
224
+ return [{}, false]
225
+ }
226
+
227
+ let customDisplayNames = []
228
+ if (identitySelect === 'custom') {
229
+ customDisplayNames = await promptCustomNames(catalog, knownAgentIds, prompts)
230
+ }
231
+
232
+ const userNickname = await input({ message: titleIdentityNickname, default: '' })
233
+
234
+ const cfg = buildIdentityConfig(identitySelect, knownAgentIds, customDisplayNames, userNickname)
235
+ identityStore.validate(cfg, knownAgentIds)
236
+
237
+ // eslint-disable-next-line no-await-in-loop
238
+ const confirmed = await confirmIdentitySelection(catalog, knownAgentIds, cfg, prompts)
239
+ if (!confirmed) {
240
+ // D3: recusar volta à seleção de preset sem persistir absolutamente nada.
241
+ continue
242
+ }
243
+
244
+ identityStore.save(home, cfg)
245
+ return [cfg, true]
246
+ }
247
+ }
248
+
249
+ module.exports = {
250
+ identityFileExists,
251
+ resolveIdentityPreset,
252
+ applyIdentityPresetFlag,
253
+ shouldPromptIdentity,
254
+ identityPresetOptions,
255
+ buildIdentityConfig,
256
+ confirmIdentitySelection,
257
+ runIdentityWizard,
258
+ }
@@ -1,14 +1,39 @@
1
1
  'use strict'
2
2
  const { Command } = require('commander')
3
3
  const { t } = require('../i18n')
4
+ const identityStore = require('../identity')
5
+ const identityWizard = require('./identity-wizard')
6
+ const { resolveIdentityPreset, identityFileExists } = identityWizard
4
7
 
5
8
  const cmd = new Command('init')
6
9
  cmd.description(t('init.description'))
7
10
  cmd.option('--ai-tools <tools>', 'Comma-separated AI tools to configure (claude,codex,gemini,antigravity,cursor,copilot,windsurf,amazonq,kiro)', '')
8
- cmd.action(async (options) => {
11
+ cmd.option('--identity-preset <preset>', `Agent identity preset: none, neutral, ${identityStore.presetNames().join(', ')}`, 'none')
12
+ cmd.action(async (options, command) => {
13
+ const os = require('os')
9
14
  const path = require('path')
10
15
  const generators = require('../generators/init')
11
16
 
17
+ const home = os.homedir()
18
+
19
+ // A validação e persistência da flag acontecem incondicionalmente, acima
20
+ // do early-return não-TTY abaixo — é isso que faz um --identity-preset
21
+ // inválido falhar alto em CI em vez de silenciosamente não fazer nada.
22
+ const presetChanged = command.getOptionValueSource('identityPreset') === 'cli'
23
+ if (presetChanged) {
24
+ const { cfg, shouldSave } = resolveIdentityPreset(options.identityPreset)
25
+ if (shouldSave) {
26
+ identityStore.validate(cfg, identityStore.knownAgentIds())
27
+ identityStore.save(home, cfg)
28
+ }
29
+ }
30
+
31
+ // Pula o wizard de identidade inteiramente quando a flag foi passada
32
+ // explicitamente (já tratado acima) ou quando um arquivo de identidade já
33
+ // existe — re-executar init nunca deve sobrescrever silenciosamente uma
34
+ // identidade já configurada.
35
+ const skipIdentityWizard = presetChanged || identityFileExists(home)
36
+
12
37
  // Modo não-TTY: usar defaults e chamar scaffold diretamente
13
38
  if (!process.stdin.isTTY) {
14
39
  const cfg = {
@@ -158,6 +183,7 @@ cmd.action(async (options) => {
158
183
  { name: 'Kiro', value: 'kiro' },
159
184
  ],
160
185
  })
186
+
161
187
  } catch (err) {
162
188
  // Fallback quando stdin fecha inesperadamente (ex: pipe em TTY simulado)
163
189
  const cfg = {
@@ -175,6 +201,33 @@ cmd.action(async (options) => {
175
201
  return
176
202
  }
177
203
 
204
+ // O wizard de identidade roda como seu próprio formulário, logo após o
205
+ // formulário principal — o mesmo componente compartilhado (ADR D1) usado
206
+ // por `agents install`. É pulado inteiramente (nenhuma pergunta) quando a
207
+ // flag já resolveu a identidade acima, ou quando um arquivo de identidade
208
+ // já existe.
209
+ if (!skipIdentityWizard) {
210
+ const { catalog } = require('../integrations')
211
+ try {
212
+ await identityWizard.runIdentityWizard(catalog, home)
213
+ } catch {
214
+ // Fallback quando stdin fecha inesperadamente durante o wizard de
215
+ // identidade (mesma tolerância aplicada ao formulário principal acima).
216
+ await generators.scaffold({
217
+ projectName: path.basename(process.cwd()),
218
+ projectType: 'governance',
219
+ frontend: '',
220
+ backend: '',
221
+ pkgManager: 'npm',
222
+ hooks: 'none',
223
+ ci: 'none',
224
+ })
225
+ console.log(`\n${t('init.success')}`)
226
+ require('../generators/init').printArchitectNextSteps(process.cwd())
227
+ return
228
+ }
229
+ }
230
+
178
231
  const cfg = { projectName, projectType, frontend, backend, backendFramework, pkgManager, hooks, ci, requireReqInCommit }
179
232
  await generators.scaffold(cfg)
180
233
 
@@ -1,7 +1,11 @@
1
1
  'use strict'
2
2
 
3
+ const os = require('node:os')
3
4
  const { Command } = require('commander')
4
5
  const { catalog, execute, parseSurfaces } = require('../integrations')
6
+ const identityStore = require('../identity')
7
+ const identityWizard = require('./identity-wizard')
8
+ const { t } = require('../i18n')
5
9
 
6
10
  const csv = value => String(value).split(',').map(entry => entry.trim()).filter(Boolean)
7
11
  const collect = (value, previous) => previous.concat(value)
@@ -39,6 +43,7 @@ async function promptAmbiguousSurfaces(kind, options, prompts = require('@inquir
39
43
  function createLifecycleCommand(kind) {
40
44
  const root = new Command(kind).description(`Manage trackfw ${kind}`)
41
45
  for (const operation of ['list', 'install', 'uninstall', 'update']) {
46
+ const mutation = operation !== 'list'
42
47
  const command = new Command(operation)
43
48
  .option('--targets <targets>', 'Comma-separated target CLIs', csv)
44
49
  .option('--items <items>', `Comma-separated ${kind} IDs`, csv)
@@ -46,16 +51,67 @@ function createLifecycleCommand(kind) {
46
51
  .option('--surface <target=surface>', 'Surface selection (repeatable)', collect, [])
47
52
  .option('--json', 'Print deterministic JSON')
48
53
  .option('--force', 'Replace or remove modified artifacts')
49
- command.action(async options => {
54
+
55
+ // Flags de identidade são exclusivas de agents (ADR D5): skills não têm
56
+ // identidade, e createLifecycleCommand é compartilhado entre `agents` e
57
+ // `skills` — sem este filtro por kind, `trackfw skills install
58
+ // --identity` aceitaria silenciosamente uma flag sem nenhum efeito.
59
+ // Espelha internal/commands/integrations_flags.go:addIntegrationFlags.
60
+ if (mutation && kind === 'agents') {
61
+ command
62
+ .option('--identity', 'Reconfigure agent identity even if ~/.trackfw/identity.json already exists')
63
+ .option('--identity-preset <preset>', `Agent identity preset (non-interactive): none, neutral, ${identityStore.presetNames().join(', ')}`)
64
+ }
65
+
66
+ command.action(async (options, cmd) => {
50
67
  options.surfaces = options.surface || []
51
68
  if (options.scope !== 'project' && options.scope !== 'global') throw new Error(`Unsupported scope: ${options.scope}`)
52
- const mutation = operation !== 'list'
69
+
70
+ // O booleano de --identity nunca deve chegar a execute()/buildPlans()
71
+ // sob a chave "identity" — essa chave ali é reservada para uma Config
72
+ // de identidade já resolvida (ver src/integrations/index.js:execute).
73
+ // Colidir os dois faria "agents install --identity" renderizar uma
74
+ // identidade booleana nos artefatos em vez da Config real.
75
+ const forceIdentity = options.identity === true
76
+ delete options.identity
77
+
78
+ // --identity-preset é validado e persistido incondicionalmente, acima
79
+ // de qualquer ramo dependente de TTY abaixo — isso é o que faz um
80
+ // --identity-preset inválido falhar alto em CI em vez de
81
+ // silenciosamente não fazer nada. Espelha init.js e
82
+ // internal/commands/integrations_flags.go:executeIntegrationMutation.
83
+ let presetChanged = false
84
+ if (mutation && kind === 'agents') {
85
+ presetChanged = cmd.getOptionValueSource('identityPreset') === 'cli'
86
+ if (presetChanged) identityWizard.applyIdentityPresetFlag(os.homedir(), options.identityPreset, operation)
87
+ }
88
+
53
89
  if (mutation && (!options.targets || !options.targets.length)) {
54
90
  if (!process.stdin.isTTY) throw new Error(`${operation} requires --targets in non-interactive mode`)
55
91
  await promptSelection(kind, options)
56
92
  }
57
93
  if (mutation && process.stdin.isTTY) await promptAmbiguousSurfaces(kind, options)
58
94
  options.allSurfaces = operation === 'list'
95
+
96
+ // Disparo do wizard de identidade (ADR D2): mostrado somente quando o
97
+ // caminho da flag acima ainda não resolveu a identidade desta
98
+ // execução, e somente para agents (nunca skills, D5). Roda depois da
99
+ // seleção de alvo/superfície e antes de execute() para que a
100
+ // identidade recém-gravada pelo wizard seja a que é renderizada nos
101
+ // planos abaixo. Espelha
102
+ // internal/commands/integrations_flags.go:executeIntegrationMutation.
103
+ if (mutation && kind === 'agents' && !presetChanged) {
104
+ const homeRoot = os.homedir()
105
+ const identityExists = identityWizard.identityFileExists(homeRoot)
106
+ const isTTY = Boolean(process.stdin.isTTY)
107
+ if (identityWizard.shouldPromptIdentity(kind, isTTY, identityExists, forceIdentity)) {
108
+ await identityWizard.runIdentityWizard(catalog, homeRoot)
109
+ } else if (identityExists && !options.json) {
110
+ const existing = identityStore.load(homeRoot)
111
+ console.log(t('identity.inUse', { count: String(Object.keys(existing.agents || {}).length) }))
112
+ }
113
+ }
114
+
59
115
  const output = execute(kind, operation, options)
60
116
  console.log(options.json ? JSON.stringify(output) : human(output))
61
117
  })
@@ -2,7 +2,9 @@
2
2
 
3
3
  const { Command } = require('commander');
4
4
  const fs = require('fs');
5
+ const os = require('os');
5
6
  const path = require('path');
7
+ const identityStore = require('../identity');
6
8
 
7
9
  function readUpdateConfig(rootDir) {
8
10
  const yaml = path.join(rootDir, 'trackfw.yaml');
@@ -62,6 +64,11 @@ cmd.action(() => {
62
64
  const generators = require('../generators/init');
63
65
  const discover = require('./discover');
64
66
 
67
+ // A identidade é carregada uma única vez, fora de qualquer try/catch —
68
+ // um identity.json corrompido deve abortar o comando inteiro, nunca cair
69
+ // silenciosamente para os nomes neutros default.
70
+ const identityConfig = identityStore.load(os.homedir());
71
+
65
72
  console.log('trackfw update — re-aplicando templates atuais...\n');
66
73
 
67
74
  // 1. Agent rules (marker-delimited, idempotent)
@@ -77,7 +84,7 @@ cmd.action(() => {
77
84
  const roots = { projectRoot: cwd };
78
85
  const manager = new IntegrationManager(roots);
79
86
  for (const kind of ['agents', 'skills']) {
80
- const plans = buildPlans(kind, { targets: ['codex'], scope: 'project' });
87
+ const plans = buildPlans(kind, { targets: ['codex'], scope: 'project', identity: identityConfig });
81
88
  const statuses = manager.inspect(plans);
82
89
  const existing = plans.filter((_, index) => statuses[index].state !== 'not-installed');
83
90
  manager.update(existing);
@@ -15,10 +15,21 @@
15
15
  "projectType_fullstack": "Full-stack (frontend + backend)",
16
16
  "projectType_frontend": "Frontend only",
17
17
  "projectType_backend": "Backend only",
18
- "projectType_governance": "Governance only (no build stack)"
18
+ "projectType_governance": "Governance only (no build stack)",
19
+ "identityPreset": "How would you like to name your agents?",
20
+ "identityCustomName": "Display name for the agent",
21
+ "identityNickname": "What should we call you? (optional)"
19
22
  },
20
23
  "success": "✓ trackfw initialized — run 'trackfw status' to see your governance state."
21
24
  },
25
+ "identity": {
26
+ "inUse": "identity: {{count}} custom agent(s)",
27
+ "wizard": {
28
+ "confirmHeader": "── Confirmation ──────────────────────────────",
29
+ "confirmQuestion": "Confirm?",
30
+ "nicknameRowLabel": "What we'll call you:"
31
+ }
32
+ },
22
33
  "adr": {
23
34
  "description": "Manage Architecture Decision Records",
24
35
  "new": {