skillshop 2.0.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.
Files changed (62) hide show
  1. package/README.md +273 -100
  2. package/agents-content/000_communication-writing.block.md +11 -0
  3. package/agents-content/001_external-actions.block.md +9 -0
  4. package/agents-content/AGENTS.md +11 -0
  5. package/package.json +6 -3
  6. package/skills-content/architecture-scanner/SKILL.md +274 -0
  7. package/skills-content/architecture-scanner/files.json +10 -0
  8. package/skills-content/architecture-scanner/scan.sh +823 -0
  9. package/skills-content/change-review/SKILL.md +76 -0
  10. package/skills-content/change-review/agents/openai.yaml +4 -0
  11. package/skills-content/changelog/SKILL.md +99 -0
  12. package/skills-content/changeset/changeset.md +80 -0
  13. package/skills-content/code-buddy/SKILL.md +136 -0
  14. package/skills-content/design-system-catalog/SKILL.md +106 -0
  15. package/skills-content/design-system-catalog/extract-components.mjs +441 -0
  16. package/skills-content/design-system-catalog/generate-catalog.sh +255 -0
  17. package/skills-content/flare/SKILL.md +96 -0
  18. package/skills-content/grilling/SKILL.md +31 -0
  19. package/skills-content/install-sigmap/SKILL.md +97 -0
  20. package/skills-content/one-shot/SKILL.md +51 -0
  21. package/skills-content/release/SKILL.md +128 -0
  22. package/skills-content/shadow-clone/SKILL.md +228 -0
  23. package/skills-content/shadow-clone/extract-signatures.mjs +202 -0
  24. package/skills-content/shadow-clone/scan-repo.mjs +343 -0
  25. package/skills-content/ship/SKILL.md +95 -0
  26. package/skills-content/shots/SKILL.md +89 -0
  27. package/skills-content/shots/agents/openai.yaml +4 -0
  28. package/skills-content/skillshop/SKILL.md +157 -0
  29. package/skills-content/skillz/SKILL.md +15 -0
  30. package/skills-content/transcript-clean/SKILL.md +55 -0
  31. package/skills-content/transcript-edit/SKILL.md +80 -0
  32. package/skills-content/vitest/GENERATION.md +5 -0
  33. package/skills-content/vitest/SKILL.md +52 -0
  34. package/skills-content/vitest/references/advanced-environments.md +264 -0
  35. package/skills-content/vitest/references/advanced-projects.md +300 -0
  36. package/skills-content/vitest/references/advanced-type-testing.md +237 -0
  37. package/skills-content/vitest/references/advanced-vi.md +249 -0
  38. package/skills-content/vitest/references/core-cli.md +166 -0
  39. package/skills-content/vitest/references/core-config.md +174 -0
  40. package/skills-content/vitest/references/core-describe.md +193 -0
  41. package/skills-content/vitest/references/core-expect.md +219 -0
  42. package/skills-content/vitest/references/core-hooks.md +244 -0
  43. package/skills-content/vitest/references/core-test-api.md +233 -0
  44. package/skills-content/vitest/references/features-concurrency.md +250 -0
  45. package/skills-content/vitest/references/features-context.md +238 -0
  46. package/skills-content/vitest/references/features-coverage.md +207 -0
  47. package/skills-content/vitest/references/features-filtering.md +211 -0
  48. package/skills-content/vitest/references/features-mocking.md +265 -0
  49. package/skills-content/vitest/references/features-snapshots.md +207 -0
  50. package/skills-content/workleaf/SKILL.md +77 -0
  51. package/skills-content/worklog/SKILL.md +145 -0
  52. package/skills-content/worklog/references/summary-format.md +114 -0
  53. package/skills-content/worktree/SKILL.md +71 -0
  54. package/src/agents-instructions.js +256 -0
  55. package/src/cli.js +87 -23
  56. package/src/import.js +12 -7
  57. package/src/index.js +1 -0
  58. package/src/legacy-agents-content.js +36 -0
  59. package/src/legacy-cli.js +1822 -0
  60. package/src/library.js +9 -0
  61. package/src/specifier.js +11 -0
  62. package/src/user-install.js +48 -0
