siesa-agents 2.1.71 → 2.1.73

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siesa-agents",
3
- "version": "2.1.71",
3
+ "version": "2.1.73",
4
4
  "description": "Paquete para instalar y configurar agentes SIESA en tu proyecto",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -30,7 +30,42 @@ Output to the user:
30
30
 
31
31
  ---
32
32
 
33
- ## 2. MANDATORY RULE PHASE COMMIT AT WORKFLOW END
33
+ ## 2. INITIALIZATIONCHECK FOR EXISTING UX DESIGN DOCUMENT
34
+
35
+ Before executing any workflow step, search for an existing UX design document:
36
+
37
+ 1. Look for a file matching `*ux-design-specification*.md` inside the `{planning_artifacts}/` folder.
38
+ - Glob pattern to check: `{planning_artifacts}/*ux-design-specification*.md`
39
+
40
+ ---
41
+
42
+ ## 3. DECISION BRANCH
43
+
44
+ ### If `ux-design-specification.md` EXISTS
45
+
46
+ Use the **AskUserQuestion** tool to present the following options (respect `communication_language` from config):
47
+
48
+ > Se encontró un documento de especificación UX/UI existente en `{planning_artifacts}/ux-design-specification.md`.
49
+ >
50
+ > ¿Qué deseas hacer?
51
+ >
52
+ > **[1] Editar** — Abrir el documento para revisarlo y modificarlo de forma colaborativa.
53
+ > **[2] Resumen** — Generar un resumen ejecutivo del documento actual.
54
+
55
+ Wait for the user's selection and act accordingly:
56
+
57
+ - **Option 1 — Edit:** Read the full contents of `{planning_artifacts}/ux-design-specification.md`, present it to the user section by section, and facilitate collaborative editing. Apply changes directly to the file using the Edit tool.
58
+ - **Option 2 — Summary:** Read the full contents of `{planning_artifacts}/ux-design-specification.md` and produce a concise executive summary covering: design vision, target users, design system decisions, visual foundations, component strategy, and any open design questions.
59
+
60
+ ---
61
+
62
+ ### If `ux-design-specification.md` does NOT EXIST
63
+
64
+ LOAD the FULL `@_bmad/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md`, READ its entire contents and follow its directions exactly!
65
+
66
+ ---
67
+
68
+ ## 4. MANDATORY RULE — PHASE COMMIT AT WORKFLOW END
34
69
 
35
70
  **TRIGGER:** Immediately after all workflow steps complete and the UX Design document has been generated/saved.
36
71
 
@@ -39,6 +39,8 @@ Before doing anything else, search for an existing architecture document:
39
39
 
40
40
  ### If `architecture.md` EXISTS
41
41
 
42
+ Use the **AskUserQuestion** tool to present the following options:
43
+
42
44
  Present the user with the following options (respect `communication_language` from config):
43
45
 
44
46
  > Se encontró un documento de arquitectura existente en `{planning_artifacts}/architecture.md`.
@@ -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.
@@ -1,3 +1,45 @@
1
+ # REGLA OBLIGATORIA: MENÚ DE INICIO
2
+
3
+ **TRIGGER:** Al inicio del modo `interactive`, **antes del Step 1**.
4
+
5
+ Invocar `AskUserQuestion` con la siguiente configuración:
6
+
7
+ ```json
8
+ {
9
+ "questions": [
10
+ {
11
+ "question": "¿Qué deseas ejecutar?",
12
+ "header": "Modo",
13
+ "multiSelect": false,
14
+ "options": [
15
+ {
16
+ "label": "Sprint Status + Retrospective",
17
+ "description": "Ejecuta el resumen del sprint y encadena automáticamente con el workflow de Retrospective al finalizar."
18
+ },
19
+ {
20
+ "label": "Solo Sprint Status",
21
+ "description": "Ejecuta únicamente el resumen del sprint sin encadenar con Retrospective."
22
+ }
23
+ ]
24
+ }
25
+ ]
26
+ }
27
+ ```
28
+
29
+ - Si elige `Sprint Status + Retrospective`: guardar `run_retrospective = true`
30
+ - Si elige `Solo Sprint Status`: guardar `run_retrospective = false`
31
+
32
+ **Al finalizar el Step 5** (después de que el usuario elija su acción), si `run_retrospective == true`:
33
+
34
+ ```
35
+ ---
36
+ ➡️ Encadenando con Retrospective tal como seleccionaste al inicio...
37
+ ```
38
+
39
+ Luego invocar `/bmad:bmm:workflows:retrospective`.
40
+
41
+ ---
42
+
1
43
  # REGLA OBLIGATORIA: SPRINT-STATUS CON SINCRONIZACIÓN DE FEATURE-STATUS
2
44
 
3
45
  **TRIGGER:** Cada vez que se ejecute `/sprint-status`.
@@ -43,6 +43,13 @@ Copy the file `@_siesa-agents/resources/architecture/architecture-both.md ` to `
43
43
  - Do not modify the file contents — copy as-is.
44
44
  - After copying, confirm the file exists at the destination.
45
45
 
46
+ Create base UX/UI specifications
47
+ Copy the file `@_siesa-agents/resources/ux-ui/ux-design-specification.md` to `{planning_artifacts}/ux-design-specification.md.md`.
48
+
49
+ - If the destination directory does not exist, create it first.
50
+ - Do not modify the file contents — copy as-is.
51
+ - After copying, confirm the file exists at the destination.
52
+
46
53
  Check and clone Frontend:
47
54
  ```bash
48
55
  # Only run if apps/Frontend does NOT exist
@@ -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
+ })()