sinapse-ai 1.14.0 → 1.15.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.
@@ -75,9 +75,16 @@ function buildStatusLine(data) {
75
75
 
76
76
  // 5 & 6. Agent & Squad (from session cache + API fallback)
77
77
  const cache = readSessionCache();
78
+ const badges = loadBadges();
79
+ const agentBadge = resolveBadge(cache, badges);
78
80
  const activeAgent = cache.agent || data.agent?.name || '';
79
81
  const activeSquad = cache.squad || '';
80
- if (activeAgent) parts.push(`${cyan}\ud83e\udd16 ${activeAgent}${reset}`);
82
+ if (agentBadge) {
83
+ // Public badge: {emoji} {Name} (e.g. "\ud83d\udcbb Pixel") instead of the raw id.
84
+ parts.push(`${cyan}${agentBadge.emoji} ${agentBadge.name}${reset}`);
85
+ } else if (activeAgent) {
86
+ parts.push(`${cyan}\ud83e\udd16 ${activeAgent}${reset}`);
87
+ }
81
88
  if (activeSquad) parts.push(`${orange}\ud83c\udfaf ${activeSquad}${reset}`);
82
89
 
83
90
  // 6.5 Orchestration \u2014 Imperator + active specialists (TTL 6h)
@@ -90,7 +97,7 @@ function buildStatusLine(data) {
90
97
  }
91
98
  const ativos = (cache.specialists || []).filter(s => nowSec - s.ts < TTL);
92
99
  if (ativos.length > 0) {
93
- parts.push(`${cyan}\ud83e\udded ${ativos.length} especialistas: ${ativos.map(s => s.id).join(', ')}${reset}`);
100
+ parts.push(`${cyan}\ud83e\udded ${ativos.length} especialistas: ${ativos.map(s => (badges && badges[s.id] ? badges[s.id].name : s.id)).join(', ')}${reset}`);
94
101
  }
95
102
 
96
103
  // 7. Project:Branch
@@ -169,6 +176,33 @@ function readSessionCache() {
169
176
  return { agent: '', squad: '', role: null, imperator: null, specialists: [] };
170
177
  }
171
178
 
179
+ function loadBadges() {
180
+ // id -> {emoji, area, name}. Keys are bare ids or "squad/id"; index by the last
181
+ // segment to match what the session-cache stores. Best-effort — null on absence.
182
+ try {
183
+ const p = path.join(os.homedir(), '.claude', 'agent-badges.json');
184
+ const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
185
+ if (!raw || typeof raw !== 'object') return null;
186
+ const byId = {};
187
+ for (const key of Object.keys(raw)) {
188
+ const v = raw[key];
189
+ if (v && v.emoji && v.name) byId[key.split('/').pop()] = v;
190
+ }
191
+ return Object.keys(byId).length ? byId : null;
192
+ } catch {
193
+ return null;
194
+ }
195
+ }
196
+
197
+ function resolveBadge(cache, badges) {
198
+ if (!badges) return null;
199
+ if (cache.role === 'orqx' && cache.agent) {
200
+ return badges[`${cache.agent}-orqx`] || badges[cache.agent] || null;
201
+ }
202
+ if (cache.agent) return badges[cache.agent] || null;
203
+ return null;
204
+ }
205
+
172
206
  function simpleHash(str) {
173
207
  let hash = 0;
174
208
  for (let i = 0; i < str.length; i++) {
@@ -33,6 +33,8 @@ const os = require('os');
33
33
 
34
34
  const MASTERS = new Set(['sinapse', 'snps', 'sinapse-orqx', 'snps-orqx']);
35
35
  const SPECIALIST_CAP = 6;
36
+ const BADGE_TTL = 21600; // 6h — mirrors the statusline freshness window.
37
+ const IMPERATOR_KEY = 'snps-orqx'; // badge key for the master / Imperator voice.
36
38
 
37
39
  // Identical to track-agent.sh and statusline-script.js.
38
40
  function simpleHash(str) {
@@ -132,6 +134,111 @@ function applyName(cache, name, ts) {
132
134
  }
133
135
  }
134
136
 
137
+ // ── Badge injection ──────────────────────────────────────────────────────────
138
+ // The detector also surfaces the speaking agent's public badge to the model via
139
+ // the UserPromptSubmit `additionalContext` channel, so every response can open
140
+ // with the right seal. Pure best-effort: any failure → no injection, never block.
141
+
142
+ // Load ~/.claude/agent-badges.json and index by last path segment. Keys are
143
+ // either bare ids ("developer") or "squad/id" ("squad-brand/brand-orqx"); the
144
+ // session-cache only ever stores the bare id, so we index on the last segment.
145
+ function loadBadges() {
146
+ try {
147
+ const p = path.join(os.homedir(), '.claude', 'agent-badges.json');
148
+ const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
149
+ if (!raw || typeof raw !== 'object') return null;
150
+ const byId = {};
151
+ for (const key of Object.keys(raw)) {
152
+ const v = raw[key];
153
+ if (!v || typeof v !== 'object' || !v.emoji || !v.name) continue;
154
+ byId[key.split('/').pop()] = v;
155
+ }
156
+ return Object.keys(byId).length ? byId : null;
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
161
+
162
+ function fullBadge(b) {
163
+ return `▌ ${b.emoji} · SNPS · ${b.area || ''} · ${b.name}`.replace(/ · · /, ' · ');
164
+ }
165
+ function shortBadge(b) {
166
+ return `▌ ${b.emoji} ${b.name}`;
167
+ }
168
+
169
+ // Resolve the primary speaking voice. cache.role reflects the LAST classified
170
+ // mention, so it doubles as the recency signal (imperator vs orqx vs specialist).
171
+ function resolvePrimary(cache, byId) {
172
+ if (cache.role === 'imperator') return byId[IMPERATOR_KEY] || null;
173
+ if (cache.role === 'orqx' && cache.agent) {
174
+ return byId[`${cache.agent}-orqx`] || byId[cache.agent] || null;
175
+ }
176
+ if (cache.agent) return byId[cache.agent] || null;
177
+ if (cache.imperator && cache.imperator.active) return byId[IMPERATOR_KEY] || null;
178
+ return null;
179
+ }
180
+
181
+ // Build the additionalContext string, or null when no SINAPSE voice is active.
182
+ function buildBadgeContext(cache) {
183
+ const byId = loadBadges();
184
+ if (!byId) return null;
185
+
186
+ const now = nowSec();
187
+ const primary = resolvePrimary(cache, byId);
188
+
189
+ // The specialist roster is only meaningful while the Imperator is coordinating
190
+ // a multi-agent orchestration; a plain specialist↔specialist switch shows just
191
+ // the active voice (no roster noise).
192
+ const imperatorActive =
193
+ cache.imperator && cache.imperator.active && now - (cache.imperator.ts || 0) < BADGE_TTL;
194
+ const specs = imperatorActive
195
+ ? (cache.specialists || [])
196
+ .filter((s) => s && s.id && s.id !== cache.agent && now - (s.ts || 0) < BADGE_TTL)
197
+ .map((s) => byId[s.id])
198
+ .filter(Boolean)
199
+ : [];
200
+
201
+ if (!primary && specs.length === 0) return null;
202
+
203
+ const lines = [];
204
+ lines.push('<sinapse-selo>');
205
+ lines.push('Identidade pública dos agentes SINAPSE. Aplique o protocolo de selo na sua resposta.');
206
+ lines.push('');
207
+ if (primary) {
208
+ lines.push(`Voz ativa → cheio: ${fullBadge(primary)} curto: ${shortBadge(primary)}`);
209
+ }
210
+ if (specs.length) {
211
+ lines.push('Especialistas em jogo nesta orquestração:');
212
+ for (const b of specs) lines.push(` • ${fullBadge(b)} (curto: ${shortBadge(b)})`);
213
+ }
214
+ lines.push('');
215
+ lines.push('Protocolo (NON-NEGOTIABLE):');
216
+ lines.push('1. Abra a resposta com o selo do agente que está falando — 1ª linha, SÓ na troca de voz. Mesma voz em blocos seguidos não repete o selo.');
217
+ lines.push('2. Estreia da voz nesta conversa = selo CHEIO. Volta de uma voz já apresentada = selo CURTO.');
218
+ lines.push('3. Orquestrador (Imperator / -orqx): selo → apresenta o PLANO → ESPERA o "pode ir" do usuário → executa os handoffs (cada agente entra com seu selo) → consolida no fim.');
219
+ lines.push('4. O selo (emoji · SNPS · área · nome) É a identidade pública — nunca exponha @ids técnicos nem estrutura interna ao usuário.');
220
+ lines.push('</sinapse-selo>');
221
+
222
+ return lines.join('\n');
223
+ }
224
+
225
+ function emitBadge(cache) {
226
+ try {
227
+ const ctx = buildBadgeContext(cache);
228
+ if (!ctx) return;
229
+ process.stdout.write(
230
+ JSON.stringify({
231
+ hookSpecificOutput: {
232
+ hookEventName: 'UserPromptSubmit',
233
+ additionalContext: ctx,
234
+ },
235
+ })
236
+ );
237
+ } catch {
238
+ /* fail-soft: a badge is never worth blocking a prompt. */
239
+ }
240
+ }
241
+
135
242
  function main() {
136
243
  const input = readStdin();
137
244
  if (!input) process.exit(0);
@@ -197,17 +304,20 @@ function main() {
197
304
  }
198
305
  }
199
306
 
200
- if (!changed) process.exit(0);
201
-
202
- cache.version = 2;
203
- cache.updated = new Date().toISOString();
204
-
205
- try {
206
- fs.writeFileSync(cacheFile, JSON.stringify(cache));
207
- } catch {
208
- process.exit(0);
307
+ if (changed) {
308
+ cache.version = 2;
309
+ cache.updated = new Date().toISOString();
310
+ try {
311
+ fs.writeFileSync(cacheFile, JSON.stringify(cache));
312
+ } catch {
313
+ /* cache write is best-effort — still try to inject the badge below. */
314
+ }
209
315
  }
210
316
 
317
+ // Inject the active agent's badge even on continuation turns (no new mention
318
+ // keeps the same voice active). Fail-soft and silent when no voice is active.
319
+ emitBadge(cache);
320
+
211
321
  process.exit(0);
212
322
  }
213
323
 
package/CHANGELOG.md CHANGED
@@ -1,12 +1,9 @@
1
- ## [1.14.0](https://github.com/caioimori/sinapse-ai/compare/1.13.0...1.14.0) (2026-06-24)
1
+ ## [1.15.0](https://github.com/caioimori/sinapse-ai/compare/1.14.0...1.15.0) (2026-06-24)
2
2
 
3
3
  ### Features
4
4
 
5
- * **agents:** potencializa 170 agentes com a base de engenharia de software ([#278](https://github.com/caioimori/sinapse-ai/issues/278)) ([86a1578](https://github.com/caioimori/sinapse-ai/commit/86a15781a704ee809ed444224b0aefbb02d57f06)), closes [#264](https://github.com/caioimori/sinapse-ai/issues/264)
6
-
7
- ### Maintenance
8
-
9
- * **release:** sincroniza main com a v1.13.0 publicada (package.json + CHANGELOG) ([#277](https://github.com/caioimori/sinapse-ai/issues/277)) ([0c64e27](https://github.com/caioimori/sinapse-ai/commit/0c64e273dfe31ca06c4c72c91064f2a0e87b59ad))
5
+ * **chrome-brain:** scripts nativos Node, ensure que nao mata janela boa, janela fixa + login ([e1f921b](https://github.com/caioimori/sinapse-ai/commit/e1f921b76627fe9eb2322fa81e35c31dfffe3862))
6
+ * **statusline:** selos de identidade de agente + banner de install ([a80a353](https://github.com/caioimori/sinapse-ai/commit/a80a3532eb5143d882872301aad2aadc7e9abe79))
10
7
 
11
8
  # Changelog
12
9
 
@@ -48,7 +48,7 @@ const {
48
48
 
49
49
  async function cmdInstallGlobal(opts = {}) {
50
50
  const logger = getLogger();
51
- header();
51
+ header({ force: true });
52
52
 
53
53
  // Story 10.20 — Upsert detection
54
54
  const force = Boolean(opts.force);
@@ -82,41 +82,12 @@ async function cmdInstallGlobal(opts = {}) {
82
82
  logger.always(`${DIM} Language: ${label} (from saved config; pass --reconfigure to change)${NC}`);
83
83
  }
84
84
  if (!language) {
85
+ // Approved install redesign: Portuguese is the default and is NO longer
86
+ // prompted ("Idioma: Portugues (padrao)"). The resolved language is still
87
+ // saved to settings.language below and shown in the install preview. English
88
+ // stays available for power users via a pre-set settings.language=english.
85
89
  language = 'pt';
86
- // Story 10.46 — multi-signal gate replaces the old `process.stdin.isTTY`
87
- // check that silently defaulted to `pt` in Git Bash + Windows.
88
- if (detectInteractiveMode()) {
89
- try {
90
- const inquirer = require('inquirer');
91
- const langAnswer = await inquirer.prompt([{
92
- type: 'list',
93
- name: 'language',
94
- message: 'Language / Idioma:',
95
- choices: [
96
- { name: 'Portugues', value: 'pt' },
97
- { name: 'English', value: 'en' },
98
- ],
99
- default: 'pt',
100
- }]);
101
- language = langAnswer.language;
102
- } catch {
103
- // Fallback: readline
104
- const readline = require('readline');
105
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
106
- language = await new Promise((resolve) => {
107
- logger.always(` ${CYAN}Language / Idioma:${NC}`);
108
- logger.always(` ${GREEN}1${NC}) Portugues`);
109
- logger.always(` ${GREEN}2${NC}) English`);
110
- rl.question(` ${BOLD}[1/2]:${NC} `, (answer) => {
111
- rl.close();
112
- resolve((answer || '1').trim() === '2' ? 'en' : 'pt');
113
- });
114
- });
115
- }
116
- } else {
117
- // Story 10.46 — surface the silent default so users on CI / pipes know.
118
- warnNonInteractive();
119
- }
90
+ if (!detectInteractiveMode()) warnNonInteractive();
120
91
  }
121
92
 
122
93
  // Save language to ~/.claude/settings.json
@@ -168,16 +139,23 @@ async function cmdInstallGlobal(opts = {}) {
168
139
  hookCount = fs.readdirSync(path.join(ROOT, '.claude', 'hooks')).filter(f => /\.(cjs|js|py|sh)$/.test(f)).length;
169
140
  } catch { /* hooks dir optional */ }
170
141
 
142
+ // Framed "o que será instalado" box (B&W). Rows are plain text so the width
143
+ // math stays correct — inline ANSI codes would break padEnd alignment.
144
+ const boxRows = [
145
+ `${squads.length} squads · ${agentTotal} agentes especializados`,
146
+ 'Orquestrador master — @sinapse / @snps',
147
+ ];
148
+ if (hookCount) boxRows.push(`${hookCount} hooks de proteção + regras do framework`);
149
+ boxRows.push(`Editor(es): ${llmLabel}`);
150
+ boxRows.push(`Idioma: ${langLabelMap[language] || language}`);
151
+ boxRows.push(`Modo: ${isUpsert ? 'atualizar instalação existente' : 'instalação nova'}`);
152
+ boxRows.push('Destino: ~/.sinapse (núcleo) + ~/.claude/agents (agentes)');
153
+ const boxTitle = ' o que será instalado ';
154
+ const innerW = Math.max(boxTitle.length + 1, ...boxRows.map((r) => r.length + 1)) + 1;
171
155
  logger.always('');
172
- logger.always(`${BOLD}──── O que será instalado ────${NC}`);
173
- logger.always(` ${BOLD}${squads.length}${NC} squads · ${BOLD}${agentTotal}${NC} agentes especializados`);
174
- logger.always(` Orquestrador master (Imperator) — chamável por ${CYAN}@sinapse${NC} / ${CYAN}@snps${NC}`);
175
- if (hookCount) logger.always(` ${hookCount} hooks de proteção + regras do framework`);
176
- logger.always(` Editor(es): ${BOLD}${llmLabel}${NC}`);
177
- logger.always(` Idioma: ${langLabelMap[language] || language}`);
178
- logger.always(` Modo: ${isUpsert ? 'atualizar instalação existente' : 'instalação nova'}`);
179
- logger.always(` Destino: ${DIM}~/.sinapse/${NC} (núcleo) + ${DIM}~/.claude/agents/${NC} (agentes)`);
180
- logger.always(`${BOLD}──────────────────────────────${NC}`);
156
+ logger.always(`${DIM} ┌─${boxTitle}${'─'.repeat(innerW - boxTitle.length - 1)}┐${NC}`);
157
+ for (const r of boxRows) logger.always(`${DIM} │${NC} ${r.padEnd(innerW - 1)}${DIM}│${NC}`);
158
+ logger.always(`${DIM} └${'─'.repeat(innerW)}┘${NC}`);
181
159
 
182
160
  if (detectInteractiveMode()) {
183
161
  const inquirer = require('inquirer');
package/bin/lib/header.js CHANGED
@@ -8,11 +8,15 @@ const {
8
8
  } = require('../../.sinapse-ai/core/logger');
9
9
  const { VERSION, WHITE, BOLD, DIM, NC } = require('./constants');
10
10
 
11
- function header() {
11
+ function header(opts = {}) {
12
12
  const logger = getLogger();
13
- // Story A.2 AC 7 — ASCII art only on --verbose / --debug OR first-run.
14
- // Never in --quiet or --json mode.
15
- if (!shouldShowHeader(logger)) return;
13
+ const force = !!opts.force;
14
+ // --json (machine output) and --quiet always suppress the banner.
15
+ if (!logger || logger.json || logger.threshold === 0 /* LEVELS.error */) return;
16
+ // Outside json/quiet: show on --verbose / --debug, on first run, OR when the
17
+ // caller forces it. The `install` command forces it so the brand banner is
18
+ // always visible at install time (Story A.2 AC 7 kept for non-forced paths).
19
+ if (!force && !shouldShowHeader(logger)) return;
16
20
  const W = `${WHITE}${BOLD}`;
17
21
  const lines = [
18
22
  '',
@@ -23,17 +27,17 @@ function header() {
23
27
  `${W} ███████║██║ ╚████║██║ ███████║ ██║ ██║██║${NC}`,
24
28
  `${W} ╚══════╝╚═╝ ╚═══╝╚═╝ ╚══════╝ ╚═╝ ╚═╝╚═╝${NC}`,
25
29
  '',
26
- `${DIM} Seu copiloto de inteligencia artificial${NC}`,
27
- `${DIM} v${VERSION}${NC}`,
30
+ `${DIM} Orquestracao de inteligencia artificial · v${VERSION}${NC}`,
31
+ '',
32
+ `${DIM} ┼${'─'.repeat(50)}┼${NC}`,
28
33
  '',
29
34
  ];
30
- // Write directly to stdout so the header still shows on first-run even when
31
- // the logger level is `warn` (default). `shouldShowHeader` is the single
32
- // gate that decides whether we get here at all.
35
+ // Write directly to stdout so the header still shows even when the logger
36
+ // level is `warn` (default). The gates above decide whether we get here.
33
37
  for (const line of lines) {
34
38
  try { process.stdout.write(`${line}\n`); } catch { /* ignore */ }
35
39
  }
36
- // Mark first-run done so subsequent runs stay clean unless --verbose.
40
+ // Mark first-run done so subsequent (non-forced) runs stay clean.
37
41
  markFirstRunDone();
38
42
  }
39
43
 
@@ -10,8 +10,9 @@
10
10
  *
11
11
  * What it installs:
12
12
  * - ~/.claude/statusline-script.js (renders the statusline)
13
- * - ~/.claude/hooks/track-agent.cjs (UserPromptSubmit detector — writes session-cache)
13
+ * - ~/.claude/hooks/track-agent.cjs (UserPromptSubmit detector — writes session-cache + injects the agent badge)
14
14
  * - ~/.claude/hooks/track-agent-clear.cjs (Stop hook — clears session-cache on session end)
15
+ * - ~/.claude/agent-badges.json (id -> {emoji, area, name} — read by the detector + statusline)
15
16
  * - ~/.claude/session-cache/ (per-cwd cache directory)
16
17
  * - settings.statusLine (graceful skip if already set)
17
18
  * - settings.hooks.UserPromptSubmit → track-agent.cjs (idempotent guard)
@@ -41,6 +42,7 @@ function resolveSources(sourceCoreDir) {
41
42
  script: path.join(templatesDir, 'statusline-script.js'),
42
43
  detector: path.join(templatesDir, 'track-agent.cjs'),
43
44
  clear: path.join(templatesDir, 'track-agent-clear.cjs'),
45
+ badges: path.join(templatesDir, 'agent-badges.json'),
44
46
  };
45
47
  }
46
48
 
@@ -122,6 +124,15 @@ async function setupStatusline(opts = {}) {
122
124
  result.files.push(clearTarget);
123
125
  }
124
126
 
127
+ // Agent badge map (id -> {emoji, area, name}). Read by the detector hook to
128
+ // inject the speaking agent's badge, and by the statusline to render it.
129
+ // Best-effort: a missing source never blocks the rest of the install.
130
+ if (await fse.pathExists(sources.badges)) {
131
+ const badgesTarget = path.join(claudeDir, 'agent-badges.json');
132
+ await fse.copy(sources.badges, badgesTarget);
133
+ result.files.push(badgesTarget);
134
+ }
135
+
125
136
  // Read existing global settings (back-compat with any prior config).
126
137
  let settings = {};
127
138
  try {