@groupby/ai-dev 0.5.15 → 0.5.17

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.
Files changed (41) hide show
  1. package/package.json +1 -1
  2. package/teams/fhr-orchestration/skills/create-jira/SKILL.md +47 -0
  3. package/teams/fhr-orchestration/skills/create-jira/TEMPLATE.md +10 -0
  4. package/teams/fhr-orchestration/skills/create-plan/SKILL.md +48 -0
  5. package/teams/fhr-orchestration/skills/create-plan/TEMPLATE.md +13 -0
  6. package/teams/fhr-orchestration/skills/init-harness/SKILL.md +164 -0
  7. package/teams/fhr-orchestration/skills/init-harness/assets/TEMPLATE.CLAUDE.md +88 -0
  8. package/teams/fhr-orchestration/skills/init-harness/assets/TEMPLATE.architecture.md +152 -0
  9. package/teams/fhr-orchestration/skills/init-harness/assets/TEMPLATE.coding-guidelines.md +320 -0
  10. package/teams/fhr-orchestration/skills/init-harness/assets/TEMPLATE.glossary.md +55 -0
  11. package/teams/fhr-orchestration/skills/init-harness/assets/TEMPLATE.harness-gap-log.md +59 -0
  12. package/teams/fhr-orchestration/skills/use-case-writer/SKILL.md +123 -0
  13. package/teams/fhr-orchestration/skills/use-case-writer/TEMPLATE.md +39 -0
  14. package/teams/fhr-orchestration/skills/write-spec/SKILL.md +56 -0
  15. package/teams/fhr-orchestration/skills/write-spec/TEMPLATE.md +29 -0
  16. package/teams/firstspirit-caas/mcp/jira-tools.py +2122 -0
  17. package/teams/firstspirit-caas/mcp/test_bamboo_artifacts.py +315 -0
  18. package/teams/firstspirit-caas/mcp/test_jira_links.py +192 -0
  19. package/teams/firstspirit-caas/mcp/test_update_jira_ticket.py +161 -0
  20. package/teams/firstspirit-caas/resources/mcp-setup.md +57 -0
  21. package/teams/firstspirit-caas/skills/chronicle/SKILL.md +105 -0
  22. package/teams/firstspirit-caas/skills/create-jira-ticket/SKILL.md +183 -0
  23. package/teams/firstspirit-caas/skills/get-jira-ticket/SKILL.md +125 -0
  24. package/teams/firstspirit-caas/skills/handle-red-cve-plan/SKILL.md +316 -0
  25. package/teams/firstspirit-caas/skills/handle-red-cve-plan/references/suppression.md +79 -0
  26. package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/gen_suppression.py +116 -0
  27. package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/parse_json_report.py +183 -0
  28. package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/parse_report.py +255 -0
  29. package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/test_gen_suppression.py +171 -0
  30. package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/test_parse_json_report.py +244 -0
  31. package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/test_parse_report.py +464 -0
  32. package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/testdata/sample-report.html +39 -0
  33. package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/testdata/sample-report.json +76 -0
  34. package/teams/firstspirit-caas/skills/handle-weekly-cve-plans/SKILL.md +238 -0
  35. package/teams/firstspirit-caas/skills/retrospect/SKILL.md +94 -0
  36. package/teams/firstspirit-caas/skills/retrospect/extract_prompts.py +392 -0
  37. package/teams/firstspirit-caas/skills/review/SKILL.md +131 -0
  38. package/teams/firstspirit-caas/skills/review-finalize/SKILL.md +103 -0
  39. package/teams/firstspirit-caas/skills/specify/SKILL.md +239 -0
  40. package/teams/firstspirit-caas/skills/summarise-for-jira-comment/SKILL.md +162 -0
  41. package/teams/firstspirit-caas/skills/write-releasenotes/SKILL.md +177 -0
