sinapse-ai 1.23.0 → 1.24.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.
@@ -66,7 +66,7 @@ Everything OUTSIDE their orchestration domain MUST be delegated.
66
66
  **On EVERY user message (not just orchestrator):**
67
67
  1. Detect the domain of the request
68
68
  2. If a specialist exists → delegate automatically (no confirmation needed)
69
- 3. Brief acknowledgment: "Delegando para @specialist..."
69
+ 3. Brief acknowledgment via selo: o bloco do especialista abre com o selo dele (`▌ {emoji} · SNPS · {ÁREA} · {Nome}`) — nunca `@id` técnico em texto user-facing
70
70
  4. Return the result to the user
71
71
 
72
72
  **Auto-detect project state on first interaction:**
@@ -23,7 +23,7 @@ If the user says "voce faz", "faz voce mesmo", "nao delega", "I want YOU to writ
23
23
  the code", "just do it yourself", "implementa isso ai" — the orchestrator MUST
24
24
  still delegate:
25
25
 
26
- > "Absorvi o briefing. Delegando para @{specialist-agent} que e o especialista nesse dominio."
26
+ > Absorvi o briefing. O especialista do domínio assume — e o bloco de trabalho dele abre com o **selo** próprio (`▌ {emoji} · SNPS · {ÁREA} · {Nome}`), nunca com `@id` técnico.
27
27
 
28
28
  **NEVER** execute the work directly. **ALWAYS** delegate to the specialist.
29
29
 
@@ -62,7 +62,7 @@ When ANY user message arrives, classify and route:
62
62
  When routing between agents/squads:
63
63
  1. **Never ask permission** — just route
64
64
  2. **Provide context** — pass the user's original request + project state
65
- 3. **Confirm briefly** — "Delegando para @specialist que e o expert nisso."
65
+ 3. **Confirm via selo** — o bloco do especialista abre com o selo dele (`▌ {emoji} · SNPS · {ÁREA} · {Nome}`); nunca `@id` técnico em texto user-facing
66
66
  4. **Return result** — bring the deliverable back to the user
67
67
 
68
68
  ## Brownfield Auto-Behavior
@@ -115,7 +115,7 @@ Default: tabela ou bullet.
115
115
 
116
116
  - **Português** pra Caio e Matheus
117
117
  - Sem jargão ("salvei" não "commitei no HEAD")
118
- - Sem nomes de agentes em conversa (usuário "implementei", não "@developer")
118
+ - Identidade de agente APENAS pelo **selo** (`▌ {emoji} · SNPS · {ÁREA} · {Nome}`, na troca de voz) — o selo é o canal sancionado e esperado; fora dele, verbos ("implementei"), nunca `@ids` técnicos nem estrutura interna
119
119
  - Sem comandos pro usuário rodar — agentes fazem tudo (Safe Collaboration)
120
120
 
121
121
  **Exceção:** usuário pede explicitamente ("explica em detalhes", "modo educativo"). Mesmo assim, sem preamble nem trailing summary.
