trackfw 2.15.1 → 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 +174 -0
- package/package.json +1 -1
- package/src/commands/identity-wizard.js +258 -0
- package/src/commands/init.js +54 -1
- package/src/commands/integrations.js +58 -2
- package/src/commands/update.js +8 -1
- package/src/i18n/locales/en-US.json +12 -1
- package/src/i18n/locales/es-ES.json +12 -1
- package/src/i18n/locales/pt-BR.json +12 -1
- package/src/identity/config.js +134 -0
- package/src/identity/index.js +19 -0
- package/src/identity/preset.js +197 -0
- package/src/identity/slug.js +62 -0
- package/src/integrations/index.js +12 -2
- package/src/integrations/manager.js +50 -0
- package/src/integrations/render.js +149 -14
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.
|
|
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
|
+
}
|
package/src/commands/init.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
})
|
package/src/commands/update.js
CHANGED
|
@@ -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": {
|