trackfw 2.15.1 → 3.0.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 +183 -0
- package/package.json +1 -1
- package/src/commands/identity-wizard.js +258 -0
- package/src/commands/init.js +65 -4
- package/src/commands/integrations.js +133 -6
- package/src/commands/update.js +8 -1
- package/src/generators/init.js +28 -5
- 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
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// knownAgentIds retorna a lista canônica de ids de agentes conhecidos pelo
|
|
4
|
+
// trackfw, em ordem estável e determinística. Espelha
|
|
5
|
+
// internal/identity/preset.go:KnownAgentIDs.
|
|
6
|
+
function knownAgentIds() {
|
|
7
|
+
return [
|
|
8
|
+
'architect',
|
|
9
|
+
'backend',
|
|
10
|
+
'frontend',
|
|
11
|
+
'qa',
|
|
12
|
+
'infra',
|
|
13
|
+
'security',
|
|
14
|
+
'dba',
|
|
15
|
+
'ux',
|
|
16
|
+
'code-quality',
|
|
17
|
+
'data',
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// presets contém o catálogo de presets temáticos de identidade. display_name
|
|
22
|
+
// e slug são HARDCODED aqui — não derivados chamando slugify em tempo de
|
|
23
|
+
// execução — porque essa é uma decisão explícita do ADR: hardcoding evita
|
|
24
|
+
// depender do comportamento de normalização Unicode sendo idêntico entre os
|
|
25
|
+
// CLIs Go, Node.js e Python. Espelha internal/identity/preset.go:presets.
|
|
26
|
+
const PRESETS = {
|
|
27
|
+
greek: {
|
|
28
|
+
architect: { display_name: 'Zeus', slug: 'zeus' },
|
|
29
|
+
backend: { display_name: 'Apolo', slug: 'apolo' },
|
|
30
|
+
frontend: { display_name: 'Afrodite', slug: 'afrodite' },
|
|
31
|
+
qa: { display_name: 'Ártemis', slug: 'artemis' },
|
|
32
|
+
infra: { display_name: 'Ares', slug: 'ares' },
|
|
33
|
+
security: { display_name: 'Hades', slug: 'hades' },
|
|
34
|
+
dba: { display_name: 'Poseidon', slug: 'poseidon' },
|
|
35
|
+
ux: { display_name: 'Atena', slug: 'atena' },
|
|
36
|
+
'code-quality': { display_name: 'Hefesto', slug: 'hefesto' },
|
|
37
|
+
data: { display_name: 'Métis', slug: 'metis' },
|
|
38
|
+
},
|
|
39
|
+
norse: {
|
|
40
|
+
architect: { display_name: 'Odin', slug: 'odin' },
|
|
41
|
+
backend: { display_name: 'Thor', slug: 'thor' },
|
|
42
|
+
frontend: { display_name: 'Freya', slug: 'freya' },
|
|
43
|
+
qa: { display_name: 'Heimdall', slug: 'heimdall' },
|
|
44
|
+
infra: { display_name: 'Tyr', slug: 'tyr' },
|
|
45
|
+
security: { display_name: 'Vidar', slug: 'vidar' },
|
|
46
|
+
dba: { display_name: 'Njord', slug: 'njord' },
|
|
47
|
+
ux: { display_name: 'Idun', slug: 'idun' },
|
|
48
|
+
'code-quality': { display_name: 'Bragi', slug: 'bragi' },
|
|
49
|
+
data: { display_name: 'Mimir', slug: 'mimir' },
|
|
50
|
+
},
|
|
51
|
+
potter: {
|
|
52
|
+
architect: { display_name: 'Dumbledore', slug: 'dumbledore' },
|
|
53
|
+
backend: { display_name: 'Snape', slug: 'snape' },
|
|
54
|
+
frontend: { display_name: 'Luna', slug: 'luna' },
|
|
55
|
+
qa: { display_name: 'Moody', slug: 'moody' },
|
|
56
|
+
infra: { display_name: 'Hagrid', slug: 'hagrid' },
|
|
57
|
+
security: { display_name: 'Kingsley', slug: 'kingsley' },
|
|
58
|
+
dba: { display_name: 'Flitwick', slug: 'flitwick' },
|
|
59
|
+
ux: { display_name: 'Tonks', slug: 'tonks' },
|
|
60
|
+
'code-quality': { display_name: 'Hermione', slug: 'hermione' },
|
|
61
|
+
data: { display_name: 'Trelawney', slug: 'trelawney' },
|
|
62
|
+
},
|
|
63
|
+
thrones: {
|
|
64
|
+
architect: { display_name: 'Tyrion', slug: 'tyrion' },
|
|
65
|
+
backend: { display_name: 'Jon', slug: 'jon' },
|
|
66
|
+
frontend: { display_name: 'Sansa', slug: 'sansa' },
|
|
67
|
+
qa: { display_name: 'Arya', slug: 'arya' },
|
|
68
|
+
infra: { display_name: 'Brienne', slug: 'brienne' },
|
|
69
|
+
security: { display_name: 'Varys', slug: 'varys' },
|
|
70
|
+
dba: { display_name: 'Samwell', slug: 'samwell' },
|
|
71
|
+
ux: { display_name: 'Margaery', slug: 'margaery' },
|
|
72
|
+
'code-quality': { display_name: 'Stannis', slug: 'stannis' },
|
|
73
|
+
data: { display_name: 'Bran', slug: 'bran' },
|
|
74
|
+
},
|
|
75
|
+
chaves: {
|
|
76
|
+
architect: { display_name: 'Girafales', slug: 'girafales' },
|
|
77
|
+
backend: { display_name: 'Madruga', slug: 'madruga' },
|
|
78
|
+
frontend: { display_name: 'Chiquinha', slug: 'chiquinha' },
|
|
79
|
+
qa: { display_name: 'Florinda', slug: 'florinda' },
|
|
80
|
+
infra: { display_name: 'Barriga', slug: 'barriga' },
|
|
81
|
+
security: { display_name: 'Quico', slug: 'quico' },
|
|
82
|
+
dba: { display_name: 'Clotilde', slug: 'clotilde' },
|
|
83
|
+
ux: { display_name: 'Popis', slug: 'popis' },
|
|
84
|
+
'code-quality': { display_name: 'Nhonho', slug: 'nhonho' },
|
|
85
|
+
data: { display_name: 'Godinez', slug: 'godinez' },
|
|
86
|
+
},
|
|
87
|
+
pioneers: {
|
|
88
|
+
architect: { display_name: 'Turing', slug: 'turing' },
|
|
89
|
+
backend: { display_name: 'Ritchie', slug: 'ritchie' },
|
|
90
|
+
frontend: { display_name: 'Berners-Lee', slug: 'berners-lee' },
|
|
91
|
+
qa: { display_name: 'Hamilton', slug: 'hamilton' },
|
|
92
|
+
infra: { display_name: 'Torvalds', slug: 'torvalds' },
|
|
93
|
+
security: { display_name: 'Diffie', slug: 'diffie' },
|
|
94
|
+
dba: { display_name: 'Codd', slug: 'codd' },
|
|
95
|
+
ux: { display_name: 'Norman', slug: 'norman' },
|
|
96
|
+
'code-quality': { display_name: 'Knuth', slug: 'knuth' },
|
|
97
|
+
data: { display_name: 'Hopper', slug: 'hopper' },
|
|
98
|
+
},
|
|
99
|
+
starwars: {
|
|
100
|
+
architect: { display_name: 'Yoda', slug: 'yoda' },
|
|
101
|
+
backend: { display_name: 'Han', slug: 'han' },
|
|
102
|
+
frontend: { display_name: 'Leia', slug: 'leia' },
|
|
103
|
+
qa: { display_name: 'Ahsoka', slug: 'ahsoka' },
|
|
104
|
+
infra: { display_name: 'Chewbacca', slug: 'chewbacca' },
|
|
105
|
+
security: { display_name: 'Vader', slug: 'vader' },
|
|
106
|
+
dba: { display_name: 'R2-D2', slug: 'r2-d2' },
|
|
107
|
+
ux: { display_name: 'Padmé', slug: 'padme' },
|
|
108
|
+
'code-quality': { display_name: 'Obi-Wan', slug: 'obi-wan' },
|
|
109
|
+
data: { display_name: 'C-3PO', slug: 'c-3po' },
|
|
110
|
+
},
|
|
111
|
+
tolkien: {
|
|
112
|
+
architect: { display_name: 'Gandalf', slug: 'gandalf' },
|
|
113
|
+
backend: { display_name: 'Aragorn', slug: 'aragorn' },
|
|
114
|
+
frontend: { display_name: 'Arwen', slug: 'arwen' },
|
|
115
|
+
qa: { display_name: 'Legolas', slug: 'legolas' },
|
|
116
|
+
infra: { display_name: 'Gimli', slug: 'gimli' },
|
|
117
|
+
security: { display_name: 'Boromir', slug: 'boromir' },
|
|
118
|
+
dba: { display_name: 'Elrond', slug: 'elrond' },
|
|
119
|
+
ux: { display_name: 'Galadriel', slug: 'galadriel' },
|
|
120
|
+
'code-quality': { display_name: 'Faramir', slug: 'faramir' },
|
|
121
|
+
data: { display_name: 'Bilbo', slug: 'bilbo' },
|
|
122
|
+
},
|
|
123
|
+
turma: {
|
|
124
|
+
architect: { display_name: 'Franjinha', slug: 'franjinha' },
|
|
125
|
+
backend: { display_name: 'Cebolinha', slug: 'cebolinha' },
|
|
126
|
+
frontend: { display_name: 'Magali', slug: 'magali' },
|
|
127
|
+
qa: { display_name: 'Mônica', slug: 'monica' },
|
|
128
|
+
infra: { display_name: 'Cascão', slug: 'cascao' },
|
|
129
|
+
security: { display_name: 'Bidu', slug: 'bidu' },
|
|
130
|
+
dba: { display_name: 'Marocas', slug: 'marocas' },
|
|
131
|
+
ux: { display_name: 'Anjinho', slug: 'anjinho' },
|
|
132
|
+
'code-quality': { display_name: 'Titi', slug: 'titi' },
|
|
133
|
+
data: { display_name: 'Chico', slug: 'chico' },
|
|
134
|
+
},
|
|
135
|
+
egyptian: {
|
|
136
|
+
architect: { display_name: 'Thoth', slug: 'thoth' },
|
|
137
|
+
backend: { display_name: 'Rá', slug: 'ra' },
|
|
138
|
+
frontend: { display_name: 'Ísis', slug: 'isis' },
|
|
139
|
+
qa: { display_name: 'Hórus', slug: 'horus' },
|
|
140
|
+
infra: { display_name: 'Ptah', slug: 'ptah' },
|
|
141
|
+
security: { display_name: 'Anúbis', slug: 'anubis' },
|
|
142
|
+
dba: { display_name: 'Seshat', slug: 'seshat' },
|
|
143
|
+
ux: { display_name: 'Bastet', slug: 'bastet' },
|
|
144
|
+
'code-quality': { display_name: 'Maat', slug: 'maat' },
|
|
145
|
+
data: { display_name: 'Osíris', slug: 'osiris' },
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// presetOrder é a ordem canônica e estável em que os nomes de presets são
|
|
150
|
+
// listados (ex.: por presetNames e em mensagens de erro).
|
|
151
|
+
const PRESET_ORDER = ['greek', 'norse', 'potter', 'thrones', 'chaves', 'pioneers', 'starwars', 'tolkien', 'turma', 'egyptian']
|
|
152
|
+
|
|
153
|
+
// preset retorna a Config de identidade para o nome de preset temático dado.
|
|
154
|
+
// Se name não for um preset conhecido, lança erro listando os nomes válidos.
|
|
155
|
+
//
|
|
156
|
+
// A Config retornada é sempre uma cópia: mutá-la não afeta a tabela de
|
|
157
|
+
// presets no nível do módulo nem qualquer outra Config retornada por uma
|
|
158
|
+
// chamada anterior a preset().
|
|
159
|
+
function preset(name) {
|
|
160
|
+
const agents = PRESETS[name]
|
|
161
|
+
if (!agents) {
|
|
162
|
+
throw new Error(`identity: preset desconhecido "${name}" (validos: ${presetNames().join(', ')})`)
|
|
163
|
+
}
|
|
164
|
+
const copied = {}
|
|
165
|
+
for (const [id, agent] of Object.entries(agents)) {
|
|
166
|
+
copied[id] = { display_name: agent.display_name, slug: agent.slug }
|
|
167
|
+
}
|
|
168
|
+
return { schema_version: 1, agents: copied }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// presetNames retorna os nomes de todos os presets conhecidos, na ordem
|
|
172
|
+
// estável: greek, norse, potter, thrones, chaves, pioneers, starwars,
|
|
173
|
+
// tolkien, turma, egyptian.
|
|
174
|
+
function presetNames() {
|
|
175
|
+
return [...PRESET_ORDER]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Verificação em tempo de carga do módulo: presetOrder e PRESETS devem estar
|
|
179
|
+
// em sincronia — guarda defensiva contra drift silencioso entre o mapa e a
|
|
180
|
+
// ordem documentada (espelha o init() de internal/identity/preset.go).
|
|
181
|
+
;(function assertPresetOrderInSync() {
|
|
182
|
+
if (PRESET_ORDER.length !== Object.keys(PRESETS).length) {
|
|
183
|
+
throw new Error('identity: presetOrder e PRESETS fora de sincronia')
|
|
184
|
+
}
|
|
185
|
+
const seen = new Set()
|
|
186
|
+
for (const name of PRESET_ORDER) {
|
|
187
|
+
if (!PRESETS[name]) {
|
|
188
|
+
throw new Error(`identity: preset "${name}" listado em presetOrder mas ausente de PRESETS`)
|
|
189
|
+
}
|
|
190
|
+
seen.add(name)
|
|
191
|
+
}
|
|
192
|
+
if (seen.size !== PRESET_ORDER.length) {
|
|
193
|
+
throw new Error('identity: presetOrder contem nomes duplicados')
|
|
194
|
+
}
|
|
195
|
+
})()
|
|
196
|
+
|
|
197
|
+
module.exports = { knownAgentIds, preset, presetNames }
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// maxSlugLength é o tamanho máximo aceito para um slug gerado.
|
|
4
|
+
const MAX_SLUG_LENGTH = 40
|
|
5
|
+
|
|
6
|
+
// collapseDashes colapsa sequências de '-' em um único '-'.
|
|
7
|
+
function collapseDashes(value) {
|
|
8
|
+
let out = ''
|
|
9
|
+
let previousDash = false
|
|
10
|
+
for (const char of value) {
|
|
11
|
+
if (char === '-') {
|
|
12
|
+
if (previousDash) continue
|
|
13
|
+
previousDash = true
|
|
14
|
+
} else {
|
|
15
|
+
previousDash = false
|
|
16
|
+
}
|
|
17
|
+
out += char
|
|
18
|
+
}
|
|
19
|
+
return out
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// slugify converte input em um slug normalizado que casa com
|
|
23
|
+
// ^[a-z0-9]+(-[a-z0-9]+)*$.
|
|
24
|
+
//
|
|
25
|
+
// Passos, na ordem exata (espelham internal/identity/slug.go):
|
|
26
|
+
// 1. Normalização Unicode NFD + remoção de marcas diacríticas (\p{Mn}).
|
|
27
|
+
// 2. Lowercase.
|
|
28
|
+
// 3. Espaços (incluindo sequências) e underscores viram '-'.
|
|
29
|
+
// 4. Qualquer caractere fora de [a-z0-9-] é descartado.
|
|
30
|
+
// 5. Sequências de '-' colapsam em um único '-'.
|
|
31
|
+
// 6. '-' nas bordas (início/fim) são removidos.
|
|
32
|
+
// 7. Resultado vazio é erro.
|
|
33
|
+
// 8. Resultado com mais de 40 caracteres é erro.
|
|
34
|
+
//
|
|
35
|
+
// slugify nunca "corrige" silenciosamente uma entrada degenerada — sempre
|
|
36
|
+
// lança erro em vez de adivinhar.
|
|
37
|
+
function slugify(input) {
|
|
38
|
+
const folded = String(input).normalize('NFD').replace(/\p{Mn}/gu, '')
|
|
39
|
+
const lowered = folded.toLowerCase()
|
|
40
|
+
const replaced = lowered.replace(/[ _]/g, '-')
|
|
41
|
+
|
|
42
|
+
let filtered = ''
|
|
43
|
+
for (const char of replaced) {
|
|
44
|
+
if ((char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char === '-') {
|
|
45
|
+
filtered += char
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const collapsed = collapseDashes(filtered)
|
|
50
|
+
const trimmed = collapsed.replace(/^-+/, '').replace(/-+$/, '')
|
|
51
|
+
|
|
52
|
+
if (trimmed === '') {
|
|
53
|
+
throw new Error(`identity: slug vazio para "${input}"`)
|
|
54
|
+
}
|
|
55
|
+
if (trimmed.length > MAX_SLUG_LENGTH) {
|
|
56
|
+
throw new Error(`identity: slug "${trimmed}" excede o tamanho maximo de ${MAX_SLUG_LENGTH} caracteres (tem ${trimmed.length})`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return trimmed
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { slugify, MAX_SLUG_LENGTH }
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const os = require('node:os')
|
|
4
|
+
|
|
3
5
|
const { catalog, items, target, surfaceFor, readAsset } = require('./catalog')
|
|
4
6
|
const { render } = require('./render')
|
|
5
7
|
const { IntegrationManager } = require('./manager')
|
|
6
8
|
const { legacyHashes } = require('./legacy')
|
|
9
|
+
const identityStore = require('../identity')
|
|
7
10
|
|
|
8
11
|
function parseSurfaces(values = []) {
|
|
9
12
|
const result = {}
|
|
@@ -40,6 +43,7 @@ function selections(kind, options = {}) {
|
|
|
40
43
|
|
|
41
44
|
function buildPlans(kind, options = {}) {
|
|
42
45
|
const selected = selections(kind, options)
|
|
46
|
+
const identityConfig = options.identity || { agents: {} }
|
|
43
47
|
const plans = []
|
|
44
48
|
for (const { target: targetEntry, surface } of selected.targets) {
|
|
45
49
|
const capability = surface.capabilities[kind]
|
|
@@ -50,7 +54,7 @@ function buildPlans(kind, options = {}) {
|
|
|
50
54
|
for (const item of selected.itemEntries) {
|
|
51
55
|
for (const installPath of paths) {
|
|
52
56
|
const destination = installPath.path.replace('{{id}}', item.id)
|
|
53
|
-
const content = render({ target: targetEntry.id, kind, item, content: readAsset(item), capability, destination })
|
|
57
|
+
const content = render({ target: targetEntry.id, kind, item, content: readAsset(item), capability, destination, identity: identityConfig })
|
|
54
58
|
const claim = { target: targetEntry.id, surface: surface.id, scope, kind, item: item.id }
|
|
55
59
|
plans.push({
|
|
56
60
|
claim,
|
|
@@ -89,7 +93,13 @@ function result(kind, plans, statuses) {
|
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
function execute(kind, operation, options = {}, roots = {}) {
|
|
92
|
-
|
|
96
|
+
// A identidade é resolvida do disco antes de buildPlans — pular esta etapa
|
|
97
|
+
// reverteria silenciosamente os nomes customizados dos agentes para os
|
|
98
|
+
// defaults neutros (espelha internal/commands/integrations_flags.go:
|
|
99
|
+
// executeIntegrationMutation/executeIntegrationList).
|
|
100
|
+
const homeRoot = roots.homeRoot || os.homedir()
|
|
101
|
+
const identityConfig = options.identity !== undefined ? options.identity : identityStore.load(homeRoot)
|
|
102
|
+
const plans = buildPlans(kind, { ...options, identity: identityConfig })
|
|
93
103
|
const manager = new IntegrationManager(roots)
|
|
94
104
|
let statuses
|
|
95
105
|
if (operation === 'list') statuses = manager.inspect(plans)
|
|
@@ -5,6 +5,8 @@ const fs = require('node:fs')
|
|
|
5
5
|
const os = require('node:os')
|
|
6
6
|
const path = require('node:path')
|
|
7
7
|
|
|
8
|
+
const { frontmatterName } = require('./render')
|
|
9
|
+
|
|
8
10
|
const SCHEMA_VERSION = 1
|
|
9
11
|
const sha256 = content => crypto.createHash('sha256').update(content).digest('hex')
|
|
10
12
|
const claimKey = claim => [claim.target, claim.surface, claim.scope, claim.kind, claim.item].join('\u0000')
|
|
@@ -145,6 +147,7 @@ class IntegrationManager {
|
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
preflight(operation, { plan, file }, manifest, force) {
|
|
150
|
+
if (operation !== 'uninstall') this.detectNameCollision(plan, file, force)
|
|
148
151
|
const status = this.inspectResolved(plan, file, manifest)
|
|
149
152
|
const record = manifest.artifacts[file]
|
|
150
153
|
const owned = Boolean(record && record.claims.some(claim => claimKey(claim) === claimKey(plan.claim)))
|
|
@@ -159,6 +162,53 @@ class IntegrationManager {
|
|
|
159
162
|
}
|
|
160
163
|
}
|
|
161
164
|
|
|
165
|
+
// detectNameCollision protege contra dois artefatos de agente gerenciados
|
|
166
|
+
// distintos declarando o mesmo "name" de frontmatter dentro do mesmo
|
|
167
|
+
// diretório de destino (ADR ADR-2026-07-25-identidade-personalizavel-de-
|
|
168
|
+
// agentes, seção D4). Isso importa porque, com identidades customizáveis,
|
|
169
|
+
// dois item.id diferentes poderiam resolver para o mesmo nome renderizado
|
|
170
|
+
// (ex.: dois agentes ambos apontados para o slug "zeus"), e algumas
|
|
171
|
+
// surfaces usam esse campo name para descoberta de agentes.
|
|
172
|
+
//
|
|
173
|
+
// Limitação: a varredura só inspeciona irmãos ".md", porque é o único
|
|
174
|
+
// formato onde temos uma forma barata e sem dependências (frontmatterName)
|
|
175
|
+
// de ler de volta o name declarado a partir de bytes já renderizados.
|
|
176
|
+
// Artefatos JSON (cli-agent-json/agent-json) e TOML (custom-agent-toml)
|
|
177
|
+
// não são varridos para colisões. Espelha
|
|
178
|
+
// internal/integrations/manager.go:detectNameCollision.
|
|
179
|
+
detectNameCollision(plan, file, force) {
|
|
180
|
+
if (plan.claim.kind !== 'agents') return
|
|
181
|
+
if (path.extname(file) !== '.md') return
|
|
182
|
+
const desiredName = frontmatterName(plan.content)
|
|
183
|
+
if (!desiredName) return
|
|
184
|
+
const directory = path.dirname(file)
|
|
185
|
+
let entries
|
|
186
|
+
try {
|
|
187
|
+
entries = fs.readdirSync(directory, { withFileTypes: true })
|
|
188
|
+
} catch (err) {
|
|
189
|
+
if (err.code === 'ENOENT') return
|
|
190
|
+
throw new Error(`scan ${directory} for name collisions: ${err.message}`)
|
|
191
|
+
}
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
if (entry.isDirectory() || path.extname(entry.name) !== '.md') continue
|
|
194
|
+
const candidate = path.join(directory, entry.name)
|
|
195
|
+
if (candidate === file) continue
|
|
196
|
+
let data
|
|
197
|
+
try {
|
|
198
|
+
data = fs.readFileSync(candidate)
|
|
199
|
+
} catch {
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
const candidateName = frontmatterName(data)
|
|
203
|
+
if (!candidateName || candidateName !== desiredName) continue
|
|
204
|
+
if (force) {
|
|
205
|
+
process.stderr.write(`aviso: ${candidate} declara o mesmo name ${desiredName} que ${file}; prosseguindo por --force\n`)
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
throw new Error(`artifact ${file} declares name ${desiredName} which collides with existing file ${candidate}`)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
162
212
|
inspectResolved(plan, file, manifest) {
|
|
163
213
|
const record = manifest.artifacts[file]
|
|
164
214
|
const managed = Boolean(record && record.claims.some(claim => claimKey(claim) === claimKey(plan.claim)))
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { lookup: lookupIdentity, agentName } = require('../identity')
|
|
4
|
+
|
|
3
5
|
// Mapa de model: nomes canônicos do catálogo → tier aceito pelo Antigravity CLI (agy)
|
|
4
6
|
const MODEL_MAP = { opus: 'pro', sonnet: 'flash' }
|
|
5
7
|
// Valores que já são tiers válidos do agy e devem passar sem transformação
|
|
@@ -65,38 +67,171 @@ function resolveModel(model) {
|
|
|
65
67
|
return MODEL_MAP[model] || ''
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
// toolsFor retorna SET_ARCH para
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
// toolsFor retorna SET_ARCH para o agente canônico "architect" (item.id do
|
|
71
|
+
// catálogo, não o nome renderizado — que pode ser customizado pela
|
|
72
|
+
// identidade), SET_IMPL para os demais. IDs proibidos (edit_file, read_file,
|
|
73
|
+
// find, view_code_item, view_file_outline, call_mcp_tool) nunca fazem parte
|
|
74
|
+
// de nenhum dos conjuntos.
|
|
75
|
+
function toolsFor(itemId) {
|
|
76
|
+
return itemId === 'architect' ? SET_ARCH : SET_IMPL
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// greetingLine monta a primeira linha injetada no corpo do agente quando há
|
|
80
|
+
// identidade configurada. Sem apelido configurado, apenas o display name do
|
|
81
|
+
// agente é mencionado. Espelha internal/integrations/render.go:greetingLine.
|
|
82
|
+
function greetingLine(displayName, nickname) {
|
|
83
|
+
if (!nickname) return `Você é ${displayName}.`
|
|
84
|
+
return `Você é ${displayName}. Trate o usuário como ${nickname}.`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// insertBodyPrefix insere prefix como a nova primeira linha da seção de
|
|
88
|
+
// corpo de um markdown cru (frontmatter + corpo), seguida de linha em
|
|
89
|
+
// branco. Se source não tiver frontmatter reconhecível, prefix é inserido no
|
|
90
|
+
// topo. Reusa a mesma detecção de fronteira do frontmatter usada por
|
|
91
|
+
// markdownParts, para que Rota A e Rota B concordem sobre onde o corpo
|
|
92
|
+
// começa. Espelha internal/integrations/render.go:insertBodyPrefix.
|
|
93
|
+
function insertBodyPrefix(source, prefix) {
|
|
94
|
+
const trimmed = String(source).trim()
|
|
95
|
+
if (!prefix) return trimmed
|
|
96
|
+
if (!trimmed.startsWith('---\n')) return `${prefix}\n\n${trimmed}`
|
|
97
|
+
const end = trimmed.indexOf('\n---', 4)
|
|
98
|
+
if (end < 0) return `${prefix}\n\n${trimmed}`
|
|
99
|
+
const insertAt = end + 4
|
|
100
|
+
const head = trimmed.slice(0, insertAt)
|
|
101
|
+
const rest = trimmed.slice(insertAt).replace(/^\n+/, '')
|
|
102
|
+
if (rest === '') return `${head}\n\n${prefix}`
|
|
103
|
+
return `${head}\n\n${prefix}\n\n${rest}`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// rewriteFrontmatterFields substitui as linhas "name:" e "description:" do
|
|
107
|
+
// frontmatter de um markdown cru por name e description, preservando toda
|
|
108
|
+
// outra linha do frontmatter byte a byte (ordem, espaçamento, estilo de
|
|
109
|
+
// aspas) e deixando o corpo intocado. Usado pela Rota B (o branch default de
|
|
110
|
+
// render) para que representações que consomem o frontmatter cru —
|
|
111
|
+
// principalmente "subagent" (claude, gemini, cursor, copilot, kiro-ide,
|
|
112
|
+
// windsurf) — recebam a identidade customizada. Se o frontmatter não tiver
|
|
113
|
+
// "name:" ou "description:", a chave é simplesmente deixada ausente — esta
|
|
114
|
+
// função nunca inventa uma chave que não existia. Se source não tiver
|
|
115
|
+
// frontmatter reconhecível, source é retornado sem alteração (trimado).
|
|
116
|
+
// Espelha internal/integrations/render.go:rewriteFrontmatterFields.
|
|
117
|
+
function rewriteFrontmatterFields(source, name, description) {
|
|
118
|
+
const trimmed = String(source).trim()
|
|
119
|
+
if (!trimmed.startsWith('---\n')) return trimmed
|
|
120
|
+
const end = trimmed.indexOf('\n---', 4)
|
|
121
|
+
if (end < 0) return trimmed
|
|
122
|
+
const frontmatter = trimmed.slice(4, end)
|
|
123
|
+
const rest = trimmed.slice(end) // começa com "\n---", seguido do corpo
|
|
124
|
+
|
|
125
|
+
const lines = frontmatter.split('\n').map(line => {
|
|
126
|
+
const separator = line.indexOf(':')
|
|
127
|
+
if (separator < 0) return line
|
|
128
|
+
const key = line.slice(0, separator).trim()
|
|
129
|
+
let replacement
|
|
130
|
+
if (key === 'name') replacement = name
|
|
131
|
+
else if (key === 'description') replacement = description
|
|
132
|
+
else return line
|
|
133
|
+
const value = line.slice(separator + 1).trim()
|
|
134
|
+
const quoted = value.length >= 2 && value.startsWith('"') && value.endsWith('"')
|
|
135
|
+
return quoted ? `${key}: "${replacement}"` : `${key}: ${replacement}`
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
return `---\n${lines.join('\n')}${rest}`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// frontmatterName extrai apenas o campo "name" de um frontmatter YAML
|
|
142
|
+
// delimitado por ---, sem os valores default aplicados por markdownParts.
|
|
143
|
+
// Retorna undefined quando o arquivo não tem frontmatter reconhecível ou não
|
|
144
|
+
// declara "name". Usado pela detecção de colisão em manager.js. Espelha
|
|
145
|
+
// internal/integrations/render.go:frontmatterName.
|
|
146
|
+
function frontmatterName(content) {
|
|
147
|
+
const text = String(content).trim()
|
|
148
|
+
if (!text.startsWith('---\n')) return undefined
|
|
149
|
+
const end = text.indexOf('\n---', 4)
|
|
150
|
+
if (end < 0) return undefined
|
|
151
|
+
const frontmatter = text.slice(4, end)
|
|
152
|
+
for (const line of frontmatter.split('\n')) {
|
|
153
|
+
const separator = line.indexOf(':')
|
|
154
|
+
if (separator < 0) continue
|
|
155
|
+
const key = line.slice(0, separator).trim()
|
|
156
|
+
if (key !== 'name') continue
|
|
157
|
+
let value = line.slice(separator + 1).trim()
|
|
158
|
+
value = value.replace(/^"+/, '').replace(/"+$/, '')
|
|
159
|
+
if (value === '') return undefined
|
|
160
|
+
return value
|
|
161
|
+
}
|
|
162
|
+
return undefined
|
|
73
163
|
}
|
|
74
164
|
|
|
75
|
-
|
|
165
|
+
// render converte um item canônico do catálogo para a representação nativa
|
|
166
|
+
// declarada por uma surface alvo. Quando identity carrega uma identidade
|
|
167
|
+
// customizada para item.id, o name/description/body renderizados são
|
|
168
|
+
// personalizados (ADR ADR-2026-07-25-identidade-personalizavel-de-agentes,
|
|
169
|
+
// seções D1/D2/D6).
|
|
170
|
+
//
|
|
171
|
+
// render tem duas rotas de saída:
|
|
172
|
+
// - Rota A: "custom-agent-toml", "cli-agent-json", "agent-json" e
|
|
173
|
+
// "agent-directory" trabalham a partir de name/description/body já
|
|
174
|
+
// separados do frontmatter por markdownParts.
|
|
175
|
+
// - Rota B: o branch default, usado pela representação "subagent" (claude,
|
|
176
|
+
// gemini, cursor, copilot, kiro-ide, windsurf), retorna o source cru
|
|
177
|
+
// normalizado com o frontmatter ainda anexado. Quando há identidade
|
|
178
|
+
// configurada, suas linhas "name:"/"description:" são reescritas no
|
|
179
|
+
// lugar (ver rewriteFrontmatterFields) — necessário porque a seleção de
|
|
180
|
+
// subagent do Claude Code lê apenas o frontmatter, nunca o corpo.
|
|
181
|
+
//
|
|
182
|
+
// Ambas as rotas devem receber a injeção de identidade. Quando não há
|
|
183
|
+
// identidade configurada para item.id, name/description/body ficam
|
|
184
|
+
// exatamente como markdownParts produziu e o branch default retorna
|
|
185
|
+
// normalize(content) — a mesma expressão usada antes de existir suporte a
|
|
186
|
+
// identidade — então a saída sem identidade é garantida byte a byte
|
|
187
|
+
// inalterada por construção, não por coincidência.
|
|
188
|
+
function render({ kind, content, capability, item, identity: cfg }) {
|
|
76
189
|
if (kind === 'skills') return normalize(content)
|
|
190
|
+
|
|
191
|
+
const id = item && cfg ? lookupIdentity(cfg, item.id) : undefined
|
|
77
192
|
const parts = markdownParts(content)
|
|
193
|
+
let { name, description, body } = parts
|
|
194
|
+
let greeting = ''
|
|
195
|
+
if (id) {
|
|
196
|
+
greeting = greetingLine(id.display_name, (cfg && cfg.user_nickname) || '')
|
|
197
|
+
name = agentName(id.slug)
|
|
198
|
+
description = `${id.display_name} — ${description}`
|
|
199
|
+
body = `${greeting}\n\n${body}`
|
|
200
|
+
}
|
|
201
|
+
|
|
78
202
|
if (capability.representation === 'custom-agent-toml') {
|
|
79
|
-
return `name = ${JSON.stringify(
|
|
203
|
+
return `name = ${JSON.stringify(name.replaceAll('-', '_'))}\ndescription = ${JSON.stringify(description)}\ndeveloper_instructions = ${JSON.stringify(body)}\n`
|
|
80
204
|
}
|
|
81
205
|
if (capability.representation === 'cli-agent-json' || capability.representation === 'agent-json') {
|
|
82
|
-
|
|
206
|
+
// Ordem alfabética das chaves (description, name, prompt) é obrigatória:
|
|
207
|
+
// Go serializa via json.MarshalIndent(map[string]string{...}), cujo encoder
|
|
208
|
+
// ordena as chaves alfabeticamente, e Python usa sort_keys. JSON.stringify
|
|
209
|
+
// preserva a ordem de inserção, então a ordem é fixada aqui à mão. Sem
|
|
210
|
+
// isso, instalar por um CLI e listar por outro reporta falso `modified`
|
|
211
|
+
// (o manifest indexa artefatos por sha256 do conteúdo).
|
|
212
|
+
return `${JSON.stringify({ description, name, prompt: body }, null, 2)}\n`
|
|
83
213
|
}
|
|
84
214
|
if (capability.representation === 'agent-directory') {
|
|
85
215
|
// Reconstrói o frontmatter para o Antigravity CLI (agy):
|
|
86
216
|
// - mapeia model canônico para o tier aceito (opus→pro, sonnet→flash)
|
|
87
|
-
// - injeta tools: SET_IMPL ou SET_ARCH dependendo do
|
|
217
|
+
// - injeta tools: SET_IMPL ou SET_ARCH dependendo do item.id (não do
|
|
218
|
+
// nome renderizado, que pode ser customizado pela identidade)
|
|
88
219
|
// - omite campos não suportados pelo agy
|
|
89
220
|
const mappedModel = resolveModel(parts.model)
|
|
90
|
-
const tools = toolsFor(
|
|
91
|
-
let out = `---\nname: ${
|
|
221
|
+
const tools = toolsFor(item && item.id)
|
|
222
|
+
let out = `---\nname: ${name}\ndescription: ${description}\n`
|
|
92
223
|
if (mappedModel) out += `model: ${mappedModel}\n`
|
|
93
224
|
out += 'tools:\n'
|
|
94
225
|
for (const tool of tools) out += ` - ${tool}\n`
|
|
95
226
|
out += '---\n'
|
|
96
|
-
if (
|
|
227
|
+
if (body) out += `${body}\n`
|
|
97
228
|
return out
|
|
98
229
|
}
|
|
99
|
-
|
|
230
|
+
|
|
231
|
+
if (!id) return normalize(content)
|
|
232
|
+
const withBody = insertBodyPrefix(content, greeting)
|
|
233
|
+
const withFrontmatter = rewriteFrontmatterFields(withBody, name, description)
|
|
234
|
+
return normalize(withFrontmatter)
|
|
100
235
|
}
|
|
101
236
|
|
|
102
|
-
module.exports = { render, markdownParts }
|
|
237
|
+
module.exports = { render, markdownParts, frontmatterName, greetingLine, insertBodyPrefix, rewriteFrontmatterFields }
|