siesa-agents 2.1.72 → 2.1.74
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/skills/delivery-sa-agent-guides/SKILL.md +162 -0
- package/mcp.json +7 -0
- package/package.json +1 -1
- package/siesa-agents/bmm/workflows/4-implementation/code-review/workflow_ext.md +80 -0
- package/siesa-agents/bmm/workflows/4-implementation/create-story/workflow_ext.md +35 -1
- package/siesa-agents/bmm/workflows/4-implementation/dev-story/workflow_ext.md +35 -1
- package/siesa-agents/observability/README.md +81 -0
- package/siesa-agents/observability/scripts/sa-emit.js +223 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: delivery-sa-agent-guides
|
|
3
|
+
description: Generate client-ready delivery guides from user-guide documents indexed in the mcp-siesa-docs MCP. Use this skill when a delivery team member needs to transform user-guide content into simple, end-user friendly material. Always trigger on /delivery-agent-guides. Also trigger when the user says "generar guía de entrega", "guía para el cliente", "documentación de entrega para [project]", "guía para usuario final", or describes wanting to turn indexed user-guide docs into something a non-technical client can understand.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Delivery Agent Guides
|
|
7
|
+
|
|
8
|
+
Transform user-guide documents (from the `mcp-siesa-docs` MCP) into a clear, client-ready delivery guide — plain language, no jargon, for non-technical end users.
|
|
9
|
+
|
|
10
|
+
The MCP is the only source of content. The local filesystem is not scanned.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Flow
|
|
15
|
+
|
|
16
|
+
### Step 1: Discover projects with user-guides (automatic)
|
|
17
|
+
|
|
18
|
+
As soon as the skill is invoked, without asking the user first:
|
|
19
|
+
|
|
20
|
+
1. Call `list_collections()` to confirm `mcp-siesa-docs` is available.
|
|
21
|
+
2. Call `search_docs("user guide proyectos disponibles")` to find projects whose indexed docs include a `user-guide` directory.
|
|
22
|
+
3. Build the candidate list: **only include projects that have at least one document in their `user-guide` directory**. Projects with only feature specs, épicas, stories, or architecture docs are excluded (Option A).
|
|
23
|
+
4. Present the list to the user:
|
|
24
|
+
```
|
|
25
|
+
Proyectos con guías de usuario disponibles:
|
|
26
|
+
[1] {project-name-1}
|
|
27
|
+
[2] {project-name-2}
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
¿Para cuál proyecto quieres generar la guía de entrega?
|
|
31
|
+
```
|
|
32
|
+
5. The user replies with a number or project name.
|
|
33
|
+
|
|
34
|
+
If zero projects qualify, tell the user plainly ("No encontré proyectos con user-guide indexado en el MCP") and stop.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
### Step 2: Pull the user-guide content from the MCP
|
|
39
|
+
|
|
40
|
+
1. `search_docs("user guide [proyecto seleccionado]")` to list the user-guide documents of the chosen project.
|
|
41
|
+
2. For each result, call `get_document(path)` to fetch the full content.
|
|
42
|
+
3. **Strict filter**: process only documents whose `path` is inside the project's `user-guide` directory. Ignore feature specs, épicas, stories, architecture, or any other technical doc, even if they show up in results.
|
|
43
|
+
|
|
44
|
+
Keep the list of `path`s used — you will cite them in the final confirmation.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### Step 3: Write the delivery guide
|
|
49
|
+
|
|
50
|
+
Write one unified `delivery-guide.md`. All content in Spanish. Audience: non-technical end user.
|
|
51
|
+
|
|
52
|
+
**Content to keep and rewrite in plain language:**
|
|
53
|
+
- Feature name and purpose
|
|
54
|
+
- Step-by-step workflows
|
|
55
|
+
- Key concepts and field definitions (simplified)
|
|
56
|
+
- Troubleshooting and FAQs
|
|
57
|
+
- Warnings (⚠️) — rephrase without technical detail; keep the practical implication
|
|
58
|
+
|
|
59
|
+
**Content to transform:**
|
|
60
|
+
- Mermaid diagrams → rewrite as one or two plain sentences describing the flow. Never emit Mermaid syntax.
|
|
61
|
+
|
|
62
|
+
**Content to omit:**
|
|
63
|
+
- `📸 [Screenshot: ...]` placeholders
|
|
64
|
+
- `[Source: FX Story Y.Z]` traceability references
|
|
65
|
+
- Screenshot index tables
|
|
66
|
+
- Implementation component names (ContactManager, EmptyState, ErrorPanel, etc.) — describe the behavior instead
|
|
67
|
+
- `*Generado: ...` footers
|
|
68
|
+
|
|
69
|
+
**Output template:**
|
|
70
|
+
|
|
71
|
+
```markdown
|
|
72
|
+
---
|
|
73
|
+
project: {project-name}
|
|
74
|
+
generated_date: {YYYY-MM-DD}
|
|
75
|
+
sources:
|
|
76
|
+
- {mcp-path-1}
|
|
77
|
+
- {mcp-path-2}
|
|
78
|
+
audience: Usuario final no técnico
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
# Guía de {Nombre del Proyecto}
|
|
82
|
+
|
|
83
|
+
## ¿Qué es {Nombre del Proyecto}?
|
|
84
|
+
2-3 sentences. What the app does and who it is for. No technical terms.
|
|
85
|
+
|
|
86
|
+
## ¿Para qué sirve?
|
|
87
|
+
What problems it solves, from the user's perspective.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## {Feature 1}
|
|
92
|
+
|
|
93
|
+
### ¿Qué es?
|
|
94
|
+
One paragraph. Plain description.
|
|
95
|
+
|
|
96
|
+
### ¿Para qué sirve?
|
|
97
|
+
The practical benefit to the user.
|
|
98
|
+
|
|
99
|
+
### ¿Cómo se usa?
|
|
100
|
+
Numbered step-by-step. One clear action per step.
|
|
101
|
+
Include warnings inline (⚠️ ...) directly before the step that can cause an issue.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## {Feature 2}
|
|
106
|
+
(same structure)
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Solución de Problemas
|
|
111
|
+
Plain Q&A: "¿Qué hago si...? → ..."
|
|
112
|
+
|
|
113
|
+
## Preguntas Frecuentes
|
|
114
|
+
Plain Q&A. No technical language.
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Writing guidelines:**
|
|
118
|
+
- Target a reader who is comfortable with a smartphone but has no software background.
|
|
119
|
+
- Active voice, direct instructions: "Haz clic en...", "Escribe...", "Selecciona..."
|
|
120
|
+
- Short sentences. If a sentence needs a comma to explain a technical concept, rewrite it.
|
|
121
|
+
- If a concept has no practical value to the end user, drop it.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
### Step 4: Save and confirm
|
|
126
|
+
|
|
127
|
+
Save to:
|
|
128
|
+
```
|
|
129
|
+
_bmad-output/documentation-artifacts/delivery-guides/{project-name}/delivery-guide.md
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Create the directory if it doesn't exist.
|
|
133
|
+
|
|
134
|
+
Then confirm:
|
|
135
|
+
```
|
|
136
|
+
Guía guardada en: _bmad-output/documentation-artifacts/delivery-guides/{project-name}/delivery-guide.md
|
|
137
|
+
|
|
138
|
+
Features incluidas:
|
|
139
|
+
- {feature 1}
|
|
140
|
+
- {feature 2}
|
|
141
|
+
|
|
142
|
+
Fuentes del MCP:
|
|
143
|
+
- {mcp-path-1}
|
|
144
|
+
- {mcp-path-2}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Transformation Reference
|
|
150
|
+
|
|
151
|
+
| Technical element | Delivery guide treatment |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `deep linking` / URL única por registro | "Cada registro tiene su propia dirección web que puedes guardar o compartir" |
|
|
154
|
+
| `ContactManager`, `EmptyState`, `ErrorPanel` | Drop the name; describe what the user sees |
|
|
155
|
+
| `NIT/RUC` | Keep — explain once: "número de identificación tributaria (NIT/RUC)" |
|
|
156
|
+
| `Toast` notification | "mensaje de confirmación que aparece brevemente en pantalla" |
|
|
157
|
+
| Reintentar button on error | "si aparece un error, haz clic en Reintentar" |
|
|
158
|
+
| Panel de dos columnas | "La pantalla se divide en dos partes: a la izquierda la lista, a la derecha el detalle" |
|
|
159
|
+
| Validación inline | "mensajes de error junto al campo que necesita corrección" |
|
|
160
|
+
| Mermaid code block | One or two plain sentences describing the flow |
|
|
161
|
+
| `[Source: FX Story Y.Z]` | Omit |
|
|
162
|
+
| `📸 [Screenshot: ...]` | Omit |
|
package/mcp.json
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"mcpServers": {
|
|
3
|
+
"mcp-siesa-docs": {
|
|
4
|
+
"type": "http",
|
|
5
|
+
"url": "https://platform-architecture-mcp-siesa-103767536676.us-east1.run.app/mcp",
|
|
6
|
+
"headers": {
|
|
7
|
+
"Authorization": "Bearer none"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
3
10
|
"atlassian": {
|
|
4
11
|
"type": "sse",
|
|
5
12
|
"url": "https://mcp.atlassian.com/v1/sse"
|
package/package.json
CHANGED
|
@@ -139,3 +139,83 @@ Update frontmatter of `{outputFile}`: `stepsCompleted: [1, 2, 3, 4, 5, 6]`
|
|
|
139
139
|
Output: `✅ Jira sync complete. Proceeding to commit...`
|
|
140
140
|
|
|
141
141
|
Then **immediately load, read, and execute step-07-commit-push.md** — no menu, no waiting for user input.
|
|
142
|
+
|
|
143
|
+
# MANDATORY RULE: OBSERVABILITY EVENTS
|
|
144
|
+
|
|
145
|
+
**TRIGGER:** Every time the code-review workflow is executed.
|
|
146
|
+
|
|
147
|
+
**PURPOSE:** Emit lifecycle events to measure workflow duration per story. Events are sent to Loki for observability dashboards.
|
|
148
|
+
|
|
149
|
+
**SCRIPT PATH:** `{project_root}/_siesa-agents/observability/scripts/sa-emit.js`
|
|
150
|
+
|
|
151
|
+
## 4. After step-01 identifies the story (before proceeding to step-02)
|
|
152
|
+
|
|
153
|
+
Once `{{story_key}}` has been determined in step-01-load-story, and BEFORE proceeding to step-02, execute via Bash:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
node "{project_root}/_siesa-agents/observability/scripts/sa-emit.js" --event workflow.started --story "{{story_key}}" --phase code-review
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## 4.5. In step-04, when the user selects a fix option (before executing it)
|
|
160
|
+
|
|
161
|
+
When the user selects one of the fix options **[1], [2], or [3]**, emit `fix.started` BEFORE executing the option, then emit `fix.finished` AFTER it completes:
|
|
162
|
+
|
|
163
|
+
**When user selects [1] Fix automatically:**
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
node "{project_root}/_siesa-agents/observability/scripts/sa-emit.js" --event fix.started --story "{{story_key}}" --phase code-review --fix-option auto_fix
|
|
167
|
+
```
|
|
168
|
+
*(execute the fix)* then:
|
|
169
|
+
```bash
|
|
170
|
+
node "{project_root}/_siesa-agents/observability/scripts/sa-emit.js" --event fix.finished --story "{{story_key}}" --phase code-review --fix-option auto_fix
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**When user selects [2] Create Action Items:**
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
node "{project_root}/_siesa-agents/observability/scripts/sa-emit.js" --event fix.started --story "{{story_key}}" --phase code-review --fix-option action_items
|
|
177
|
+
```
|
|
178
|
+
*(create the action items)* then:
|
|
179
|
+
```bash
|
|
180
|
+
node "{project_root}/_siesa-agents/observability/scripts/sa-emit.js" --event fix.finished --story "{{story_key}}" --phase code-review --fix-option action_items
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**When user selects [3] Show Details:**
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
node "{project_root}/_siesa-agents/observability/scripts/sa-emit.js" --event fix.started --story "{{story_key}}" --phase code-review --fix-option show_details
|
|
187
|
+
```
|
|
188
|
+
*(show the details)* then:
|
|
189
|
+
```bash
|
|
190
|
+
node "{project_root}/_siesa-agents/observability/scripts/sa-emit.js" --event fix.finished --story "{{story_key}}" --phase code-review --fix-option show_details
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## 5. In step-05, after updating sprint-status.yaml
|
|
194
|
+
|
|
195
|
+
After step-05-sync-sprint updates `sprint-status.yaml`, emit the transition based on the actual new status determined by the review outcome:
|
|
196
|
+
|
|
197
|
+
**If the review PASSED (story transitions to `done`):**
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
node "{project_root}/_siesa-agents/observability/scripts/sa-emit.js" --event status.changed --story "{{story_key}}" --phase code-review --from review --to done
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**If the review FAILED and story goes back to `in-progress` (rework):**
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
node "{project_root}/_siesa-agents/observability/scripts/sa-emit.js" --event status.changed --story "{{story_key}}" --phase code-review --from review --to in-progress
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## 6. In step-07, at the very end of the workflow (after commit/push completes)
|
|
210
|
+
|
|
211
|
+
At the end of step-07-commit-push, after the final output, execute via Bash:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
node "{project_root}/_siesa-agents/observability/scripts/sa-emit.js" --event workflow.finished --story "{{story_key}}" --phase code-review
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Observability execution rules
|
|
218
|
+
|
|
219
|
+
- If any `sa-emit` call fails, log the error and **continue the workflow normally**. Observability must never block the workflow.
|
|
220
|
+
- Use the exact `{{story_key}}` value as resolved in step-01 (e.g., `1-1-user-authentication`).
|
|
221
|
+
- Do NOT wait for user confirmation to execute these commands — they are silent background operations.
|
|
@@ -42,4 +42,38 @@ The following rules must be kept in memory and applied at the appropriate moment
|
|
|
42
42
|
Sub-agents **must NOT be used** when the code review workflow is invoked directly (e.g., `/create-story`). Sub-agents are only permitted when the workflow is triggered as part of **quick-dev** (e.g., `/quick-dev`).
|
|
43
43
|
|
|
44
44
|
- If invoked directly → follow the standard sequential workflow steps without spawning any sub-agents.
|
|
45
|
-
- If invoked from quick-dev → sub-agents may be used as defined by the quick-dev orchestration.
|
|
45
|
+
- If invoked from quick-dev → sub-agents may be used as defined by the quick-dev orchestration.
|
|
46
|
+
|
|
47
|
+
# MANDATORY RULE: OBSERVABILITY EVENTS
|
|
48
|
+
|
|
49
|
+
**TRIGGER:** Every time the create-story workflow is executed.
|
|
50
|
+
|
|
51
|
+
**PURPOSE:** Emit lifecycle events to measure workflow duration per story. Events are sent to Loki for observability dashboards.
|
|
52
|
+
|
|
53
|
+
**SCRIPT PATH:** `{project_root}/_siesa-agents/observability/scripts/sa-emit.js`
|
|
54
|
+
|
|
55
|
+
## 2. After step-01 identifies the story (before presenting the menu)
|
|
56
|
+
|
|
57
|
+
Once `{{story_key}}` has been determined (sections 1-3 of step-01), and BEFORE presenting the menu options (section 4), execute via Bash:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
node "{project_root}/_siesa-agents/observability/scripts/sa-emit.js" --event workflow.started --story "{{story_key}}" --phase create-story
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 3. In step-06, after updating sprint-status.yaml (before the Final Report)
|
|
64
|
+
|
|
65
|
+
After the sprint-status.yaml update (section 1) completes and BEFORE the Final Report (section 3), execute these two commands in sequence via Bash:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
node "{project_root}/_siesa-agents/observability/scripts/sa-emit.js" --event status.changed --story "{{story_key}}" --phase create-story --from backlog --to ready-for-dev
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
node "{project_root}/_siesa-agents/observability/scripts/sa-emit.js" --event workflow.finished --story "{{story_key}}" --phase create-story
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Observability execution rules
|
|
76
|
+
|
|
77
|
+
- If any `sa-emit` call fails, log the error and **continue the workflow normally**. Observability must never block the workflow.
|
|
78
|
+
- Use the exact `{{story_key}}` value as resolved in step-01 (e.g., `1-1-user-authentication`).
|
|
79
|
+
- Do NOT wait for user confirmation to execute these commands — they are silent background operations.
|
|
@@ -106,4 +106,38 @@ Example: `develop-legacy-erp-nomina-gaduranb-rq1234-nueva-interfaz`
|
|
|
106
106
|
Sub-agents **must NOT be used** when the code review workflow is invoked directly (e.g., `/dev-story`). Sub-agents are only permitted when the workflow is triggered as part of **quick-dev** (e.g., `/quick-dev`).
|
|
107
107
|
|
|
108
108
|
- If invoked directly → follow the standard sequential workflow steps without spawning any sub-agents.
|
|
109
|
-
- If invoked from quick-dev → sub-agents may be used as defined by the quick-dev orchestration.
|
|
109
|
+
- If invoked from quick-dev → sub-agents may be used as defined by the quick-dev orchestration.
|
|
110
|
+
|
|
111
|
+
# MANDATORY RULE: OBSERVABILITY EVENTS
|
|
112
|
+
|
|
113
|
+
**TRIGGER:** Every time the dev-story workflow is executed.
|
|
114
|
+
|
|
115
|
+
**PURPOSE:** Emit lifecycle events to measure workflow duration per story. Events are sent to Loki for observability dashboards.
|
|
116
|
+
|
|
117
|
+
**SCRIPT PATH:** `{project_root}/_siesa-agents/observability/scripts/sa-emit.js`
|
|
118
|
+
|
|
119
|
+
## 5. After step-01 identifies the story (before presenting the menu)
|
|
120
|
+
|
|
121
|
+
Once `{{story_key}}` has been determined in step-01-find-story, and BEFORE presenting the menu or proceeding to step-02, execute via Bash:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
node "{project_root}/_siesa-agents/observability/scripts/sa-emit.js" --event workflow.started --story "{{story_key}}" --phase dev-story
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## 6. In step-11, after marking the story as review in sprint-status.yaml
|
|
128
|
+
|
|
129
|
+
After step-11-mark-review updates `sprint-status.yaml` (`in-progress → review`), and BEFORE proceeding to step-12, execute these two commands in sequence via Bash:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
node "{project_root}/_siesa-agents/observability/scripts/sa-emit.js" --event status.changed --story "{{story_key}}" --phase dev-story --from in-progress --to review
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
node "{project_root}/_siesa-agents/observability/scripts/sa-emit.js" --event workflow.finished --story "{{story_key}}" --phase dev-story
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Observability execution rules
|
|
140
|
+
|
|
141
|
+
- If any `sa-emit` call fails, log the error and **continue the workflow normally**. Observability must never block the workflow.
|
|
142
|
+
- Use the exact `{{story_key}}` value as resolved in step-01 (e.g., `1-1-user-authentication`).
|
|
143
|
+
- Do NOT wait for user confirmation to execute these commands — they are silent background operations.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Observability Module
|
|
2
|
+
|
|
3
|
+
Instrumentación de los workflows BMAD para medir el tiempo real por historia de usuario.
|
|
4
|
+
|
|
5
|
+
## Qué hace
|
|
6
|
+
|
|
7
|
+
Cada workflow (`create-story`, `dev-story`, `code-review`) emite **3 eventos** a Loki via el script `sa-emit.js`:
|
|
8
|
+
|
|
9
|
+
1. `workflow.started` — al inicio del workflow, después de identificar la historia
|
|
10
|
+
2. `status.changed` — cuando cambia el estado en `sprint-status.yaml`
|
|
11
|
+
3. `workflow.finished` — al finalizar el workflow (incluye `duration_ms`)
|
|
12
|
+
|
|
13
|
+
## Script: `sa-emit.js`
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Inicio de workflow
|
|
17
|
+
node sa-emit.js --event workflow.started --story "1-1-user-auth" --phase "create-story"
|
|
18
|
+
|
|
19
|
+
# Transición de estado
|
|
20
|
+
node sa-emit.js --event status.changed --story "1-1-user-auth" --phase "create-story" --from "backlog" --to "ready-for-dev"
|
|
21
|
+
|
|
22
|
+
# Fin de workflow (calcula duration_ms automáticamente)
|
|
23
|
+
node sa-emit.js --event workflow.finished --story "1-1-user-auth" --phase "create-story"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Parámetros
|
|
27
|
+
|
|
28
|
+
| Parámetro | Requerido | Valores |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `--event` | Sí | `workflow.started`, `workflow.finished`, `status.changed`, `fix.started`, `fix.finished` |
|
|
31
|
+
| `--story` | Sí | Story key (e.g. `1-1-user-auth`) |
|
|
32
|
+
| `--phase` | Sí | `create-story`, `dev-story`, `code-review` |
|
|
33
|
+
| `--from` | Solo en transition | Estado origen (e.g. `backlog`) |
|
|
34
|
+
| `--to` | Solo en transition | Estado destino (e.g. `ready-for-dev`) |
|
|
35
|
+
| `--fix-option` | Solo en fix.* | `auto_fix`, `action_items`, `show_details` |
|
|
36
|
+
|
|
37
|
+
### Configuración
|
|
38
|
+
|
|
39
|
+
| Variable de entorno | Default | Descripción |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `SA_OTLP_ENDPOINT` | `http://localhost:4318` | URL base del OTel collector (OTLP HTTP) |
|
|
42
|
+
|
|
43
|
+
### Fallback
|
|
44
|
+
|
|
45
|
+
Si el OTel collector no está disponible, los eventos se guardan en `~/.claude/observability/buffer/events.jsonl` para reenvío manual.
|
|
46
|
+
|
|
47
|
+
## Estructura del evento
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"event": "workflow.finished",
|
|
52
|
+
"story_id": "1-1-user-auth",
|
|
53
|
+
"epic_id": "1",
|
|
54
|
+
"phase": "create-story",
|
|
55
|
+
"duration_ms": 1140000
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Queries en Grafana (Loki)
|
|
60
|
+
|
|
61
|
+
```logql
|
|
62
|
+
# Timeline de una historia
|
|
63
|
+
{job="bmad"} | json | story_id="1-1-user-auth"
|
|
64
|
+
|
|
65
|
+
# Duración de workflows finalizados
|
|
66
|
+
{job="bmad"} | json | event="workflow.finished"
|
|
67
|
+
|
|
68
|
+
# Historias completadas (última semana)
|
|
69
|
+
count_over_time({job="bmad"} | json | event="status.changed" | to="done" [7d])
|
|
70
|
+
|
|
71
|
+
# Interacciones del usuario en un workflow (telemetría nativa)
|
|
72
|
+
{service_name="claude-code"} | json | event_name="user_prompt" | session_id="<id>"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Transiciones por workflow
|
|
76
|
+
|
|
77
|
+
| Workflow | Transiciones emitidas |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `create-story` | `backlog → ready-for-dev` |
|
|
80
|
+
| `dev-story` | `ready-for-dev → in-progress`, `in-progress → review` |
|
|
81
|
+
| `code-review` | `review → done` o `review → in-progress` (rework) |
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// sa-emit.js — Emit BMAD workflow lifecycle events via OTLP to the OTel collector
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// node sa-emit.js --event workflow.started --story "1-1-user-auth" --phase "create-story"
|
|
6
|
+
// node sa-emit.js --event status.changed --story "1-1-user-auth" --phase "create-story" --from "backlog" --to "ready-for-dev"
|
|
7
|
+
// node sa-emit.js --event workflow.finished --story "1-1-user-auth" --phase "create-story"
|
|
8
|
+
// node sa-emit.js --event fix.started --story "1-1-user-auth" --phase "code-review" --fix-option "auto_fix"
|
|
9
|
+
// node sa-emit.js --event fix.finished --story "1-1-user-auth" --phase "code-review" --fix-option "auto_fix"
|
|
10
|
+
//
|
|
11
|
+
// Optional overrides (useful for simulations / multi-engineer scenarios):
|
|
12
|
+
// --engineer "<name>" Override git config user.name for this emit
|
|
13
|
+
// --project-id "<name>" Override the git-remote-derived project id
|
|
14
|
+
// --timestamp-offset-ms <ms> Subtract ms from the event timestamp (backdate the log record)
|
|
15
|
+
//
|
|
16
|
+
// Environment:
|
|
17
|
+
// SA_OTLP_ENDPOINT — OTel collector HTTP endpoint (default: http://localhost:4318)
|
|
18
|
+
// SA_OTLP_HEADERS — Additional headers, comma-separated: "Authorization=Bearer xxx,X-Other=yyy"
|
|
19
|
+
|
|
20
|
+
'use strict'
|
|
21
|
+
|
|
22
|
+
const fs = require('fs')
|
|
23
|
+
const path = require('path')
|
|
24
|
+
const os = require('os')
|
|
25
|
+
const { execSync } = require('child_process')
|
|
26
|
+
|
|
27
|
+
const VALID_EVENTS = ['workflow.started', 'workflow.finished', 'status.changed', 'fix.started', 'fix.finished']
|
|
28
|
+
const VALID_PHASES = ['create-story', 'dev-story', 'code-review']
|
|
29
|
+
const VALID_FIX_OPTIONS = ['auto_fix', 'action_items', 'show_details']
|
|
30
|
+
|
|
31
|
+
function parseArgs(argv) {
|
|
32
|
+
const result = {}
|
|
33
|
+
let i = 0
|
|
34
|
+
while (i < argv.length) {
|
|
35
|
+
const arg = argv[i]
|
|
36
|
+
if (arg.startsWith('--')) {
|
|
37
|
+
const key = arg.slice(2)
|
|
38
|
+
const next = argv[i + 1]
|
|
39
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
40
|
+
result[key] = next
|
|
41
|
+
i += 2
|
|
42
|
+
} else {
|
|
43
|
+
result[key] = true
|
|
44
|
+
i += 1
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
i += 1
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return result
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const args = parseArgs(process.argv.slice(2))
|
|
54
|
+
const event = args['event']
|
|
55
|
+
const story = args['story']
|
|
56
|
+
const phase = args['phase']
|
|
57
|
+
const from = args['from']
|
|
58
|
+
const to = args['to']
|
|
59
|
+
let fixOption = args['fix-option']
|
|
60
|
+
const engineerOverride = args['engineer']
|
|
61
|
+
const projectIdOverride = args['project-id']
|
|
62
|
+
const timestampOffsetMs = args['timestamp-offset-ms'] ? parseInt(args['timestamp-offset-ms'], 10) : 0
|
|
63
|
+
|
|
64
|
+
if (!event || !story || !phase) {
|
|
65
|
+
console.error('Error: --event, --story, and --phase are required')
|
|
66
|
+
console.error('Usage: node sa-emit.js --event <event> --story <story> --phase <phase> [--from <from>] [--to <to>] [--fix-option <opt>]')
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
if (!VALID_EVENTS.includes(event)) {
|
|
70
|
+
console.error(`Error: --event must be one of: ${VALID_EVENTS.join(', ')}`)
|
|
71
|
+
process.exit(1)
|
|
72
|
+
}
|
|
73
|
+
if (!VALID_PHASES.includes(phase)) {
|
|
74
|
+
console.error(`Error: --phase must be one of: ${VALID_PHASES.join(', ')}`)
|
|
75
|
+
process.exit(1)
|
|
76
|
+
}
|
|
77
|
+
if (fixOption && !VALID_FIX_OPTIONS.includes(fixOption)) {
|
|
78
|
+
console.error(`Error: --fix-option must be one of: ${VALID_FIX_OPTIONS.join(', ')}`)
|
|
79
|
+
process.exit(1)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const otlpEndpoint = process.env.SA_OTLP_ENDPOINT || 'http://localhost:4318'
|
|
83
|
+
const stateDir = path.join(os.homedir(), '.claude', 'observability')
|
|
84
|
+
|
|
85
|
+
const epicIdMatch = story.match(/^(\d+)-/)
|
|
86
|
+
const epicId = epicIdMatch ? epicIdMatch[1] : 'unknown'
|
|
87
|
+
|
|
88
|
+
let projectId = 'unknown'
|
|
89
|
+
if (projectIdOverride) {
|
|
90
|
+
projectId = projectIdOverride
|
|
91
|
+
} else {
|
|
92
|
+
try {
|
|
93
|
+
const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf8' }).trim()
|
|
94
|
+
const m = remoteUrl.match(/[:/]([^/:]+\/[^/]+?)(?:\.git)?$/)
|
|
95
|
+
if (m) projectId = m[1]
|
|
96
|
+
} catch (_) {}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let engineer = 'unknown'
|
|
100
|
+
if (engineerOverride) {
|
|
101
|
+
engineer = engineerOverride
|
|
102
|
+
} else {
|
|
103
|
+
try {
|
|
104
|
+
const gitUser = execSync('git config user.name', { encoding: 'utf8' }).trim()
|
|
105
|
+
if (gitUser) engineer = gitUser
|
|
106
|
+
} catch (_) {}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const nowMs = Date.now()
|
|
110
|
+
const eventTimeMs = nowMs - (Number.isFinite(timestampOffsetMs) ? timestampOffsetMs : 0)
|
|
111
|
+
const tsNano = `${eventTimeMs}000000`
|
|
112
|
+
|
|
113
|
+
const safeKey = story.replace(/[^a-zA-Z0-9-]/g, '')
|
|
114
|
+
const stateFile = path.join(stateDir, `wf-${phase}-${safeKey}.json`)
|
|
115
|
+
let durationMs = null
|
|
116
|
+
|
|
117
|
+
if (event === 'workflow.started') {
|
|
118
|
+
fs.mkdirSync(stateDir, { recursive: true })
|
|
119
|
+
fs.writeFileSync(stateFile, JSON.stringify({ start_ms: nowMs }))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (event === 'workflow.finished') {
|
|
123
|
+
if (fs.existsSync(stateFile)) {
|
|
124
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'))
|
|
125
|
+
durationMs = nowMs - state.start_ms
|
|
126
|
+
fs.unlinkSync(stateFile)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const fixStateFile = path.join(stateDir, `fix-${phase}-${safeKey}.json`)
|
|
131
|
+
|
|
132
|
+
if (event === 'fix.started') {
|
|
133
|
+
fs.mkdirSync(stateDir, { recursive: true })
|
|
134
|
+
fs.writeFileSync(fixStateFile, JSON.stringify({ start_ms: nowMs, fix_option: fixOption }))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (event === 'fix.finished') {
|
|
138
|
+
if (fs.existsSync(fixStateFile)) {
|
|
139
|
+
const fixState = JSON.parse(fs.readFileSync(fixStateFile, 'utf8'))
|
|
140
|
+
durationMs = nowMs - fixState.start_ms
|
|
141
|
+
if (!fixOption) fixOption = fixState.fix_option
|
|
142
|
+
fs.unlinkSync(fixStateFile)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const attributes = [
|
|
147
|
+
{ key: 'event', value: { stringValue: event } },
|
|
148
|
+
{ key: 'story_id', value: { stringValue: story } },
|
|
149
|
+
{ key: 'epic_id', value: { stringValue: epicId } },
|
|
150
|
+
{ key: 'phase', value: { stringValue: phase } },
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
if (from) attributes.push({ key: 'from', value: { stringValue: from } })
|
|
154
|
+
if (to) attributes.push({ key: 'to', value: { stringValue: to } })
|
|
155
|
+
if (fixOption) attributes.push({ key: 'fix_option', value: { stringValue: fixOption } })
|
|
156
|
+
if (durationMs !== null) attributes.push({ key: 'duration_ms', value: { intValue: durationMs } })
|
|
157
|
+
|
|
158
|
+
const otlpBody = {
|
|
159
|
+
resourceLogs: [
|
|
160
|
+
{
|
|
161
|
+
resource: {
|
|
162
|
+
attributes: [
|
|
163
|
+
{ key: 'service.name', value: { stringValue: 'bmad' } },
|
|
164
|
+
{ key: 'project_id', value: { stringValue: projectId } },
|
|
165
|
+
{ key: 'engineer', value: { stringValue: engineer } },
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
scopeLogs: [
|
|
169
|
+
{
|
|
170
|
+
scope: { name: 'bmad.observability' },
|
|
171
|
+
logRecords: [
|
|
172
|
+
{
|
|
173
|
+
timeUnixNano: tsNano,
|
|
174
|
+
observedTimeUnixNano: tsNano,
|
|
175
|
+
severityNumber: 9,
|
|
176
|
+
severityText: 'INFO',
|
|
177
|
+
body: { stringValue: `${event} | story=${story} phase=${phase} project=${projectId} engineer=${engineer}` },
|
|
178
|
+
attributes,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function parseHeaders(raw) {
|
|
188
|
+
const out = {}
|
|
189
|
+
if (!raw) return out
|
|
190
|
+
for (const pair of raw.split(',')) {
|
|
191
|
+
const idx = pair.indexOf('=')
|
|
192
|
+
if (idx === -1) continue
|
|
193
|
+
const key = pair.slice(0, idx).trim()
|
|
194
|
+
const val = pair.slice(idx + 1).trim()
|
|
195
|
+
if (key) out[key] = val
|
|
196
|
+
}
|
|
197
|
+
return out
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
;(async () => {
|
|
201
|
+
try {
|
|
202
|
+
const headers = {
|
|
203
|
+
'Content-Type': 'application/json',
|
|
204
|
+
...parseHeaders(process.env.SA_OTLP_HEADERS),
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const response = await fetch(`${otlpEndpoint}/v1/logs`, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers,
|
|
210
|
+
body: JSON.stringify(otlpBody),
|
|
211
|
+
})
|
|
212
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
213
|
+
|
|
214
|
+
let msg = `[sa-emit] ${event} | story=${story} phase=${phase} project=${projectId} engineer=${engineer}`
|
|
215
|
+
if (durationMs !== null) msg += ` duration=${durationMs}ms`
|
|
216
|
+
console.log(msg)
|
|
217
|
+
} catch (_) {
|
|
218
|
+
const bufferDir = path.join(stateDir, 'buffer')
|
|
219
|
+
fs.mkdirSync(bufferDir, { recursive: true })
|
|
220
|
+
fs.appendFileSync(path.join(bufferDir, 'events.jsonl'), JSON.stringify(otlpBody) + '\n')
|
|
221
|
+
console.log(`[sa-emit] BUFFERED (collector unavailable) | ${event} story=${story}`)
|
|
222
|
+
}
|
|
223
|
+
})()
|