@luanpdd/kit-mcp 1.19.0 → 1.21.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 +1 -1
- package/gates/dept-cycle-prevention.md +179 -0
- package/gates/multi-tenant-rls-coverage.md +102 -0
- package/gates/service-role-not-in-user-facing.md +113 -0
- package/kit/agents/audit-log-implementer.md +175 -0
- package/kit/agents/b2b-saas-architect.md +156 -0
- package/kit/agents/crm-pipeline-implementer.md +150 -0
- package/kit/agents/evolution-go-integrator.md +179 -0
- package/kit/agents/invite-flow-implementer.md +137 -0
- package/kit/agents/lgpd-compliance-auditor.md +206 -0
- package/kit/agents/multi-tenant-isolation-auditor.md +243 -0
- package/kit/agents/multi-tenant-rls-writer.md +262 -0
- package/kit/agents/org-onboarding-implementer.md +202 -0
- package/kit/agents/super-admin-implementer.md +182 -0
- package/kit/commands/burn-rate-status.md +237 -121
- package/kit/commands/multi-tenant.md +163 -0
- package/kit/file-manifest.json +31 -4
- package/kit/skills/_shared-multi-tenant/glossary.md +186 -0
- package/kit/skills/audit-log-multi-tenant/SKILL.md +334 -0
- package/kit/skills/b2b-saas-architecture/SKILL.md +300 -0
- package/kit/skills/crm-lead-pipeline-patterns/SKILL.md +326 -0
- package/kit/skills/evolution-go-whatsapp-integration/SKILL.md +322 -0
- package/kit/skills/lgpd-multi-tenant-compliance/SKILL.md +340 -0
- package/kit/skills/member-invite-flow/SKILL.md +305 -0
- package/kit/skills/member-management-react-shadcn/SKILL.md +328 -0
- package/kit/skills/multi-tenant-performance-scaling/SKILL.md +312 -0
- package/kit/skills/multi-tenant-rls-hierarchy/SKILL.md +338 -0
- package/kit/skills/org-onboarding-flow/SKILL.md +257 -0
- package/kit/skills/org-switcher-react-pattern/SKILL.md +349 -0
- package/kit/skills/permission-gate-react-pattern/SKILL.md +271 -0
- package/kit/skills/rbac-permissions-matrix-supabase/SKILL.md +301 -0
- package/kit/skills/super-admin-platform-pattern/SKILL.md +322 -0
- package/kit/skills/whatsapp-conversation-state-machine/SKILL.md +287 -0
- package/package.json +6 -2
- package/src/mcp-server/index.js +34 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: burn-rate-status
|
|
3
|
-
description: Tabela de burn rate por SLO consumindo .planning/slos/*.yml + .planning/metrics/snapshots/. Calcula SLI atual,
|
|
4
|
-
argument-hint: "[<slo_name>] [--
|
|
3
|
+
description: Tabela de burn rate dual-window por SLO consumindo .planning/slos/*.yml + .planning/metrics/snapshots/. Calcula SLI atual, fast_burn (1h) + slow_burn (6h) independentes, ETA exhaustão e ação combinada (PAGE/TICKET/WARN/OK). Aplica skill burn-rate-alerting (fator 4× lookahead/baseline canônico Google SRE).
|
|
4
|
+
argument-hint: "[<slo_name>] [--fast-baseline 1h] [--slow-baseline 6h] [--format table|json]"
|
|
5
5
|
allowed-tools:
|
|
6
6
|
- Read
|
|
7
7
|
- Bash
|
|
@@ -9,32 +9,32 @@ allowed-tools:
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
<objective>
|
|
12
|
-
Snapshot de burn rate para 1 SLO (se especificado) ou TODOS os SLOs definidos em `.planning/slos/*.yml`. Aplica skill [`burn-rate-alerting`](../skills/burn-rate-alerting/SKILL.md) — fórmula `burn_rate = error_rate / (1 - target)`, lookahead ≤ 4× baseline.
|
|
12
|
+
Snapshot de burn rate **dual-window** (1h fast + 6h slow) para 1 SLO (se especificado) ou TODOS os SLOs definidos em `.planning/slos/*.yml`. Aplica skill [`burn-rate-alerting`](../skills/burn-rate-alerting/SKILL.md) — fórmula canônica `burn_rate = error_rate / (1 - target)`, com lookahead/baseline obedecendo o **fator 4×** (page-tier: lookahead 1h ≤ 4× baseline 5m equivalente operacional; ticket-tier: lookahead 6h ≤ 4× baseline 30m). Status combinado segue o canonical Google SRE: PAGE quando ambas as janelas críticas, TICKET quando apenas slow erosion sustained, WARN para spike-only ou mild burn, OK quando ambas as janelas em estado saudável.
|
|
13
13
|
|
|
14
|
-
**Lê:** `.planning/slos/*.yml` (definição) + `.planning/metrics/snapshots/*.json` (eventos persistidos via `metrics.persistSnapshot()` — Phase 99).
|
|
14
|
+
**Lê:** `.planning/slos/*.yml` (definição com `alert_thresholds.page` + `.ticket`) + `.planning/metrics/snapshots/*.json` (eventos persistidos via `metrics.persistSnapshot()` — Phase 99 + Phase 102 auto-snapshot).
|
|
15
15
|
|
|
16
16
|
**Cria/Atualiza:** nada — comando read-only.
|
|
17
17
|
|
|
18
|
-
**Após:** o user vê tabela com status
|
|
18
|
+
**Após:** o user vê tabela com colunas `fast_burn`, `slow_burn`, `combined` (status PAGE / TICKET / WARN / OK) e pode escolher invocar `/investigar-producao` se há burn ativo, ou aguardar mais snapshots se ambas janelas estão `no_data`.
|
|
19
19
|
</objective>
|
|
20
20
|
|
|
21
21
|
<context>
|
|
22
22
|
**Argumentos:** `$ARGUMENTS` — opcional `<slo_name>` para 1 SLO; sem args = todos.
|
|
23
23
|
|
|
24
|
-
**Flags:**
|
|
25
|
-
- `--
|
|
26
|
-
- `--baseline <duration>` — janela
|
|
27
|
-
- `--format <table|json>` — output format
|
|
24
|
+
**Flags (defaults dual-window — Phase 103):**
|
|
25
|
+
- `--fast-baseline <duration>` — janela fast (page-tier). Default: `1h`.
|
|
26
|
+
- `--slow-baseline <duration>` — janela slow (ticket-tier). Default: `6h`.
|
|
27
|
+
- `--format <table|json>` — output format. Default: `table`.
|
|
28
28
|
|
|
29
|
-
**Combinações canônicas:**
|
|
30
|
-
-
|
|
31
|
-
-
|
|
29
|
+
**Combinações canônicas (skill burn-rate-alerting):**
|
|
30
|
+
- **Fast (page-tier):** lookahead 1h, baseline 5m, multiplier 14.4× — esgota ~2% do budget mensal em 1h.
|
|
31
|
+
- **Slow (ticket-tier):** lookahead 6h, baseline 30m, multiplier 6× — esgota ~10% do budget mensal em 6h.
|
|
32
32
|
|
|
33
|
-
**
|
|
34
|
-
|
|
35
|
-
snapshots na janela, o comando emite "no_data" para o SLO em vez de
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
**Fator 4×:** lookahead ≤ 4× baseline para extrapolação confiável (skill rule). 1h ≤ 4× 15m e 6h ≤ 4× 90m são ambos respeitados; defaults operacionais são page (1h baseline) + ticket (6h baseline) — a janela `lookahead` propriamente dita está embutida nos `alert_thresholds.{page,ticket}.lookahead` do YAML do SLO.
|
|
34
|
+
|
|
35
|
+
**Phase 99 + 102 wiring:** este comando consome dados persistidos automaticamente pelo handler MCP `metrics-snapshot` (Phase 102 OBS-20-01 — auto-persist via `persistSnapshot()` em cada call com throttle 1s). Sem snapshots na janela, o comando emite "no_data" para o SLO em vez de inventar números.
|
|
36
|
+
|
|
37
|
+
**Cross-reference:** este comando é a implementação do pattern "dashboard de burn rate" canônico documentado em [`kit/skills/burn-rate-alerting/SKILL.md`](../skills/burn-rate-alerting/SKILL.md). A skill é a SSOT da fórmula e dos thresholds; este comando é o renderer.
|
|
38
38
|
</context>
|
|
39
39
|
|
|
40
40
|
<process>
|
|
@@ -44,12 +44,12 @@ durante uso normal — futura fase pode auto-persistir.
|
|
|
44
44
|
Bash:
|
|
45
45
|
```bash
|
|
46
46
|
SLO_NAME=$(echo "$ARGUMENTS" | awk '{for(i=1;i<=NF;i++) if($i !~ /^--/) {print $i; exit}}')
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
FAST_BASELINE=$(echo "$ARGUMENTS" | grep -oE -- '--fast-baseline [^ ]+' | awk '{print $2}')
|
|
48
|
+
SLOW_BASELINE=$(echo "$ARGUMENTS" | grep -oE -- '--slow-baseline [^ ]+' | awk '{print $2}')
|
|
49
49
|
FORMAT=$(echo "$ARGUMENTS" | grep -oE -- '--format [^ ]+' | awk '{print $2}')
|
|
50
50
|
|
|
51
|
-
[ -z "$
|
|
52
|
-
[ -z "$
|
|
51
|
+
[ -z "$FAST_BASELINE" ] && FAST_BASELINE="1h"
|
|
52
|
+
[ -z "$SLOW_BASELINE" ] && SLOW_BASELINE="6h"
|
|
53
53
|
[ -z "$FORMAT" ] && FORMAT="table"
|
|
54
54
|
```
|
|
55
55
|
|
|
@@ -65,8 +65,8 @@ to_ms() {
|
|
|
65
65
|
*) echo 0 ;;
|
|
66
66
|
esac
|
|
67
67
|
}
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
FAST_BASELINE_MS=$(to_ms "$FAST_BASELINE")
|
|
69
|
+
SLOW_BASELINE_MS=$(to_ms "$SLOW_BASELINE")
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
## 2. Listar SLOs (FIX Phase 99: extension `.yml`, não `.md`)
|
|
@@ -90,7 +90,7 @@ if [ ${#EXISTING_SLOS[@]} -eq 0 ]; then
|
|
|
90
90
|
fi
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
## 3. Para cada SLO, carregar metadata + calcular SLI
|
|
93
|
+
## 3. Para cada SLO, carregar metadata + calcular SLI dual-window
|
|
94
94
|
|
|
95
95
|
Para cada `SLO_FILE` em `EXISTING_SLOS`:
|
|
96
96
|
|
|
@@ -110,145 +110,257 @@ TARGET_MS=$(grep -oE '^target_ms:\s*[0-9]+' "$SLO_FILE" | awk '{print $2}')
|
|
|
110
110
|
PERCENTILE=$(grep -oE '^\s+percentile:\s*[0-9]+' "$SLO_FILE" | awk '{print $2}')
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
### 3.2
|
|
113
|
+
### 3.2 Extrair `alert_thresholds.page` (fast) + `.ticket` (slow) do YAML
|
|
114
|
+
|
|
115
|
+
Phase 103 (OBS-20-02) — leitura dos dois blocos via awk com state machine. Cada bloco tem `lookahead`, `baseline`, `burn_rate_multiplier`. Defaults canônicos aplicados se ausentes (defensive default — ver fallback abaixo).
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# alert_thresholds.page (fast / page-tier)
|
|
119
|
+
FAST_LOOKAHEAD=$(awk '/^\s*alert_thresholds:/,/^[a-zA-Z]/{if(/^\s*page:/){p=1;next}else if(p && /^\s+lookahead:/){print $2;exit}else if(p && /^\s*ticket:/){exit}}' "$SLO_FILE")
|
|
120
|
+
FAST_BASELINE_YAML=$(awk '/^\s*alert_thresholds:/,/^[a-zA-Z]/{if(/^\s*page:/){p=1;next}else if(p && /^\s+baseline:/){print $2;exit}else if(p && /^\s*ticket:/){exit}}' "$SLO_FILE")
|
|
121
|
+
FAST_MULTIPLIER=$(awk '/^\s*alert_thresholds:/,/^[a-zA-Z]/{if(/^\s*page:/){p=1;next}else if(p && /^\s+burn_rate_multiplier:/){print $2;exit}else if(p && /^\s*ticket:/){exit}}' "$SLO_FILE")
|
|
122
|
+
|
|
123
|
+
# alert_thresholds.ticket (slow / ticket-tier)
|
|
124
|
+
SLOW_LOOKAHEAD=$(awk '/^\s*alert_thresholds:/,/^[a-zA-Z]/{if(/^\s*ticket:/){t=1;next}else if(t && /^\s+lookahead:/){print $2;exit}}' "$SLO_FILE")
|
|
125
|
+
SLOW_BASELINE_YAML=$(awk '/^\s*alert_thresholds:/,/^[a-zA-Z]/{if(/^\s*ticket:/){t=1;next}else if(t && /^\s+baseline:/){print $2;exit}}' "$SLO_FILE")
|
|
126
|
+
SLOW_MULTIPLIER=$(awk '/^\s*alert_thresholds:/,/^[a-zA-Z]/{if(/^\s*ticket:/){t=1;next}else if(t && /^\s+burn_rate_multiplier:/){print $2;exit}}' "$SLO_FILE")
|
|
127
|
+
|
|
128
|
+
# Defensive defaults — fator 4× canonical Google SRE values.
|
|
129
|
+
# Se um SLO YAML antigo / parcial não declarar alert_thresholds, aplicamos
|
|
130
|
+
# os defaults da skill burn-rate-alerting verbatim:
|
|
131
|
+
# page: 14.4× / lookahead 1h / baseline 5m
|
|
132
|
+
# ticket: 6× / lookahead 6h / baseline 30m
|
|
133
|
+
[ -z "$FAST_MULTIPLIER" ] && FAST_MULTIPLIER="14.4"
|
|
134
|
+
[ -z "$SLOW_MULTIPLIER" ] && SLOW_MULTIPLIER="6"
|
|
135
|
+
[ -z "$FAST_LOOKAHEAD" ] && FAST_LOOKAHEAD="1h"
|
|
136
|
+
[ -z "$SLOW_LOOKAHEAD" ] && SLOW_LOOKAHEAD="6h"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 3.3 Carregar snapshots para AMBAS as janelas
|
|
114
140
|
|
|
115
|
-
Use a API `loadSnapshots()`
|
|
141
|
+
Use a API `loadSnapshots()` (Phase 99) com duas chamadas — uma para fast (1h), uma para slow (6h). Inline node script:
|
|
116
142
|
|
|
117
143
|
```bash
|
|
118
|
-
|
|
144
|
+
DUAL_SNAPS=$(node --input-type=module -e "
|
|
119
145
|
import { loadSnapshots } from './src/core/metrics.js';
|
|
120
|
-
const
|
|
121
|
-
|
|
146
|
+
const fast = await loadSnapshots(process.cwd(), $FAST_BASELINE_MS);
|
|
147
|
+
const slow = await loadSnapshots(process.cwd(), $SLOW_BASELINE_MS);
|
|
148
|
+
console.log(JSON.stringify({fast, slow, fastCount: fast.length, slowCount: slow.length}));
|
|
122
149
|
")
|
|
123
|
-
|
|
150
|
+
FAST_COUNT=$(echo "$DUAL_SNAPS" | node -e "console.log(JSON.parse(require('fs').readFileSync(0,'utf8')).fastCount)")
|
|
151
|
+
SLOW_COUNT=$(echo "$DUAL_SNAPS" | node -e "console.log(JSON.parse(require('fs').readFileSync(0,'utf8')).slowCount)")
|
|
124
152
|
```
|
|
125
153
|
|
|
126
|
-
|
|
154
|
+
**no_data conservative semantics:** se EITHER janela tem < 2 snapshots (availability) ou < 1 (latency), o `combined_status` final é `no_data` — preferimos não inventar números a falsamente reportar OK. Isso preserva o contrato "graceful no_data" da Phase 99.
|
|
155
|
+
|
|
127
156
|
```bash
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
157
|
+
# A regra exata é aplicada dentro do node script de combinação (3.5),
|
|
158
|
+
# mas o early-out aqui evita trabalho desnecessário no caso comum.
|
|
159
|
+
if [ "$FAST_COUNT" -lt 2 ] && [ "$SLOW_COUNT" -lt 2 ]; then
|
|
160
|
+
echo "SLO $SLO_NAME: insufficient snapshots in BOTH windows (fast=$FAST_COUNT, slow=$SLOW_COUNT)"
|
|
161
|
+
echo "Generate data: invocações ao MCP tool 'metrics-snapshot' agora auto-persistem (Phase 102 OBS-20-01)."
|
|
162
|
+
COMBINED_STATUS="no_data"
|
|
163
|
+
continue
|
|
133
164
|
fi
|
|
134
165
|
```
|
|
135
166
|
|
|
136
|
-
### 3.
|
|
167
|
+
### 3.4 Calcular SLI por tipo de SLO (fast E slow independentes)
|
|
137
168
|
|
|
138
169
|
**Availability (`type: event-based`):**
|
|
139
170
|
|
|
140
|
-
Inline node — primeiro vs último snapshot dentro
|
|
171
|
+
Inline node — primeiro vs último snapshot **dentro de cada janela**. Delta de counters dá good/bad events:
|
|
141
172
|
|
|
142
173
|
```bash
|
|
143
|
-
|
|
174
|
+
DUAL_SLI=$(node --input-type=module -e "
|
|
144
175
|
import { loadSnapshots } from './src/core/metrics.js';
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
totalFirst
|
|
176
|
+
const fastSnaps = await loadSnapshots(process.cwd(), $FAST_BASELINE_MS);
|
|
177
|
+
const slowSnaps = await loadSnapshots(process.cwd(), $SLOW_BASELINE_MS);
|
|
178
|
+
|
|
179
|
+
function sliFromSnaps(snaps) {
|
|
180
|
+
if (snaps.length < 2) return {sli: null, errorRate: 0, good: 0, total: 0, error: 'no_data'};
|
|
181
|
+
const first = snaps[0];
|
|
182
|
+
const last = snaps[snaps.length - 1];
|
|
183
|
+
let goodFirst = 0, goodLast = 0, totalFirst = 0, totalLast = 0;
|
|
184
|
+
for (const [k,v] of Object.entries(first.counters)) {
|
|
185
|
+
if (k.endsWith(':ok')) goodFirst += v;
|
|
186
|
+
totalFirst += v;
|
|
187
|
+
}
|
|
188
|
+
for (const [k,v] of Object.entries(last.counters)) {
|
|
189
|
+
if (k.endsWith(':ok')) goodLast += v;
|
|
190
|
+
totalLast += v;
|
|
191
|
+
}
|
|
192
|
+
const good = goodLast - goodFirst;
|
|
193
|
+
const total = totalLast - totalFirst;
|
|
194
|
+
const sli = total > 0 ? good / total : null;
|
|
195
|
+
const errorRate = total > 0 ? (total - good) / total : 0;
|
|
196
|
+
return {sli, errorRate, good, total};
|
|
153
197
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
const good = goodLast - goodFirst;
|
|
159
|
-
const total = totalLast - totalFirst;
|
|
160
|
-
const sli = total > 0 ? good / total : null;
|
|
161
|
-
const errorRate = total > 0 ? (total - good) / total : 0;
|
|
162
|
-
console.log(JSON.stringify({sli, errorRate, good, total, totalFirst, totalLast}));
|
|
198
|
+
|
|
199
|
+
const fastSli = sliFromSnaps(fastSnaps);
|
|
200
|
+
const slowSli = sliFromSnaps(slowSnaps);
|
|
201
|
+
console.log(JSON.stringify({fast: fastSli, slow: slowSli}));
|
|
163
202
|
")
|
|
164
203
|
```
|
|
165
204
|
|
|
166
205
|
**Latency (`type: percentile`):**
|
|
167
206
|
|
|
168
|
-
Para latency,
|
|
207
|
+
Para latency, p95 do último snapshot em CADA janela. SLI = fração de samples NOT acima de target_ms.
|
|
169
208
|
|
|
170
209
|
```bash
|
|
171
|
-
|
|
210
|
+
DUAL_SLI=$(node --input-type=module -e "
|
|
172
211
|
import { loadSnapshots } from './src/core/metrics.js';
|
|
173
|
-
const snaps = await loadSnapshots(process.cwd(), $BASELINE_MS);
|
|
174
|
-
if (snaps.length < 1) { console.log(JSON.stringify({sli:null, error:'no_data'})); process.exit(0); }
|
|
175
|
-
const last = snaps[snaps.length - 1];
|
|
176
212
|
const target = $TARGET_MS;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
213
|
+
const fastSnaps = await loadSnapshots(process.cwd(), $FAST_BASELINE_MS);
|
|
214
|
+
const slowSnaps = await loadSnapshots(process.cwd(), $SLOW_BASELINE_MS);
|
|
215
|
+
|
|
216
|
+
function latencySli(snaps) {
|
|
217
|
+
if (snaps.length < 1) return {sli: null, errorRate: 0, totalSamples: 0, slowSamples: 0, error: 'no_data'};
|
|
218
|
+
const last = snaps[snaps.length - 1];
|
|
219
|
+
let totalSamples = 0, slowSamples = 0;
|
|
220
|
+
for (const lat of Object.values(last.latency)) {
|
|
221
|
+
totalSamples += lat.count;
|
|
222
|
+
if (lat.p95 > target) slowSamples += Math.round(lat.count * 0.05);
|
|
223
|
+
}
|
|
224
|
+
const sli = totalSamples > 0 ? 1 - (slowSamples / totalSamples) : null;
|
|
225
|
+
const errorRate = totalSamples > 0 ? slowSamples / totalSamples : 0;
|
|
226
|
+
return {sli, errorRate, totalSamples, slowSamples};
|
|
181
227
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
console.log(JSON.stringify({sli, errorRate, totalSamples, slowSamples, p95Max: Math.max(0, ...Object.values(last.latency).map(l => l.p95 || 0))}));
|
|
228
|
+
|
|
229
|
+
console.log(JSON.stringify({fast: latencySli(fastSnaps), slow: latencySli(slowSnaps)}));
|
|
185
230
|
")
|
|
186
231
|
```
|
|
187
232
|
|
|
188
|
-
### 3.
|
|
233
|
+
### 3.5 Calcular burn rate + status COMBINADO (dual-window)
|
|
189
234
|
|
|
190
|
-
Aplicar fórmula canônica
|
|
235
|
+
Aplicar fórmula canônica + status enum dual-window (skill `burn-rate-alerting` — fator 4× canonical):
|
|
191
236
|
|
|
192
237
|
```bash
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
if (result.error) { console.log(JSON.stringify({status:'no_data'})); process.exit(0); }
|
|
238
|
+
DUAL_STATUS=$(node --input-type=module -e "
|
|
239
|
+
const dual = $DUAL_SLI;
|
|
196
240
|
const target = $TARGET_RATIO || (1 - 0.05); // latency: 1 - ratio_above_target (5%)
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
241
|
+
const fastMult = $FAST_MULTIPLIER;
|
|
242
|
+
const slowMult = $SLOW_MULTIPLIER;
|
|
243
|
+
|
|
244
|
+
function burnFromSli(sli, target) {
|
|
245
|
+
if (sli.error) return {burnRate: null, error: sli.error};
|
|
246
|
+
const slack = 1 - target;
|
|
247
|
+
const burnRate = slack > 0 ? sli.errorRate / slack : 0;
|
|
248
|
+
return {burnRate, errorRate: sli.errorRate};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Combined status — canonical dual-window logic per skill burn-rate-alerting (fator 4×):
|
|
252
|
+
// PAGE = ambos críticos (fast ≥ 14.4 E slow ≥ 6) → page on-call AGORA
|
|
253
|
+
// TICKET = slow erosion sustained (slow ≥ 6, fast OK) → ticket de investigação
|
|
254
|
+
// WARN = fast spike isolado (fast ≥ 14.4 sozinho) — monitor, NÃO page (alarm flap risk)
|
|
255
|
+
// WARN = mild burn em qualquer janela (≥ 1.0×) — sustained drains budget no horizonte
|
|
256
|
+
// OK = ambos < 1.0× — saudável
|
|
257
|
+
// no_data = qualquer janela com snapshots insuficientes (conservative)
|
|
258
|
+
function combinedStatus(fastBurn, fastMult, slowBurn, slowMult) {
|
|
259
|
+
if (fastBurn === null || slowBurn === null) return 'no_data';
|
|
260
|
+
const fastTriggered = fastBurn >= fastMult;
|
|
261
|
+
const slowTriggered = slowBurn >= slowMult;
|
|
262
|
+
if (fastTriggered && slowTriggered) return 'PAGE';
|
|
263
|
+
if (slowTriggered) return 'TICKET';
|
|
264
|
+
if (fastTriggered) return 'WARN';
|
|
265
|
+
if (fastBurn >= 1.0 || slowBurn >= 1.0) return 'WARN';
|
|
266
|
+
return 'OK';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const fastBurn = burnFromSli(dual.fast, target);
|
|
270
|
+
const slowBurn = burnFromSli(dual.slow, target);
|
|
271
|
+
const combined = combinedStatus(fastBurn.burnRate, fastMult, slowBurn.burnRate, slowMult);
|
|
272
|
+
|
|
273
|
+
let action;
|
|
274
|
+
switch (combined) {
|
|
275
|
+
case 'PAGE':
|
|
276
|
+
action = 'Page on-call NOW — invoke /investigar-producao';
|
|
277
|
+
break;
|
|
278
|
+
case 'TICKET':
|
|
279
|
+
action = 'Open ticket — slow erosion sustained, investigate before budget exhausted';
|
|
280
|
+
break;
|
|
281
|
+
case 'WARN':
|
|
282
|
+
action = 'Monitor — burn rate ≥1× either window, sustained drains budget';
|
|
283
|
+
break;
|
|
284
|
+
case 'no_data':
|
|
285
|
+
action = '— (await more snapshots; auto-persist via metrics-snapshot tool — Phase 102)';
|
|
286
|
+
break;
|
|
287
|
+
default:
|
|
288
|
+
action = '—';
|
|
214
289
|
}
|
|
215
290
|
|
|
216
|
-
// ETA exhaustion (predictive).
|
|
217
|
-
|
|
218
|
-
const
|
|
291
|
+
// ETA exhaustion (predictive). Use slow window (more stable signal for budget extrapolation).
|
|
292
|
+
// For burn=0 (no errors), ETA is ∞.
|
|
293
|
+
const slowBurnRate = slowBurn.burnRate;
|
|
294
|
+
const baselineHours = $SLOW_BASELINE_MS / 3600000;
|
|
295
|
+
const eta = (slowBurnRate !== null && slowBurnRate > 0)
|
|
296
|
+
? (1 / slowBurnRate) * 30 * 24 / baselineHours
|
|
297
|
+
: null;
|
|
219
298
|
const etaStr = eta === null ? '—' : (eta < 24 ? eta.toFixed(1) + 'h' : (eta/24).toFixed(1) + 'd');
|
|
220
299
|
|
|
221
|
-
|
|
300
|
+
const fastBurnFmt = fastBurn.burnRate === null ? '—' : fastBurn.burnRate.toFixed(2) + '×';
|
|
301
|
+
const slowBurnFmt = slowBurn.burnRate === null ? '—' : slowBurn.burnRate.toFixed(2) + '×';
|
|
302
|
+
|
|
303
|
+
// fast_status / slow_status são derivados em isolation (informativo na tabela);
|
|
304
|
+
// combined_status é o veredito operacional.
|
|
305
|
+
function singleStatus(burn, mult) {
|
|
306
|
+
if (burn === null) return 'no_data';
|
|
307
|
+
if (burn >= mult) return mult === parseFloat('$FAST_MULTIPLIER') ? 'PAGE-FAST' : 'TICKET-SLOW';
|
|
308
|
+
if (burn >= 1.0) return 'WARN';
|
|
309
|
+
return 'OK';
|
|
310
|
+
}
|
|
311
|
+
const fastStatus = singleStatus(fastBurn.burnRate, fastMult);
|
|
312
|
+
const slowStatus = singleStatus(slowBurn.burnRate, slowMult);
|
|
313
|
+
|
|
314
|
+
console.log(JSON.stringify({
|
|
315
|
+
fast_burn: fastBurnFmt,
|
|
316
|
+
slow_burn: slowBurnFmt,
|
|
317
|
+
fast_status: fastStatus,
|
|
318
|
+
slow_status: slowStatus,
|
|
319
|
+
combined_status: combined,
|
|
320
|
+
action: action,
|
|
321
|
+
eta: etaStr,
|
|
322
|
+
}));
|
|
222
323
|
")
|
|
223
324
|
```
|
|
224
325
|
|
|
225
|
-
### 3.
|
|
326
|
+
### 3.6 Acumular linha da tabela (colunas dual-window)
|
|
226
327
|
|
|
227
328
|
```bash
|
|
228
|
-
SLO_ROWS+=("| $SLO_NAME | ${TARGET_RATIO:-${TARGET_MS}ms p$PERCENTILE} | $
|
|
329
|
+
SLO_ROWS+=("| $SLO_NAME | ${TARGET_RATIO:-${TARGET_MS}ms p$PERCENTILE} | ${FAST_BURN} | ${SLOW_BURN} | **${COMBINED_STATUS}** | $ETA | $ACTION |")
|
|
229
330
|
```
|
|
230
331
|
|
|
231
|
-
## 4. Renderizar tabela mestra
|
|
332
|
+
## 4. Renderizar tabela mestra (Phase 103 dual-window)
|
|
232
333
|
|
|
233
334
|
```text
|
|
234
335
|
═══════════════════════════════════════════════════════════
|
|
235
|
-
framework
|
|
236
|
-
|
|
336
|
+
framework ▸ BURN-RATE-STATUS (dual-window) ▸ {timestamp}
|
|
337
|
+
fast_baseline=$FAST_BASELINE slow_baseline=$SLOW_BASELINE
|
|
338
|
+
fast_multiplier=$FAST_MULTIPLIER (page) slow_multiplier=$SLOW_MULTIPLIER (ticket)
|
|
339
|
+
snapshots fast=$FAST_COUNT slow=$SLOW_COUNT
|
|
237
340
|
═══════════════════════════════════════════════════════════
|
|
238
341
|
|
|
239
|
-
| SLO | Target |
|
|
240
|
-
|
|
342
|
+
| SLO | Target | Fast (1h) | Slow (6h) | Combined | ETA exhaustão | Ação |
|
|
343
|
+
|---|---|---|---|---|---|---|
|
|
241
344
|
{$SLO_ROWS}
|
|
242
345
|
```
|
|
243
346
|
|
|
347
|
+
**Exemplo concreto:**
|
|
348
|
+
|
|
349
|
+
```markdown
|
|
350
|
+
| SLO | Target | Fast (1h) | Slow (6h) | Combined | ETA exhaustão | Ação |
|
|
351
|
+
|---|---|---|---|---|---|---|
|
|
352
|
+
| mcp-tool-availability | 99.5% | 0.42× OK | 0.18× OK | **OK** | — | — |
|
|
353
|
+
| mcp-tool-latency | 200ms p95 | 16.0× PAGE-FAST | 8.5× TICKET-SLOW | **PAGE** | 4.2h | Page on-call NOW — invoke /investigar-producao |
|
|
354
|
+
```
|
|
355
|
+
|
|
244
356
|
## 5. Sugerir próximas ações
|
|
245
357
|
|
|
246
358
|
```bash
|
|
247
359
|
# Contar status counts
|
|
248
|
-
PAGE_COUNT=$(echo "$SLO_ROWS" | grep -c "PAGE" || echo 0)
|
|
249
|
-
TICKET_COUNT=$(echo "$SLO_ROWS" | grep -c "TICKET" || echo 0)
|
|
250
|
-
WARN_COUNT=$(echo "$SLO_ROWS" | grep -c "WARN" || echo 0)
|
|
251
|
-
NO_DATA_COUNT=$(echo "$SLO_ROWS" | grep -c "no_data" || echo 0)
|
|
360
|
+
PAGE_COUNT=$(echo "$SLO_ROWS" | grep -c "\*\*PAGE\*\*" || echo 0)
|
|
361
|
+
TICKET_COUNT=$(echo "$SLO_ROWS" | grep -c "\*\*TICKET\*\*" || echo 0)
|
|
362
|
+
WARN_COUNT=$(echo "$SLO_ROWS" | grep -c "\*\*WARN\*\*" || echo 0)
|
|
363
|
+
NO_DATA_COUNT=$(echo "$SLO_ROWS" | grep -c "\*\*no_data\*\*" || echo 0)
|
|
252
364
|
```
|
|
253
365
|
|
|
254
366
|
Output:
|
|
@@ -256,37 +368,41 @@ Output:
|
|
|
256
368
|
## Próximas ações
|
|
257
369
|
|
|
258
370
|
{Se PAGE_COUNT > 0:}
|
|
259
|
-
⚠ {PAGE_COUNT} SLO(s) em PAGE — invocar /investigar-producao "<slo_name> burn
|
|
371
|
+
⚠ {PAGE_COUNT} SLO(s) em PAGE (ambas janelas críticas) — invocar /investigar-producao "<slo_name> dual-window burn"
|
|
260
372
|
|
|
261
373
|
{Se TICKET_COUNT > 0:}
|
|
262
|
-
☐ {TICKET_COUNT} SLO(s) em TICKET — abrir issue, investigar antes do budget esgotar
|
|
374
|
+
☐ {TICKET_COUNT} SLO(s) em TICKET (slow erosion sustained) — abrir issue, investigar antes do budget esgotar
|
|
263
375
|
|
|
264
376
|
{Se WARN_COUNT > 0:}
|
|
265
|
-
ⓘ {WARN_COUNT} SLO(s) em WARN —
|
|
377
|
+
ⓘ {WARN_COUNT} SLO(s) em WARN — fast spike isolado ou mild burn ≥ 1× (não page; monitor)
|
|
266
378
|
|
|
267
379
|
{Se NO_DATA_COUNT > 0:}
|
|
268
|
-
⊘ {NO_DATA_COUNT} SLO(s) sem dados
|
|
380
|
+
⊘ {NO_DATA_COUNT} SLO(s) sem dados em pelo menos uma janela — Phase 102 auto-persist deve popular .planning/metrics/snapshots/ automaticamente em chamadas ao MCP tool 'metrics-snapshot'
|
|
269
381
|
```
|
|
270
382
|
|
|
271
383
|
## 6. Modo `/loop` (idempotência)
|
|
272
384
|
|
|
273
385
|
Se chamado dentro de `/loop`, comportamento idempotente:
|
|
274
386
|
- Snapshot fresh em cada invocação (não acumular state).
|
|
275
|
-
- Output curto se
|
|
276
|
-
- Acionar AskUserQuestion APENAS quando algum SLO transiciona OK → WARN/TICKET/PAGE
|
|
387
|
+
- Output curto se `combined_status` não mudou (apenas linha-resumo; sem repetir tabela completa).
|
|
388
|
+
- Acionar AskUserQuestion APENAS quando algum SLO transiciona OK → WARN/TICKET/PAGE no `combined_status`.
|
|
277
389
|
|
|
278
390
|
</process>
|
|
279
391
|
|
|
280
392
|
<success_criteria>
|
|
281
|
-
- [ ] $ARGUMENTS parseados (SLO opcional + flags --
|
|
393
|
+
- [ ] $ARGUMENTS parseados (SLO opcional + flags --fast-baseline/--slow-baseline/--format)
|
|
282
394
|
- [ ] SLOs descobertos via glob `.planning/slos/*.yml` (FIX Phase 99: extension `.yml`, não `.md`)
|
|
283
|
-
- [ ]
|
|
284
|
-
- [ ]
|
|
285
|
-
- [ ]
|
|
286
|
-
- [ ]
|
|
287
|
-
- [ ]
|
|
288
|
-
- [ ]
|
|
289
|
-
- [ ]
|
|
290
|
-
- [ ]
|
|
291
|
-
- [ ]
|
|
395
|
+
- [ ] alert_thresholds.page (fast) + alert_thresholds.ticket (slow) extraídos via awk com state machine
|
|
396
|
+
- [ ] Defensive defaults aplicados (14.4 / 6 / 1h / 6h) se YAML omitir blocos
|
|
397
|
+
- [ ] Snapshots carregados via `loadSnapshots()` em DUAS chamadas (fast + slow)
|
|
398
|
+
- [ ] SLI calculado por tipo (event-based ratio para availability, percentile para latency) em CADA janela
|
|
399
|
+
- [ ] Burn rate calculado pela fórmula `error_rate / (1 - target)` (skill [`burn-rate-alerting`](../skills/burn-rate-alerting/SKILL.md)) para fast E slow independentemente
|
|
400
|
+
- [ ] Status combinado dual-window: **PAGE** (ambos críticos) / **TICKET** (slow only) / **WARN** (fast only OR mild ≥ 1×) / **OK** (ambos < 1×) / **no_data** (qualquer janela com snapshots insuficientes)
|
|
401
|
+
- [ ] Tabela markdown agregada com colunas Fast (1h) / Slow (6h) / Combined explícitas
|
|
402
|
+
- [ ] ETA exhaustão computada (predictive forecast — usa slow window por estabilidade)
|
|
403
|
+
- [ ] Sugestões de próximas ações contextualizadas pelo combined_status
|
|
404
|
+
- [ ] Idempotente em /loop (sem acúmulo de state; transição combined_status dispara AskUserQuestion)
|
|
405
|
+
- [ ] no_data graceful — Phase 102 auto-persist mencionado como solução
|
|
406
|
+
- [ ] Skill burn-rate-alerting cross-referenced no objective + inline no node script + inline na tabela de fallback (3 hits ≥ 2 mínimo)
|
|
407
|
+
- [ ] Fator 4× explícito no contexto (canonical Google SRE)
|
|
292
408
|
</success_criteria>
|