@@ -0,0 +1,392 @@
1
+ #!/usr/bin/env python3
2
+ """Deterministic session-transcript extractor + HTML renderer for the retrospect skill.
3
+
4
+ Two modes:
5
+
6
+ extract Parse a Claude Code session transcript (.jsonl) and emit a data JSON
7
+ holding EVERY user prompt (numbered 1..N) with a compact trace of what
8
+ followed each one. Prints a small JSON summary on stdout.
9
+
10
+ render Combine the extract data JSON with a Claude-authored analysis JSON into
11
+ the final HTML report. The script owns rendering of all N prompts, so a
12
+ prompt can never be silently dropped; a prompt missing analysis is
13
+ rendered with a visible "analysis missing" marker.
14
+
15
+ Completeness is mechanical: the prompt list comes from the transcript, not from a
16
+ model's memory.
17
+ """
18
+
19
+ import argparse
20
+ import glob
21
+ import html
22
+ import json
23
+ import os
24
+ import re
25
+ import sys
26
+ from datetime import datetime
27
+
28
+ WRAPPER_TAGS = ("system-reminder", "command-message", "command-name",
29
+ "command-args", "local-command-stdout", "local-command-stderr")
30
+
31
+ CMD_NAME_RE = re.compile(r"<command-name>(.*?)</command-name>", re.DOTALL)
32
+
33
+
34
+ def find_transcript(explicit):
35
+ if explicit:
36
+ if not os.path.isfile(explicit):
37
+ sys.exit(f"transcript not found: {explicit}")
38
+ return explicit
39
+ cwd = os.getcwd()
40
+ slug = re.sub(r"[/.]", "-", cwd)
41
+ proj_dir = os.path.expanduser(f"~/.claude/projects/{slug}")
42
+ if not os.path.isdir(proj_dir):
43
+ sys.exit(f"no project transcript dir for cwd (looked in {proj_dir}); "
44
+ f"pass --transcript explicitly")
45
+ files = glob.glob(os.path.join(proj_dir, "*.jsonl"))
46
+ if not files:
47
+ sys.exit(f"no .jsonl transcripts in {proj_dir}")
48
+ return max(files, key=os.path.getmtime)
49
+
50
+
51
+ def strip_wrappers(text):
52
+ for tag in WRAPPER_TAGS:
53
+ text = re.sub(rf"<{tag}>.*?</{tag}>", "", text, flags=re.DOTALL)
54
+ return text.strip()
55
+
56
+
57
+ def text_blocks(content):
58
+ """Return list of (text) blocks. Skip tool_result-bearing messages entirely."""
59
+ if isinstance(content, str):
60
+ return [content]
61
+ if not isinstance(content, list):
62
+ return []
63
+ if any(isinstance(b, dict) and b.get("type") == "tool_result" for b in content):
64
+ return [] # this user message is a tool result, not a human prompt
65
+ out = []
66
+ for b in content:
67
+ if isinstance(b, dict) and b.get("type") == "text":
68
+ out.append(b.get("text", ""))
69
+ return out
70
+
71
+
72
+ def summarize_assistant(content, limit=240):
73
+ texts, tools = [], []
74
+ if isinstance(content, str):
75
+ texts.append(content)
76
+ elif isinstance(content, list):
77
+ for b in content:
78
+ if not isinstance(b, dict):
79
+ continue
80
+ if b.get("type") == "text":
81
+ texts.append(b.get("text", ""))
82
+ elif b.get("type") == "tool_use":
83
+ tools.append(b.get("name", "?"))
84
+ text = " ".join(t.strip() for t in texts if t.strip())
85
+ text = re.sub(r"\s+", " ", text)
86
+ if len(text) > limit:
87
+ text = text[:limit].rstrip() + "…"
88
+ return text, tools
89
+
90
+
91
+ def parse(transcript):
92
+ """Return ordered list of prompt dicts: {n, text, trace}."""
93
+ records = []
94
+ with open(transcript, encoding="utf-8") as fh:
95
+ for line in fh:
96
+ line = line.strip()
97
+ if not line:
98
+ continue
99
+ try:
100
+ records.append(json.loads(line))
101
+ except json.JSONDecodeError:
102
+ continue
103
+
104
+ prompts = []
105
+ n = 0
106
+ i = 0
107
+ while i < len(records):
108
+ rec = records[i]
109
+ if rec.get("type") == "user" and not rec.get("isMeta"):
110
+ msg = rec.get("message") or {}
111
+ blocks = text_blocks(msg.get("content"))
112
+ raw = "\n\n".join(blocks) if blocks else ""
113
+ cleaned = strip_wrappers(raw)
114
+ if not cleaned:
115
+ cmd = CMD_NAME_RE.search(raw)
116
+ if cmd and cmd.group(1).strip():
117
+ cleaned = cmd.group(1).strip() # slash-command invocation
118
+ if cleaned:
119
+ n += 1
120
+ trace = collect_trace(records, i + 1)
121
+ prompts.append({"n": n, "text": cleaned, "trace": trace})
122
+ i += 1
123
+ return prompts
124
+
125
+
126
+ def collect_trace(records, start):
127
+ """Compact trace from `start` until the next genuine user prompt."""
128
+ assistant_texts, tool_names = [], []
129
+ j = start
130
+ while j < len(records):
131
+ rec = records[j]
132
+ rtype = rec.get("type")
133
+ if rtype == "user" and not rec.get("isMeta"):
134
+ msg = rec.get("message") or {}
135
+ if text_blocks(msg.get("content")): # next real prompt
136
+ break
137
+ if rtype == "assistant":
138
+ msg = rec.get("message") or {}
139
+ text, tools = summarize_assistant(msg.get("content"))
140
+ if text:
141
+ assistant_texts.append(text)
142
+ tool_names.extend(tools)
143
+ j += 1
144
+ summary = " ".join(assistant_texts)
145
+ summary = re.sub(r"\s+", " ", summary)
146
+ if len(summary) > 400:
147
+ summary = summary[:400].rstrip() + "…"
148
+ return {"assistant_summary": summary, "tools": tool_names}
149
+
150
+
151
+ def cmd_extract(args):
152
+ transcript = find_transcript(args.transcript)
153
+ prompts = parse(transcript)
154
+ outdir = args.outdir
155
+ os.makedirs(outdir, exist_ok=True)
156
+ stamp = datetime.now().strftime("%Y-%m-%d-%H%M")
157
+ base = os.path.join(outdir, stamp)
158
+ data_file = f"{base}-data.json"
159
+ payload = {
160
+ "transcript": transcript,
161
+ "stamp": stamp,
162
+ "count": len(prompts),
163
+ "prompts": prompts,
164
+ }
165
+ with open(data_file, "w", encoding="utf-8") as fh:
166
+ json.dump(payload, fh, indent=2)
167
+ print(json.dumps({
168
+ "data_file": data_file,
169
+ "analysis_file": f"{base}-analysis.json",
170
+ "html_file": f"{base}-retrospect.html",
171
+ "count": len(prompts),
172
+ "transcript": transcript,
173
+ }, indent=2))
174
+
175
+
176
+ def render_block(p, analysis):
177
+ a = analysis.get(str(p["n"])) or analysis.get(p["n"])
178
+ missing = a is None
179
+ problem = bool(a and a.get("problem"))
180
+ cls = "prompt problem" if problem else "prompt"
181
+ if missing:
182
+ cls = "prompt missing"
183
+ parts = [f'<section class="{cls}" id="p{p["n"]}">']
184
+ parts.append(f'<h2><span class="num">#{p["n"]}</span> '
185
+ f'<span class="badge">{ "PROBLEM" if problem else ("MISSING ANALYSIS" if missing else "OK") }</span></h2>')
186
+ parts.append('<div class="field"><span class="label">Prompt</span>'
187
+ f'<pre class="userprompt">{html.escape(p["text"])}</pre></div>')
188
+ tr = p.get("trace") or {}
189
+ if tr.get("assistant_summary"):
190
+ parts.append('<div class="field"><span class="label">Result</span>'
191
+ f'<p>{html.escape(tr["assistant_summary"])}</p></div>')
192
+ if tr.get("tools"):
193
+ parts.append('<div class="field"><span class="label">Tools</span>'
194
+ f'<p class="tools">{html.escape(", ".join(tr["tools"]))}</p></div>')
195
+ if missing:
196
+ parts.append('<div class="field warn">⚠ analysis missing — fill this prompt '
197
+ 'in the analysis JSON and re-render.</div>')
198
+ elif problem:
199
+ parts.append('<div class="field"><span class="label">What went wrong</span>'
200
+ f'<p>{html.escape(a.get("problem",""))}</p></div>')
201
+ loc = html.escape(a.get("location", ""))
202
+ parts.append('<div class="field"><span class="label">Improvement</span>'
203
+ f'<p>{html.escape(a.get("suggestion",""))}</p>'
204
+ f'{ f"<p class=loc>Target: {loc}</p>" if loc else "" }</div>')
205
+ else:
206
+ note = a.get("note") or "No issue."
207
+ parts.append(f'<div class="field"><span class="label">Note</span>'
208
+ f'<p>{html.escape(note)}</p></div>')
209
+ parts.append('</section>')
210
+ return "\n".join(parts)
211
+
212
+
213
+ CSS = """
214
+ *{box-sizing:border-box}
215
+ body{font:15px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;margin:0;color:#1a1a1a;background:#fafafa;display:flex;align-items:flex-start}
216
+ nav.toc{position:sticky;top:0;align-self:flex-start;width:260px;min-width:260px;height:100vh;overflow-y:auto;padding:1.25rem 1rem;background:#fff;border-right:1px solid #e0e0e0;font-size:13px}
217
+ nav.toc h2{font-size:12px;text-transform:uppercase;letter-spacing:.06em;color:#999;margin:.2rem 0 .6rem}
218
+ nav.toc ul{list-style:none;margin:0 0 1rem;padding:0}
219
+ nav.toc ul ul{margin:.2rem 0 .4rem .8rem}
220
+ nav.toc a{color:#333;text-decoration:none;display:block;padding:.12rem 0}
221
+ nav.toc a:hover{color:#1565c0}
222
+ nav.toc a.flag{color:#e53935}
223
+ nav.toc .grp{font-weight:600;margin-top:.5rem}
224
+ main{flex:1;max-width:900px;margin:0 auto;padding:2rem 1.5rem}
225
+ header{border-bottom:2px solid #ddd;margin-bottom:1.5rem}
226
+ h1{margin-bottom:.2rem}
227
+ .meta{color:#666;font-size:13px;margin-bottom:1rem}
228
+ .summary{background:#fff;border:1px solid #e0e0e0;border-radius:8px;padding:.75rem 1rem;margin-bottom:1rem;display:flex;align-items:center;gap:1rem;flex-wrap:wrap}
229
+ .summary b{font-size:18px}
230
+ button#toggleAll{margin-left:auto;font:13px inherit;padding:.35rem .8rem;border:1px solid #bbb;border-radius:6px;background:#f4f4f4;cursor:pointer}
231
+ button#toggleAll:hover{background:#eaeaea}
232
+ .suggestions{background:#fff;border:1px solid #e0e0e0;border-radius:8px;padding:1rem 1.25rem;margin-bottom:1.5rem}
233
+ .suggestions h2{margin-top:0}
234
+ .suggestions ol{margin:.4rem 0 0;padding-left:1.3rem}
235
+ .suggestions li{margin:.5rem 0}
236
+ .suggestions a{font-weight:700;color:#e53935;text-decoration:none}
237
+ details.group{background:transparent;border:none;margin-bottom:1.25rem}
238
+ details.group>summary{cursor:pointer;list-style:none;font-size:15px;font-weight:700;padding:.5rem .75rem;background:#eef1f4;border:1px solid #dde2e7;border-radius:8px;display:flex;align-items:center;gap:.5rem}
239
+ details.group>summary::-webkit-details-marker{display:none}
240
+ details.group>summary::before{content:"\\25B8";color:#888;font-size:12px}
241
+ details.group[open]>summary::before{content:"\\25BE"}
242
+ details.group>summary .gcount{color:#888;font-weight:500;font-size:13px}
243
+ details.group>summary .gflag{margin-left:auto;font-size:11px;font-weight:700;color:#fff;background:#e53935;border-radius:4px;padding:.1rem .45rem}
244
+ .group-body{padding:.75rem 0 0 .25rem}
245
+ section.prompt{background:#fff;border:1px solid #e0e0e0;border-left:5px solid #4caf50;border-radius:8px;padding:1rem 1.25rem;margin-bottom:1rem;scroll-margin-top:1rem}
246
+ section.problem{border-left-color:#e53935;background:#fff6f6}
247
+ section.missing{border-left-color:#fb8c00;background:#fff8ef}
248
+ section.prompt h2{font-size:16px;margin:0 0 .6rem;display:flex;align-items:center;gap:.6rem}
249
+ .num{color:#888}
250
+ .badge{font-size:11px;font-weight:700;letter-spacing:.05em;padding:.15rem .5rem;border-radius:4px;background:#4caf50;color:#fff}
251
+ .problem .badge{background:#e53935}
252
+ .missing .badge{background:#fb8c00}
253
+ .field{margin:.5rem 0}
254
+ .label{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#999;margin-bottom:.15rem}
255
+ pre.userprompt{white-space:pre-wrap;word-break:break-word;background:#f4f4f4;border-radius:6px;padding:.6rem .75rem;margin:0;font:13px/1.45 ui-monospace,Menlo,monospace}
256
+ .tools{font:13px ui-monospace,Menlo,monospace;color:#555}
257
+ .loc{font-size:13px;color:#1565c0;margin-top:.3rem}
258
+ .warn{color:#e65100;font-weight:600}
259
+ @media(max-width:720px){body{display:block}nav.toc{position:static;width:auto;height:auto;border-right:none;border-bottom:1px solid #e0e0e0}}
260
+ """
261
+
262
+ JS = """
263
+ const btn=document.getElementById('toggleAll');
264
+ function refresh(){const ds=[...document.querySelectorAll('details.group')];
265
+ btn.textContent=ds.some(d=>!d.open)?'Expand all':'Collapse all';}
266
+ btn.addEventListener('click',()=>{const ds=[...document.querySelectorAll('details.group')];
267
+ const open=ds.some(d=>!d.open);ds.forEach(d=>d.open=open);refresh();});
268
+ // expand the group containing a targeted prompt when following an anchor
269
+ function openTarget(){const h=location.hash;if(!h)return;
270
+ const el=document.querySelector(h);if(!el)return;
271
+ const d=el.closest('details.group');if(d){d.open=true;refresh();}el.scrollIntoView();}
272
+ window.addEventListener('hashchange',openTarget);
273
+ document.querySelectorAll('details.group').forEach(d=>d.addEventListener('toggle',refresh));
274
+ refresh();openTarget();
275
+ """
276
+
277
+
278
+ def get_analysis(analysis, n):
279
+ return analysis.get(str(n)) or analysis.get(n)
280
+
281
+
282
+ def group_prompts(prompts, analysis):
283
+ """Group prompts by their analysis `topic`, in first-appearance order.
284
+
285
+ Returns list of (topic, [prompt,...]). Prompts with no topic fall into
286
+ a trailing 'Ungrouped' bucket.
287
+ """
288
+ order, groups = {}, []
289
+ for p in prompts:
290
+ a = get_analysis(analysis, p["n"]) or {}
291
+ topic = (a.get("topic") or "").strip() or "Ungrouped"
292
+ if topic not in order:
293
+ order[topic] = len(groups)
294
+ groups.append((topic, []))
295
+ groups[order[topic]][1].append(p)
296
+ return groups
297
+
298
+
299
+ def cmd_render(args):
300
+ with open(args.data, encoding="utf-8") as fh:
301
+ data = json.load(fh)
302
+ analysis = {}
303
+ if args.analysis and os.path.isfile(args.analysis):
304
+ with open(args.analysis, encoding="utf-8") as fh:
305
+ analysis = json.load(fh)
306
+ prompts = data["prompts"]
307
+ flagged = sum(1 for p in prompts if (get_analysis(analysis, p["n"]) or {}).get("problem"))
308
+ missing = sum(1 for p in prompts if get_analysis(analysis, p["n"]) is None)
309
+ groups = group_prompts(prompts, analysis)
310
+
311
+ # suggestions list: rank flagged prompts by impact (desc), cap at top 10
312
+ flagged_pairs = [(p, get_analysis(analysis, p["n"]))
313
+ for p in prompts if (get_analysis(analysis, p["n"]) or {}).get("problem")]
314
+ flagged_pairs.sort(key=lambda pa: (-(pa[1].get("impact") or 0), pa[0]["n"]))
315
+ top = flagged_pairs[:10]
316
+ sugg_items = []
317
+ for p, a in top:
318
+ loc = a.get("location", "")
319
+ sugg_items.append(
320
+ f'<li><a href="#p{p["n"]}">#{p["n"]}</a> {html.escape(a.get("suggestion") or a.get("problem",""))}'
321
+ + (f' <span class="loc">→ {html.escape(loc)}</span>' if loc else "") + '</li>')
322
+ if sugg_items:
323
+ more = (f'<p class="meta">Showing top 10 of {len(flagged_pairs)} by impact.</p>'
324
+ if len(flagged_pairs) > 10 else '')
325
+ sugg_html = f'<ol>{"".join(sugg_items)}</ol>{more}'
326
+ else:
327
+ sugg_html = '<p class="warn" style="color:#2e7d32">No issues flagged — clean session.</p>'
328
+ shown = len(sugg_items)
329
+
330
+ # table of contents + grouped body
331
+ toc_groups, body_groups = [], []
332
+ for gi, (topic, gprompts) in enumerate(groups):
333
+ gflag = sum(1 for p in gprompts if (get_analysis(analysis, p["n"]) or {}).get("problem"))
334
+ sub = "".join(
335
+ f'<li><a class="{ "flag" if (get_analysis(analysis,p["n"]) or {}).get("problem") else "" }" '
336
+ f'href="#p{p["n"]}">#{p["n"]} {html.escape(p["text"][:42])}</a></li>'
337
+ for p in gprompts)
338
+ toc_groups.append(
339
+ f'<li class="grp"><a href="#g{gi}">{html.escape(topic)}</a><ul>{sub}</ul></li>')
340
+ flag_badge = f'<span class="gflag">{gflag}</span>' if gflag else ''
341
+ blocks = "".join(render_block(p, analysis) for p in gprompts)
342
+ body_groups.append(
343
+ f'<details class="group" id="g{gi}"><summary>{html.escape(topic)}'
344
+ f'<span class="gcount">({len(gprompts)})</span>{flag_badge}</summary>'
345
+ f'<div class="group-body">{blocks}</div></details>')
346
+
347
+ html_file = args.out or args.data.replace("-data.json", "-retrospect.html")
348
+ doc = f"""<!doctype html><html lang="en"><head><meta charset="utf-8">
349
+ <title>Session Retrospect {html.escape(data['stamp'])}</title>
350
+ <style>{CSS}</style></head><body>
351
+ <nav class="toc"><h2>Contents</h2>
352
+ <ul><li><a href="#suggestions">Suggestions ({shown})</a></li></ul>
353
+ <h2>Prompts</h2><ul>{''.join(toc_groups)}</ul></nav>
354
+ <main>
355
+ <header><h1>Session Retrospect</h1>
356
+ <div class="meta">{html.escape(data['stamp'])} · transcript: {html.escape(os.path.basename(data['transcript']))}</div></header>
357
+ <div class="summary"><span><b>{data['count']}</b> prompts</span><span><b>{flagged}</b> flagged</span><span><b>{missing}</b> missing analysis</span>
358
+ <button id="toggleAll" type="button">Expand all</button></div>
359
+ <section id="suggestions" class="suggestions"><h2>Suggestions</h2>{sugg_html}</section>
360
+ {''.join(body_groups)}
361
+ </main>
362
+ <script>{JS}</script>
363
+ </body></html>"""
364
+ with open(html_file, "w", encoding="utf-8") as fh:
365
+ fh.write(doc)
366
+ print(json.dumps({
367
+ "html_file": html_file,
368
+ "count": data["count"],
369
+ "flagged": flagged,
370
+ "missing_analysis": missing,
371
+ "groups": len(groups),
372
+ }, indent=2))
373
+
374
+
375
+ def main():
376
+ ap = argparse.ArgumentParser(description=__doc__)
377
+ sub = ap.add_subparsers(dest="cmd", required=True)
378
+ e = sub.add_parser("extract")
379
+ e.add_argument("--transcript", help="explicit .jsonl path; default = newest in cwd's project dir")
380
+ e.add_argument("--outdir", default=".claude/retrospects")
381
+ e.set_defaults(func=cmd_extract)
382
+ r = sub.add_parser("render")
383
+ r.add_argument("--data", required=True)
384
+ r.add_argument("--analysis")
385
+ r.add_argument("--out")
386
+ r.set_defaults(func=cmd_render)
387
+ args = ap.parse_args()
388
+ args.func(args)
389
+
390
+
391
+ if __name__ == "__main__":
392
+ main()
@@ -0,0 +1,131 @@
1
+ ---
2
+ name: review
3
+ description: Use when the user asks to review a JIRA ticket — phrases like "review HP-1234", "start a review for this ticket", "let's review this issue". Triggers on requests to run a structured pre-merge review of all PRs linked to a ticket.
4
+ ---
5
+
6
+ # Review
7
+
8
+ ## Overview
9
+
10
+ Drives the automated phase of a JIRA ticket review: fetches the ticket and its linked PRs across all repositories, checks out branches, runs per-PR agent analysis (code, release notes, documentation), and surfaces findings in temporary markdown files with a summary table. Hands off to unstructured Claude conversation for the human review phase.
11
+
12
+ **Core principle:** Gather evidence first, present clearly, never merge anything.
13
+
14
+ ## When to use
15
+
16
+ - User asks to review / start a review for a JIRA ticket
17
+ - User invokes with an explicit key ("review HP-1234") or from a branch where a key is parseable
18
+
19
+ ## When NOT to use
20
+
21
+ - User wants to wrap up an already-reviewed ticket — use `review-finalize`
22
+ - User wants to enrich the ticket description — use `specify`
23
+ - User wants to post a work summary — use `summarise-for-jira-comment`
24
+
25
+ ## Workflow
26
+
27
+ ### 1. Identify the ticket key
28
+
29
+ - Default: parse `[A-Z]+-\d+` from the current branch name
30
+ - Override: explicit key in user message
31
+ - Unresolvable or ambiguous: ask before proceeding
32
+
33
+ ### 2. Fetch ticket
34
+
35
+ Call `mcp__plugin_planetexpress_jira-tools__get_jira_ticket`. Capture: summary, description, status, issuetype, any release-notes field.
36
+
37
+ ### 3. Spec check
38
+
39
+ Compare the fetched description against the story/bug shape expected by `specify`. If the description is still vague or a one-liner, warn the user and suggest running `specify` first. Do not block the review — surface it as a finding.
40
+
41
+ ### 4. Dev info check
42
+
43
+ Call `mcp__plugin_planetexpress_jira-tools__get_jira_dev_info`. Collect linked branches, commits, and PR references across all repositories.
44
+
45
+ ### 5. Discover PRs
46
+
47
+ Call `mcp__plugin_planetexpress_jira-tools__search_bitbucket_prs` or use links from dev info to enumerate all PRs associated with this ticket. Collect: repository slug, PR id, PR title, PR link, branch name, target branch, PR status (open/merged/declined).
48
+
49
+ ### 6. Check out branches
50
+
51
+ For each open PR, check out the corresponding branch locally:
52
+ - Determine the local clone path from context (working directory, recent git operations, known repo locations)
53
+ - If the local folder for a repository is not obvious, ask the user where it is cloned before proceeding
54
+ - Run `git fetch` and `git checkout <branch>` in each repo
55
+
56
+ ### 7. CI/build status
57
+
58
+ For each branch, call `mcp__plugin_planetexpress_jira-tools__get_bitbucket_commit_build_status` (or `get_bamboo_build` if a Bamboo plan key is resolvable). Record pass/fail/pending per repository.
59
+
60
+ ### 8. Linked tickets
61
+
62
+ Call `mcp__plugin_planetexpress_jira-tools__get_jira_ticket` for each linked/blocking issue. Present the list and ask the user which (if any) are relevant to include in this review. Only carry forward those the user confirms.
63
+
64
+ ### 9. Triage per repo
65
+
66
+ For each repository with a PR, present:
67
+
68
+ | Repository | PR | Branch | CI | Changed files | Changed lines |
69
+ |---|---|---|---|---|---|
70
+ | `repo-name` | [PR-123](link) | `feature/HP-xxx` | ✅ pass | 14 | +320 / -80 |
71
+
72
+ Always run a review agent. Ask the user per repository:
73
+
74
+ > Run the review agent for `<repo>` as a **background subagent** or **in the main conversation**?
75
+ > (Suggest background for larger/more complex PRs; inline for small/trivial ones.)
76
+
77
+ ### 10. Agent analysis
78
+
79
+ For each PR, run the review agent (background or inline per step 9). The agent covers three areas:
80
+
81
+ #### Code review
82
+ General code quality analysis: correctness, consistency, potential issues, test coverage. (Further sharpening in future skill versions.)
83
+
84
+ #### Release notes check
85
+ 1. Check the repository's `CLAUDE.md` and any referenced docs for guidance on where release notes live (PR changes vs. JIRA field).
86
+ 2. If PR changes: verify release notes are present in the diff and adequate.
87
+ 3. If JIRA field: check the field on the ticket fetched in step 2.
88
+ 4. If no guidance found: prompt the user for the expected location and suggest adding a `CLAUDE.md` entry for this repository.
89
+
90
+ #### Documentation check
91
+ 1. Check the repository's `CLAUDE.md` and any referenced docs to locate customer-facing documentation.
92
+ 2. Assess whether the ticket scope or PR changes imply that documentation updates were needed or requested.
93
+ 3. If so, verify that adequate documentation changes are included in the PR.
94
+ 4. If no guidance on documentation location is found: prompt the user and suggest adding a `CLAUDE.md` entry for this repository.
95
+
96
+ ### 11. Present findings
97
+
98
+ For each repository/PR:
99
+ - Write findings to `/tmp/review-<REPO>-<PR_ID>.md` or - if that path does not exist - to `/c/tmp/review-<REPO>-<PR_ID>.md`(create or overwrite)
100
+ - Structure the file with sections: Code Review, Release Notes, Documentation, Summary
101
+
102
+ In the main conversation, present a summary table:
103
+
104
+ | Repository | PR | Findings file | Findings |
105
+ |---|---|---|---|
106
+ | `repo-name` | [PR-123](link) | `/tmp/review-repo-name-123.md` | 7 |
107
+
108
+ **Hand off:** Inform the user that the automated analysis is complete and they can now freely review the findings with Claude. When ready to wrap up, invoke `review-finalize`.
109
+
110
+ ## Edge cases
111
+
112
+ | Situation | Behaviour |
113
+ |---|---|
114
+ | No ticket key resolvable | Ask the user; do not guess |
115
+ | No PRs found | Inform the user; offer to continue with manual repo/branch input |
116
+ | PR is already merged or declined | Include in the table but note status; skip branch checkout |
117
+ | Local clone not found | Ask the user for the path before attempting checkout |
118
+ | `CLAUDE.md` has no release notes guidance | Prompt user; suggest `CLAUDE.md` addition; record as a finding |
119
+ | `CLAUDE.md` has no documentation guidance | Prompt user; suggest `CLAUDE.md` addition; record as a finding |
120
+ | CI result unavailable | Note as unknown in the table; do not block |
121
+ | `get_jira_dev_info` returns no linked branches | Warn the user; fall back to manual PR discovery via `search_bitbucket_prs` |
122
+
123
+ ## Red flags — STOP and reconsider
124
+
125
+ | Thought | Reality |
126
+ |---|---|
127
+ | "I'll just pick the most likely clone path" | If not obvious, ask. A wrong path causes a checkout in the wrong repo. |
128
+ | "Release notes look thin but I'll skip it" | Always check; surface it as a finding even if it's minor. |
129
+ | "No CLAUDE.md entry for docs — I'll assume none needed" | Absence of guidance is itself a finding. Prompt the user. |
130
+ | "I'll merge the branch since it looks ready" | Never. Present PR links only. Merges are the user's decision. |
131
+ | "CI is pending — I'll wait before continuing" | Note the pending status and continue. Don't block the workflow on CI. |
@@ -0,0 +1,103 @@
1
+ ---
2
+ name: review-finalize
3
+ description: Use when the human review phase of a JIRA ticket review is complete and the user wants to wrap up — phrases like "finalize the review", "wrap up the review", "close out the review for HP-1234", "let's finalize HP-1234". Triggers after the unstructured review conversation following the `review` skill.
4
+ ---
5
+
6
+ # Review Finalize
7
+
8
+ ## Overview
9
+
10
+ Wraps up a JIRA ticket review after the human review phase: runs the final functional-test gate, compiles all findings and interaction outcomes into a review summary, posts it as a JIRA comment, and presents PR links for the user to merge at their own discretion.
11
+
12
+ **Core principle:** Summarize what happened, never merge anything.
13
+
14
+ ## When to use
15
+
16
+ - The human review phase (free-form Claude conversation following `review`) is complete
17
+ - User wants to wrap up, post findings, and get PR links
18
+ - Use after `review` — this skill assumes findings files from that skill exist
19
+
20
+ ## When NOT to use
21
+
22
+ - The automated review phase has not been run yet — use `review` first
23
+ - User wants to post a general work summary unrelated to a review — use `summarise-for-jira-comment`
24
+
25
+ ## Workflow
26
+
27
+ ### 1. Identify context
28
+
29
+ Resolve the ticket key from context (current branch, earlier conversation, or explicit user input). Locate the findings files written by `review` at `/tmp/review-<REPO>-<PR_ID>.md`. If files are missing or the ticket key is ambiguous, ask the user before continuing.
30
+
31
+ ### 2. Functional test gate
32
+
33
+ Ask the user explicitly:
34
+
35
+ > Has the feature been manually tested against the ticket spec?
36
+
37
+ Do not proceed to posting until the user answers. Record the response (yes / no / partial / not applicable) and include it in the summary.
38
+
39
+ ### 3. Compile summary
40
+
41
+ Aggregate findings from all `/tmp/review-<REPO>-<PR_ID>.md` files and from the interaction outcomes appended during the human review phase. Structure the JIRA comment as:
42
+
43
+ ```
44
+ *Review summary for <KEY>*
45
+
46
+ *Repositories reviewed:*
47
+ - <repo-name>: [PR-123|link] — <N> findings (<brief disposition, e.g. "all addressed", "2 open">)
48
+
49
+ *Release notes:* <adequate / missing / not applicable>
50
+ *Documentation:* <adequate / missing / not applicable>
51
+ *Functional test:* <yes / no / partial / not applicable>
52
+
53
+ *Notes:*
54
+ - <any non-obvious outcomes or unresolved items from the review conversation>
55
+ ```
56
+
57
+ Keep bullets scannable. Do not enumerate code-level details — those live in the findings files. Do not include file paths, class names, or commit hashes unless they are the specific point of a note.
58
+
59
+ ### 4. Present for approval
60
+
61
+ Show the exact comment text that will be posted to JIRA, verbatim. End with:
62
+
63
+ > Reply 'post' to publish to \<KEY\>, or give edits.
64
+
65
+ Strict gate:
66
+ - Affirmative-and-explicit ("post", "yes, post", "ship it") → post
67
+ - Edits → revise, re-present, wait again
68
+ - Anything else → do not post; treat as edits or a question
69
+
70
+ ### 5. Post comment
71
+
72
+ Call `mcp__plugin_planetexpress_jira-tools__add_jira_comment` with the approved text. Confirm in chat with the comment URL/ID returned.
73
+
74
+ ### 6. Present PR links
75
+
76
+ Present all reviewed PRs as a list for the user to act on:
77
+
78
+ > PRs ready for your review and merge decision:
79
+ > - `repo-name`: [PR-123 — Feature/HP-xxx](link) — status: open
80
+ > - `repo-name-2`: [PR-45 — Feature/HP-xxx](link) — status: open
81
+
82
+ **Claude does not merge, approve, or close any PR.** The user follows the links and acts at their own discretion.
83
+
84
+ ## Edge cases
85
+
86
+ | Situation | Behaviour |
87
+ |---|---|
88
+ | Findings files missing | Ask the user if `review` was run; if not, suggest running it first |
89
+ | Ticket key ambiguous | Ask before proceeding |
90
+ | Functional test answer is "no" | Record it honestly in the summary; do not block posting |
91
+ | All PRs already merged | Note status in the PR list; still present for completeness |
92
+ | `add_jira_comment` fails | Surface error verbatim; do not claim success |
93
+ | User says "looks good" instead of "post" | Strict gate — wait for explicit "post" / "yes, post" / "ship it" |
94
+
95
+ ## Red flags — STOP and reconsider
96
+
97
+ | Thought | Reality |
98
+ |---|---|
99
+ | "Functional test was implied by earlier conversation — I'll skip the gate" | Always ask explicitly. Record the answer. |
100
+ | "I'll merge the PR since it looks approved" | Never. Present links only. Merges are the user's decision. |
101
+ | "The findings files have enough detail — I'll copy them into the comment" | The comment is high-level. Details live in the files. |
102
+ | "User said 'looks good' — that's approval to post" | Strict gate: needs explicit "post" / "yes, post" / "ship it". |
103
+ | "I'll skip the PR list since they're already visible in JIRA" | Always present the list explicitly — the user needs to act on them. |