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.
- package/.claude/rules/mandatory-delegation-reference.md +1 -1
- package/.claude/rules/mandatory-delegation.md +1 -1
- package/.claude/rules/squad-awareness.md +1 -1
- package/.claude/rules/token-economy.md +1 -1
- package/.sinapse-ai/development/agents/snps-orqx.md +14 -1
- package/.sinapse-ai/install-manifest.yaml +12 -8
- package/.sinapse-ai/product/templates/statusline/track-agent-clear.cjs +22 -3
- package/.sinapse-ai/product/templates/statusline/track-agent.cjs +94 -7
- package/.sinapse-ai/product/templates/statusline/track-delegation.cjs +162 -0
- package/CHANGELOG.md +14 -2
- package/bin/lib/setup-statusline.js +43 -12
- package/package.json +1 -1
- package/packages/installer/src/wizard/ide-config-generator.js +8 -0
|
@@ -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:
|
|
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
|
-
>
|
|
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
|
|
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
|
-
-
|
|
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,
|
|
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.
|
|
10
|
+
version: 1.24.0
|
|
11
11
|
generator: scripts/generate-install-manifest.js
|
|
12
|
-
file_count:
|
|
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:
|
|
1507
|
+
hash: sha256:d557a1c47a3bc82efdcbf799ca4363ea19a1883244edc5557474625315cb6fe8
|
|
1508
1508
|
type: agent
|
|
1509
|
-
size:
|
|
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:
|
|
4239
|
+
hash: sha256:1258406127c7f3c66f2b17e9a25d26c725a7112443a1a17127a6f29b831c3f4f
|
|
4240
4240
|
type: template
|
|
4241
|
-
size:
|
|
4241
|
+
size: 3171
|
|
4242
4242
|
- path: product/templates/statusline/track-agent.cjs
|
|
4243
|
-
hash: sha256:
|
|
4243
|
+
hash: sha256:b8ab372aba97c4c3d82d78b5130eb44f47ffe8e74b925fe7ef75f5fb4393519a
|
|
4244
4244
|
type: template
|
|
4245
|
-
size:
|
|
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
|
-
* -
|
|
9
|
-
* -
|
|
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
|
-
|
|
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
|
|
184
|
-
if (!
|
|
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
|
|
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.
|
|
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
|
-
* **
|
|
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)
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
settings.hooks.
|
|
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
|
-
|
|
174
|
-
|
|
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
|
@@ -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). */
|