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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ jobs:
run: |
VERSION=${GITHUB_REF#refs/tags/v}
WHL_VERSION=$(python3 -c "import re,sys; v='${VERSION}'; m=re.match(r'^(\d+\.\d+\.\d+)-(.+)$',v); print(m.group(1)+'+'+m.group(2).replace('-','.')if m else v)")
pip install "wheels/periscope-${WHL_VERSION}-py3-none-manylinux_2_28_x86_64.whl"
pip install "wheels/periscope_agentsview-${WHL_VERSION}-py3-none-manylinux_2_28_x86_64.whl"
periscope --version

- name: Publish to PyPI
Expand Down
56 changes: 51 additions & 5 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: Matryoshka model, branch roles, and fork invariants for diazMelgare

> **Status:** Living document. Update after every upstream replay and fork enhancement.
> **Canonical integration branch:** `merged`
> **Last updated:** 2026-07-28
> **Last updated:** 2026-07-30

**Operator policy:** [`docs/guides/periscope-upstream-sync-blueprint.md`](guides/periscope-upstream-sync-blueprint.md)
**Rename checklist:** [`docs/guides/agentsview-to-periscope-rename-catalogue.md`](guides/agentsview-to-periscope-rename-catalogue.md)
Expand Down Expand Up @@ -171,26 +171,72 @@ The shipping integration line uses

## Versioning and release tags

Semver pre-release identity:
### Fork release tags (Layer 3 — Periscope identity)

Fork releases use a **grandmother-derived semver** plus a **`-periscope.N`**
suffix that marks the fork’s major AgentsView integrative merge generation on
`merged`. Product binaries and CI artifacts stay branded `periscope`; the
`agentsview` mirror branch name is unrelated to release tags.

**Formula:**

```
v0.(upstream_minor + 1).2-periscope.2
v0.(upstream_minor + 1).2-periscope.N
```

| Part | Meaning |
| --- | --- |
| `v0.(upstream_minor + 1)` | kenn-io/agentsview release minor + 1 (readable upstream base) |
| `.2` | Periscope patch slot on that upstream-derived minor (established convention) |
| `-periscope.N` | Fork identity generation — **N** = major AgentsView → `merged` integrative absorption count |

**Grandmother → fork tag map (canonical examples):**

| Grandmother (`kenn-io/agentsview`) | Fork release tag | Note |
| --- | --- | --- |
| v0.28.x (latentsignal era) | `v0.29.2-periscope.2` | First shipped fork generation |
| v0.39.0 (current grandmother) | **`v0.40.2-periscope.3`** | **Third** major AgentsView integrative merge onto `merged` |

**Current target (post–third AgentsView merge):** `v0.40.2-periscope.3` — bump
`jetbrains-plugin/gradle.properties`, release `-ldflags`, and operator tags when
that integrative merge lands on `merged` (not when the mirror alone advances).

Historical tags (`v0.29.2-periscope.2-*`, `v0.29.2-periscope.3-*`) remain valid
release pointers; do not rewrite them.

### Desktop app semver (parallel track)

`desktop/src-tauri/tauri.conf.json` `version` follows the **grandmother desktop
tree** (currently `0.12.1` on both mirror and `merged`). `productName` on
`merged` is **Periscope**; on the mirror it remains **AgentsView**. Bump desktop
semver when grandmother bumps it during replay — independent of the fork tag
suffix above.

### Commit-suffixed release tags

Tags **always embed the short commit hash** of the release commit. Convention
adopted in `5bd2e8a` (May 2026):

```
v{semver}-{8-char-commit} e.g. v0.29.2-periscope.2-657a1090
v{semver}-{8-char-commit} e.g. v0.40.2-periscope.3-657a1090
```

```bash
COMMIT=$(git rev-parse --short=8 HEAD)
VERSION="v0.29.2-periscope.2"
VERSION="v0.40.2-periscope.3"
git tag -a "${VERSION}-${COMMIT}" -m "Release ${VERSION}-${COMMIT}"
# Push only after operator review — never automatic from tooling here.
```

After each major AgentsView integrative merge onto `merged`:

```bash
# Example: grandmother v0.39.0 → v0.40.2-periscope.3 (third absorption)
OLD_VERSION="v0.29.2-periscope.2"
NEW_VERSION="v0.40.2-periscope.3"
# Update jetbrains-plugin/gradle.properties and release ldflags; then tag as above.
```

---

## Desktop app identity
Expand Down
27 changes: 17 additions & 10 deletions docs/guides/periscope-modernization-status.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Periscope modernization status

**Updated:** 2026-07-28
**Updated:** 2026-07-30
**Base commit:** `2b6e5128` (3way modernization replay)
**Integration branch:** `merged`
**Replaces:** historical `PROGRESS.md` completion ledger (archive only — not active truth)
Expand Down Expand Up @@ -35,26 +35,33 @@ related specs.

---

## Fork release tags (Layer 3 — Periscope identity)

See [`ARCHITECTURE.md`](../ARCHITECTURE.md) § Versioning for the full matryoshka
context. Summary:

| Track | Rule |
| --- | --- |
| Fork release tag | `v0.(upstream_minor + 1).2-periscope.N` — **N** = major AgentsView → `merged` merge generation |
| Grandmother v0.39.0 | Target **`v0.40.2-periscope.3`** (third integrative absorption) |
| Desktop `tauri.conf.json` | Follows grandmother semver (`0.12.1` today); `productName` **Periscope** on `merged` |

Historical shipped tag: `v0.29.2-periscope.2-*` (grandmother v0.28 era).

## Release tag convention (5bd2e8a)

Fork releases **always embed the short commit hash** of the release commit.
Adopted in commit `5bd2e8a421c326a9a40e73437930ba76f475b9cb` (May 2026).

```
v{semver}-{8-char-commit} e.g. v0.29.2-periscope.2-657a1090
```

Semver pre-release identity on `merged`:

```
v0.(upstream_minor + 1).2-periscope.2
v{semver}-{8-char-commit} e.g. v0.40.2-periscope.3-657a1090
```

Operator tagging (after verification — never automatic from tooling):

```bash
COMMIT=$(git rev-parse --short=8 HEAD)
VERSION="v0.29.2-periscope.2"
VERSION="v0.40.2-periscope.3"
git tag -a "${VERSION}-${COMMIT}" -m "Release ${VERSION}-${COMMIT}"
```

Expand Down Expand Up @@ -117,7 +124,7 @@ shell environment overrides (see `desktop/src-tauri/src/lib.rs`).

# Release tag (operator, after verification)
COMMIT=$(git rev-parse --short=8 HEAD)
git tag -a "v0.29.2-periscope.2-${COMMIT}" -m "Release v0.29.2-periscope.2-${COMMIT}"
git tag -a "v0.40.2-periscope.3-${COMMIT}" -m "Release v0.40.2-periscope.3-${COMMIT}"
```

---
Expand Down
18 changes: 15 additions & 3 deletions scripts/build_wheels.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ def normalize_wheel_version(version: str) -> str:
return version


# ---------------------------------------------------------------------------
# Package identity
# ---------------------------------------------------------------------------

# PyPI project name. The upstream name "periscope" is registered by an
# unrelated project (a subtitle downloader), so this fork publishes under
# a distinct distribution name. The installed console script and the
# internal module/import name stay "periscope" -- only the PyPI project
# identity (and therefore the wheel filename and dist-info dir) differ.
PYPI_DIST_NAME = "periscope-agentsview"
PYPI_DIST_NAME_NORMALIZED = PYPI_DIST_NAME.replace("-", "_")

# ---------------------------------------------------------------------------
# Platform constants
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -217,8 +229,8 @@ def build_wheel(

# Normalize version for PEP 440 compliance (dashes → local identifier)
whl_version = normalize_wheel_version(version)
dist_info = f"periscope-{whl_version}.dist-info"
whl_name = f"periscope-{whl_version}-py3-none-{wheel_tag}.whl"
dist_info = f"{PYPI_DIST_NAME_NORMALIZED}-{whl_version}.dist-info"
whl_name = f"{PYPI_DIST_NAME_NORMALIZED}-{whl_version}-py3-none-{wheel_tag}.whl"

output_dir.mkdir(parents=True, exist_ok=True)
whl_path = output_dir / whl_name
Expand Down Expand Up @@ -271,7 +283,7 @@ def _add(arcname: str, data: bytes, unix_mode: int = 0o644) -> None:
def _build_metadata(version: str, readme: str | None) -> str:
lines = [
"Metadata-Version: 2.1",
"Name: periscope",
f"Name: {PYPI_DIST_NAME}",
f"Version: {version}",
"Summary: Local web viewer for AI agent sessions",
"Home-page: https://github.com/latentsignal-org/periscope",
Expand Down
38 changes: 19 additions & 19 deletions scripts/build_wheels_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ def test_nested_path_in_tar_gz(self, tmp_path: Path) -> None:
class TestBuildWheel:
def test_wheel_filename_matches_convention(self, tmp_path: Path) -> None:
whl = build_wheel(b"fake", tmp_path, "0.15.0", "linux_amd64")
assert whl.name == "periscope-0.15.0-py3-none-manylinux_2_28_x86_64.whl"
assert whl.name == "periscope_agentsview-0.15.0-py3-none-manylinux_2_28_x86_64.whl"

def test_wheel_is_valid_zip(self, tmp_path: Path) -> None:
whl = build_wheel(b"fake", tmp_path, "0.15.0", "linux_amd64")
Expand All @@ -226,10 +226,10 @@ def test_wheel_contains_expected_files(self, tmp_path: Path) -> None:
assert "periscope/__init__.py" in names
assert "periscope/__main__.py" in names
assert "periscope/bin/periscope" in names
assert "periscope-0.15.0.dist-info/METADATA" in names
assert "periscope-0.15.0.dist-info/WHEEL" in names
assert "periscope-0.15.0.dist-info/entry_points.txt" in names
assert "periscope-0.15.0.dist-info/RECORD" in names
assert "periscope_agentsview-0.15.0.dist-info/METADATA" in names
assert "periscope_agentsview-0.15.0.dist-info/WHEEL" in names
assert "periscope_agentsview-0.15.0.dist-info/entry_points.txt" in names
assert "periscope_agentsview-0.15.0.dist-info/RECORD" in names

def test_binary_has_executable_permissions(self, tmp_path: Path) -> None:
whl = build_wheel(b"fake", tmp_path, "0.15.0", "linux_amd64")
Expand Down Expand Up @@ -264,9 +264,9 @@ def test_windows_wheel_uses_exe_binary(self, tmp_path: Path) -> None:
def test_metadata_required_fields(self, tmp_path: Path) -> None:
whl = build_wheel(b"fake", tmp_path, "0.15.0", "linux_amd64")
with zipfile.ZipFile(whl) as zf:
metadata = zf.read("periscope-0.15.0.dist-info/METADATA").decode()
metadata = zf.read("periscope_agentsview-0.15.0.dist-info/METADATA").decode()
assert "Metadata-Version: 2.1" in metadata
assert "Name: periscope" in metadata
assert "Name: periscope-agentsview" in metadata
assert "Version: 0.15.0" in metadata
assert "Requires-Python: >=3.9" in metadata
assert "License: MIT" in metadata
Expand All @@ -275,21 +275,21 @@ def test_metadata_required_fields(self, tmp_path: Path) -> None:
def test_wheel_file_has_root_is_purelib_false(self, tmp_path: Path) -> None:
whl = build_wheel(b"fake", tmp_path, "0.15.0", "linux_amd64")
with zipfile.ZipFile(whl) as zf:
wheel_meta = zf.read("periscope-0.15.0.dist-info/WHEEL").decode()
wheel_meta = zf.read("periscope_agentsview-0.15.0.dist-info/WHEEL").decode()
assert "Root-Is-Purelib: false" in wheel_meta
assert "Generator: periscope-build-wheels" in wheel_meta

def test_entry_points_correct(self, tmp_path: Path) -> None:
whl = build_wheel(b"fake", tmp_path, "0.15.0", "linux_amd64")
with zipfile.ZipFile(whl) as zf:
ep = zf.read("periscope-0.15.0.dist-info/entry_points.txt").decode()
ep = zf.read("periscope_agentsview-0.15.0.dist-info/entry_points.txt").decode()
assert "[console_scripts]" in ep
assert "periscope = periscope:main" in ep

def test_record_contains_hashes(self, tmp_path: Path) -> None:
whl = build_wheel(b"fake", tmp_path, "0.15.0", "linux_amd64")
with zipfile.ZipFile(whl) as zf:
record = zf.read("periscope-0.15.0.dist-info/RECORD").decode()
record = zf.read("periscope_agentsview-0.15.0.dist-info/RECORD").decode()
# Each non-RECORD entry should have a sha256 hash
lines = [ln for ln in record.splitlines() if ln.strip()]
record_line = None
Expand All @@ -306,7 +306,7 @@ def test_readme_included_in_metadata(self, tmp_path: Path) -> None:
readme = "# periscope\nA great tool."
whl = build_wheel(b"fake", tmp_path, "0.15.0", "linux_amd64", readme=readme)
with zipfile.ZipFile(whl) as zf:
metadata = zf.read("periscope-0.15.0.dist-info/METADATA").decode()
metadata = zf.read("periscope_agentsview-0.15.0.dist-info/METADATA").decode()
assert "A great tool." in metadata

def test_init_py_uses_execvp_on_unix(self, tmp_path: Path) -> None:
Expand All @@ -330,12 +330,12 @@ def test_wheel_filename_with_normalized_version(self, tmp_path: Path) -> None:
)
assert (
whl.name
== "periscope-0.29.2+periscope.2.258218f5-py3-none-manylinux_2_28_x86_64.whl"
== "periscope_agentsview-0.29.2+periscope.2.258218f5-py3-none-manylinux_2_28_x86_64.whl"
)

def test_wheel_filename_darwin_arm64(self, tmp_path: Path) -> None:
whl = build_wheel(b"fake", tmp_path, "1.0.0", "darwin_arm64")
assert whl.name == "periscope-1.0.0-py3-none-macosx_11_0_arm64.whl"
assert whl.name == "periscope_agentsview-1.0.0-py3-none-macosx_11_0_arm64.whl"


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -381,12 +381,12 @@ def test_correct_wheel_names(self, tmp_path: Path) -> None:
wheels = build_all_wheels(input_dir, output_dir, "0.15.0")
names = {w.name for w in wheels}
expected = {
"periscope-0.15.0-py3-none-manylinux_2_28_x86_64.whl",
"periscope-0.15.0-py3-none-manylinux_2_28_aarch64.whl",
"periscope-0.15.0-py3-none-macosx_11_0_x86_64.whl",
"periscope-0.15.0-py3-none-macosx_11_0_arm64.whl",
"periscope-0.15.0-py3-none-win_amd64.whl",
"periscope-0.15.0-py3-none-win_arm64.whl",
"periscope_agentsview-0.15.0-py3-none-manylinux_2_28_x86_64.whl",
"periscope_agentsview-0.15.0-py3-none-manylinux_2_28_aarch64.whl",
"periscope_agentsview-0.15.0-py3-none-macosx_11_0_x86_64.whl",
"periscope_agentsview-0.15.0-py3-none-macosx_11_0_arm64.whl",
"periscope_agentsview-0.15.0-py3-none-win_amd64.whl",
"periscope_agentsview-0.15.0-py3-none-win_arm64.whl",
}
assert names == expected

Expand Down