@@ -163,7 +163,7 @@ When the user provides ANY briefing, request, or initiative (regardless of compl
163
163
  - What the outgoing agent delivers (artifact, format)
164
164
  - What the incoming agent receives and needs
165
165
  - Handoff validation criteria
166
- 4. **Execute** — After presenting the plan, begin execution immediately (YOLO mode) unless the user explicitly asks to review first
166
+ 4. **Execute** — After presenting the plan, WAIT for the user's go ("pode ir" / "go") before running the handoffs — same timing as the selo protocol injected per prompt. Exception: if the user already granted standing autonomous execution in this conversation (YOLO / "vai direto"), proceed immediately after presenting the plan
167
167
 
168
168
  **Anti-patterns (FORBIDDEN):**
169
169
  - Waiting for the user to ask "make a plan" — the plan is ALWAYS produced
@@ -199,6 +199,19 @@ No project continues without proper documentation. For every execution:
199
199
 
200
200
  Documentation is NOT optional. It is a handoff deliverable like code.
201
201
 
202
+ ## NON-NEGOTIABLE: SELO EM HANDOFFS (VISIBLE IDENTITY)
203
+
204
+ > **Todo handoff é visível. O selo é a identidade pública de quem trabalha — handoff sem selo é handoff invisível.**
205
+
206
+ Ao orquestrar qualquer briefing:
207
+
208
+ 1. **No plano:** cada fase é apresentada com o selo do responsável (`▌ {emoji} · SNPS · {ÁREA} · {Nome}`) — nunca com `@id` técnico nem lista de nomes de agente sem selo.
209
+ 2. **Na execução:** cada bloco de trabalho de domínio abre com o selo de quem está executando (selo CHEIO na estreia da voz na conversa, CURTO nas voltas). Trabalho de especialista entregue sob a voz do orquestrador viola este protocolo.
210
+ 3. **Na consolidação:** o Imperator retoma com o próprio selo e entrega o resultado em linguagem de negócio.
211
+ 4. **Fonte dos selos:** o hook injeta o elenco (núcleo + orquestradores de squad) no bloco `<sinapse-selo>`; pra especialista de squad fora do elenco, consultar `~/.claude/agent-badges.json` (chave `squad/id`).
212
+
213
+ O selo é o ÚNICO canal sancionado de identidade de agente em texto user-facing. `@ids` técnicos, matrizes de handoff e estrutura interna seguem proibidos fora dele.
214
+
202
215
  ## COMPLETE AGENT DEFINITION
203
216
 
204
217
  ```yaml
@@ -7,9 +7,9 @@
7
7
  # - SHA256 hashes for change detection
8
8
  # - File types for categorization
9
9
  #
10
- version: 1.23.0
10
+ version: 1.24.0
11
11
  generator: scripts/generate-install-manifest.js
12
- file_count: 1155
12
+ file_count: 1156
13
13
  files:
14
14
  - path: cli/commands/config/index.js
15
15
  hash: sha256:bfa83cb1dc111b0b30dd298dc0abc2150b73f939b6cd4458effa8e6d407bc9e2
@@ -1504,9 +1504,9 @@ files:
1504
1504
  type: agent
1505
1505
  size: 21342
1506
1506
  - path: development/agents/snps-orqx.md
1507
- hash: sha256:8e4145cdf25f57d366e0f6b3563e907c7a3ba8b2d6d3af0a6e573927f4a0321c
1507
+ hash: sha256:d557a1c47a3bc82efdcbf799ca4363ea19a1883244edc5557474625315cb6fe8
1508
1508
  type: agent
1509
- size: 43871
1509
+ size: 45205
1510
1510
  - path: development/agents/sprint-lead.md
1511
1511
  hash: sha256:df8dbd1de7b9c1238cbe3629d307563bbac1515869a5658dc7286b3dcbb55078
1512
1512
  type: agent
@@ -4236,13 +4236,17 @@ files:
4236
4236
  type: template
4237
4237
  size: 9200
4238
4238
  - path: product/templates/statusline/track-agent-clear.cjs
4239
- hash: sha256:3cda606de2463374e60440ba6cae9ca98e39b782b19c6ae39aa7569a1b211f09
4239
+ hash: sha256:1258406127c7f3c66f2b17e9a25d26c725a7112443a1a17127a6f29b831c3f4f
4240
4240
  type: template
4241
- size: 2137
4241
+ size: 3171
4242
4242
  - path: product/templates/statusline/track-agent.cjs
4243
- hash: sha256:0194c9a5750ed9e7a11fb5de21060021f64636f21e8b91039543abccaf6a4c86
4243
+ hash: sha256:b8ab372aba97c4c3d82d78b5130eb44f47ffe8e74b925fe7ef75f5fb4393519a
4244
4244
  type: template
4245
- size: 10990
4245
+ size: 15433
4246
+ - path: product/templates/statusline/track-delegation.cjs
4247
+ hash: sha256:17dc12b0ee286c752cddf6186cee6f25d19c1a59f3769090fa061a32d796c187
4248
+ type: template
4249
+ size: 5265
4246
4250
  - path: product/templates/story-tmpl.yaml
4247
4251
  hash: sha256:3dbcd522e69b86715f32eaf0cd13ce3a2631b5d2fc4f068c33df9593b6dbbfc2
4248
4252
  type: template
@@ -5,8 +5,10 @@
5
5
  * Called via Claude Code Stop hook (when the assistant turn ends).
6
6
  *
7
7
  * Soft-clears the per-CWD session cache at end of turn:
8
- * - specialists[] -> [] (no longer "active" specialists)
9
- * - imperator.active -> false (imperator no longer active)
8
+ * - imperator.active -> false (turn-scoped flag; role survives)
9
+ * - PRESERVES specialists[] (orchestration cast spans turns — the 6h
10
+ * TTL in track-agent.cjs/statusline handles expiry; wiping per turn made
11
+ * cross-turn delegation tracking impossible)
10
12
  * - PRESERVES squad and agent (last-known, for statusline back-compat)
11
13
  * - regrava updated (ISO)
12
14
  *
@@ -49,7 +51,19 @@ function main() {
49
51
 
50
52
  if (!raw || typeof raw !== 'object') process.exit(0);
51
53
 
54
+ // Freeze the voice-freshness point BEFORE refreshing `updated` below.
55
+ // Legacy caches (pre-voiceTs) inherit their ORIGINAL last-activity time —
56
+ // without this, every Stop would refresh `updated` and resurrect expired
57
+ // ("ghost") voices on the next prompt, defeating the voice TTL.
58
+ let voiceTs = Number.isFinite(raw.voiceTs) && raw.voiceTs > 0 ? raw.voiceTs : 0;
59
+ if (!voiceTs && typeof raw.updated === 'string' && raw.updated) {
60
+ const parsed = Date.parse(raw.updated);
61
+ if (Number.isFinite(parsed)) voiceTs = Math.floor(parsed / 1000);
62
+ }
63
+
52
64
  // Build a v2 cache preserving squad + agent, clearing the active layers.
65
+ // voiceTs is preserved so the primary voice keeps its 6h freshness window
66
+ // across turns of the same session (track-agent.cjs enforces the TTL).
53
67
  const cache = {
54
68
  version: 2,
55
69
  agent: typeof raw.agent === 'string' ? raw.agent : '',
@@ -59,7 +73,12 @@ function main() {
59
73
  active: false,
60
74
  ts: raw.imperator && Number.isFinite(raw.imperator.ts) ? raw.imperator.ts : 0,
61
75
  },
62
- specialists: [],
76
+ specialists: Array.isArray(raw.specialists)
77
+ ? raw.specialists.filter(
78
+ (s) => s && typeof s === 'object' && typeof s.id === 'string' && s.id
79
+ )
80
+ : [],
81
+ voiceTs,
63
82
  updated: new Date().toISOString(),
64
83
  };
65
84
 
@@ -16,6 +16,12 @@
16
16
  * - /squad:cmd -> sets squad (when not :agents:)
17
17
  * - *exit / *reset -> clears agent/squad/role/specialists + imperator.active
18
18
  *
19
+ * Badge injection (selo):
20
+ * - Primary voice has a 6h freshness TTL (voiceTs) — no more ghost voices
21
+ * leaking across sessions.
22
+ * - With the Imperator active, the block also ships the handoff CAST (core
23
+ * agents + squad orchestrators) so auto-routed voices can stamp their seal.
24
+ *
19
25
  * Writes the per-CWD cache to:
20
26
  * ~/.claude/session-cache/{simpleHash(cwd)}.json (Session-Cache v2 shape)
21
27
  *
@@ -68,6 +74,7 @@ function normalizeCache(raw) {
68
74
  role: null,
69
75
  imperator: { active: false, ts: 0 },
70
76
  specialists: [],
77
+ voiceTs: 0,
71
78
  updated: '',
72
79
  };
73
80
  if (!raw || typeof raw !== 'object') return base;
@@ -75,6 +82,17 @@ function normalizeCache(raw) {
75
82
  base.agent = typeof raw.agent === 'string' ? raw.agent : '';
76
83
  base.squad = typeof raw.squad === 'string' ? raw.squad : '';
77
84
  base.role = typeof raw.role === 'string' ? raw.role : null;
85
+ base.voiceTs = Number.isFinite(raw.voiceTs) && raw.voiceTs > 0 ? raw.voiceTs : 0;
86
+ base.updated = typeof raw.updated === 'string' ? raw.updated : '';
87
+ // One-time migration for legacy caches (written before voiceTs existed):
88
+ // freeze the voice-freshness point at the cache's ORIGINAL last-activity
89
+ // time. This runs at READ time, before any writer refreshes `updated`, and
90
+ // the migrated value persists on the next write — so `updated` refreshes
91
+ // (Stop hook, delegation tracker, /squad:cmd) can never resurrect a ghost.
92
+ if (!base.voiceTs && base.updated) {
93
+ const parsed = Date.parse(base.updated);
94
+ if (Number.isFinite(parsed)) base.voiceTs = Math.floor(parsed / 1000);
95
+ }
78
96
 
79
97
  if (raw.imperator && typeof raw.imperator === 'object') {
80
98
  base.imperator = {
@@ -102,6 +120,9 @@ function applyName(cache, name, ts) {
102
120
  if (!name) return;
103
121
  const lower = name.toLowerCase();
104
122
 
123
+ // Any explicit mention renews the primary-voice freshness window (TTL below).
124
+ cache.voiceTs = ts;
125
+
105
126
  if (MASTERS.has(lower)) {
106
127
  // Master -> imperator. Does NOT enter specialists.
107
128
  cache.imperator = { active: true, ts };
@@ -142,18 +163,42 @@ function applyName(cache, name, ts) {
142
163
  // Load ~/.claude/agent-badges.json and index by last path segment. Keys are
143
164
  // either bare ids ("developer") or "squad/id" ("squad-brand/brand-orqx"); the
144
165
  // session-cache only ever stores the bare id, so we index on the last segment.
166
+ // Also derives the handoff CAST (core agents + squad orchestrators) so the
167
+ // model can stamp the seal of any agent it routes to — auto-routing means the
168
+ // user never types those @ids, so the roster can't come from the prompt.
169
+ // A few badge entries carry double-escaped emoji ("\\U0001F9E0" as literal
170
+ // text). Decode them for display; garbage never reaches the user's selo.
171
+ function fixEmoji(e) {
172
+ if (typeof e !== 'string' || !e) return '◆';
173
+ if (!e.includes('\\u') && !e.includes('\\U')) return e;
174
+ try {
175
+ const decoded = e
176
+ .replace(/\\U([0-9A-Fa-f]{8})/g, (_, h) => String.fromCodePoint(parseInt(h, 16)))
177
+ .replace(/\\u([0-9A-Fa-f]{4})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
178
+ return decoded || '◆';
179
+ } catch {
180
+ return '◆';
181
+ }
182
+ }
183
+
145
184
  function loadBadges() {
146
185
  try {
147
186
  const p = path.join(os.homedir(), '.claude', 'agent-badges.json');
148
187
  const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
149
188
  if (!raw || typeof raw !== 'object') return null;
150
189
  const byId = {};
190
+ const core = [];
191
+ const orqx = [];
151
192
  for (const key of Object.keys(raw)) {
152
193
  const v = raw[key];
153
194
  if (!v || typeof v !== 'object' || !v.emoji || !v.name) continue;
154
- byId[key.split('/').pop()] = v;
195
+ const id = key.split('/').pop();
196
+ const b = { emoji: fixEmoji(v.emoji), area: v.area, name: v.name };
197
+ byId[id] = b;
198
+ if (!key.includes('/')) core.push({ id, b });
199
+ else if (id.endsWith('-orqx')) orqx.push({ id, b });
155
200
  }
156
- return Object.keys(byId).length ? byId : null;
201
+ return Object.keys(byId).length ? { byId, core, orqx } : null;
157
202
  } catch {
158
203
  return null;
159
204
  }
@@ -178,20 +223,35 @@ function resolvePrimary(cache, byId) {
178
223
  return null;
179
224
  }
180
225
 
226
+ // Freshness of the primary voice: an explicit mention within BADGE_TTL keeps
227
+ // the voice alive; older than that → the voice expired. Kills ghost voices
228
+ // that used to leak across sessions (the primary voice had no TTL at all).
229
+ // voiceTs is the SINGLE source here — legacy caches are migrated at read time
230
+ // by normalizeCache (frozen at their original `updated`), so a `updated`
231
+ // refresh by any writer can never resurrect an expired voice.
232
+ function voiceIsFresh(cache, now) {
233
+ const ts = Number.isFinite(cache.voiceTs) && cache.voiceTs > 0 ? cache.voiceTs : 0;
234
+ return ts > 0 && now - ts < BADGE_TTL;
235
+ }
236
+
181
237
  // Build the additionalContext string, or null when no SINAPSE voice is active.
182
238
  function buildBadgeContext(cache) {
183
- const byId = loadBadges();
184
- if (!byId) return null;
239
+ const badges = loadBadges();
240
+ if (!badges) return null;
241
+ const byId = badges.byId;
185
242
 
186
243
  const now = nowSec();
187
- const primary = resolvePrimary(cache, byId);
244
+ const primary = voiceIsFresh(cache, now) ? resolvePrimary(cache, byId) : null;
188
245
 
189
246
  // The specialist roster is only meaningful while the Imperator is coordinating
190
247
  // a multi-agent orchestration; a plain specialist↔specialist switch shows just
191
- // the active voice (no roster noise).
248
+ // the active voice (no roster noise). `imperator.active` is turn-scoped (the
249
+ // Stop hook lowers it), so cross-turn orchestration is detected by the ROLE,
250
+ // which survives turns and expires with the voice TTL.
192
251
  const imperatorActive =
193
252
  cache.imperator && cache.imperator.active && now - (cache.imperator.ts || 0) < BADGE_TTL;
194
- const specs = imperatorActive
253
+ const imperatorLeading = imperatorActive || (cache.role === 'imperator' && !!primary);
254
+ const specs = imperatorLeading
195
255
  ? (cache.specialists || [])
196
256
  .filter((s) => s && s.id && s.id !== cache.agent && now - (s.ts || 0) < BADGE_TTL)
197
257
  .map((s) => byId[s.id])
@@ -211,6 +271,32 @@ function buildBadgeContext(cache) {
211
271
  lines.push('Especialistas em jogo nesta orquestração:');
212
272
  for (const b of specs) lines.push(` • ${fullBadge(b)} (curto: ${shortBadge(b)})`);
213
273
  }
274
+ // With the Imperator coordinating, ship the handoff cast (core + squad
275
+ // orchestrators) so every voice the plan routes to can enter with its own
276
+ // seal — auto-routing means these ids never appear in the user's prompt.
277
+ if (imperatorLeading && (badges.core.length || badges.orqx.length)) {
278
+ const cast = badges.core.filter((e) => e.id !== IMPERATOR_KEY).concat(badges.orqx);
279
+ const castLines = cast.map(
280
+ (e) => ` ${e.id} → ${e.b.emoji} · ${e.b.area || '—'} · ${e.b.name}`
281
+ );
282
+ // Hard budget: the whole <sinapse-selo> block stays ≤ 2500 chars even as
283
+ // squads grow. RESERVE covers the protocol tail pushed after this section.
284
+ const RESERVE = 900;
285
+ let room = 2500 - RESERVE - lines.join('\n').length;
286
+ const kept = [];
287
+ for (const l of castLines) {
288
+ if (room - (l.length + 1) < 0) break;
289
+ kept.push(l);
290
+ room -= l.length + 1;
291
+ }
292
+ lines.push('');
293
+ lines.push('Elenco pra handoffs — cada voz entra com o selo `▌ {emoji} · SNPS · {área} · {nome}`:');
294
+ lines.push(...kept);
295
+ if (kept.length < castLines.length) {
296
+ lines.push(` … +${castLines.length - kept.length} no arquivo abaixo`);
297
+ }
298
+ lines.push(' (especialista de squad fora do elenco: chave "squad/id" em ~/.claude/agent-badges.json)');
299
+ }
214
300
  lines.push('');
215
301
  lines.push('Protocolo (NON-NEGOTIABLE):');
216
302
  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.');
@@ -278,6 +364,7 @@ function main() {
278
364
  cache.role = null;
279
365
  cache.specialists = [];
280
366
  cache.imperator = { active: false, ts: cache.imperator.ts || 0 };
367
+ cache.voiceTs = 0;
281
368
  changed = true;
282
369
  } else {
283
370
  // /ns:agents:name (e.g. /SINAPSE:agents:developer) -> classify as @name.
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SINAPSE Delegation Tracker — Session-Cache v2
4
+ *
5
+ * Called via Claude Code PreToolUse hook with matcher "Task".
6
+ *
7
+ * Auto-routing means the user never types specialist @ids — so the prompt
8
+ * detector (track-agent.cjs) can't see programmatic delegations. This hook
9
+ * closes that gap: every real delegation (Task tool spawn) registers its
10
+ * `subagent_type` in the per-CWD session-cache `specialists[]`, so the next
11
+ * turn's badge injection lists the voices actually in play and the statusline
12
+ * can show the orchestration cast.
13
+ *
14
+ * Rules:
15
+ * - Only ids that resolve to a badge in ~/.claude/agent-badges.json enter
16
+ * the cache (harness-generic types like "general-purpose" are ignored).
17
+ * - Never mutates the primary voice (agent/role) — delegation does not
18
+ * change who is speaking; the orchestrator remains the voice.
19
+ * - Cap of 6 specialists (oldest evicted), mirroring track-agent.cjs.
20
+ *
21
+ * OBSERVER ONLY / FAIL-OPEN: always exits 0 — a tracking hook must NEVER
22
+ * block a Task call, on any error whatsoever.
23
+ *
24
+ * Installed automatically by `npx sinapse-ai install`.
25
+ * Source: .sinapse-ai/product/templates/statusline/
26
+ */
27
+
28
+ 'use strict';
29
+
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+ const os = require('os');
33
+
34
+ const SPECIALIST_CAP = 6;
35
+ // Masters are never a delegated "specialist in play" — they ARE the primary
36
+ // voice. Squad orchestrators delegated as subagents DO enter the cast.
37
+ const MASTERS = new Set(['sinapse', 'snps', 'sinapse-orqx', 'snps-orqx']);
38
+
39
+ // Identical to track-agent.cjs and statusline-script.js.
40
+ function simpleHash(str) {
41
+ let h = 0;
42
+ for (let i = 0; i < str.length; i++) {
43
+ h = ((h << 5) - h) + str.charCodeAt(i);
44
+ h |= 0;
45
+ }
46
+ return Math.abs(h).toString(16);
47
+ }
48
+
49
+ function nowSec() {
50
+ return Math.floor(Date.now() / 1000);
51
+ }
52
+
53
+ // Badge ids indexed by last path segment (same contract as track-agent.cjs).
54
+ function loadBadgeIds() {
55
+ try {
56
+ const p = path.join(os.homedir(), '.claude', 'agent-badges.json');
57
+ const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
58
+ if (!raw || typeof raw !== 'object') return null;
59
+ const ids = new Set();
60
+ for (const key of Object.keys(raw)) {
61
+ const v = raw[key];
62
+ if (!v || typeof v !== 'object' || !v.emoji || !v.name) continue;
63
+ ids.add(key.split('/').pop().toLowerCase());
64
+ }
65
+ return ids.size ? ids : null;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ function main() {
72
+ let input;
73
+ try {
74
+ input = JSON.parse(fs.readFileSync(0, 'utf8'));
75
+ } catch {
76
+ process.exit(0);
77
+ }
78
+
79
+ if (!input || input.tool_name !== 'Task') process.exit(0);
80
+
81
+ const subagent = ((input.tool_input || {}).subagent_type || '').toLowerCase().trim();
82
+ if (!subagent || MASTERS.has(subagent)) process.exit(0);
83
+
84
+ // Only SINAPSE agents (present in the badge map) enter the cast.
85
+ const badgeIds = loadBadgeIds();
86
+ if (!badgeIds || !badgeIds.has(subagent)) process.exit(0);
87
+
88
+ const cacheDir = path.join(os.homedir(), '.claude', 'session-cache');
89
+ const cacheFile = path.join(cacheDir, `${simpleHash(process.cwd())}.json`);
90
+
91
+ let cache;
92
+ try {
93
+ cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
94
+ } catch {
95
+ cache = null;
96
+ }
97
+ if (!cache || typeof cache !== 'object') {
98
+ cache = {
99
+ version: 2,
100
+ agent: '',
101
+ squad: '',
102
+ role: null,
103
+ imperator: { active: false, ts: 0 },
104
+ specialists: [],
105
+ voiceTs: 0,
106
+ updated: '',
107
+ };
108
+ }
109
+ if (!Array.isArray(cache.specialists)) cache.specialists = [];
110
+
111
+ const ts = nowSec();
112
+ const existing = cache.specialists.find(
113
+ (s) => s && typeof s.id === 'string' && s.id.toLowerCase() === subagent
114
+ );
115
+ if (existing) {
116
+ existing.ts = ts; // renew freshness, no duplicate
117
+ } else {
118
+ cache.specialists.push({ id: subagent, label: subagent, icon: '🤖', ts });
119
+ if (cache.specialists.length > SPECIALIST_CAP) {
120
+ let minIdx = 0;
121
+ for (let i = 1; i < cache.specialists.length; i++) {
122
+ if (cache.specialists[i].ts < cache.specialists[minIdx].ts) minIdx = i;
123
+ }
124
+ cache.specialists.splice(minIdx, 1);
125
+ }
126
+ }
127
+
128
+ cache.version = 2;
129
+ // Freeze the voice-freshness point BEFORE refreshing `updated` — a legacy
130
+ // cache (pre-voiceTs) must not have its ghost voice resurrected by this
131
+ // write (same migration as track-agent.cjs/track-agent-clear.cjs).
132
+ if (!(Number.isFinite(cache.voiceTs) && cache.voiceTs > 0)) {
133
+ let frozen = 0;
134
+ if (typeof cache.updated === 'string' && cache.updated) {
135
+ const parsed = Date.parse(cache.updated);
136
+ if (Number.isFinite(parsed)) frozen = Math.floor(parsed / 1000);
137
+ }
138
+ cache.voiceTs = frozen;
139
+ }
140
+ cache.updated = new Date().toISOString();
141
+
142
+ try {
143
+ fs.mkdirSync(cacheDir, { recursive: true });
144
+ // Atomic (tmp+rename): concurrent readers (statusline, track-agent) must
145
+ // never see a torn file. Parallel Task spawns remain last-write-wins —
146
+ // acceptable for an observer-only cast (worst case: one specialist waits
147
+ // for its next delegation to appear).
148
+ const tmp = `${cacheFile}.${process.pid}.tmp`;
149
+ fs.writeFileSync(tmp, JSON.stringify(cache));
150
+ fs.renameSync(tmp, cacheFile);
151
+ } catch {
152
+ /* best-effort */
153
+ }
154
+
155
+ process.exit(0);
156
+ }
157
+
158
+ try {
159
+ main();
160
+ } catch {
161
+ process.exit(0);
162
+ }
package/CHANGELOG.md CHANGED
@@ -1,8 +1,12 @@
1
- ## [1.23.0](https://github.com/caioimori/sinapse-ai/compare/1.22.0...1.23.0) (2026-07-08)
1
+ ## [1.24.0](https://github.com/caioimori/sinapse-ai/compare/1.23.0...1.24.0) (2026-07-09)
2
2
 
3
3
  ### Features
4
4
 
5
- * **gate:** cobertura doc-first por extensao em qualquer pasta marco de release [Story mesa2-docfirst-gate-coverage] ([#371](https://github.com/caioimori/sinapse-ai/issues/371)) ([a77b4e1](https://github.com/caioimori/sinapse-ai/commit/a77b4e1b3d4ba250e488d229071e341e3f763bfc)), closes [#370](https://github.com/caioimori/sinapse-ai/issues/370)
5
+ * **selo:** handoffs visiveis elenco injetado, delegacao rastreada, voz com TTL, regras reconciliadas [Story selo-handoff-visibility] ([#373](https://github.com/caioimori/sinapse-ai/issues/373)) ([0d8c58f](https://github.com/caioimori/sinapse-ai/commit/0d8c58f0bb0644b30e8c864cba18da785e958352))
6
+
7
+ ### Maintenance
8
+
9
+ * **release:** sync da main pos-release 1.23.0 ([#372](https://github.com/caioimori/sinapse-ai/issues/372)) ([4e1bd6c](https://github.com/caioimori/sinapse-ai/commit/4e1bd6c3ef36940c57dfb7398dde52b4c853662e))
6
10
 
7
11
  # Changelog
8
12
 
@@ -13,6 +17,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
13
17
 
14
18
  ## [Unreleased]
15
19
 
20
+ ## [1.23.0] — 2026-07-08 — 🛡️ Gates doc-first cobrem código em qualquer pasta
21
+
22
+ > Minor. Atualização segura via `npx sinapse-ai update`. Fecha a brecha de cobertura dos gates doc-first: a detecção deixa de depender de uma allowlist de 8 pastas e passa a valer por extensão de código em qualquer lugar do projeto.
23
+
24
+ ### Features
25
+
26
+ - **gate:** amplia cobertura dos gates doc-first pra código em qualquer pasta — detecção por (pasta de código expandida OU extensão de código), isentando config-by-pattern (`*.config.*`, `*.d.ts`, `.*rc`) e build outputs (`.next/`, `dist/`, `coverage/`, `.vercel/`); exceção `isFrameworkRepo` no story gate (Art. III); +25 testes, fail-open preservado (#370, #371)
27
+
16
28
  ## [1.22.0] — 2026-07-07 — 🎛️ Mesa AF-20260704: guards doc-first + economia de contexto + calibração do juiz
17
29
 
18
30
  > Minor. Atualização segura via `npx sinapse-ai update`. Executa integralmente a Mesa da rodada 2 (épicos `epic-rodada2-mesa` + `epic-rodada2-mesa-fase2`, PRs #357–#368) — dos guards de qualidade ao contexto enxuto, tudo com verificação adversarial.
@@ -12,11 +12,13 @@
12
12
  * - ~/.claude/statusline-script.js (renders the statusline)
13
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/hooks/track-delegation.cjs (PreToolUse[Task] — registers real delegations in the cast)
15
16
  * - ~/.claude/agent-badges.json (id -> {emoji, area, name} — read by the detector + statusline)
16
17
  * - ~/.claude/session-cache/ (per-cwd cache directory)
17
18
  * - settings.statusLine (graceful skip if already set)
18
19
  * - settings.hooks.UserPromptSubmit → track-agent.cjs (idempotent guard)
19
20
  * - settings.hooks.Stop → track-agent-clear.cjs (idempotent guard)
21
+ * - settings.hooks.PreToolUse[Task] → track-delegation.cjs (idempotent guard)
20
22
  *
21
23
  * Design notes:
22
24
  * - FAIL-SOFT: statusline is non-critical. Any unexpected error returns a
@@ -43,6 +45,7 @@ function resolveSources(sourceCoreDir) {
43
45
  script: path.join(templatesDir, 'statusline-script.js'),
44
46
  detector: path.join(templatesDir, 'track-agent.cjs'),
45
47
  clear: path.join(templatesDir, 'track-agent-clear.cjs'),
48
+ delegation: path.join(templatesDir, 'track-delegation.cjs'),
46
49
  badges: path.join(templatesDir, 'agent-badges.json'),
47
50
  };
48
51
  }
@@ -77,7 +80,7 @@ function nodeCommand(targetPath) {
77
80
  * @param {string} [opts.sourceCoreDir] Absolute path to the installed `.sinapse-ai` directory.
78
81
  * Defaults to the repo's `.sinapse-ai` relative to this module (`../../.sinapse-ai`).
79
82
  * @param {string} [opts.homeDir] Override the home directory (testing). Defaults to os.homedir().
80
- * @returns {Promise<{installed:boolean, reason?:string, statusLineSet:boolean, detectorHook:boolean, clearHook:boolean, files:string[]}>}
83
+ * @returns {Promise<{installed:boolean, reason?:string, statusLineSet:boolean, detectorHook:boolean, clearHook:boolean, delegationHook:boolean, files:string[]}>}
81
84
  */
82
85
  async function setupStatusline(opts = {}) {
83
86
  const result = {
@@ -85,6 +88,7 @@ async function setupStatusline(opts = {}) {
85
88
  statusLineSet: false,
86
89
  detectorHook: false,
87
90
  clearHook: false,
91
+ delegationHook: false,
88
92
  files: [],
89
93
  };
90
94
 
@@ -102,6 +106,7 @@ async function setupStatusline(opts = {}) {
102
106
  return result;
103
107
  }
104
108
  const hasClear = await fse.pathExists(sources.clear);
109
+ const hasDelegation = await fse.pathExists(sources.delegation);
105
110
 
106
111
  const claudeDir = path.join(homeDir, '.claude');
107
112
  const hooksDir = path.join(claudeDir, 'hooks');
@@ -110,6 +115,7 @@ async function setupStatusline(opts = {}) {
110
115
  const scriptTarget = path.join(claudeDir, 'statusline-script.js');
111
116
  const detectorTarget = path.join(hooksDir, 'track-agent.cjs');
112
117
  const clearTarget = path.join(hooksDir, 'track-agent-clear.cjs');
118
+ const delegationTarget = path.join(hooksDir, 'track-delegation.cjs');
113
119
 
114
120
  // Ensure directories (hooks + session-cache the trackers write to).
115
121
  await fse.ensureDir(hooksDir);
@@ -124,6 +130,10 @@ async function setupStatusline(opts = {}) {
124
130
  await fse.copy(sources.clear, clearTarget);
125
131
  result.files.push(clearTarget);
126
132
  }
133
+ if (hasDelegation) {
134
+ await fse.copy(sources.delegation, delegationTarget);
135
+ result.files.push(delegationTarget);
136
+ }
127
137
 
128
138
  // Agent badge map (id -> {emoji, area, name}). Read by the detector hook to
129
139
  // inject the speaking agent's badge, and by the statusline to render it.
@@ -153,16 +163,25 @@ async function setupStatusline(opts = {}) {
153
163
  result.statusLineSet = true;
154
164
  }
155
165
 
156
- // Hooks — register the .cjs detector (UserPromptSubmit) + clear (Stop), idempotently.
166
+ // Hooks — register the .cjs detector (UserPromptSubmit) + clear (Stop) +
167
+ // delegation tracker (PreToolUse[Task]), idempotently.
157
168
  if (!settings.hooks || typeof settings.hooks !== 'object') {
158
169
  settings.hooks = {};
159
170
  }
160
171
 
161
- if (!Array.isArray(settings.hooks.UserPromptSubmit)) {
162
- settings.hooks.UserPromptSubmit = [];
163
- }
164
- if (!hasHookCommand(settings.hooks.UserPromptSubmit, 'track-agent.cjs')) {
165
- settings.hooks.UserPromptSubmit.push({
172
+ // PRESERVATION GUARD: if the user's settings carry an event in a legacy /
173
+ // hand-written NON-ARRAY shape, we must not clobber it (that would destroy
174
+ // their hooks). Skip registration for that event instead — never destroy.
175
+ const eventArray = (event) => {
176
+ if (settings.hooks[event] === undefined || settings.hooks[event] === null) {
177
+ settings.hooks[event] = [];
178
+ }
179
+ return Array.isArray(settings.hooks[event]) ? settings.hooks[event] : null;
180
+ };
181
+
182
+ const upsArr = eventArray('UserPromptSubmit');
183
+ if (upsArr && !hasHookCommand(upsArr, 'track-agent.cjs')) {
184
+ upsArr.push({
166
185
  matcher: '',
167
186
  hooks: [{ type: 'command', command: nodeCommand(detectorTarget), timeout: 10 }],
168
187
  });
@@ -170,11 +189,9 @@ async function setupStatusline(opts = {}) {
170
189
  }
171
190
 
172
191
  if (hasClear) {
173
- if (!Array.isArray(settings.hooks.Stop)) {
174
- settings.hooks.Stop = [];
175
- }
176
- if (!hasHookCommand(settings.hooks.Stop, 'track-agent-clear.cjs')) {
177
- settings.hooks.Stop.push({
192
+ const stopArr = eventArray('Stop');
193
+ if (stopArr && !hasHookCommand(stopArr, 'track-agent-clear.cjs')) {
194
+ stopArr.push({
178
195
  matcher: '',
179
196
  hooks: [{ type: 'command', command: nodeCommand(clearTarget), timeout: 10 }],
180
197
  });
@@ -182,6 +199,20 @@ async function setupStatusline(opts = {}) {
182
199
  }
183
200
  }
184
201
 
202
+ // Delegation tracker — PreToolUse on the Task tool: registers each real
203
+ // (programmatic) delegation in the session-cache so auto-routed specialists
204
+ // surface in the badge context and statusline. Observer-only, fail-open.
205
+ if (hasDelegation) {
206
+ const preArr = eventArray('PreToolUse');
207
+ if (preArr && !hasHookCommand(preArr, 'track-delegation.cjs')) {
208
+ preArr.push({
209
+ matcher: 'Task',
210
+ hooks: [{ type: 'command', command: nodeCommand(delegationTarget), timeout: 5 }],
211
+ });
212
+ result.delegationHook = true;
213
+ }
214
+ }
215
+
185
216
  await fse.ensureDir(path.dirname(globalSettingsPath));
186
217
  // Atomic (tmp+rename): a crash mid-write must never leave a torn settings.json
187
218
  atomicWriteFileSync(globalSettingsPath, JSON.stringify(settings, null, 2), 'utf8');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sinapse-ai",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "description": "SINAPSE AI: Framework de orquestracao de IA — 17 squads, 172 agentes especializados",
5
5
  "bin": {
6
6
  "sinapse": "bin/sinapse.js",
@@ -659,6 +659,14 @@ const HOOK_EVENT_MAP = {
659
659
  matcher: null,
660
660
  timeout: 10,
661
661
  },
662
+ // Delegation tracker — registers each real Task-tool delegation in the
663
+ // session-cache so auto-routed specialists surface in the badge context.
664
+ // Observer-only, fail-open (always exit 0).
665
+ 'track-delegation.cjs': {
666
+ event: 'PreToolUse',
667
+ matcher: 'Task',
668
+ timeout: 5,
669
+ },
662
670
  };
663
671
 
664
672
  /** Default event config for unmapped hooks (backwards compatible). */