@jakkrichm/create-nexus-devflow 2.1.0 → 2.2.0
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/dist/bin/create-nexus-devflow.js +28 -4
- package/dist/bin/create-nexus-devflow.js.map +1 -1
- package/dist/lib/project-metadata.d.ts +1 -1
- package/dist/lib/project-metadata.js +3 -2
- package/dist/lib/project-metadata.js.map +1 -1
- package/dist/lib/update.js +7 -3
- package/dist/lib/update.js.map +1 -1
- package/package.json +1 -1
- package/template/.agents/skills/adopt/SKILL.md +58 -5
- package/template/.agents/skills/doctor/SKILL.md +23 -16
- package/template/.agents/skills/implement/SKILL.md +2 -2
- package/template/.agents/skills/onboard/SKILL.md +18 -17
- package/template/.agents/skills/rollback/SKILL.md +1 -1
- package/template/.claude/skills/00-explore/SKILL.md +2 -2
- package/template/.claude/skills/10-define/SKILL.md +2 -2
- package/template/.claude/skills/20-spec/SKILL.md +1 -2
- package/template/.claude/skills/30-plan/SKILL.md +1 -2
- package/template/.claude/skills/40-execute/SKILL.md +2 -2
- package/template/.claude/skills/50-verify/SKILL.md +2 -2
- package/template/.claude/skills/60-report/SKILL.md +2 -2
- package/template/.claude/skills/70-deliver/SKILL.md +2 -2
- package/template/.claude/skills/adopt/SKILL.md +191 -75
- package/template/.claude/skills/audit/SKILL.md +267 -133
- package/template/.claude/skills/autopilot/SKILL.md +226 -167
- package/template/.claude/skills/brainstorm/SKILL.md +62 -0
- package/template/.claude/skills/brief/SKILL.md +93 -92
- package/template/.claude/skills/check/SKILL.md +96 -76
- package/template/.claude/skills/ci/SKILL.md +140 -61
- package/template/.claude/skills/complete/SKILL.md +156 -101
- package/template/.claude/skills/convert-any-to-md/SKILL.md +2 -2
- package/template/.claude/skills/convert-any-to-md/references/setup.md +29 -0
- package/template/.claude/skills/convert-any-to-md/scripts/convert_any_to_md.py +487 -0
- package/template/.claude/skills/convert-any-to-md/scripts/requirements.txt +3 -0
- package/template/.claude/skills/debug/SKILL.md +124 -49
- package/template/.claude/skills/devflow/SKILL.md +9 -3
- package/template/.claude/skills/discovery/SKILL.md +150 -129
- package/template/.claude/skills/doctor/SKILL.md +195 -72
- package/template/.claude/skills/feature/SKILL.md +195 -151
- package/template/.claude/skills/fix/SKILL.md +41 -90
- package/template/.claude/skills/idea/SKILL.md +2 -2
- package/template/.claude/skills/implement/SKILL.md +189 -46
- package/template/.claude/skills/onboard/SKILL.md +216 -85
- package/template/.claude/skills/overview/SKILL.md +44 -29
- package/template/.claude/skills/prototype/SKILL.md +82 -27
- package/template/.claude/skills/release/SKILL.md +119 -130
- package/template/.claude/skills/report-html/SKILL.md +2 -2
- package/template/.claude/skills/rollback/SKILL.md +123 -77
- package/template/.claude/skills/status/SKILL.md +109 -0
- package/template/.claude/skills/test/SKILL.md +2 -2
- package/template/.claude/skills/tests/SKILL.md +126 -0
- package/template/.claude/skills/try/SKILL.md +77 -65
- package/template/AGENTS.md +2 -1
- package/template/devflow/build-plan.md +8 -0
- package/template/devflow/history/features/README.md +5 -0
- package/template/devflow/history/fixes/README.md +5 -0
- package/template/devflow/history/rollbacks/README.md +5 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Convert any document (.xlsx, .pdf, .docx, .txt, .csv, .log, .json, .yaml, etc.)
|
|
3
|
+
or a directory of mixed files into clean Markdown, outputting by default into `devflow/reference/`.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python convert_any_to_md.py <input> [-o OUTPUT] [--recursive]
|
|
7
|
+
|
|
8
|
+
Arguments:
|
|
9
|
+
<input> Path to a single document or a directory.
|
|
10
|
+
-o, --output Target output directory (default: devflow/reference).
|
|
11
|
+
--recursive Recursively process subdirectories when input is a directory.
|
|
12
|
+
|
|
13
|
+
Exit codes:
|
|
14
|
+
0 - All requested conversions succeeded
|
|
15
|
+
1 - One or more conversions failed (partial success in batch mode)
|
|
16
|
+
2 - Required dependency missing
|
|
17
|
+
3 - Invalid input path
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import hashlib
|
|
22
|
+
import posixpath
|
|
23
|
+
import re
|
|
24
|
+
import shutil
|
|
25
|
+
import sys
|
|
26
|
+
import zipfile
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from xml.etree import ElementTree as ET
|
|
29
|
+
|
|
30
|
+
EXIT_OK = 0
|
|
31
|
+
EXIT_CONVERSION_FAILED = 1
|
|
32
|
+
EXIT_MISSING_DEPENDENCY = 2
|
|
33
|
+
EXIT_INVALID_INPUT = 3
|
|
34
|
+
|
|
35
|
+
# Namespaces for OOXML
|
|
36
|
+
_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships"
|
|
37
|
+
_MAIN_NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
|
38
|
+
_R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
39
|
+
_A_NS = "http://schemas.openxmlformats.org/drawingml/2006/main"
|
|
40
|
+
|
|
41
|
+
# MarkItDown placeholder pattern for docx
|
|
42
|
+
_PLACEHOLDER_IMAGE_RE = re.compile(
|
|
43
|
+
r'!\[([^\]]*)\]\(data:image/[a-zA-Z0-9.+-]+;base64[^)]*\)'
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Sheet header pattern for xlsx
|
|
47
|
+
_SHEET_HEADER_RE = re.compile(r"^## (.+)$", re.MULTILINE)
|
|
48
|
+
|
|
49
|
+
SUPPORTED_TEXT_EXTS = {
|
|
50
|
+
".txt", ".log", ".csv", ".tsv", ".json", ".yaml", ".yml",
|
|
51
|
+
".md", ".markdown", ".py", ".js", ".ts", ".html", ".xml", ".css"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
IGNORE_EXTS = {
|
|
55
|
+
".exe", ".dll", ".so", ".dylib", ".bin", ".zip", ".tar", ".gz",
|
|
56
|
+
".7z", ".rar", ".iso", ".pyc", ".class", ".o", ".obj"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def get_markitdown():
|
|
60
|
+
"""Lazy import MarkItDown if available."""
|
|
61
|
+
try:
|
|
62
|
+
from markitdown import MarkItDown
|
|
63
|
+
return MarkItDown()
|
|
64
|
+
except ImportError:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
# ============================================================================
|
|
68
|
+
# Excel Handler
|
|
69
|
+
# ============================================================================
|
|
70
|
+
|
|
71
|
+
def _normalize_rel_path(base_dir: str, target: str) -> str:
|
|
72
|
+
if target.startswith("/"):
|
|
73
|
+
return target.lstrip("/")
|
|
74
|
+
return posixpath.normpath(posixpath.join(base_dir, target))
|
|
75
|
+
|
|
76
|
+
def _xlsx_sheet_media(xlsx_path: Path):
|
|
77
|
+
try:
|
|
78
|
+
with zipfile.ZipFile(xlsx_path) as z:
|
|
79
|
+
names = set(z.namelist())
|
|
80
|
+
if "xl/workbook.xml" not in names or "xl/_rels/workbook.xml.rels" not in names:
|
|
81
|
+
return {}
|
|
82
|
+
workbook_xml = z.read("xl/workbook.xml")
|
|
83
|
+
workbook_rels_xml = z.read("xl/_rels/workbook.xml.rels")
|
|
84
|
+
|
|
85
|
+
sheet_rid = {}
|
|
86
|
+
for sheet_el in ET.fromstring(workbook_xml).iter(f"{{{_MAIN_NS}}}sheet"):
|
|
87
|
+
name = sheet_el.get("name")
|
|
88
|
+
rid = sheet_el.get(f"{{{_R_NS}}}id")
|
|
89
|
+
if name and rid:
|
|
90
|
+
sheet_rid[name] = rid
|
|
91
|
+
|
|
92
|
+
rid_target = {}
|
|
93
|
+
for rel in ET.fromstring(workbook_rels_xml).findall(f"{{{_REL_NS}}}Relationship"):
|
|
94
|
+
rid_target[rel.get("Id")] = rel.get("Target")
|
|
95
|
+
|
|
96
|
+
result = {}
|
|
97
|
+
for sheet_name, rid in sheet_rid.items():
|
|
98
|
+
target = rid_target.get(rid)
|
|
99
|
+
if not target:
|
|
100
|
+
continue
|
|
101
|
+
sheet_path = _normalize_rel_path("xl", target)
|
|
102
|
+
if sheet_path not in names or "/" not in sheet_path:
|
|
103
|
+
continue
|
|
104
|
+
sheet_dir, sheet_file = sheet_path.rsplit("/", 1)
|
|
105
|
+
sheet_rels_path = f"{sheet_dir}/_rels/{sheet_file}.rels"
|
|
106
|
+
if sheet_rels_path not in names:
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
drawing_rid = None
|
|
110
|
+
for d in ET.fromstring(z.read(sheet_path)).iter(f"{{{_MAIN_NS}}}drawing"):
|
|
111
|
+
drawing_rid = d.get(f"{{{_R_NS}}}id")
|
|
112
|
+
break
|
|
113
|
+
if not drawing_rid:
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
drawing_target = None
|
|
117
|
+
for rel in ET.fromstring(z.read(sheet_rels_path)).findall(f"{{{_REL_NS}}}Relationship"):
|
|
118
|
+
if rel.get("Id") == drawing_rid:
|
|
119
|
+
drawing_target = rel.get("Target")
|
|
120
|
+
break
|
|
121
|
+
if not drawing_target:
|
|
122
|
+
continue
|
|
123
|
+
drawing_path = _normalize_rel_path(sheet_dir, drawing_target)
|
|
124
|
+
if drawing_path not in names or "/" not in drawing_path:
|
|
125
|
+
continue
|
|
126
|
+
drawing_dir, drawing_file = drawing_path.rsplit("/", 1)
|
|
127
|
+
drawing_rels_path = f"{drawing_dir}/_rels/{drawing_file}.rels"
|
|
128
|
+
if drawing_rels_path not in names:
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
drawing_rel_map = {}
|
|
132
|
+
for rel in ET.fromstring(z.read(drawing_rels_path)).findall(f"{{{_REL_NS}}}Relationship"):
|
|
133
|
+
drawing_rel_map[rel.get("Id")] = rel.get("Target")
|
|
134
|
+
|
|
135
|
+
media_paths = []
|
|
136
|
+
for blip in ET.fromstring(z.read(drawing_path)).iter(f"{{{_A_NS}}}blip"):
|
|
137
|
+
embed_rid = blip.get(f"{{{_R_NS}}}embed")
|
|
138
|
+
if not embed_rid:
|
|
139
|
+
continue
|
|
140
|
+
rel_target = drawing_rel_map.get(embed_rid)
|
|
141
|
+
if not rel_target:
|
|
142
|
+
continue
|
|
143
|
+
media_path = _normalize_rel_path(drawing_dir, rel_target)
|
|
144
|
+
if media_path in names:
|
|
145
|
+
media_paths.append(media_path)
|
|
146
|
+
|
|
147
|
+
if media_paths:
|
|
148
|
+
result[sheet_name] = media_paths
|
|
149
|
+
return result
|
|
150
|
+
except Exception:
|
|
151
|
+
return {}
|
|
152
|
+
|
|
153
|
+
def extract_xlsx_images(xlsx_path: Path, img_dir: Path):
|
|
154
|
+
sheet_media = _xlsx_sheet_media(xlsx_path)
|
|
155
|
+
if not sheet_media:
|
|
156
|
+
return {}
|
|
157
|
+
|
|
158
|
+
written = {}
|
|
159
|
+
with zipfile.ZipFile(xlsx_path) as z:
|
|
160
|
+
names_in_zip = set(z.namelist())
|
|
161
|
+
for sheet_idx, (sheet_name, media_paths) in enumerate(sheet_media.items(), start=1):
|
|
162
|
+
safe_name = re.sub(r"[^A-Za-z0-9_.-]+", "_", sheet_name).strip("_") or "sheet"
|
|
163
|
+
safe_prefix = f"sheet{sheet_idx:03d}_{safe_name}"
|
|
164
|
+
files = []
|
|
165
|
+
for idx, media_path in enumerate(media_paths, start=1):
|
|
166
|
+
if media_path not in names_in_zip:
|
|
167
|
+
continue
|
|
168
|
+
ext = Path(media_path).suffix.lstrip(".").lower() or "bin"
|
|
169
|
+
if ext == "jpg":
|
|
170
|
+
ext = "jpeg"
|
|
171
|
+
fname = f"{safe_prefix}_img{idx:03d}.{ext}"
|
|
172
|
+
dest = img_dir / fname
|
|
173
|
+
img_dir.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
with z.open(media_path) as src, open(dest, "wb") as dst:
|
|
175
|
+
shutil.copyfileobj(src, dst)
|
|
176
|
+
files.append(fname)
|
|
177
|
+
if files:
|
|
178
|
+
written[sheet_name] = files
|
|
179
|
+
return written
|
|
180
|
+
|
|
181
|
+
def convert_excel(file_path: Path, out_dir: Path, md_engine) -> bool:
|
|
182
|
+
img_dir = out_dir / "img"
|
|
183
|
+
sheet_images = extract_xlsx_images(file_path, img_dir)
|
|
184
|
+
|
|
185
|
+
if md_engine:
|
|
186
|
+
res = md_engine.convert(str(file_path))
|
|
187
|
+
text = res.text_content
|
|
188
|
+
else:
|
|
189
|
+
text = f"# {file_path.stem}\n\n*(MarkItDown engine not available. Basic conversion applied)*\n"
|
|
190
|
+
|
|
191
|
+
if sheet_images:
|
|
192
|
+
matches = list(_SHEET_HEADER_RE.finditer(text))
|
|
193
|
+
if matches:
|
|
194
|
+
parts = []
|
|
195
|
+
last_end = 0
|
|
196
|
+
for i, m in enumerate(matches):
|
|
197
|
+
sheet_name = m.group(1).strip()
|
|
198
|
+
next_start = matches[i + 1].start() if i + 1 < len(matches) else len(text)
|
|
199
|
+
section = text[last_end:next_start]
|
|
200
|
+
last_end = next_start
|
|
201
|
+
|
|
202
|
+
if sheet_name in sheet_images:
|
|
203
|
+
img_lines = ["\n\n#### Images in this sheet\n"]
|
|
204
|
+
for fname in sheet_images[sheet_name]:
|
|
205
|
+
img_lines.append(f"\n")
|
|
206
|
+
section = section.rstrip() + "\n" + "".join(img_lines)
|
|
207
|
+
parts.append(section)
|
|
208
|
+
text = "".join(parts)
|
|
209
|
+
|
|
210
|
+
out_md = out_dir / f"{file_path.stem}.md"
|
|
211
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
212
|
+
out_md.write_text(text, encoding="utf-8")
|
|
213
|
+
return True
|
|
214
|
+
|
|
215
|
+
# ============================================================================
|
|
216
|
+
# PDF Handler
|
|
217
|
+
# ============================================================================
|
|
218
|
+
|
|
219
|
+
def extract_pdf_images(pdf_path: Path, img_dir: Path):
|
|
220
|
+
try:
|
|
221
|
+
import fitz
|
|
222
|
+
except ImportError:
|
|
223
|
+
return {}
|
|
224
|
+
|
|
225
|
+
written_by_page = {}
|
|
226
|
+
try:
|
|
227
|
+
doc = fitz.open(str(pdf_path))
|
|
228
|
+
except Exception:
|
|
229
|
+
return written_by_page
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
for page_index in range(len(doc)):
|
|
233
|
+
page = doc[page_index]
|
|
234
|
+
page_label = page_index + 1
|
|
235
|
+
raw_images = []
|
|
236
|
+
try:
|
|
237
|
+
xobjects = page.get_images(full=True)
|
|
238
|
+
except Exception:
|
|
239
|
+
xobjects = []
|
|
240
|
+
|
|
241
|
+
for img in xobjects:
|
|
242
|
+
xref = img[0]
|
|
243
|
+
try:
|
|
244
|
+
base_image = doc.extract_image(xref)
|
|
245
|
+
except Exception:
|
|
246
|
+
continue
|
|
247
|
+
img_bytes = base_image.get("image") or b""
|
|
248
|
+
if not img_bytes:
|
|
249
|
+
continue
|
|
250
|
+
ext = (base_image.get("ext") or "png").lower()
|
|
251
|
+
raw_images.append((img_bytes, ext))
|
|
252
|
+
|
|
253
|
+
if not raw_images:
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
saved_files = []
|
|
257
|
+
img_dir.mkdir(parents=True, exist_ok=True)
|
|
258
|
+
for idx, (img_bytes, ext) in enumerate(raw_images, start=1):
|
|
259
|
+
fname = f"page{page_label:03d}_img{idx:03d}.{ext}"
|
|
260
|
+
dest = img_dir / fname
|
|
261
|
+
dest.write_bytes(img_bytes)
|
|
262
|
+
saved_files.append(fname)
|
|
263
|
+
|
|
264
|
+
if saved_files:
|
|
265
|
+
written_by_page[page_label] = saved_files
|
|
266
|
+
finally:
|
|
267
|
+
doc.close()
|
|
268
|
+
return written_by_page
|
|
269
|
+
|
|
270
|
+
def convert_pdf(file_path: Path, out_dir: Path, md_engine) -> bool:
|
|
271
|
+
img_dir = out_dir / "img"
|
|
272
|
+
page_images = extract_pdf_images(file_path, img_dir)
|
|
273
|
+
|
|
274
|
+
if md_engine:
|
|
275
|
+
res = md_engine.convert(str(file_path))
|
|
276
|
+
text = res.text_content
|
|
277
|
+
else:
|
|
278
|
+
text = f"# {file_path.stem}\n\n*(MarkItDown engine not available)*\n"
|
|
279
|
+
|
|
280
|
+
if page_images:
|
|
281
|
+
appendix = ["\n\n## Extracted Images\n"]
|
|
282
|
+
for page_num in sorted(page_images.keys()):
|
|
283
|
+
appendix.append(f"\n### Page {page_num}\n")
|
|
284
|
+
for fname in page_images[page_num]:
|
|
285
|
+
appendix.append(f"\n")
|
|
286
|
+
text = text.rstrip() + "".join(appendix)
|
|
287
|
+
|
|
288
|
+
out_md = out_dir / f"{file_path.stem}.md"
|
|
289
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
290
|
+
out_md.write_text(text, encoding="utf-8")
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
# ============================================================================
|
|
294
|
+
# Word Handler
|
|
295
|
+
# ============================================================================
|
|
296
|
+
|
|
297
|
+
def _word_ordered_media(docx_path: Path):
|
|
298
|
+
try:
|
|
299
|
+
with zipfile.ZipFile(docx_path) as z:
|
|
300
|
+
if "word/document.xml" not in z.namelist() or "word/_rels/document.xml.rels" not in z.namelist():
|
|
301
|
+
return []
|
|
302
|
+
rels_xml = z.read("word/_rels/document.xml.rels")
|
|
303
|
+
doc_xml = z.read("word/document.xml")
|
|
304
|
+
except Exception:
|
|
305
|
+
return []
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
rels_root = ET.fromstring(rels_xml)
|
|
309
|
+
doc_root = ET.fromstring(doc_xml)
|
|
310
|
+
except Exception:
|
|
311
|
+
return []
|
|
312
|
+
|
|
313
|
+
rel_map = {rel.get("Id"): rel.get("Target") for rel in rels_root.findall(f"{{{_REL_NS}}}Relationship")}
|
|
314
|
+
ordered_rel_ids = []
|
|
315
|
+
for elem in doc_root.iter():
|
|
316
|
+
tag = elem.tag.rsplit("}", 1)[-1]
|
|
317
|
+
rid = elem.get(f"{{{_R_NS}}}embed") if tag == "blip" else (elem.get(f"{{{_R_NS}}}id") if tag == "imagedata" else None)
|
|
318
|
+
if rid:
|
|
319
|
+
ordered_rel_ids.append(rid)
|
|
320
|
+
|
|
321
|
+
ordered_media = []
|
|
322
|
+
for rid in ordered_rel_ids:
|
|
323
|
+
target = rel_map.get(rid)
|
|
324
|
+
if not target or "media/" not in target:
|
|
325
|
+
continue
|
|
326
|
+
media_path = target.lstrip("/") if target.startswith("/") else posixpath.normpath(target if target.startswith("word/") else posixpath.join("word", target))
|
|
327
|
+
ordered_media.append((rid, media_path))
|
|
328
|
+
return ordered_media
|
|
329
|
+
|
|
330
|
+
def extract_word_images(docx_path: Path, img_dir: Path):
|
|
331
|
+
ordered_media = _word_ordered_media(docx_path)
|
|
332
|
+
if not ordered_media:
|
|
333
|
+
return []
|
|
334
|
+
|
|
335
|
+
written = []
|
|
336
|
+
with zipfile.ZipFile(docx_path) as z:
|
|
337
|
+
names_in_zip = set(z.namelist())
|
|
338
|
+
for idx, (rid, media_path) in enumerate(ordered_media, start=1):
|
|
339
|
+
if media_path not in names_in_zip:
|
|
340
|
+
continue
|
|
341
|
+
ext = Path(media_path).suffix.lstrip(".").lower() or "bin"
|
|
342
|
+
if ext == "jpg":
|
|
343
|
+
ext = "jpeg"
|
|
344
|
+
fname = f"img{idx:03d}.{ext}"
|
|
345
|
+
dest = img_dir / fname
|
|
346
|
+
img_dir.mkdir(parents=True, exist_ok=True)
|
|
347
|
+
with z.open(media_path) as src, open(dest, "wb") as dst:
|
|
348
|
+
shutil.copyfileobj(src, dst)
|
|
349
|
+
written.append(fname)
|
|
350
|
+
return written
|
|
351
|
+
|
|
352
|
+
def convert_word(file_path: Path, out_dir: Path, md_engine) -> bool:
|
|
353
|
+
img_dir = out_dir / "img"
|
|
354
|
+
images = extract_word_images(file_path, img_dir)
|
|
355
|
+
|
|
356
|
+
if md_engine:
|
|
357
|
+
res = md_engine.convert(str(file_path))
|
|
358
|
+
text = res.text_content
|
|
359
|
+
else:
|
|
360
|
+
text = f"# {file_path.stem}\n\n*(MarkItDown engine not available)*\n"
|
|
361
|
+
|
|
362
|
+
if images:
|
|
363
|
+
img_idx = 0
|
|
364
|
+
def replace_img(match):
|
|
365
|
+
nonlocal img_idx
|
|
366
|
+
alt_text = match.group(1)
|
|
367
|
+
if img_idx < len(images):
|
|
368
|
+
fname = images[img_idx]
|
|
369
|
+
img_idx += 1
|
|
370
|
+
return f""
|
|
371
|
+
return match.group(0)
|
|
372
|
+
|
|
373
|
+
text = _PLACEHOLDER_IMAGE_RE.sub(replace_img, text)
|
|
374
|
+
|
|
375
|
+
out_md = out_dir / f"{file_path.stem}.md"
|
|
376
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
377
|
+
out_md.write_text(text, encoding="utf-8")
|
|
378
|
+
return True
|
|
379
|
+
|
|
380
|
+
# ============================================================================
|
|
381
|
+
# Plaintext Handler
|
|
382
|
+
# ============================================================================
|
|
383
|
+
|
|
384
|
+
def convert_text(file_path: Path, out_dir: Path) -> bool:
|
|
385
|
+
try:
|
|
386
|
+
content = file_path.read_text(encoding="utf-8", errors="replace")
|
|
387
|
+
except Exception as exc:
|
|
388
|
+
print(f"ERROR reading {file_path}: {exc}", file=sys.stderr)
|
|
389
|
+
return False
|
|
390
|
+
|
|
391
|
+
ext = file_path.suffix.lower()
|
|
392
|
+
if ext in {".md", ".markdown"}:
|
|
393
|
+
md_text = content
|
|
394
|
+
else:
|
|
395
|
+
lang_map = {
|
|
396
|
+
".py": "python", ".js": "javascript", ".ts": "typescript",
|
|
397
|
+
".json": "json", ".yaml": "yaml", ".yml": "yaml",
|
|
398
|
+
".html": "html", ".xml": "xml", ".css": "css", ".csv": "csv"
|
|
399
|
+
}
|
|
400
|
+
lang = lang_map.get(ext, "")
|
|
401
|
+
if lang:
|
|
402
|
+
md_text = f"# {file_path.name}\n\n```{lang}\n{content}\n```\n"
|
|
403
|
+
else:
|
|
404
|
+
md_text = f"# {file_path.name}\n\n{content}\n"
|
|
405
|
+
|
|
406
|
+
out_md = out_dir / f"{file_path.stem}.md"
|
|
407
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
408
|
+
out_md.write_text(md_text, encoding="utf-8")
|
|
409
|
+
return True
|
|
410
|
+
|
|
411
|
+
# ============================================================================
|
|
412
|
+
# Main Router
|
|
413
|
+
# ============================================================================
|
|
414
|
+
|
|
415
|
+
def convert_single_file(file_path: Path, base_out_dir: Path, md_engine) -> bool:
|
|
416
|
+
ext = file_path.suffix.lower()
|
|
417
|
+
out_dir = base_out_dir / file_path.stem
|
|
418
|
+
|
|
419
|
+
if ext in IGNORE_EXTS:
|
|
420
|
+
return True
|
|
421
|
+
|
|
422
|
+
print(f"Converting [{ext or 'text'}] {file_path.name} -> {out_dir}")
|
|
423
|
+
|
|
424
|
+
if ext == ".xlsx":
|
|
425
|
+
return convert_excel(file_path, out_dir, md_engine)
|
|
426
|
+
elif ext == ".pdf":
|
|
427
|
+
return convert_pdf(file_path, out_dir, md_engine)
|
|
428
|
+
elif ext == ".docx":
|
|
429
|
+
return convert_word(file_path, out_dir, md_engine)
|
|
430
|
+
elif ext in SUPPORTED_TEXT_EXTS or ext == "":
|
|
431
|
+
return convert_text(file_path, out_dir)
|
|
432
|
+
else:
|
|
433
|
+
# Fallback to text conversion for unknown extensions
|
|
434
|
+
return convert_text(file_path, out_dir)
|
|
435
|
+
|
|
436
|
+
def main():
|
|
437
|
+
parser = argparse.ArgumentParser(
|
|
438
|
+
description="Convert documents to Markdown and save to devflow/reference/"
|
|
439
|
+
)
|
|
440
|
+
parser.add_argument("input", help="Path to a file or directory to convert")
|
|
441
|
+
parser.add_argument(
|
|
442
|
+
"-o", "--output",
|
|
443
|
+
default="devflow/reference",
|
|
444
|
+
help="Target output directory (default: devflow/reference)"
|
|
445
|
+
)
|
|
446
|
+
parser.add_argument(
|
|
447
|
+
"--recursive",
|
|
448
|
+
action="store_true",
|
|
449
|
+
help="Recursively process subdirectories"
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
args = parser.parse_args()
|
|
453
|
+
input_path = Path(args.input).resolve()
|
|
454
|
+
base_out_dir = Path(args.output).resolve()
|
|
455
|
+
|
|
456
|
+
if not input_path.exists():
|
|
457
|
+
print(f"ERROR: Input path not found: {input_path}", file=sys.stderr)
|
|
458
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
459
|
+
|
|
460
|
+
md_engine = get_markitdown()
|
|
461
|
+
|
|
462
|
+
if input_path.is_file():
|
|
463
|
+
success = convert_single_file(input_path, base_out_dir, md_engine)
|
|
464
|
+
sys.exit(EXIT_OK if success else EXIT_CONVERSION_FAILED)
|
|
465
|
+
elif input_path.is_dir():
|
|
466
|
+
pattern = "**/*" if args.recursive else "*"
|
|
467
|
+
files = [p for p in input_path.glob(pattern) if p.is_file()]
|
|
468
|
+
|
|
469
|
+
if not files:
|
|
470
|
+
print(f"No files found in {input_path}")
|
|
471
|
+
sys.exit(EXIT_OK)
|
|
472
|
+
|
|
473
|
+
success_count = 0
|
|
474
|
+
fail_count = 0
|
|
475
|
+
for f in files:
|
|
476
|
+
if f.suffix.lower() in IGNORE_EXTS:
|
|
477
|
+
continue
|
|
478
|
+
if convert_single_file(f, base_out_dir, md_engine):
|
|
479
|
+
success_count += 1
|
|
480
|
+
else:
|
|
481
|
+
fail_count += 1
|
|
482
|
+
|
|
483
|
+
print(f"\nBatch summary: {success_count} succeeded, {fail_count} failed.")
|
|
484
|
+
sys.exit(EXIT_OK if fail_count == 0 else EXIT_CONVERSION_FAILED)
|
|
485
|
+
|
|
486
|
+
if __name__ == "__main__":
|
|
487
|
+
main()
|
|
@@ -1,70 +1,145 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: debug
|
|
3
|
-
description: "[
|
|
3
|
+
description: "[devflow][B] Diagnose a failing test, broken build, crash, error, regression, or unexpected behavior without editing source or Blueprint state. Reproduces the symptom with the smallest safe command, localizes the failing path, tests competing hypotheses, identifies the root cause when evidence supports one, and reports a repair handoff to /fix or /implement. Use when the user runs /debug, invokes $debug, asks why something is failing or broken, wants a root-cause investigation, or asks to diagnose before fixing."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# debug - find the cause before changing the code
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Where this sits in the workflow:
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
reported failure -> [debug] -> /fix or /implement
|
|
11
|
+
(test, build, (reproduce, (spec a new fix, or
|
|
12
|
+
crash, behavior) isolate, repair active work)
|
|
13
|
+
explain)
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
```
|
|
15
|
+
`/debug` separates diagnosis from repair. It gathers evidence, narrows the
|
|
16
|
+
failure to a specific cause when possible, and stops with a useful handoff. It
|
|
17
|
+
does not make the code "temporarily work" while investigating.
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
## Input
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
Accept a symptom, failing command, error message, or unexpected behavior. Examples:
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- Rule: **Do not propose a code fix before the reproduction story is verified.**
|
|
23
|
+
/debug npm test fails in cart-total.test.js
|
|
24
|
+
/debug the upload route returns 500 for PNG files
|
|
25
|
+
/debug why does the production build fail?
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- Formulate specific criteria and evidence that would *falsify* each hypothesis.
|
|
27
|
+
With no useful symptom, ask for the expected behavior, actual behavior, and
|
|
28
|
+
smallest known reproduction. Do not guess which problem the user means.
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
- Trace code execution paths end-to-end (stack traces, logs, variable states, async boundaries).
|
|
32
|
-
- Inspect recent commits or configuration changes that touch the affected boundary.
|
|
33
|
-
- Test hypotheses methodically using tests and logging without altering business logic.
|
|
30
|
+
## Step 1 - establish the boundary
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
- State precisely *why* the bug occurred (underlying invariant violation).
|
|
37
|
-
- Define the minimal, robust architectural fix direction.
|
|
38
|
-
- Propose regression prevention measures (unit test, type guard, linter rule).
|
|
32
|
+
Read the project instructions and the context relevant to the failure:
|
|
39
33
|
|
|
40
|
-
|
|
34
|
+
- `AGENTS.md` and its real commands
|
|
35
|
+
- `devflow/context/project-overview.md`
|
|
36
|
+
- `devflow/context/coding-standards.md`
|
|
37
|
+
- `devflow/context/current-feature.md`
|
|
38
|
+
- the reported error, failing output, and affected files
|
|
39
|
+
- git status, diff, and recent log when a regression is possible
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
State the symptom and what would count as reproducing it. Note whether the
|
|
42
|
+
failure belongs to an active feature or is an unplanned bug.
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
devflow/debug/rca-{slug}.md
|
|
47
|
-
```
|
|
44
|
+
Do not treat a dirty working tree as permission to discard or rewrite anything.
|
|
45
|
+
Use the diff as evidence and preserve it.
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
```markdown
|
|
51
|
-
## Debug Summary
|
|
47
|
+
## Step 2 - reproduce safely
|
|
52
48
|
|
|
53
|
-
|
|
54
|
-
2. **Evidence**: [Error logs, stack trace, file:line references]
|
|
55
|
-
3. **Investigation Path**: [Hypotheses tested and falsification proof]
|
|
56
|
-
4. **Root Cause**: [The exact mechanism causing the failure]
|
|
57
|
-
5. **Fix Direction**: [Recommended scoped change]
|
|
58
|
-
6. **Regression Guard**: [Reproduction test to add before fixing]
|
|
59
|
-
```
|
|
49
|
+
Run the smallest existing command or interaction that can reproduce the symptom.
|
|
60
50
|
|
|
61
|
-
|
|
51
|
+
- Prefer one focused test, request, CLI command, or input over the entire suite.
|
|
52
|
+
- Capture the exact exit code, error, stack trace, output, response, console
|
|
53
|
+
error, or failed request.
|
|
54
|
+
- Reuse an already-running local app when available. If reproduction requires a
|
|
55
|
+
long-running server that is not running, ask the user to start it and provide
|
|
56
|
+
the documented command.
|
|
57
|
+
- Do not install dependencies, change configuration, run migrations, mutate
|
|
58
|
+
production data, contact external users, or use destructive commands to force
|
|
59
|
+
a reproduction.
|
|
60
|
+
- Do not edit code to add logs or probes. Use existing logs, debuggers,
|
|
61
|
+
read-only inspection, or one-off commands that do not change project files.
|
|
62
|
+
- Compare git status after diagnostic commands. If one changes tracked or
|
|
63
|
+
untracked project files, stop and report those paths. Do not clean, restore,
|
|
64
|
+
or hide the changes.
|
|
65
|
+
|
|
66
|
+
If the symptom cannot be reproduced, say what was attempted and what evidence is
|
|
67
|
+
missing. Continue with static investigation only when it can produce a clearly
|
|
68
|
+
labeled hypothesis, not a claimed root cause.
|
|
69
|
+
|
|
70
|
+
## Step 3 - localize the failure
|
|
71
|
+
|
|
72
|
+
Trace from the observed failure toward the smallest responsible area.
|
|
73
|
+
|
|
74
|
+
Use the evidence that fits the project:
|
|
75
|
+
|
|
76
|
+
- the first relevant application frame in a stack trace
|
|
77
|
+
- the smallest failing test and its inputs
|
|
78
|
+
- request and response data at the failing boundary
|
|
79
|
+
- console and network errors
|
|
80
|
+
- callers, imports, data flow, and configuration reads
|
|
81
|
+
- `git diff`, `git log`, and `git blame` for a suspected regression
|
|
82
|
+
- comparison with a nearby working path or input
|
|
83
|
+
|
|
84
|
+
Separate facts from hypotheses. Test the cheapest safe competing explanations
|
|
85
|
+
first. Do not stop at the first plausible line, blame a dependency without
|
|
86
|
+
evidence, or confuse the place an error surfaced with the place it originated.
|
|
87
|
+
|
|
88
|
+
## Step 4 - confirm or narrow
|
|
89
|
+
|
|
90
|
+
A root cause is confirmed only when the evidence connects all three:
|
|
91
|
+
|
|
92
|
+
1. the triggering input or state
|
|
93
|
+
2. the responsible code, configuration, or contract
|
|
94
|
+
3. the observed failure
|
|
95
|
+
|
|
96
|
+
When safe and read-only, vary one input or run a smaller focused command to
|
|
97
|
+
confirm the connection. Do not change implementation or tests to prove the fix.
|
|
98
|
+
|
|
99
|
+
Use one of these verdicts:
|
|
100
|
+
|
|
101
|
+
- **Confirmed** - evidence identifies the cause and explains the failure.
|
|
102
|
+
- **Likely** - evidence narrows the cause, but one specific proof is unavailable.
|
|
103
|
+
- **Blocked** - the failure cannot be reproduced or required evidence is
|
|
104
|
+
inaccessible.
|
|
105
|
+
|
|
106
|
+
## Step 5 - report and hand off
|
|
107
|
+
|
|
108
|
+
Give a concise debug report:
|
|
109
|
+
|
|
110
|
+
- symptom and reproduction
|
|
111
|
+
- verdict
|
|
112
|
+
- root cause or leading hypothesis
|
|
113
|
+
- evidence, including commands and relevant paths
|
|
114
|
+
- affected behavior and likely repair boundary
|
|
115
|
+
- what was not verified
|
|
116
|
+
- exact next action
|
|
117
|
+
|
|
118
|
+
Choose the next action without writing files:
|
|
119
|
+
|
|
120
|
+
- Active feature or fix caused the failure -> return the diagnosis to
|
|
121
|
+
`/implement`.
|
|
122
|
+
- No active work item and the bug is confirmed -> recommend
|
|
123
|
+
`/fix "<concise bug and confirmed cause>"`.
|
|
124
|
+
- Cause is only likely or blocked -> recommend the next diagnostic evidence, not
|
|
125
|
+
a speculative repair.
|
|
126
|
+
- The issue is planned product work rather than a defect -> point to
|
|
127
|
+
`/feature`.
|
|
62
128
|
|
|
63
|
-
##
|
|
129
|
+
## Rules
|
|
130
|
+
|
|
131
|
+
- Diagnose, do not repair. Never edit source, tests, configuration, lockfiles, or
|
|
132
|
+
Blueprint files.
|
|
133
|
+
- Never create, switch, merge, or delete branches. Never commit or push.
|
|
134
|
+
- Do not update the findings ledger. `/audit` owns recorded code-quality
|
|
135
|
+
findings; `/debug` reports one investigated failure in chat.
|
|
136
|
+
- Evidence outranks confidence. Label uncertainty and failed reproduction
|
|
137
|
+
honestly.
|
|
138
|
+
- Preserve the user's working tree and running processes.
|
|
139
|
+
- Do not broaden one failure into a general audit or refactor.
|
|
64
140
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
- **Handoff**: `test` (write repro test), `40-execute` (execute fix), `50-verify` (re-check)
|
|
141
|
+
## Formatting
|
|
142
|
+
|
|
143
|
+
Format the output to match the project's conventions in
|
|
144
|
+
`devflow/context/ai-interaction.md`: concise, scannable markdown with a short
|
|
145
|
+
evidence list and a clear next action.
|