Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions skills/distill-knowledge/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ compatibility: Requires uv, ffmpeg, ffprobe, Python ≥3.10, and OPENAI_API_KEY
license: MIT
metadata:
author: Dim Kharitonov <dimds@fastmail.com> (https://github.com/dimdasci)
version: "1.1.0"
version: "1.2.0"
---

# Convert Recording → Knowledge Markdown

Emit `outbox/{meeting-slug}/transcript.md`; screenshots inline when useful; structured docs only on request. `{meeting-slug}` = `kebab-case-topic-YYYYMMDD`. Never touch `inbox/` or `knowledge/`.

References (load on demand): [setup](references/setup.md) · [output templates](references/output-templates.md) · [ffmpeg](references/ffmpeg.md) · [transcribe CLI](references/transcribe-cli.md) · [prep audio CLI](references/prep-audio-cli.md) · [chunked transcription](references/chunked-transcription.md) · [structured docs](references/structured-docs.md) · spot-check: [`scripts/extract_clip.py`](scripts/extract_clip.py).
References (load on demand): [setup](references/setup.md) · [output templates](references/output-templates.md) · [ffmpeg](references/ffmpeg.md) · [transcribe CLI](references/transcribe-cli.md) · [prep audio CLI](references/prep-audio-cli.md) · [chunked transcription](references/chunked-transcription.md) · [structured docs](references/structured-docs.md) · [cleanup CLI](scripts/cleanup.py) · spot-check: [`scripts/extract_clip.py`](scripts/extract_clip.py).

Scripts run via `uv run --script` (PEP 723). All support `--help`. On first run verify [setup prerequisites](references/setup.md).

Expand Down Expand Up @@ -85,9 +85,19 @@ Technical / multilingual / mumbled audio → VTT-aligned path strongly preferred

### Steps 7–10 — Structured docs (conditional)

**Gate 2** — ask: structured docs or transcript only? If transcript only → report + stop.
**Gate 2** — ask: structured docs or transcript only? If transcript only → cleanup + report + stop.

Otherwise → [structured docs reference](references/structured-docs.md): plan topics, emit `summary.md` + `topics/{slug}.md`, report, stop.
Otherwise → [structured docs reference](references/structured-docs.md): plan topics, emit `summary.md` + `topics/{slug}.md`, cleanup, report, stop.

### Step 11 — Cleanup (mandatory, after user accepts)

Once the user confirms the results are acceptable, remove temporary prep artifacts via [`cleanup.py`](scripts/cleanup.py):

```bash
uv run --script scripts/cleanup.py --slug {meeting-slug}
```

The script verifies `outbox/{meeting-slug}/transcript.md` exists before deleting `tmp/prep/{meeting-slug}/`. Use `--dry-run` to preview. **Never use `rm` directly — all temp removal goes through this script.**

## Fidelity rule

Expand All @@ -107,3 +117,4 @@ When two transcripts agree on meaning → faithful. When they disagree and neith
- **Don't load rich `--prompt`** on non-diarize model. Prompts leak as fabrications. Vocab list + 1-line topic max. See [prompt-hallucination warning](references/transcribe-cli.md#prompt-hallucination-warning).
- **Don't auto-pick VTT decisions.** Always surface assessment; user confirms at Gate 1.
- **Don't write outside `outbox/{meeting-slug}/`.** Temp files → `tmp/`; finals → `outbox/`.
- **Don't run `rm` on temp files.** Always use [`cleanup.py`](scripts/cleanup.py) — it validates output exists and only operates under `tmp/`.
138 changes: 138 additions & 0 deletions skills/distill-knowledge/scripts/cleanup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10"
# dependencies = []
# ///
"""Remove temporary prep artifacts after the user accepts the transcript.

Safety constraints:
- Only deletes directories under `<project-root>/tmp/`.
- Refuses to act if the final transcript is missing in outbox.
- Never shells out to `rm`; uses Python's shutil.rmtree.

Run with:
uv run --script skills/distill-knowledge/scripts/cleanup.py \
--slug <meeting-slug> [--project-root .]
"""

from __future__ import annotations

import argparse
import shutil
import sys
from pathlib import Path
from typing import NoReturn


def _die(message: str, code: int = 1) -> NoReturn:
print(f"Error: {message}", file=sys.stderr)
raise SystemExit(code)


def _resolve_project_root(raw: str) -> Path:
"""Resolve and validate the project root directory."""
root = Path(raw).resolve()
if not root.is_dir():
_die(f"Project root is not a directory: {root}")
return root


def _assert_under_tmp(path: Path, tmp_root: Path) -> None:
"""Hard guard: path must be strictly inside tmp_root."""
try:
path.resolve().relative_to(tmp_root.resolve())
except ValueError:
_die(
f"Refusing to delete {path}: it is not under {tmp_root}. "
"This script only removes directories inside the tmp/ folder."
)


def _dir_size(path: Path) -> int:
"""Total bytes of all files in a directory tree."""
return sum(f.stat().st_size for f in path.rglob("*") if f.is_file())


def _human_size(nbytes: int) -> str:
"""Format byte count as a human-readable string."""
for unit in ("B", "KB", "MB", "GB"):
if nbytes < 1024:
return f"{nbytes:.1f} {unit}"
nbytes /= 1024 # type: ignore[assignment]
return f"{nbytes:.1f} TB"


def main() -> None:
parser = argparse.ArgumentParser(
description="Remove temporary prep artifacts for a completed meeting."
)
parser.add_argument(
"--slug",
required=True,
help="Meeting slug (e.g. architecture-dima-mark-20260211)",
)
parser.add_argument(
"--project-root",
default=".",
help="Project root directory (default: current directory)",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be deleted without actually deleting",
)

args = parser.parse_args()
slug: str = args.slug
root = _resolve_project_root(args.project_root)
tmp_root = root / "tmp"
prep_dir = tmp_root / "prep" / slug
outbox_dir = root / "outbox" / slug
transcript = outbox_dir / "transcript.md"

# --- Pre-flight checks ---

if not tmp_root.is_dir():
_die(f"tmp/ directory does not exist: {tmp_root}")

if not prep_dir.is_dir():
_die(f"Nothing to clean: {prep_dir} does not exist")

_assert_under_tmp(prep_dir, tmp_root)

if not transcript.is_file():
_die(
f"Final transcript not found at {transcript}. "
"Refusing to delete temp artifacts before the output is confirmed. "
"Finish the transcript first, then re-run cleanup."
)

# --- Inventory ---

files = list(prep_dir.rglob("*"))
file_count = sum(1 for f in files if f.is_file())
dir_count = sum(1 for f in files if f.is_dir())
total_bytes = _dir_size(prep_dir)

print(f"Slug: {slug}", file=sys.stderr)
print(f"Prep dir: {prep_dir}", file=sys.stderr)
print(f"Transcript: {transcript} ✓", file=sys.stderr)
print(f"Files: {file_count}", file=sys.stderr)
print(f"Directories: {dir_count}", file=sys.stderr)
print(f"Size: {_human_size(total_bytes)}", file=sys.stderr)

if args.dry_run:
print(
"\n[dry-run] Would delete the above. Pass without --dry-run to execute.",
file=sys.stderr,
)
return

# --- Delete ---

shutil.rmtree(prep_dir)
print(f"\nDeleted {prep_dir} — freed {_human_size(total_bytes)}", file=sys.stderr)


if __name__ == "__main__":
main()
106 changes: 106 additions & 0 deletions skills/distill-knowledge/scripts/tests/test_cleanup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Tests for cleanup.py safety logic."""

from __future__ import annotations

import json
import subprocess
import sys
from pathlib import Path

SCRIPT = str(Path(__file__).resolve().parent.parent / "cleanup.py")


def _run(
tmp_path: Path, slug: str, *, dry_run: bool = False, extra_args: list[str] | None = None
) -> subprocess.CompletedProcess[str]:
cmd = [
sys.executable,
SCRIPT,
"--slug",
slug,
"--project-root",
str(tmp_path),
]
if dry_run:
cmd.append("--dry-run")
if extra_args:
cmd.extend(extra_args)
return subprocess.run(cmd, capture_output=True, text=True)


def _setup_project(tmp_path: Path, slug: str, *, with_transcript: bool = True) -> None:
"""Create minimal project structure with tmp/prep and optionally outbox."""
prep = tmp_path / "tmp" / "prep" / slug
prep.mkdir(parents=True)
# Create some temp files
(prep / "stripped.ogg").write_bytes(b"\x00" * 1024)
(prep / "manifest.json").write_text(json.dumps({"version": 1}))
chunks = prep / "chunks"
chunks.mkdir()
(chunks / "chunk_00.ogg").write_bytes(b"\x00" * 512)

if with_transcript:
outbox = tmp_path / "outbox" / slug
outbox.mkdir(parents=True)
(outbox / "transcript.md").write_text("# Test transcript\n")


def test_refuses_without_transcript(tmp_path: Path) -> None:
"""Cleanup must refuse if outbox transcript is missing."""
slug = "no-transcript-slug"
_setup_project(tmp_path, slug, with_transcript=False)

result = _run(tmp_path, slug)
assert result.returncode != 0
assert "Final transcript not found" in result.stderr
# Prep dir must still exist
assert (tmp_path / "tmp" / "prep" / slug).is_dir()


def test_refuses_nonexistent_slug(tmp_path: Path) -> None:
"""Cleanup must fail if prep dir doesn't exist."""
(tmp_path / "tmp").mkdir(parents=True)
result = _run(tmp_path, "ghost-slug")
assert result.returncode != 0
assert "does not exist" in result.stderr


def test_dry_run_preserves_files(tmp_path: Path) -> None:
"""Dry run should report but not delete."""
slug = "dry-run-test"
_setup_project(tmp_path, slug)

result = _run(tmp_path, slug, dry_run=True)
assert result.returncode == 0
assert "dry-run" in result.stderr
# Nothing deleted
assert (tmp_path / "tmp" / "prep" / slug / "stripped.ogg").exists()


def test_actual_cleanup(tmp_path: Path) -> None:
"""Actual cleanup should remove prep dir when transcript exists."""
slug = "real-cleanup"
_setup_project(tmp_path, slug)

prep = tmp_path / "tmp" / "prep" / slug
assert prep.is_dir()

result = _run(tmp_path, slug)
assert result.returncode == 0
assert "Deleted" in result.stderr
assert "freed" in result.stderr
# Prep dir gone
assert not prep.exists()
# Transcript untouched
assert (tmp_path / "outbox" / slug / "transcript.md").is_file()


def test_reports_file_count_and_size(tmp_path: Path) -> None:
"""Dry run output should include file count and size."""
slug = "inventory-check"
_setup_project(tmp_path, slug)

result = _run(tmp_path, slug, dry_run=True)
assert "Files:" in result.stderr
assert "Size:" in result.stderr
assert "Directories:" in result.stderr
Loading