qualia-framework 2.4.9 → 2.5.1

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.
@@ -1 +1 @@
1
- 2.4.9
1
+ 2.5.1
@@ -0,0 +1,429 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Qualia Report — DOCX Generator
4
+
5
+ Reads JSON report data from stdin, produces a branded DOCX file.
6
+
7
+ Usage:
8
+ echo '{"project": "...", ...}' | python3 generate-report-docx.py /path/to/output.docx
9
+
10
+ Input JSON schema:
11
+ {
12
+ "project": "project-name",
13
+ "user": "Fawzi Goussous",
14
+ "date": "2026-04-06",
15
+ "time": "14:30",
16
+ "branch": "main",
17
+ "duration": "~3 hours",
18
+ "phase": "Phase 3 of 6 — Auth Integration",
19
+ "done": [...],
20
+ "deviations": [...],
21
+ "blockers": [...],
22
+ "next": [...],
23
+ "commits": [...]
24
+ }
25
+ """
26
+
27
+ import json
28
+ import sys
29
+ import os
30
+ from datetime import datetime
31
+
32
+ from docx import Document
33
+ from docx.shared import Inches, Pt, Cm, RGBColor, Emu
34
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
35
+ from docx.enum.table import WD_TABLE_ALIGNMENT
36
+ from docx.oxml.ns import qn, nsdecls
37
+ from docx.oxml import parse_xml
38
+
39
+ # ─── Brand constants ─────────────────────────────────────────────────────────
40
+
41
+ TEAL = RGBColor(0x2A, 0xBF, 0xBF)
42
+ TEAL_DARK = RGBColor(0x1E, 0x96, 0x96)
43
+ DARK = RGBColor(0x1A, 0x1A, 0x2E)
44
+ CHARCOAL = RGBColor(0x2D, 0x2D, 0x3F)
45
+ GRAY = RGBColor(0x6B, 0x7B, 0x8D)
46
+ LIGHT_GRAY = RGBColor(0x9A, 0xA5, 0xB0)
47
+ WHITE = RGBColor(0xFF, 0xFF, 0xFF)
48
+
49
+ TEAL_HEX = "2ABFBF"
50
+ TEAL_DARK_HEX = "1E9696"
51
+ DARK_HEX = "1A1A2E"
52
+ INFO_BG = "F0FAFA"
53
+ COMMIT_BG = "F5F7F9"
54
+
55
+ LOGO_PATH = os.path.join(os.path.dirname(__file__), "..", "assets", "qualia-logo.png")
56
+
57
+ FONT_BODY = "Segoe UI"
58
+ FONT_HEADING = "Segoe UI Semibold"
59
+ FONT_MONO = "Cascadia Code"
60
+
61
+
62
+ # ─── Helpers ──────────────────────────────────────────────────────────────────
63
+
64
+ def remove_table_borders(table):
65
+ """Remove all borders from a table."""
66
+ tbl = table._tbl
67
+ tblPr = tbl.tblPr if tbl.tblPr is not None else parse_xml(f'<w:tblPr {nsdecls("w")}/>')
68
+ borders = parse_xml(
69
+ f'<w:tblBorders {nsdecls("w")}>'
70
+ f' <w:top w:val="none" w:sz="0" w:space="0" w:color="auto"/>'
71
+ f' <w:left w:val="none" w:sz="0" w:space="0" w:color="auto"/>'
72
+ f' <w:bottom w:val="none" w:sz="0" w:space="0" w:color="auto"/>'
73
+ f' <w:right w:val="none" w:sz="0" w:space="0" w:color="auto"/>'
74
+ f' <w:insideH w:val="none" w:sz="0" w:space="0" w:color="auto"/>'
75
+ f' <w:insideV w:val="none" w:sz="0" w:space="0" w:color="auto"/>'
76
+ f'</w:tblBorders>'
77
+ )
78
+ tblPr.append(borders)
79
+
80
+
81
+ def set_cell_shading(cell, color_hex):
82
+ """Set background color on a table cell."""
83
+ shading = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{color_hex}"/>')
84
+ cell._tc.get_or_add_tcPr().append(shading)
85
+
86
+
87
+ def set_cell_margins(cell, top=0, bottom=0, left=100, right=100):
88
+ """Set cell margins in twips."""
89
+ tc = cell._tc
90
+ tcPr = tc.get_or_add_tcPr()
91
+ tcMar = parse_xml(
92
+ f'<w:tcMar {nsdecls("w")}>'
93
+ f' <w:top w:w="{top}" w:type="dxa"/>'
94
+ f' <w:bottom w:w="{bottom}" w:type="dxa"/>'
95
+ f' <w:start w:w="{left}" w:type="dxa"/>'
96
+ f' <w:end w:w="{right}" w:type="dxa"/>'
97
+ f'</w:tcMar>'
98
+ )
99
+ tcMar_old = tcPr.find(qn('w:tcMar'))
100
+ if tcMar_old is not None:
101
+ tcPr.remove(tcMar_old)
102
+ tcPr.append(tcMar)
103
+
104
+
105
+ def add_teal_rule(doc, weight=6):
106
+ """Add a teal horizontal line."""
107
+ p = doc.add_paragraph()
108
+ p.paragraph_format.space_before = Pt(2)
109
+ p.paragraph_format.space_after = Pt(8)
110
+ pPr = p._p.get_or_add_pPr()
111
+ pBdr = parse_xml(
112
+ f'<w:pBdr {nsdecls("w")}>'
113
+ f' <w:bottom w:val="single" w:sz="{weight}" w:space="1" w:color="{TEAL_HEX}"/>'
114
+ f'</w:pBdr>'
115
+ )
116
+ pPr.append(pBdr)
117
+
118
+
119
+ def add_section_heading(doc, text):
120
+ """Add a premium section heading with teal left accent."""
121
+ p = doc.add_paragraph()
122
+ p.paragraph_format.space_before = Pt(20)
123
+ p.paragraph_format.space_after = Pt(10)
124
+ # Left border accent
125
+ pPr = p._p.get_or_add_pPr()
126
+ pBdr = parse_xml(
127
+ f'<w:pBdr {nsdecls("w")}>'
128
+ f' <w:left w:val="single" w:sz="18" w:space="8" w:color="{TEAL_HEX}"/>'
129
+ f' <w:bottom w:val="single" w:sz="2" w:space="4" w:color="E0E0E0"/>'
130
+ f'</w:pBdr>'
131
+ )
132
+ pPr.append(pBdr)
133
+ run = p.add_run(text)
134
+ run.font.name = FONT_HEADING
135
+ run.font.size = Pt(13)
136
+ run.font.color.rgb = CHARCOAL
137
+ run.font.bold = True
138
+ return p
139
+
140
+
141
+ def add_bullet(doc, text):
142
+ """Add a styled bullet point with teal bullet."""
143
+ p = doc.add_paragraph()
144
+ p.paragraph_format.space_after = Pt(5)
145
+ p.paragraph_format.left_indent = Cm(0.8)
146
+ # Teal bullet
147
+ bullet_run = p.add_run(" \u25B8 ")
148
+ bullet_run.font.name = FONT_BODY
149
+ bullet_run.font.size = Pt(10)
150
+ bullet_run.font.color.rgb = TEAL
151
+ # Text
152
+ text_run = p.add_run(text)
153
+ text_run.font.name = FONT_BODY
154
+ text_run.font.size = Pt(10.5)
155
+ text_run.font.color.rgb = DARK
156
+ return p
157
+
158
+
159
+ def add_spacer(doc, pts=6):
160
+ """Add vertical spacing."""
161
+ p = doc.add_paragraph()
162
+ p.paragraph_format.space_before = Pt(0)
163
+ p.paragraph_format.space_after = Pt(pts)
164
+ run = p.add_run("")
165
+ run.font.size = Pt(2)
166
+
167
+
168
+ # ─── Document builder ────────────────────────────────────────────────────────
169
+
170
+ def build_report(data, output_path):
171
+ doc = Document()
172
+
173
+ # ── Page setup ──
174
+ for section in doc.sections:
175
+ section.top_margin = Cm(1.8)
176
+ section.bottom_margin = Cm(1.5)
177
+ section.left_margin = Cm(2.5)
178
+ section.right_margin = Cm(2.5)
179
+
180
+ # ── Page border ──
181
+ for section in doc.sections:
182
+ sectPr = section._sectPr
183
+ pgBorders = parse_xml(
184
+ f'<w:pgBorders {nsdecls("w")} w:offsetFrom="page">'
185
+ f' <w:top w:val="single" w:sz="4" w:space="24" w:color="{TEAL_HEX}"/>'
186
+ f' <w:left w:val="single" w:sz="4" w:space="24" w:color="{TEAL_HEX}"/>'
187
+ f' <w:bottom w:val="single" w:sz="4" w:space="24" w:color="{TEAL_HEX}"/>'
188
+ f' <w:right w:val="single" w:sz="4" w:space="24" w:color="{TEAL_HEX}"/>'
189
+ f'</w:pgBorders>'
190
+ )
191
+ sectPr.append(pgBorders)
192
+
193
+ # ══════════════════════════════════════════════════════════════════════════
194
+ # HEADER — Logo left, Title + subtitle right
195
+ # ══════════════════════════════════════════════════════════════════════════
196
+
197
+ header_table = doc.add_table(rows=1, cols=2)
198
+ header_table.alignment = WD_TABLE_ALIGNMENT.CENTER
199
+ header_table.autofit = True
200
+ remove_table_borders(header_table)
201
+
202
+ # Set column widths
203
+ header_table.columns[0].width = Cm(4)
204
+ header_table.columns[1].width = Cm(12)
205
+
206
+ # Logo
207
+ logo_cell = header_table.cell(0, 0)
208
+ logo_p = logo_cell.paragraphs[0]
209
+ logo_p.alignment = WD_ALIGN_PARAGRAPH.LEFT
210
+ if os.path.exists(LOGO_PATH):
211
+ logo_p.add_run().add_picture(LOGO_PATH, width=Cm(2.5))
212
+
213
+ # Title block
214
+ title_cell = header_table.cell(0, 1)
215
+ # "Session Report" — large teal
216
+ title_p = title_cell.paragraphs[0]
217
+ title_p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
218
+ run = title_p.add_run("Session Report")
219
+ run.font.name = FONT_HEADING
220
+ run.font.size = Pt(26)
221
+ run.font.color.rgb = TEAL
222
+ run.font.bold = True
223
+ title_p.paragraph_format.space_after = Pt(2)
224
+
225
+ # "QUALIA SOLUTIONS" — subtle uppercase
226
+ sub_p = title_cell.add_paragraph()
227
+ sub_p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
228
+ sub_p.paragraph_format.space_after = Pt(0)
229
+ run = sub_p.add_run("QUALIA SOLUTIONS")
230
+ run.font.name = FONT_BODY
231
+ run.font.size = Pt(9)
232
+ run.font.color.rgb = LIGHT_GRAY
233
+ run.font.bold = True
234
+ # Letter spacing
235
+ rPr = run._r.get_or_add_rPr()
236
+ spacing = parse_xml(f'<w:spacing {nsdecls("w")} w:val="60"/>')
237
+ rPr.append(spacing)
238
+
239
+ add_teal_rule(doc, weight=12)
240
+
241
+ # ══════════════════════════════════════════════════════════════════════════
242
+ # INFO CARD — teal-tinted background, 2-column layout
243
+ # ══════════════════════════════════════════════════════════════════════════
244
+
245
+ report_date = data.get("date", datetime.now().strftime("%Y-%m-%d"))
246
+ report_time = data.get("time", datetime.now().strftime("%H:%M"))
247
+ user_name = data.get("user", "—")
248
+
249
+ info_left = [
250
+ ("Project", data.get("project", "—")),
251
+ ("Prepared by", user_name),
252
+ ]
253
+ if data.get("phase"):
254
+ info_left.append(("Phase", data["phase"]))
255
+
256
+ info_right = [
257
+ ("Date", report_date),
258
+ ("Time", report_time),
259
+ ("Branch", data.get("branch", "—")),
260
+ ("Duration", data.get("duration", "—")),
261
+ ]
262
+
263
+ max_rows = max(len(info_left), len(info_right))
264
+ info_table = doc.add_table(rows=max_rows, cols=4)
265
+ info_table.alignment = WD_TABLE_ALIGNMENT.CENTER
266
+ remove_table_borders(info_table)
267
+
268
+ # Shade entire info card
269
+ for row in info_table.rows:
270
+ for cell in row.cells:
271
+ set_cell_shading(cell, INFO_BG)
272
+ set_cell_margins(cell, top=40, bottom=40, left=120, right=60)
273
+
274
+ # Fill left columns
275
+ for i, (label, value) in enumerate(info_left):
276
+ # Label
277
+ cell_l = info_table.cell(i, 0)
278
+ cell_l.width = Cm(2.8)
279
+ p = cell_l.paragraphs[0]
280
+ p.paragraph_format.space_after = Pt(1)
281
+ run = p.add_run(label)
282
+ run.font.name = FONT_BODY
283
+ run.font.size = Pt(9)
284
+ run.font.color.rgb = GRAY
285
+ run.font.bold = True
286
+ # Value
287
+ cell_v = info_table.cell(i, 1)
288
+ cell_v.width = Cm(5)
289
+ p = cell_v.paragraphs[0]
290
+ p.paragraph_format.space_after = Pt(1)
291
+ run = p.add_run(value)
292
+ run.font.name = FONT_BODY
293
+ run.font.size = Pt(10)
294
+ run.font.color.rgb = DARK
295
+ run.font.bold = True if label == "Project" else False
296
+
297
+ # Fill right columns
298
+ for i, (label, value) in enumerate(info_right):
299
+ cell_l = info_table.cell(i, 2)
300
+ cell_l.width = Cm(2.2)
301
+ p = cell_l.paragraphs[0]
302
+ p.paragraph_format.space_after = Pt(1)
303
+ run = p.add_run(label)
304
+ run.font.name = FONT_BODY
305
+ run.font.size = Pt(9)
306
+ run.font.color.rgb = GRAY
307
+ run.font.bold = True
308
+ cell_v = info_table.cell(i, 3)
309
+ cell_v.width = Cm(4)
310
+ p = cell_v.paragraphs[0]
311
+ p.paragraph_format.space_after = Pt(1)
312
+ run = p.add_run(value)
313
+ run.font.name = FONT_BODY
314
+ run.font.size = Pt(10)
315
+ run.font.color.rgb = DARK
316
+
317
+ add_spacer(doc, 4)
318
+
319
+ # ══════════════════════════════════════════════════════════════════════════
320
+ # BODY SECTIONS
321
+ # ══════════════════════════════════════════════════════════════════════════
322
+
323
+ # ── What Was Done ──
324
+ done_items = data.get("done", [])
325
+ if done_items:
326
+ add_section_heading(doc, "What Was Done")
327
+ for item in done_items:
328
+ add_bullet(doc, item)
329
+
330
+ # ── Deviations (only if any) ──
331
+ deviations = data.get("deviations", [])
332
+ if deviations:
333
+ add_section_heading(doc, "Deviations")
334
+ for item in deviations:
335
+ add_bullet(doc, item)
336
+
337
+ # ── Blockers (only if any) ──
338
+ blockers = data.get("blockers", [])
339
+ if blockers:
340
+ add_section_heading(doc, "Blockers")
341
+ for item in blockers:
342
+ add_bullet(doc, item)
343
+
344
+ # ── Commits (compact card) ──
345
+ commits = data.get("commits", [])
346
+ if commits:
347
+ add_section_heading(doc, "Commits")
348
+ # Render in a shaded card
349
+ commit_table = doc.add_table(rows=1, cols=1)
350
+ commit_table.alignment = WD_TABLE_ALIGNMENT.LEFT
351
+ remove_table_borders(commit_table)
352
+ commit_cell = commit_table.cell(0, 0)
353
+ set_cell_shading(commit_cell, COMMIT_BG)
354
+ set_cell_margins(commit_cell, top=80, bottom=80, left=160, right=160)
355
+ # First commit in existing paragraph
356
+ p = commit_cell.paragraphs[0]
357
+ p.paragraph_format.space_after = Pt(2)
358
+ run = p.add_run(commits[0])
359
+ run.font.name = FONT_MONO
360
+ run.font.size = Pt(8.5)
361
+ run.font.color.rgb = GRAY
362
+ # Remaining commits
363
+ for c in commits[1:10]:
364
+ p = commit_cell.add_paragraph()
365
+ p.paragraph_format.space_after = Pt(2)
366
+ run = p.add_run(c)
367
+ run.font.name = FONT_MONO
368
+ run.font.size = Pt(8.5)
369
+ run.font.color.rgb = GRAY
370
+ if len(commits) > 10:
371
+ p = commit_cell.add_paragraph()
372
+ run = p.add_run(f" ... and {len(commits) - 10} more")
373
+ run.font.name = FONT_BODY
374
+ run.font.size = Pt(8)
375
+ run.font.color.rgb = LIGHT_GRAY
376
+ run.font.italic = True
377
+
378
+ # ── Next Steps ──
379
+ next_items = data.get("next", [])
380
+ if next_items:
381
+ add_section_heading(doc, "Next Steps")
382
+ for item in next_items:
383
+ add_bullet(doc, item)
384
+
385
+ # ══════════════════════════════════════════════════════════════════════════
386
+ # FOOTER
387
+ # ══════════════════════════════════════════════════════════════════════════
388
+
389
+ add_spacer(doc, 16)
390
+ add_teal_rule(doc, weight=4)
391
+
392
+ footer_p = doc.add_paragraph()
393
+ footer_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
394
+ footer_p.paragraph_format.space_after = Pt(0)
395
+
396
+ run = footer_p.add_run("Qualia Solutions")
397
+ run.font.name = FONT_HEADING
398
+ run.font.size = Pt(8)
399
+ run.font.color.rgb = TEAL
400
+ run.font.bold = True
401
+
402
+ run = footer_p.add_run(f" \u00B7 {report_date} {report_time}")
403
+ run.font.name = FONT_BODY
404
+ run.font.size = Pt(8)
405
+ run.font.color.rgb = LIGHT_GRAY
406
+
407
+ tagline_p = doc.add_paragraph()
408
+ tagline_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
409
+ run = tagline_p.add_run("qualia.solutions")
410
+ run.font.name = FONT_BODY
411
+ run.font.size = Pt(7)
412
+ run.font.color.rgb = LIGHT_GRAY
413
+
414
+ # ── Save ──
415
+ doc.save(output_path)
416
+ return output_path
417
+
418
+
419
+ # ─── Main ─────────────────────────────────────────────────────────────────────
420
+
421
+ if __name__ == "__main__":
422
+ if len(sys.argv) < 2:
423
+ print("Usage: echo '{...}' | python3 generate-report-docx.py /path/to/output.docx", file=sys.stderr)
424
+ sys.exit(1)
425
+
426
+ output_path = sys.argv[1]
427
+ data = json.load(sys.stdin)
428
+ result = build_report(data, output_path)
429
+ print(f"Report saved: {result}")
@@ -1,217 +1,166 @@
1
1
  ---
2
2
  name: qualia-report
3
- description: "Generate a session work report for owner reviewwhat was assigned, what was done, what deviated, what's next. Use this skill whenever the user says 'report', 'done for today', 'session report', 'log my work', 'what did I do', 'end of day', 'wrapping up', 'signing off', or finishes a working session. Also trigger when user mentions 'daily report', 'status update', 'work summary', or wants to document what they accomplished."
3
+ description: "Generate a session work report as a branded DOCX simple summary of what was done. Use this skill whenever the user says 'report', 'done for today', 'session report', 'log my work', 'what did I do', 'end of day', 'wrapping up', 'signing off', or finishes a working session. Also trigger when user mentions 'daily report', 'status update', 'work summary', or wants to document what they accomplished."
4
4
  ---
5
5
 
6
- # Qualia Report — Session Work Report
6
+ # Qualia Report — Session DOCX Report
7
7
 
8
- Generate a structured report of what was done in this session. This is for the owner to review assigned vs done, deviations, blockers, and next steps.
8
+ Generate a clean, branded DOCX report of what was done in this session. Simple and clearwhat happened, not every detail.
9
9
 
10
10
  ## Usage
11
11
 
12
- `/qualia-report` — Generate report for current session
13
- `/qualia-report --week` — Aggregate reports from the last 7 days
14
-
15
- ## When to Run
16
-
17
- The framework should suggest this when:
18
- - Employee says "done for today", "stopping", "wrapping up", "signing off"
19
- - Employee has been working for 1+ hours and mentions pausing
20
- - A phase is completed or verified
21
- - Before running `/qualia-pause-work` (report first, then pause)
12
+ `/qualia-report` — Generate DOCX report for current session
13
+ `/qualia-report --week` — Aggregate weekly DOCX report
22
14
 
23
15
  ## Process
24
16
 
25
17
  ### 1. Gather Session Data
26
18
 
27
- **From git (what actually changed):**
19
+ Run a **single** bash command to collect everything:
20
+
28
21
  ```bash
29
- # Commits made this session (last N hours or since last report)
30
- LAST_REPORT=$(ls -t .planning/reports/report-*.md 2>/dev/null | head -1)
22
+ echo "---GIT---"
23
+ LAST_REPORT=$(ls -t .planning/reports/report-*.docx .planning/reports/report-*.md 2>/dev/null | head -1)
31
24
  if [ -n "$LAST_REPORT" ]; then
32
- SINCE=$(grep -m1 "^Date:" "$LAST_REPORT" | sed 's/^Date: *//')
25
+ SINCE_DATE=$(stat -c %Y "$LAST_REPORT" 2>/dev/null)
26
+ SINCE=$(date -d "@$SINCE_DATE" '+%Y-%m-%d %H:%M' 2>/dev/null || echo "8 hours ago")
33
27
  else
34
28
  SINCE="8 hours ago"
35
29
  fi
36
- git log --oneline --since="$SINCE" --author="$(git config user.name)" 2>/dev/null
37
- git diff --stat HEAD~5..HEAD 2>/dev/null | tail -5
38
- ```
39
-
40
- **From STATE.md (where the project stands):**
41
- - Current phase and status
42
- - Progress percentage
43
- - Assigned to whom
44
-
45
- **From ROADMAP.md (what was planned):**
46
- - Which phase was being worked on
47
- - What the phase goal is
48
- - What the success criteria are
49
-
50
- **From SUMMARY.md files (if any were created this session):**
51
- - Phase execution summaries
52
- - Deviations noted
53
- - Decisions made
54
-
55
- **From UAT.md files (if any were created this session):**
56
- - Test results
57
- - Issues found
58
-
59
- **From git diff (what files changed):**
60
- ```bash
61
- git diff --name-only HEAD~5..HEAD 2>/dev/null | head -30
30
+ echo "SINCE:$SINCE"
31
+ echo "---COMMITS---"
32
+ git log --oneline --since="$SINCE" 2>/dev/null | head -20
33
+ echo "---STATS---"
34
+ COMMIT_COUNT=$(git log --oneline --since="$SINCE" 2>/dev/null | wc -l)
35
+ echo "COUNT:$COMMIT_COUNT"
36
+ FIRST=$(git log --format="%ar" --since="$SINCE" 2>/dev/null | tail -1)
37
+ LAST=$(git log --format="%ar" --since="$SINCE" 2>/dev/null | head -1)
38
+ echo "FIRST:$FIRST"
39
+ echo "LAST:$LAST"
40
+ echo "---PROJECT---"
41
+ echo "DIR:$(basename $(pwd))"
42
+ echo "BRANCH:$(git branch --show-current 2>/dev/null)"
43
+ echo "---STATE---"
44
+ test -f .planning/STATE.md && head -20 .planning/STATE.md || echo "no-state"
45
+ echo "---PHASE---"
46
+ test -f .planning/ROADMAP.md && grep -A2 "^## Phase" .planning/ROADMAP.md | head -6 || echo "no-roadmap"
62
47
  ```
63
48
 
64
- ### 2. Analyze: Assigned vs Done
65
-
66
- Compare what ROADMAP.md says the current phase should deliver against what was actually built:
67
-
68
- - **Planned:** [from ROADMAP.md phase goal + success criteria]
69
- - **Completed:** [from commits + SUMMARY.md]
70
- - **Partially done:** [started but not finished]
71
- - **Not started:** [planned but untouched]
72
- - **Unplanned work:** [things done that weren't in the plan]
73
-
74
- ### 3. Generate Report
75
-
76
- Create `.planning/reports/report-{YYYY-MM-DD-HHMM}.md`:
77
-
78
- ```markdown
79
- # Session Report
80
-
81
- **Project:** {from PROJECT.md or directory name}
82
- **Date:** {YYYY-MM-DD HH:MM}
83
- **Employee:** {from STATE.md "Assigned to" or git config user.name}
84
- **Duration:** {estimated from first to last commit, or "~X hours"}
85
- **Phase:** {N} of {total} — {phase name}
86
-
87
- ## What Was Assigned
88
-
89
- {Phase goal from ROADMAP.md}
90
-
91
- Success criteria:
92
- 1. {criterion 1}
93
- 2. {criterion 2}
94
- 3. {criterion 3}
95
-
96
- ## What Was Done
97
-
98
- {Summary of actual work, derived from commits and file changes}
99
-
100
- - {accomplishment 1}
101
- - {accomplishment 2}
102
- - {accomplishment 3}
103
-
104
- ### Files Changed
105
- {Top 15 files modified, grouped by area}
106
-
107
- ### Commits
108
- {List of commits this session}
109
-
110
- ## Progress
111
-
112
- | Criterion | Status |
113
- |-----------|--------|
114
- | {criterion 1} | Done / Partial / Not started |
115
- | {criterion 2} | Done / Partial / Not started |
116
- | {criterion 3} | Done / Partial / Not started |
117
-
118
- **Phase progress:** {X}% → {Y}% (moved {delta}%)
119
- **Overall project:** {A}% → {B}%
49
+ ### 2. Synthesize Keep It Simple
120
50
 
121
- ## Deviations
51
+ From the gathered data, build a **concise** summary. The report should answer:
122
52
 
123
- {Anything done differently from the plan and why}
53
+ 1. **What was done?** 3-6 bullet points summarizing accomplishments in plain language. Group related commits into single items. Don't list every file changed.
54
+ 2. **Any deviations?** — Only include if something was done differently from the plan. Skip this section entirely if everything went as planned.
55
+ 3. **Any blockers?** — Only include if something is actually blocked. Skip if none.
56
+ 4. **What's next?** — 1-3 clear next actions.
124
57
 
125
- - {deviation 1}: {reason}
58
+ **Tone:** Write for a busy founder reviewing work. Clear, factual, no filler. Each bullet should start with a verb: "Built...", "Fixed...", "Added...", "Refactored..."
126
59
 
127
- {If no deviations: "None — followed the plan."}
60
+ ### 3. Generate DOCX
128
61
 
129
- ## Blockers & Issues
62
+ Build a JSON object and pipe it to the generator:
130
63
 
131
- {Anything that slowed down or blocked work}
132
-
133
- - {blocker 1}: {status — resolved / still blocking}
134
-
135
- {If none: "None — clear session."}
136
-
137
- ## Decisions Made
138
-
139
- {Any choices that affect future work}
140
-
141
- | Decision | Why | Impact |
142
- |----------|-----|--------|
143
- | {choice} | {reason} | {what it affects} |
64
+ ```bash
65
+ mkdir -p .planning/reports
144
66
 
145
- ## Next Session
67
+ cat <<'REPORT_JSON' | python3 ~/.claude/qualia-framework/bin/generate-report-docx.py ".planning/reports/report-$(date +%Y-%m-%d-%H%M).docx"
68
+ {
69
+ "project": "<project-name>",
70
+ "user": "<git config user.name or 'Fawzi Goussous'>",
71
+ "date": "<YYYY-MM-DD>",
72
+ "time": "<HH:MM>",
73
+ "branch": "<current-branch>",
74
+ "duration": "<estimated from first to last commit, e.g. ~3 hours>",
75
+ "phase": "<Phase N of M — name, or omit if no .planning>",
76
+ "done": [
77
+ "<accomplishment 1>",
78
+ "<accomplishment 2>",
79
+ "<accomplishment 3>"
80
+ ],
81
+ "deviations": [],
82
+ "blockers": [],
83
+ "next": [
84
+ "<next action 1>",
85
+ "<next action 2>"
86
+ ],
87
+ "commits": [
88
+ "<hash> <message>",
89
+ "<hash> <message>"
90
+ ]
91
+ }
92
+ REPORT_JSON
93
+ ```
146
94
 
147
- **What to do next:**
148
- 1. {immediate next action}
149
- 2. {following action}
95
+ **Important:** The JSON must be valid. Escape any quotes in commit messages. Omit `deviations` and `blockers` arrays if empty (the generator handles missing keys gracefully).
150
96
 
151
- **Suggested command:** `/qualia-plan-phase {N}` or `/qualia-execute-phase {N}` etc.
97
+ ### 4. Commit & Auto-Upload to ERP
152
98
 
153
- ---
154
- *Generated by /qualia-report at {timestamp}*
99
+ ```bash
100
+ # Commit the report
101
+ git add .planning/reports/report-*.docx
102
+ git commit -m "report: session report $(date +%Y-%m-%d) — $(basename $(pwd))"
155
103
  ```
156
104
 
157
- ### 4. Commit Report
105
+ Then **auto-upload** the report to the ERP so it's linked to the employee's active work session. This is mandatory and happens automatically — employees cannot skip or edit the report before it reaches the ERP.
158
106
 
159
107
  ```bash
160
- mkdir -p .planning/reports
161
- git add .planning/reports/report-{timestamp}.md
162
- git commit -m "report: session report {date} — {employee name}"
163
- ```
164
-
165
- ### 5. Update STATE.md
108
+ # Auto-upload to ERP
109
+ REPORT_FILE=$(ls -t .planning/reports/report-*.docx 2>/dev/null | head -1)
110
+ EMPLOYEE_EMAIL=$(git config user.email)
111
+ PROJECT_NAME=$(basename $(pwd))
112
+ ERP_URL="https://portal.qualiasolutions.net/api/claude/report-upload"
113
+ API_KEY=$(cat ~/.claude/.env 2>/dev/null | grep CLAUDE_API_KEY | cut -d= -f2 || echo "")
114
+
115
+ if [ -z "$API_KEY" ]; then
116
+ # Try from project env
117
+ API_KEY=$(grep CLAUDE_API_KEY .env.local 2>/dev/null | cut -d= -f2 || echo "")
118
+ fi
166
119
 
167
- Update the `Last activity` and `Last worked by` fields in STATE.md.
120
+ if [ -n "$REPORT_FILE" ] && [ -n "$API_KEY" ]; then
121
+ UPLOAD_RESULT=$(curl -s -X POST "$ERP_URL" \
122
+ -H "x-api-key: $API_KEY" \
123
+ -F "file=@$REPORT_FILE" \
124
+ -F "employee_email=$EMPLOYEE_EMAIL" \
125
+ -F "project_name=$PROJECT_NAME")
126
+ echo "ERP upload: $UPLOAD_RESULT"
127
+ else
128
+ echo "WARNING: Could not auto-upload report to ERP (missing API key or report file)"
129
+ fi
130
+ ```
168
131
 
169
- ### 6. Suggest Next Steps
132
+ Then tell the user:
170
133
 
171
- > "Report saved to `.planning/reports/report-{date}.md`"
134
+ > Report saved to `.planning/reports/report-{date}.docx` and **auto-uploaded to ERP**.
172
135
  >
173
- > "Want to also save session context? Run `/qualia-pause-work`"
174
- > "Ready to keep going? Run `/qualia-plan-phase {N}`"
175
-
176
- ## Weekly Aggregate (`--week`)
177
-
178
- When run with `--week`, reads all reports from the last 7 days and produces a summary:
179
-
180
- ```markdown
181
- # Weekly Report — {project name}
182
-
183
- **Period:** {date} to {date}
184
- **Employee:** {name}
185
-
186
- ## Summary
187
-
188
- - **Sessions:** {count}
189
- - **Phases progressed:** Phase {X} → Phase {Y}
190
- - **Overall progress:** {A}% → {B}%
191
- - **Commits:** {total}
192
-
193
- ## Per-Session Breakdown
136
+ > Your report is now linked to your active work session. When you clock out, it will already be attached.
137
+ >
138
+ > Want to save session context too? Run `/qualia-pause-work`
194
139
 
195
- | Date | Phase | Progress | Key Accomplishment |
196
- |------|-------|----------|-------------------|
197
- | {date} | Phase 3 | +15% | Built auth flow |
198
- | {date} | Phase 3 | +10% | Added RLS policies |
199
- | {date} | Phase 4 | +5% | Started chat UI |
140
+ ### 5. Update STATE.md (if exists)
200
141
 
201
- ## Cumulative Deviations
142
+ Update `Last activity` and `Last worked by` fields.
202
143
 
203
- {Any patterns repeated blockers, scope changes, etc.}
144
+ ## Weekly Report (`--week`)
204
145
 
205
- ## Blockers Still Open
146
+ When run with `--week`:
206
147
 
207
- {Unresolved from any session}
148
+ 1. Read all reports from the last 7 days (check `.planning/reports/`)
149
+ 2. Read git log for the full week
150
+ 3. Build an aggregate JSON with:
151
+ - `"project"` — project name
152
+ - `"date"` — current date with "Weekly" suffix
153
+ - `"duration"` — "Week of {date} to {date}"
154
+ - `"done"` — combined accomplishments from all sessions, deduplicated
155
+ - `"next"` — forward-looking items from the most recent report
156
+ - `"commits"` — full week's commits
157
+ 4. Pipe to the same generator, save as `weekly-{date}.docx`
208
158
 
209
- ## Next Week
159
+ ## Generator Location
210
160
 
211
- {What's ahead based on ROADMAP.md}
212
- ```
161
+ `~/.claude/qualia-framework/bin/generate-report-docx.py`
213
162
 
214
- Save to `.planning/reports/weekly-{date}.md`.
163
+ Requires: `python-docx` (pre-installed). Uses Qualia logo from `~/.claude/qualia-framework/assets/qualia-logo.png`.
215
164
 
216
165
  ---
217
166
  > Stuck? Type `/qualia-idk` · Lost? Type `/qualia-help`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qualia-framework",
3
- "version": "2.4.9",
3
+ "version": "2.5.1",
4
4
  "description": "Qualia Solutions — Claude Code Framework",
5
5
  "bin": {
6
6
  "qualia-framework": "./bin/cli.js"