@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.
- package/package.json +1 -1
- package/teams/fhr-orchestration/skills/create-jira/SKILL.md +47 -0
- package/teams/fhr-orchestration/skills/create-jira/TEMPLATE.md +10 -0
- package/teams/fhr-orchestration/skills/create-plan/SKILL.md +48 -0
- package/teams/fhr-orchestration/skills/create-plan/TEMPLATE.md +13 -0
- package/teams/fhr-orchestration/skills/init-harness/SKILL.md +164 -0
- package/teams/fhr-orchestration/skills/init-harness/assets/TEMPLATE.CLAUDE.md +88 -0
- package/teams/fhr-orchestration/skills/init-harness/assets/TEMPLATE.architecture.md +152 -0
- package/teams/fhr-orchestration/skills/init-harness/assets/TEMPLATE.coding-guidelines.md +320 -0
- package/teams/fhr-orchestration/skills/init-harness/assets/TEMPLATE.glossary.md +55 -0
- package/teams/fhr-orchestration/skills/init-harness/assets/TEMPLATE.harness-gap-log.md +59 -0
- package/teams/fhr-orchestration/skills/use-case-writer/SKILL.md +123 -0
- package/teams/fhr-orchestration/skills/use-case-writer/TEMPLATE.md +39 -0
- package/teams/fhr-orchestration/skills/write-spec/SKILL.md +56 -0
- package/teams/fhr-orchestration/skills/write-spec/TEMPLATE.md +29 -0
- package/teams/firstspirit-caas/mcp/jira-tools.py +2122 -0
- package/teams/firstspirit-caas/mcp/test_bamboo_artifacts.py +315 -0
- package/teams/firstspirit-caas/mcp/test_jira_links.py +192 -0
- package/teams/firstspirit-caas/mcp/test_update_jira_ticket.py +161 -0
- package/teams/firstspirit-caas/resources/mcp-setup.md +57 -0
- package/teams/firstspirit-caas/skills/chronicle/SKILL.md +105 -0
- package/teams/firstspirit-caas/skills/create-jira-ticket/SKILL.md +183 -0
- package/teams/firstspirit-caas/skills/get-jira-ticket/SKILL.md +125 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/SKILL.md +316 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/references/suppression.md +79 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/gen_suppression.py +116 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/parse_json_report.py +183 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/parse_report.py +255 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/test_gen_suppression.py +171 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/test_parse_json_report.py +244 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/test_parse_report.py +464 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/testdata/sample-report.html +39 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/testdata/sample-report.json +76 -0
- package/teams/firstspirit-caas/skills/handle-weekly-cve-plans/SKILL.md +238 -0
- package/teams/firstspirit-caas/skills/retrospect/SKILL.md +94 -0
- package/teams/firstspirit-caas/skills/retrospect/extract_prompts.py +392 -0
- package/teams/firstspirit-caas/skills/review/SKILL.md +131 -0
- package/teams/firstspirit-caas/skills/review-finalize/SKILL.md +103 -0
- package/teams/firstspirit-caas/skills/specify/SKILL.md +239 -0
- package/teams/firstspirit-caas/skills/summarise-for-jira-comment/SKILL.md +162 -0
- 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. |
|