@@ -0,0 +1,823 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # scan.sh — Architecture scanner
4
+ #
5
+ # Scans the repository for architecturally significant files, maintains
6
+ # a files.json index, and generates architecture documentation index.
7
+ #
8
+ # Usage: .agents/skills/architecture-scanner/scan.sh
9
+ # --discover Scan repo, create/update files.json (preserves user-set priorities)
10
+ # --manifest Print JSON array of non-low files to document (full scan)
11
+ # --full Run discover + placeholders + index skeleton + verify
12
+ # --enrich Run agent-enrichment planning pass only (no file writes)
13
+ # --enrich-all Run full agent-enrichment planning pass for all files (no file writes)
14
+ # --stale Print manifest of files changed since last architecture commit (incremental)
15
+ # --verify Validate manifest coverage and index/doc link consistency
16
+ # --index Create the index skeleton if it does not exist
17
+ # --steps LIST Run selected steps from: discover, manifest, docs, stale, enrich, enrich-all, index, verify
18
+
19
+ set -euo pipefail
20
+ shopt -s globstar nullglob
21
+
22
+ REPO_ROOT="$(git rev-parse --show-toplevel)"
23
+ ARCH_DIR="$REPO_ROOT/.agents/architecture"
24
+ SKILL_DIR="$REPO_ROOT/.agents/skills/architecture-scanner"
25
+ FILES_JSON="$SKILL_DIR/files.json"
26
+ export REPO_ROOT ARCH_DIR SKILL_DIR FILES_JSON
27
+
28
+ # ── Discovery rules ──────────────────────────────────────────
29
+ # Maps glob patterns to category|default_importance.
30
+ # Globs are evaluated relative to REPO_ROOT.
31
+ # Order matters: first match wins for a given file.
32
+ DISCOVERY_RULES=(
33
+ # Root and package-level configuration
34
+ "config|high|vite.config.js"
35
+ "config|high|package*.json"
36
+ "config|high|pyproject.toml"
37
+ "config|high|Cargo.toml"
38
+ "config|high|go.mod"
39
+ "config|medium|eslint.config.js"
40
+ "config|low|playwright.config.*"
41
+
42
+ # Tests first so they stay low priority
43
+ "test|low|app/src/**/*.test.jsx"
44
+ "test|low|app/src/**/*.test.js"
45
+ "test|low|src/**/*.test.ts"
46
+ "test|low|src/**/*.test.tsx"
47
+ "test|low|src/**/*.spec.ts"
48
+ "test|low|src/**/*.spec.tsx"
49
+ "test|low|scripts/**/*.test.js"
50
+ "test|low|test/**/*.test.js"
51
+ "test|low|tests/**/*.py"
52
+ "test|low|tests/**/*.go"
53
+ "test|low|tests/**/*.rs"
54
+
55
+ # Common application entry points and interfaces
56
+ "entry|high|app/src/main.jsx"
57
+ "entry|high|src/main.js"
58
+ "entry|high|src/main.ts"
59
+ "entry|high|src/index.js"
60
+ "entry|high|src/index.ts"
61
+ "interface|high|app/src/**/*.jsx"
62
+ "interface|high|app/src/**/*.tsx"
63
+ "interface|medium|app/src/**/*.js"
64
+ "interface|medium|app/src/**/*.ts"
65
+
66
+ # Core implementation
67
+ "core|high|src/**/*.js"
68
+ "core|high|src/**/*.mjs"
69
+ "core|high|src/**/*.ts"
70
+ "core|high|src/**/*.tsx"
71
+ "core|high|src/**/*.py"
72
+ "core|high|src/**/*.go"
73
+ "core|high|src/**/*.rs"
74
+ "core|low|src/**/*.json"
75
+
76
+ # Common workspace package layouts
77
+ "config|high|packages/*/package.json"
78
+ "config|high|apps/*/package.json"
79
+ "config|high|crates/*/Cargo.toml"
80
+ )
81
+
82
+ # ── Helpers ──────────────────────────────────────────────────
83
+
84
+ # Read existing importance for a path from files.json, or return empty string
85
+ get_existing_importance() {
86
+ local filepath="$1"
87
+ if [ -f "$FILES_JSON" ]; then
88
+ # Use python3 for reliable JSON parsing
89
+ python3 -c "
90
+ import json, sys
91
+ with open('$FILES_JSON') as f:
92
+ data = json.load(f)
93
+ files = data.get('files', data) if isinstance(data, dict) else data
94
+ for entry in files:
95
+ if entry['path'] == '$filepath':
96
+ print(entry.get('importance', ''))
97
+ sys.exit(0)
98
+ print('')
99
+ " 2>/dev/null
100
+ fi
101
+ }
102
+
103
+ discover_files() {
104
+ # Collect discovered files
105
+ declare -A seen
106
+ declare -A discovered_cat
107
+ declare -A discovered_imp
108
+ local ordered_paths=()
109
+
110
+ for rule in "${DISCOVERY_RULES[@]}"; do
111
+ IFS='|' read -r category importance glob_pattern <<< "$rule"
112
+ for file in $REPO_ROOT/$glob_pattern; do
113
+ [ -f "$file" ] || continue
114
+ local relpath="${file#$REPO_ROOT/}"
115
+ if [[ -n "${seen[$relpath]+x}" ]]; then
116
+ continue
117
+ fi
118
+ seen[$relpath]=1
119
+ discovered_cat[$relpath]="$category"
120
+ discovered_imp[$relpath]="$importance"
121
+ ordered_paths+=("$relpath")
122
+ done
123
+ done
124
+
125
+ # Build new files.json, preserving existing importance values and categories
126
+ local tmp_json
127
+ tmp_json="$(mktemp)"
128
+
129
+ # Preserve existing categories array if present
130
+ python3 -c "
131
+ import json
132
+ try:
133
+ with open('$FILES_JSON') as f:
134
+ data = json.load(f)
135
+ if isinstance(data, dict) and 'categories' in data:
136
+ print(json.dumps(data['categories'], indent=4))
137
+ else:
138
+ print('null')
139
+ except:
140
+ print('null')
141
+ " > "${tmp_json}.cats"
142
+
143
+ local cats
144
+ cats="$(cat "${tmp_json}.cats")"
145
+ rm -f "${tmp_json}.cats"
146
+
147
+ if [ "$cats" = "null" ]; then
148
+ echo "{" > "$tmp_json"
149
+ echo ' "files": [' >> "$tmp_json"
150
+ else
151
+ echo "{" > "$tmp_json"
152
+ echo ' "categories": '"$cats"',' >> "$tmp_json"
153
+ echo ' "files": [' >> "$tmp_json"
154
+ fi
155
+
156
+ local first=true
157
+ for relpath in "${ordered_paths[@]}"; do
158
+ local category="${discovered_cat[$relpath]}"
159
+ local default_importance="${discovered_imp[$relpath]}"
160
+
161
+ # Preserve user-set importance if file already exists in files.json
162
+ local existing_importance
163
+ existing_importance="$(get_existing_importance "$relpath")"
164
+ local importance="${existing_importance:-$default_importance}"
165
+
166
+ if [ "$first" = true ]; then
167
+ first=false
168
+ else
169
+ echo "," >> "$tmp_json"
170
+ fi
171
+ printf ' { "path": "%s", "category": "%s", "importance": "%s" }' \
172
+ "$relpath" "$category" "$importance" >> "$tmp_json"
173
+ done
174
+
175
+ echo "" >> "$tmp_json"
176
+ echo " ]" >> "$tmp_json"
177
+ echo "}" >> "$tmp_json"
178
+
179
+ mkdir -p "$SKILL_DIR"
180
+ mv "$tmp_json" "$FILES_JSON"
181
+
182
+ # Report what was found
183
+ echo "Discovered ${#ordered_paths[@]} files. Index updated:"
184
+ echo " $FILES_JSON"
185
+ echo ""
186
+ echo "Files:"
187
+ for relpath in "${ordered_paths[@]}"; do
188
+ local category="${discovered_cat[$relpath]}"
189
+ local existing_importance
190
+ existing_importance="$(get_existing_importance "$relpath")"
191
+ local importance="$existing_importance"
192
+ printf " %-12s %-6s %s\n" "[$category]" "$importance" "$relpath"
193
+ done
194
+ }
195
+
196
+ print_manifest() {
197
+ if [ ! -f "$FILES_JSON" ]; then
198
+ echo "files.json not found. Run --discover first." >&2
199
+ exit 1
200
+ fi
201
+ python3 -c "
202
+ import json
203
+ with open('$FILES_JSON') as f:
204
+ data = json.load(f)
205
+ files = data.get('files', data) if isinstance(data, dict) else data
206
+ categories = data.get('categories', []) if isinstance(data, dict) else []
207
+ cat_map = {c['id']: c for c in categories}
208
+ out = [e for e in files if e.get('importance') != 'low']
209
+ for e in out:
210
+ e['doc'] = e['path'] + '.md'
211
+ cat = cat_map.get(e.get('category'), {})
212
+ e['category_priority'] = cat.get('priority', 'normal')
213
+ print(json.dumps(out, indent=2))
214
+ "
215
+ }
216
+
217
+ generate_placeholders() {
218
+ if [ ! -f "$FILES_JSON" ]; then
219
+ echo "files.json not found. Run --discover first." >&2
220
+ exit 1
221
+ fi
222
+ python3 - <<'PY'
223
+ import json
224
+ import os
225
+ from pathlib import Path
226
+
227
+ repo_root = os.environ["REPO_ROOT"]
228
+ arch_dir = os.environ["ARCH_DIR"]
229
+ files_json = os.environ["FILES_JSON"]
230
+
231
+ with open(files_json) as f:
232
+ data = json.load(f)
233
+
234
+ files = data.get("files", data) if isinstance(data, dict) else data
235
+ tracked = [e for e in files if e.get("importance") != "low"]
236
+ tracked_set = {e["path"] for e in tracked}
237
+
238
+ Path(arch_dir).mkdir(parents=True, exist_ok=True)
239
+ created = 0
240
+
241
+ for entry in tracked:
242
+ source_path = os.path.join(repo_root, entry["path"])
243
+ if not os.path.isfile(source_path):
244
+ continue
245
+ doc_path = os.path.join(arch_dir, f"{entry['path']}.md")
246
+ if os.path.isfile(doc_path):
247
+ continue
248
+ Path(doc_path).parent.mkdir(parents=True, exist_ok=True)
249
+ index_link = os.path.relpath(os.path.join(arch_dir, "architecture.index.md"), os.path.dirname(doc_path)).replace(os.sep, "/")
250
+ doc_lines = [
251
+ f"# `{entry['path']}`",
252
+ "",
253
+ "<!--",
254
+ f"source: {entry['path']}",
255
+ f"category: {entry.get('category', 'core')}",
256
+ f"importance: {entry['importance']}",
257
+ "-->",
258
+ "",
259
+ f"> [\u2190 Architecture Index]({index_link})",
260
+ "",
261
+ "## Meaning",
262
+ "",
263
+ "<!-- Agent: explain what this file means in the architecture, why it exists, and what would be lost if it disappeared. -->",
264
+ "",
265
+ "## Composition",
266
+ "",
267
+ "<!-- Agent: describe the important exports, control flow, data shapes, and invariants using source evidence. Include small real code samples when useful. -->",
268
+ "",
269
+ "## Dependencies",
270
+ "",
271
+ "<!-- Agent: list meaningful dependencies and what each relationship contributes. Link architecture docs where they exist. -->",
272
+ "",
273
+ "## Dependents",
274
+ "",
275
+ "<!-- Agent: identify the important consumers and why they depend on this file. -->",
276
+ "",
277
+ "## Notes",
278
+ "",
279
+ "<!-- Agent: record sturdy-bit contracts, edge cases, decisions, and documentation/CR implications. -->",
280
+ "",
281
+ ]
282
+ with open(doc_path, "w", encoding="utf-8") as f:
283
+ f.write("\n".join(doc_lines))
284
+ created += 1
285
+
286
+ for existing in Path(arch_dir).rglob("*.md"):
287
+ if existing.name == "architecture.index.md":
288
+ continue
289
+ rel = os.path.relpath(existing, repo_root).replace(os.sep, "/")
290
+ if rel.startswith("."):
291
+ continue
292
+ src = rel[:-3]
293
+ if src not in tracked_set:
294
+ existing.unlink()
295
+
296
+ print(f"created_placeholders={created}")
297
+ PY
298
+ }
299
+
300
+ print_stale() {
301
+ if [ ! -f "$FILES_JSON" ]; then
302
+ echo "files.json not found. Run --discover first." >&2
303
+ exit 1
304
+ fi
305
+ python3 -c "
306
+ import json, os, subprocess, sys
307
+
308
+ files_json = '$FILES_JSON'
309
+ arch_dir = '$ARCH_DIR'
310
+ repo_root = '$REPO_ROOT'
311
+ index_file = os.path.join(arch_dir, 'architecture.index.md')
312
+
313
+ with open(files_json) as f:
314
+ data = json.load(f)
315
+
316
+ files = data.get('files', data) if isinstance(data, dict) else data
317
+ categories = data.get('categories', []) if isinstance(data, dict) else []
318
+ cat_map = {c['id']: c for c in categories}
319
+
320
+ def git_name_only(args):
321
+ try:
322
+ out = subprocess.check_output(args, cwd=repo_root, text=True).strip()
323
+ except subprocess.CalledProcessError:
324
+ return set()
325
+ return set(out.splitlines()) if out else set()
326
+
327
+ # Find the most recent commit that touched .agents/architecture/
328
+ try:
329
+ last_arch_commit = subprocess.check_output(
330
+ ['git', 'log', '-1', '--format=%H', '--', '.agents/architecture/'],
331
+ cwd=repo_root, text=True
332
+ ).strip()
333
+ except subprocess.CalledProcessError:
334
+ last_arch_commit = ''
335
+
336
+ # Get list of source files changed since that commit (or all if no commit)
337
+ changed_files = set()
338
+ if last_arch_commit:
339
+ changed_files.update(git_name_only(['git', 'diff', '--name-only', last_arch_commit, 'HEAD']))
340
+ # Also include uncommitted changes (staged + unstaged)
341
+ changed_files.update(git_name_only(['git', 'diff', '--name-only', 'HEAD']))
342
+ changed_files.update(git_name_only(['git', 'diff', '--name-only', '--cached']))
343
+
344
+ tracked = [e for e in files if e.get('importance') != 'low']
345
+ tracked_paths = {e['path'] for e in tracked}
346
+
347
+ results = []
348
+ removed = []
349
+ removed_docs = set()
350
+ total_non_low_existing = 0
351
+
352
+ for entry in tracked:
353
+ src_rel = entry['path']
354
+ src = os.path.join(repo_root, src_rel)
355
+ doc_rel = src_rel + '.md'
356
+ doc = os.path.join(arch_dir, doc_rel)
357
+ cat = cat_map.get(entry.get('category'), {})
358
+ category_priority = cat.get('priority', 'normal')
359
+
360
+ if os.path.isfile(src):
361
+ total_non_low_existing += 1
362
+ status = None
363
+ if not os.path.isfile(doc):
364
+ status = 'missing'
365
+ elif not last_arch_commit:
366
+ # No architecture commits exist yet — everything is stale
367
+ status = 'stale'
368
+ elif src_rel in changed_files:
369
+ status = 'stale'
370
+
371
+ if status:
372
+ e = {**entry, 'doc': doc_rel, 'status': status, 'category_priority': category_priority}
373
+ if status == 'missing' and src_rel in changed_files:
374
+ e['change_type'] = 'added'
375
+ results.append(e)
376
+ elif os.path.isfile(doc):
377
+ removed.append({
378
+ **entry,
379
+ 'doc': doc_rel,
380
+ 'status': 'removed',
381
+ 'reason': 'source_deleted',
382
+ 'category_priority': category_priority
383
+ })
384
+ removed_docs.add(doc_rel)
385
+
386
+ # Find orphan docs for deleted/moved/untracked source files.
387
+ for root, _, filenames in os.walk(arch_dir):
388
+ for name in filenames:
389
+ if not name.endswith('.md') or name == 'architecture.index.md':
390
+ continue
391
+ doc_abs = os.path.join(root, name)
392
+ doc_rel = os.path.relpath(doc_abs, arch_dir).replace(os.sep, '/')
393
+ if doc_rel in removed_docs:
394
+ continue
395
+ src_rel = doc_rel[:-3]
396
+ src = os.path.join(repo_root, src_rel)
397
+ if src_rel not in tracked_paths:
398
+ reason = 'untracked_source' if os.path.isfile(src) else 'source_deleted_or_moved'
399
+ removed.append({
400
+ 'path': src_rel,
401
+ 'doc': doc_rel,
402
+ 'status': 'removed',
403
+ 'reason': reason
404
+ })
405
+ removed_docs.add(doc_rel)
406
+
407
+ index_reasons = []
408
+ if not os.path.isfile(index_file):
409
+ index_reasons.append('index_missing')
410
+ if results or removed:
411
+ index_reasons.append('assets_changed')
412
+ if '.agents/skills/architecture-scanner/files.json' in changed_files:
413
+ index_reasons.append('category_config_changed')
414
+ if any(
415
+ p.startswith('.agents/architecture/')
416
+ and p != '.agents/architecture/architecture.index.md'
417
+ for p in changed_files
418
+ ):
419
+ index_reasons.append('docs_changed')
420
+
421
+ output = {
422
+ 'index': {
423
+ 'status': 'stale' if index_reasons else 'fresh',
424
+ 'reasons': index_reasons
425
+ },
426
+ 'files': results,
427
+ 'removed': removed
428
+ }
429
+
430
+ # Suggest full scan if >50% of existing non-low files need regeneration.
431
+ if total_non_low_existing > 0 and len(results) > total_non_low_existing * 0.5:
432
+ output['suggestion'] = 'full_scan'
433
+ output['message'] = (
434
+ f'{len(results)} of {total_non_low_existing} documented files need updates. '
435
+ 'Consider running --manifest (or --full) for a full scan instead.'
436
+ )
437
+
438
+ print(json.dumps(output, indent=2))
439
+ "
440
+ }
441
+
442
+ verify_architecture() {
443
+ if [ ! -f "$FILES_JSON" ]; then
444
+ echo "files.json not found. Run --discover first." >&2
445
+ exit 1
446
+ fi
447
+ python3 - <<'PY'
448
+ import json
449
+ import os
450
+
451
+ repo_root = os.environ["REPO_ROOT"]
452
+ arch_dir = os.environ["ARCH_DIR"]
453
+ files_json = os.environ["FILES_JSON"]
454
+
455
+ with open(files_json) as f:
456
+ data = json.load(f)
457
+
458
+ files = data.get("files", data) if isinstance(data, dict) else data
459
+ tracked = [entry for entry in files if entry.get("importance") != "low"]
460
+ tracked_docs = {f"{entry['path']}.md" for entry in tracked}
461
+ tracked_sources = {entry["path"] for entry in tracked}
462
+
463
+ index_file = os.path.join(arch_dir, "architecture.index.md")
464
+ index_lines = []
465
+ if os.path.isfile(index_file):
466
+ with open(index_file, "r", encoding="utf-8") as f:
467
+ index_lines = f.read().splitlines()
468
+
469
+ results = {
470
+ "status": "ok",
471
+ "missing_docs": [],
472
+ "orphan_docs": [],
473
+ "index_missing": False,
474
+ "docs_missing_backlink": [],
475
+ "index_unlinked_docs": []
476
+ }
477
+
478
+ if not os.path.isfile(index_file):
479
+ results["status"] = "warn"
480
+ results["index_missing"] = True
481
+ else:
482
+ indexed_docs = set()
483
+ for line in index_lines:
484
+ line = line.strip()
485
+ if line.startswith("- [") and "(" in line and ")" in line:
486
+ start = line.find("(")
487
+ end = line.find(")", start + 1)
488
+ if start != -1 and end != -1:
489
+ link = line[start + 1:end]
490
+ if link.startswith("./"):
491
+ link = link[2:]
492
+ if link.endswith(".md"):
493
+ indexed_docs.add(link)
494
+
495
+ for relpath in sorted(tracked_docs):
496
+ doc = os.path.join(arch_dir, relpath)
497
+ if not os.path.isfile(doc):
498
+ results["status"] = "warn"
499
+ results["missing_docs"].append(relpath)
500
+ continue
501
+ with open(doc, "r", encoding="utf-8") as f:
502
+ text = f.read()
503
+ if "Architecture Index" not in text:
504
+ results["status"] = "warn"
505
+ results["docs_missing_backlink"].append(relpath)
506
+
507
+ for root, _, filenames in os.walk(arch_dir):
508
+ for name in filenames:
509
+ if not name.endswith(".md") or name == "architecture.index.md":
510
+ continue
511
+ rel = os.path.relpath(os.path.join(root, name), arch_dir).replace(os.sep, "/")
512
+ if rel not in tracked_docs:
513
+ results["status"] = "warn"
514
+ results["orphan_docs"].append(rel)
515
+ continue
516
+ if rel not in indexed_docs:
517
+ results["status"] = "warn"
518
+ results["index_unlinked_docs"].append(rel)
519
+
520
+ print(json.dumps(results, indent=2))
521
+ PY
522
+ }
523
+
524
+ generate_index() {
525
+ mkdir -p "$ARCH_DIR"
526
+ local index_file="$ARCH_DIR/architecture.index.md"
527
+ if [ -f "$index_file" ]; then
528
+ echo "Preserved existing index: $index_file"
529
+ return
530
+ fi
531
+
532
+ python3 - <<'PY'
533
+ import json
534
+ import os
535
+ from pathlib import Path
536
+
537
+ repo_root = os.environ["REPO_ROOT"]
538
+ arch_dir = Path(os.environ["ARCH_DIR"])
539
+ files_json = os.environ["FILES_JSON"]
540
+ index_path = arch_dir / "architecture.index.md"
541
+
542
+ with open(files_json) as f:
543
+ data = json.load(f)
544
+
545
+ files = data.get("files", data) if isinstance(data, dict) else data
546
+ categories = data.get("categories", []) if isinstance(data, dict) else []
547
+ category_map = {item["id"]: item for item in categories}
548
+ tracked = [item for item in files if item.get("importance") != "low"]
549
+
550
+ lines = [
551
+ "# Architecture Index",
552
+ "",
553
+ "> Structural index generated by the architecture-scanner skill.",
554
+ "> The agent owns the repository overview, category concepts, and file descriptions.",
555
+ "",
556
+ "## Repository overview",
557
+ "",
558
+ "<!-- Agent: explain what this repository is, the user/problem it serves, its major runtime surfaces, and the path of data through them. Ground this in package manifests and source. -->",
559
+ "",
560
+ ]
561
+
562
+ ordered_categories = sorted(categories, key=lambda item: item.get("order", 99))
563
+ for category in ordered_categories:
564
+ members = [item for item in tracked if item.get("category") == category.get("id")]
565
+ if not members:
566
+ continue
567
+ lines.extend([
568
+ f"## {category.get('name', category.get('id'))}",
569
+ "",
570
+ f"<!-- Agent: explain the concept represented by this category, its role in the repository, and how it relates to other categories. Priority: {category.get('priority', 'normal')}. -->",
571
+ "",
572
+ ])
573
+ for item in sorted(members, key=lambda value: value["path"]):
574
+ doc = f"{item['path']}.md"
575
+ lines.append(f"- [`{item['path']}`](./{doc}) <!-- Agent: add a concise meaning statement. -->")
576
+ lines.append("")
577
+
578
+ index_path.write_text("\n".join(lines), encoding="utf-8")
579
+ print(f"Created index skeleton: {index_path}")
580
+ PY
581
+ }
582
+
583
+ build_enrich_plan() {
584
+ local enrich_mode="${1:-changed}"
585
+ if [ ! -f "$FILES_JSON" ]; then
586
+ echo "files.json not found. Run --discover first." >&2
587
+ exit 1
588
+ fi
589
+
590
+ ARCH_ENRICH_MODE="$enrich_mode" python3 - <<'PY'
591
+ import json
592
+ import os
593
+ import subprocess
594
+
595
+ repo_root = os.environ["REPO_ROOT"]
596
+ arch_dir = os.environ["ARCH_DIR"]
597
+ files_json = os.environ["FILES_JSON"]
598
+ mode = os.environ.get("ARCH_ENRICH_MODE", "changed")
599
+
600
+ with open(files_json) as f:
601
+ data = json.load(f)
602
+
603
+ files = data.get("files", data) if isinstance(data, dict) else data
604
+ tracked = [e for e in files if e.get("importance") != "low"]
605
+ tracked_paths = {entry["path"] for entry in tracked}
606
+
607
+ def changed_files(since_commit):
608
+ if not since_commit:
609
+ return set()
610
+ changed = set()
611
+ for args in (
612
+ ["git", "diff", "--name-only", since_commit, "HEAD"],
613
+ ["git", "diff", "--name-only", "HEAD"],
614
+ ["git", "diff", "--name-only", "--cached"],
615
+ ):
616
+ try:
617
+ out = subprocess.check_output(args, cwd=repo_root, text=True).strip()
618
+ except subprocess.CalledProcessError:
619
+ continue
620
+ if out:
621
+ changed.update(out.splitlines())
622
+ return changed
623
+
624
+
625
+ try:
626
+ last_arch_commit = subprocess.check_output(
627
+ ["git", "log", "-1", "--format=%H", "--", ".agents/architecture/"],
628
+ cwd=repo_root,
629
+ text=True,
630
+ ).strip()
631
+ except subprocess.CalledProcessError:
632
+ last_arch_commit = ""
633
+
634
+ changed = changed_files(last_arch_commit)
635
+ if not changed:
636
+ changed = set()
637
+
638
+ create = []
639
+ update = []
640
+ delete = []
641
+
642
+ for entry in tracked:
643
+ path = entry["path"]
644
+ category = entry.get("category", "core")
645
+ importance = entry.get("importance", "medium")
646
+ source_path = os.path.join(repo_root, path)
647
+ doc_rel = f"{path}.md"
648
+ doc_path = os.path.join(arch_dir, doc_rel)
649
+
650
+ if not os.path.isfile(source_path):
651
+ continue
652
+
653
+ task = {
654
+ "path": path,
655
+ "doc": doc_rel,
656
+ "category": category,
657
+ "importance": importance,
658
+ "enrichment_focus": [
659
+ "Write the file's architectural meaning and why it exists.",
660
+ "Explain behavior, data flow, contracts, and important relationships from source evidence.",
661
+ "Record edge cases, sturdy-bit implications, and relevant records."
662
+ ],
663
+ "status": "",
664
+ "reason": "",
665
+ }
666
+
667
+ try:
668
+ with open(doc_path, "r", encoding="utf-8") as doc_file:
669
+ existing_text = doc_file.read()
670
+ except Exception:
671
+ existing_text = ""
672
+
673
+ if existing_text:
674
+ lines = existing_text.splitlines()
675
+ for i, line in enumerate(lines):
676
+ if line.startswith("## Goal") and i + 1 < len(lines):
677
+ task["existing_goal_preview"] = lines[i + 1].strip()
678
+ break
679
+
680
+ if mode == "all":
681
+ if not os.path.isfile(doc_path):
682
+ task["status"] = "create"
683
+ task["reason"] = "missing_doc"
684
+ create.append(task)
685
+ else:
686
+ task["status"] = "update"
687
+ task["reason"] = "full_rewrite"
688
+ update.append(task)
689
+ else:
690
+ if not os.path.isfile(doc_path):
691
+ task["status"] = "create"
692
+ task["reason"] = "missing_doc"
693
+ create.append(task)
694
+ elif path in changed:
695
+ task["status"] = "update"
696
+ task["reason"] = "source_changed"
697
+ update.append(task)
698
+
699
+ for root, _, names in os.walk(arch_dir):
700
+ for name in names:
701
+ if not name.endswith(".md") or name == "architecture.index.md":
702
+ continue
703
+ rel = os.path.relpath(os.path.join(root, name), arch_dir).replace(os.sep, "/")
704
+ src_rel = rel[:-3]
705
+ if src_rel not in tracked_paths and os.path.isfile(os.path.join(repo_root, src_rel)):
706
+ delete.append({"path": src_rel, "doc": rel, "reason": "not_tracked"})
707
+ elif src_rel not in tracked_paths:
708
+ delete.append({"path": src_rel, "doc": rel, "reason": "source_deleted_or_untracked"})
709
+
710
+ output = {
711
+ "mode": "agent_enrichment",
712
+ "status": "ready",
713
+ "count": {
714
+ "create": len(create),
715
+ "update": len(update),
716
+ "delete": len(delete),
717
+ },
718
+ "notes": "The scanner provides structure only. The agent must read source and write the prose.",
719
+ "create": create,
720
+ "update": update,
721
+ "delete": delete,
722
+ "next_steps": [
723
+ "For each path in create/update, read the source and write the complete architecture document.",
724
+ "For each entry in delete, remove the stale architecture doc file.",
725
+ "Read and update architecture.index.md with the repository overview, category concepts, and file meanings; preserve authored prose.",
726
+ "Run .agents/skills/architecture-scanner/scan.sh --verify to confirm consistency.",
727
+ ],
728
+ }
729
+
730
+ print(json.dumps(output, indent=2))
731
+ PY
732
+ }
733
+
734
+ run_steps() {
735
+ local steps_csv="${1:-}"
736
+ if [ -z "$steps_csv" ]; then
737
+ echo "--steps requires a comma-separated list" >&2
738
+ exit 1
739
+ fi
740
+
741
+ local old_ifs="$IFS"
742
+ IFS=','
743
+ for step in $steps_csv; do
744
+ case "$step" in
745
+ discover)
746
+ discover_files
747
+ ;;
748
+ manifest)
749
+ print_manifest
750
+ ;;
751
+ docs)
752
+ generate_placeholders
753
+ ;;
754
+ stale)
755
+ print_stale
756
+ ;;
757
+ enrich)
758
+ build_enrich_plan changed
759
+ ;;
760
+ enrich-all)
761
+ build_enrich_plan all
762
+ ;;
763
+ index)
764
+ generate_index
765
+ ;;
766
+ verify)
767
+ verify_architecture
768
+ ;;
769
+ *)
770
+ echo "Unknown step: $step" >&2
771
+ echo "Supported: discover, manifest, docs, stale, enrich, enrich-all, index, verify" >&2
772
+ exit 1
773
+ ;;
774
+ esac
775
+ done
776
+ IFS="$old_ifs"
777
+ }
778
+
779
+ case "${1:-}" in
780
+ --discover)
781
+ discover_files
782
+ ;;
783
+ --manifest)
784
+ print_manifest
785
+ ;;
786
+ --full)
787
+ discover_files
788
+ generate_placeholders
789
+ generate_index
790
+ verify_architecture
791
+ ;;
792
+ --enrich)
793
+ build_enrich_plan changed
794
+ ;;
795
+ --enrich-all)
796
+ build_enrich_plan all
797
+ ;;
798
+ --stale)
799
+ print_stale
800
+ ;;
801
+ --verify)
802
+ verify_architecture
803
+ ;;
804
+ --index)
805
+ generate_index
806
+ ;;
807
+ --steps)
808
+ run_steps "${2:-}"
809
+ ;;
810
+ *)
811
+ echo "Usage: $0 [--discover | --manifest | --full | --enrich | --enrich-all | --stale | --verify | --index | --steps]"
812
+ echo " --discover Scan repo and create/update files.json (preserves priorities)"
813
+ echo " --manifest Print JSON manifest of non-low files to document (full scan)"
814
+ echo " --full Discover + placeholders + index skeleton + verify"
815
+ echo " --enrich Generate an agent-only enrichment worklist (changed files only)"
816
+ echo " --enrich-all Generate an agent-only enrichment worklist for all non-low files"
817
+ echo " --stale Print manifest of files changed since last architecture commit (incremental)"
818
+ echo " --verify Validate manifest coverage and index/doc links"
819
+ echo " --index Create the index skeleton if it does not exist"
820
+ echo " --steps LIST Run comma-separated steps"
821
+ exit 1
822
+ ;;
823
+ esac