diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d0760fb..2fe33a9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,11 +13,32 @@ env: CARGO_TERM_COLOR: always jobs: + # --------------------------------------------------------------------------- + # Create the GitHub Release up front so the CLI and desktop jobs both upload + # into it instead of racing to create it. + # --------------------------------------------------------------------------- + create-release: + name: Create GitHub Release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Create release if missing + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release view "${{ github.ref_name }}" --repo "${{ github.repository }}" \ + || gh release create "${{ github.ref_name }}" \ + --repo "${{ github.repository }}" \ + --title "CLAN ${{ github.ref_name }}" \ + --generate-notes + # --------------------------------------------------------------------------- # Standalone `clan` CLI binaries for each platform, attached to the release. # --------------------------------------------------------------------------- cli-binaries: name: CLI — ${{ matrix.target }} + needs: create-release runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -27,10 +48,12 @@ jobs: target: x86_64-unknown-linux-gnu - os: macos-latest target: aarch64-apple-darwin - - os: macos-13 + # Intel macOS is cross-compiled from the arm64 runner — GitHub + # retired the macos-13 (last Intel) runner image. + - os: macos-latest target: x86_64-apple-darwin - - os: windows-latest - target: x86_64-pc-windows-msvc + # No Windows entry: Windows ships a single combined MSI (CLI + viewer) + # from the `desktop` job below, not a standalone CLI archive. steps: - uses: actions/checkout@v4 @@ -81,11 +104,22 @@ jobs: # --------------------------------------------------------------------------- desktop: name: Desktop — ${{ matrix.os }} + needs: create-release runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - os: [ macos-latest, ubuntu-latest, windows-latest ] + include: + # Universal macOS build: one DMG that runs on Apple Silicon and Intel. + - os: macos-latest + rust-targets: aarch64-apple-darwin,x86_64-apple-darwin + tauri-args: --target universal-apple-darwin + - os: ubuntu-latest + rust-targets: "" + tauri-args: "" + - os: windows-latest + rust-targets: "" + tauri-args: "" steps: - uses: actions/checkout@v4 @@ -106,6 +140,8 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.rust-targets }} - name: Cache cargo build uses: Swatinem/rust-cache@v2 @@ -123,6 +159,29 @@ jobs: working-directory: app run: npm ci + # Windows ships ONE combined MSI: build the `clan` CLI and stage it as a + # Tauri sidecar so the viewer installer also installs the CLI. The + # Windows-only tauri.windows.conf.json picks it up via externalBin and a + # WiX fragment (wix/cli-path.wxs) adds the install dir to the system PATH. + - name: Stage CLI into viewer installer (Windows) + if: matrix.os == 'windows-latest' + shell: pwsh + run: | + cargo build -p clan-cli --release + New-Item -ItemType Directory -Force -Path app/src-tauri/binaries | Out-Null + Copy-Item target/release/clan.exe app/src-tauri/binaries/clan-x86_64-pc-windows-msvc.exe + + # Linux .deb / .rpm install the `clan` CLI to /usr/bin (on PATH) alongside + # the viewer. tauri.linux.conf.json maps binaries/clan -> /usr/bin/clan via + # deb.files / rpm.files. The AppImage stays viewer-only (it can't place + # files on the host PATH), so the standalone CLI tarball is kept above. + - name: Stage CLI into Linux packages (Linux) + if: matrix.os == 'ubuntu-latest' + run: | + cargo build -p clan-cli --release + mkdir -p app/src-tauri/binaries + cp target/release/clan app/src-tauri/binaries/clan + - name: Build & publish desktop bundles uses: tauri-apps/tauri-action@v0 env: @@ -134,3 +193,4 @@ jobs: releaseName: CLAN ${{ github.ref_name }} releaseDraft: false prerelease: false + args: ${{ matrix.tauri-args }} diff --git a/.gitignore b/.gitignore index a1476d6..bf511a5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ target/ target-gnu/ Cargo.lock + +# CLI sidecar staged into the viewer MSI during the release build (see release.yml). +app/src-tauri/binaries/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 40cba38..9617af3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.1.2] - 2026-06-12 + +### Added + +- **`.clan` file associations** — double-clicking a `.clan` file (or "Open with") + now launches the CLAN Viewer with the file loaded, on macOS, Windows, and Linux. + +### Fixed + +- **Viewer file opening** — files passed via OS launch events open correctly. + +## [1.1.1] - 2026-06-12 + +### Added + +- **CI/CD release pipeline** — tagged releases build CLI binaries for Linux, macOS + (Apple Silicon + Intel), and Windows, plus viewer bundles (universal `.dmg`, + `.AppImage`, `.deb`, `.rpm`, `.msi`, NSIS `.exe`) automatically. + +### Changed + +- **App icon** — viewer now ships the designed CLAN constellation mark + (was a placeholder solid square). +- **Toolbar branding** — viewer toolbar renders the canonical `ClanMark` + component, animated while a file loads. +- **README** — results updated to the 2026-06-12 scorecard run, including + long-chain wall times and documented EXPECT-RED gaps. + +## [1.1.0] - 2026-06-12 + +### Added + +- **Fork/join concurrency** — per-agent namespaces, deterministic `merge`, + contested-key reporting with provenance (spec §22–§27). +- **Deferred human-view rendering** (`clan render`) and conflict adjudication. +- **Teachable CLI interface** — `agent-help`, `next:` hints, F-series guard rails. + ## [1.0.0] - 2026-06-08 First public release of CLAN — Context and Live Agent Notation. @@ -19,5 +56,8 @@ First public release of CLAN — Context and Live Agent Notation. - **`clan-cli`** — the `clan` command-line tool to create, validate, read, pack, and export `.clan` files. - **CLAN Viewer** — Tauri desktop app for rendering the human view of a `.clan` file. -[Unreleased]: https://github.com/saieeshward/clan/compare/v1.0.0...HEAD +[Unreleased]: https://github.com/saieeshward/clan/compare/v1.1.2...HEAD +[1.1.2]: https://github.com/saieeshward/clan/releases/tag/v1.1.2 +[1.1.1]: https://github.com/saieeshward/clan/releases/tag/v1.1.1 +[1.1.0]: https://github.com/saieeshward/clan/releases/tag/v1.1.0 [1.0.0]: https://github.com/saieeshward/clan/releases/tag/v1.0.0 diff --git a/Cargo.toml b/Cargo.toml index be2d674..b04e522 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/clan-sdk", "crates/clan-cli", "app/src-tauri"] [workspace.package] -version = "1.1.0" +version = "1.1.2" edition = "2021" license = "MPL-2.0" repository = "https://github.com/saieeshward/clan" diff --git a/README.md b/README.md index 53d8b03..7cea281 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ CLAN is an open container format for passing structured context between AI agent **The one-line pitch, straight from our benchmark:** CLAN's price is a modest, bounded token overhead; its product is that correctness, provenance, conflict detection, and human attribution survive *even when nobody writes a careful prompt* — which is exactly the regime real multi-agent pipelines live in.

- 30 real agents benchmarked  ·  0 unrecovered failures  ·  42% fewer revision-loop output tokens  ·  44% smaller synthesis injection  ·  0 LLM tokens per merge  ·  100% of mutations attributed  ·  <200ms every CLI command  ·  165 tests + 26-test conformance suite green + ~258 real agents benchmarked + 8/12-hop head-to-head pipelines  ·  0 unrecovered failures  ·  45–66% fewer revision-loop output tokens  ·  4/4 merge conflicts recalled with provenance  ·  0 LLM tokens per merge  ·  100% of mutations attributed  ·  <200ms every CLI command  ·  186 Rust tests + 26/26 conformance on macOS and Windows

--- @@ -34,8 +34,10 @@ CLAN replaces that with a single, self-describing file. ## Quickstart +**Install:** pre-built CLI binaries for Linux, macOS (Apple Silicon + Intel), and Windows — plus the desktop viewer (`.dmg` / `.msi` / `.AppImage`) — are on the [Releases page](https://github.com/saieeshward/clan/releases). + ```bash -# Install (from source; binaries on the Releases page) +# Or build from source cargo install --path crates/clan-cli # Create a document @@ -78,7 +80,18 @@ Agents don't need to be taught any of this: the CLI is **self-teaching**. Every ## Results — What the Benchmark Says, Including Where CLAN Loses -We ran **30 real agents** (no scripted outputs) through **11 flows** on one fixed task: serial vs parallel × CLAN vs ad-hoc files × guided vs unguided prompts × ± a live human edit. All context sizes were measured from artifacts, not estimated — the raw snapshots, per-agent receipts, and metrics live in [`test-sandbox/`](test-sandbox/) so you can audit every number below. Full write-up: [`research/14-flow-benchmark.md`](research/14-flow-benchmark.md). +Three measurement campaigns back every number below. **Campaign 1 (2026-06-10):** 30 real agents (no scripted outputs) through 11 flows on one fixed task — serial vs parallel × CLAN vs ad-hoc files × guided vs unguided prompts × ± a live human edit ([`research/14-flow-benchmark.md`](research/14-flow-benchmark.md)). **Campaign 2 (2026-06-12):** long-chain head-to-heads — an 8-hop revision pipeline and a 10-hop discovery chain, CLAN and ad-hoc arms running concurrently — plus the deterministic scorecard ([`test-sandbox/RUN-REPORT-2026-06-12.md`](test-sandbox/RUN-REPORT-2026-06-12.md)). **Campaign 3 (2026-06-12, run `-I`):** the full TESTBOOK suite end-to-end — all deterministic tests plus the heavy benchmark (5 reps × 2 arms of the 8-hop revision and 12-hop chain, ~206 real subagents, ~4M tokens), the run that surfaced the provenance-integrity finding below. All context sizes were measured from artifacts, not estimated; raw snapshots, per-agent receipts, metrics, and the accumulating ledger ([`test-sandbox/TestResult.clan`](test-sandbox/TestResult.clan)) live in [`test-sandbox/`](test-sandbox/) so you can audit everything. + +### Highlights — the wins that held across every rep + +From the full-suite run (`-I`), the results we'll stand behind without an asterisk: + +- **66% fewer output tokens to revise a document.** Eight serial edits to a 45 KB report: CLAN's surgical patch path authored **0.336×** the characters of careful hand-editing — and even when the ad-hoc arm was *handed the exact same fragments* to remove CLAN's advantage, CLAN still wrote **45% less** across 5 reps. +- **Conflicts that other workflows lose silently, CLAN catches — for free.** Four agents fork into isolated namespaces and write deliberately clashing verdicts; the deterministic merge recalls **4/4 contested keys with winner *and* loser provenance**, at **0 LLM tokens**. A human then adjudicated and overrode the mechanical winner — on the record. +- **One document, three radically different forms, zero data loss.** Agency brief → concept deck → client pitch, a new schema and view each hop: **5/5** — hop-1 data verbatim in the final, a logo asset carried across all three hops without ever being re-passed, lineage unbroken. +- **Agents learn the protocol from the tool itself.** Told only *"there's a `clan` CLI and a doc — figure it out,"* unguided agents completed a 3-hop chain with **0 guard violations and every hop attributed** — discovering the attribution flags from the CLI's own error messages. +- **A cold agent picks up an abandoned pipeline from the file alone.** Handed nothing but a `.clan` artifact, a fresh agent found the correct next step, added its hop, and recorded a decision — **no rework, no re-briefing.** +- **It's exhaustively tested and honest about its limits.** 186/186 workspace tests, 26/26 CLI conformance, ~258 real subagents driven through the suite with **0 unrecovered failures** — and a Results section that tells you exactly [where CLAN loses](#where-clan-loses-we-measured-it-so-well-say-it). ### What survives the handoff — with and without a careful prompt @@ -95,18 +108,43 @@ The core result. Final artifacts, audited per flow (serial arms shown): 〰️ = partial (one-line logs or prose-only). **Take away the careful prompt and ad-hoc collapses; CLAN's finals are byte-for-byte as complete as guided ones.** The format carries the discipline so the prompt doesn't have to. -### Measured claims (scorecard, latest run) +### Measured claims (scorecard run 2026-06-12 — [full report](test-sandbox/RUN-REPORT-2026-06-12.md)) -| Claim | Measured | Threshold | Status | +| Claim | Measured (run `-I`) | Threshold | Status | |---|---|---|:---:| -| Revision loops: CLAN patch path authors fewer output chars than ad-hoc full-rewrites | **0.576× (42% fewer)** | ≤ 0.65 | ✅ PASS | -| Synthesis hop: CLAN's merged injection beats ad-hoc re-reading every input | **0.557× (44% less)** | < 1.0 | ✅ PASS | -| TOON encoding saves vs minified JSON on tabular data | **≥ 30%** | ≥ 30% | ✅ PASS | -| Fidelity: every requested edit present, untouched fields intact | **1.0** | = 1.0 | ✅ PASS | -| Provenance: every mutating hop attributed, end-to-end | **1.125** | ≥ 1.0 | ✅ PASS | +| Revision loops: CLAN patch path authors fewer output chars than ad-hoc full-rewrites (8-hop) | **0.336× (66% fewer)** | ≤ 0.65 | ✅ PASS | +| …same claim, **composition-controlled** (ad-hoc handed the same fragments, 5 reps) | **0.554× (45% fewer)** | ≤ 0.50 | 🟡 NEAR | +| TOON encoding saves vs minified JSON on tabular data | **51–58%** | ≥ 30% | ✅ PASS | +| Fidelity: every requested edit present, untouched fields intact (8 hops × 5 heavy reps) | **8/8 in 4 of 5 reps** | = 1.0 | ⚠️ see note | +| Provenance: every mutating hop attributed, end-to-end | **10/8 hops, 0 `unknown-agent`** | ≥ 1.0 | ✅ PASS | | Reliability: agents recover from every CLI error without orchestrator help | **0 unrecovered** | = 0 | ✅ PASS | -| Agent guide is byte-identical across all files and hops (prompt-cache friendly) | **1 unique hash** | 1 | ✅ PASS | -| Fixed injection scaffolding is bounded | **≤ 3,000 chars** | ≤ 3,000 | ✅ PASS | +| Contested-key fork/merge: every conflict recalled with winner + loser provenance | **4/4 keys** | 4/4 | ✅ PASS | +| Metamorphosis: doc fully transforms per hop (new view + schema), nothing lost, asset carries | **5/5 checks** | all | ✅ PASS | +| Teachability: unguided agents reach protocol competence from `agent-help` alone | **0 violations, all attributed** | 0 | ✅ PASS | +| Cold resume: fresh agent finds the correct next step from the artifact alone | **oriented, no rework** | — | ✅ PASS | +| Agent guide is byte-identical across files within a build (prompt-cache friendly) | **1 hash / build** | 1 | ✅ PASS | +| Fixed injection scaffolding is bounded | **a = 2,611 chars** | ≤ 3,000 | ✅ PASS | +| Two-tier decision-chain compression (verbatim window, compressed tail, pinned preserved) | **correct** | — | ✅ PASS | +| Workspace unit + integration tests | **186 / 186** | all | ✅ PASS | +| CLI conformance harness | **26 / 26, 0 hard failures** | all | ✅ PASS | +| Synthesis hop: CLAN's merged injection beats ad-hoc re-reading every input | **volatile: 0.487× (`-H`) → 1.047× (`-I`)** | < 1.0 | ⚠️ NOT ROBUST | +| CLAN per-hop injection crosses below ad-hoc on long chains | **no clean crossover** | crossover | ❌ EXPECT-RED | +| Capability-requirements layer (L5) populated in handoffs | **not exercised yet** | populated | ❌ EXPECT-RED | + +**We report the latest run, not the best one.** Run-to-run variance is real (the 2026-06-10 benchmark measured the revision ratio at 0.576× on shorter chains), and two claims moved on us this run and we're saying so: the **synthesis-hop** ratio is setup-sensitive and came back **above 1.0** in run `-I` (it was 0.487× in `-H`) — treat the injected-context win as unproven, not banked. And the **fidelity** row carries a ⚠️ because one of five heavy reps exposed a real failure mode — detailed honestly in [Where CLAN loses](#where-clan-loses-we-measured-it-so-well-say-it). The wins that *are* robust — surgical output tokens, full provenance, fork/merge conflict recall, metamorphosis, teachability, cold resume — held across every rep. + +### Long chains, head-to-head: 8 and 10 hops, both arms live + +Two full pipelines ran CLAN and ad-hoc arms concurrently on the same task — an 8-hop revision pipeline (H1) and a 10-hop specialist discovery chain ending in a synthesis hop (H2), plus a cold-resume test (H3): + +| Flow | Hops | CLAN total | Ad-hoc total | CLAN faster by | +|---|:---:|:---:|:---:|:---:| +| H1 — revision pipeline | 8 | **8:20** | 10:13 | 1:53 (~18%) | +| H2 — discovery chain | 10 | **11:25** | 12:55 | 1:30 (~12%) | + +At the synthesis hop — where ad-hoc context is at its largest — CLAN finished in **1:23 vs 2:01**. A fresh agent with zero prior context (H3) located the correct next step from the `.clan` file alone in **3 orient reads**. Unguided agents (no CLAN training, just `clan agent-help`) reached correct protocol use in **≤ 4 discovery commands**, all using `patch-data` rather than raw file writes. + +The wall-time wins are honestly modest at 8–10 hops (output tokens don't dominate inference latency — input reads do). The structural point is the slope: ad-hoc injection grows O(n) with chain length, CLAN's distilled re-injection stays flat. ### Parallel agents: the merge that caught what everyone else lost @@ -145,16 +183,22 @@ Injected context per agent, serial 3-hop pipeline: | CLAN unguided (full guide each hop) | ~50,600 chars | **~12.7k** | - **Raw injected tokens at small scale: CLAN does not win.** Disciplined ad-hoc with frontier agents is ~15–40% leaner, because CLAN's context carries scaffolding (schema, decision chain, guide-or-digest) a pile of markdown files doesn't have. At 3 hops, the growth curves haven't crossed yet. +- **The crossover never cleanly happened — not even at heavy scale.** We keep a crossover claim in the scorecard and it is still red (C-CROSSOVER, EXPECT-RED): CLAN's per-hop injection stays *above* ad-hoc through the chain. The synthesis-hop win we reported earlier (0.487× in run `-H`) did **not** reproduce in run `-I` (it came back at 1.047×, i.e. slightly *worse* than ad-hoc), and the heavy-scale crossover measurement was compromised by a non-idempotent re-run. **Honest verdict: at the chain lengths we've tested, CLAN does not beat lean ad-hoc on injected context.** If your chains are short, ad-hoc is cheaper on raw tokens — CLAN's win is in authoring effort, provenance, and correctness, not per-hop injection size. The planned `read agent --since ` delta feature (v1.2) is what's designed to change this; until it ships and is measured, we don't claim the injection win. - Unguided agents pay a one-time **~2–5k token discovery cost** learning the protocol — though they reached full competence from `agent-help` alone, with zero guard-rail violations. +- **Wall-time gains are modest** (~12–18% at 8–10 hops): token-output savings don't translate 1:1 to latency. +- **The L5 capability-requirements layer is unpopulated** (C-LAYERS, EXPECT-RED): no flow exercises `patch-requirements` yet. L1–L4 (state, handoff, contracts, provenance) are all green. +- **Provenance is honest, but it is not self-verifying.** The most important finding of Campaign 3: in 1 of 5 heavy reps, a CLAN agent applied only 5 of 8 edits **and recorded attributed decisions claiming the other three were done** ("verified… already matches… no splice needed") when the data was never changed. CLAN guarantees *who* acted, *when*, and that the entry is attributed — it cannot guarantee the agent's claim about *what* it did is true. The ad-hoc arm of the same rep happened to get all 8, so this is an agent-fidelity failure, not a format failure — but the lesson is structural: **a `.clan` chain is only as truthful as the agents writing it.** What caught it was an independent verifier agent diffing claims against the actual data. Treat a verifier/`clan validate`-style diff hop as mandatory in any pipeline where fidelity matters; don't trust a self-reported "done." +- **Re-running a pipeline is not idempotent.** `patch-html --patch-action append` re-applies on a retry/resume, duplicating sections. Reseed or guard before re-running a chain. ### Verification status | Suite | Result | |---|---| -| Rust unit + integration tests | **165/165 pass** | -| Black-box CLI conformance harness | **26/26 pass, 0 hard failures** | -| Scorecard claims | **14 PASS · 0 FAIL** | -| Benchmark reliability | **30/30 agent completions, 0 unrecovered failures** | +| Rust unit + integration tests (workspace) | **186/186 pass** (SDK 47 · CLI 120 · app 19) | +| Black-box CLI conformance harness | **26/26 pass, 0 hard failures — verified on macOS and Windows** | +| Scorecard claims | **majority PASS · synthesis-hop & fidelity flagged · 2 EXPECT-RED** (documented gaps, kept red on purpose) | +| Agentic reliability (Campaign 3) | **~258 real subagents across the run, 0 unrecovered CLI failures** | +| Release pipeline | **v1.1.0+ from CI: 4 CLI targets + Windows MSI + Linux .deb/.rpm, all green** | --- @@ -237,7 +281,7 @@ A CLAN file is live: it carries its own specification, so any agent can understa ## Status -**v1.1** — fork/join concurrency (per-agent namespaces + deterministic merge), deferred human-view rendering, conflict adjudication, and the teachable CLI interface (spec §22–§27). Verified by 165 Rust tests + a 26-test black-box conformance suite in CI. +**v1.1** — fork/join concurrency (per-agent namespaces + deterministic merge), deferred human-view rendering, conflict adjudication, and the teachable CLI interface (spec §22–§27). Verified by 186 Rust tests + a 26-test black-box conformance suite in CI, with [binaries for every platform on the Releases page](https://github.com/saieeshward/clan/releases). ## Maintainers diff --git a/TODO.md b/TODO.md index 2f449d7..a5d1cd3 100644 --- a/TODO.md +++ b/TODO.md @@ -8,8 +8,8 @@ Priority order: fix blockers first, then correctness, then packaging, then optim - [X] `Cargo.toml:8` — change `license = "Apache-2.0"` to `"MPL-2.0"` (wrong license on every crate) - [x] `Cargo.toml:9` — fix `repository` URL (`xon` → `clan`) -- [ ] Add `.github/workflows/ci.yml` — cargo check + test + tsc on every PR -- [ ] Add `.github/workflows/release.yml` — tag push → CLI binaries + DMG/MSI/AppImage via `tauri-action` +- [x] Add `.github/workflows/ci.yml` — cargo check + test + tsc on every PR +- [x] Add `.github/workflows/release.yml` — tag push → CLI binaries + DMG/MSI/AppImage via `tauri-action` - [x] Add `.nvmrc` containing `20` in `app/` (Node 16 users get cryptic failures) - [x] Add `"engines": { "node": ">=20" }` to `app/package.json` @@ -90,14 +90,14 @@ Priority order: fix blockers first, then correctness, then packaging, then optim - [x] Create `crates/clan-sdk/README.md` — crates.io renders this; without it the page is blank - [x] Promote `lol_html = "2.1.0"` from `clan-sdk/Cargo.toml` into `[workspace.dependencies]` - [x] Add `[[bin]]` entry to `crates/clan-cli/Cargo.toml` declaring binary name explicitly -- [ ] Add correct logo for the app. +- [x] Add correct logo for the app. - [x] ASCII Art on first installation [OPT] ### App packaging - [x] Rename `app/package.json` `"name"` from `"app"` to `"clan-viewer"` - [x] Set `app/package.json` `"version"` to match workspace version (`1.0.0`) -- [ ] Add correct logo for the app. +- [x] Add correct logo for the app. - [ ] File Tree in App [OPT] ### Repository hygiene @@ -111,9 +111,22 @@ Priority order: fix blockers first, then correctness, then packaging, then optim - [ ] CLI: publish `clan-cli` to crates.io on tag + attach pre-built binaries (Linux x86_64/ARM, macOS ARM/Intel, Windows x86_64) via `cross` - [ ] SDK: publish `clan-sdk` to crates.io on tag -- [ ] Viewer: attach `.dmg`, `.msi`, `.AppImage` to GitHub Release via `tauri-action` +- [x] Viewer: attach `.dmg`, `.msi`, `.AppImage` to GitHub Release via `tauri-action` - [ ] Spec: host `spec/CLAN-SPEC.md` at a versioned URL and reference from crates.io docs - [ ] Packages on github for download : Discuss +- [ ] Combined Installer for all the releases + - [x] Windows + - [x] Linux + - [x] MacOs + +### Launch announcements (drafts ready 2026-06-12, kept local until posted) + +- [ ] Reddit — r/AI_Agents: benchmark-led post ("where it won and where it honestly lost") +- [ ] Reddit — r/rust: engineering-led post (SDK/CLI/Tauri, <200ms, MPL-2.0) +- [ ] Reddit — r/LocalLLaMA [OPT]: variant of the r/AI_Agents post +- [ ] LinkedIn: launch post (provenance/"where does the truth live" angle) +- [ ] Merge PR #45 to main before posting (so visitors land on the launch README) +- [ ] Update post links to the latest release tag before publishing --- diff --git a/app/src-tauri/Cargo.toml b/app/src-tauri/Cargo.toml index e4a2374..b621ab4 100644 --- a/app/src-tauri/Cargo.toml +++ b/app/src-tauri/Cargo.toml @@ -10,6 +10,7 @@ tauri-build = { version = "2", features = [] } [dependencies] tauri = { version = "2", features = [] } tauri-plugin-dialog = "2" +tauri-plugin-single-instance = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = "0.9" diff --git a/app/src-tauri/icons/128x128.png b/app/src-tauri/icons/128x128.png new file mode 100644 index 0000000..155ad90 Binary files /dev/null and b/app/src-tauri/icons/128x128.png differ diff --git a/app/src-tauri/icons/128x128@2x.png b/app/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..8d75bfe Binary files /dev/null and b/app/src-tauri/icons/128x128@2x.png differ diff --git a/app/src-tauri/icons/32x32.png b/app/src-tauri/icons/32x32.png new file mode 100644 index 0000000..b34758b Binary files /dev/null and b/app/src-tauri/icons/32x32.png differ diff --git a/app/src-tauri/icons/64x64.png b/app/src-tauri/icons/64x64.png new file mode 100644 index 0000000..c047cab Binary files /dev/null and b/app/src-tauri/icons/64x64.png differ diff --git a/app/src-tauri/icons/Square107x107Logo.png b/app/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..1815bef Binary files /dev/null and b/app/src-tauri/icons/Square107x107Logo.png differ diff --git a/app/src-tauri/icons/Square142x142Logo.png b/app/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..d823ed7 Binary files /dev/null and b/app/src-tauri/icons/Square142x142Logo.png differ diff --git a/app/src-tauri/icons/Square150x150Logo.png b/app/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..64c82a7 Binary files /dev/null and b/app/src-tauri/icons/Square150x150Logo.png differ diff --git a/app/src-tauri/icons/Square284x284Logo.png b/app/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..b7d12ac Binary files /dev/null and b/app/src-tauri/icons/Square284x284Logo.png differ diff --git a/app/src-tauri/icons/Square30x30Logo.png b/app/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..cf10024 Binary files /dev/null and b/app/src-tauri/icons/Square30x30Logo.png differ diff --git a/app/src-tauri/icons/Square310x310Logo.png b/app/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..e374bf2 Binary files /dev/null and b/app/src-tauri/icons/Square310x310Logo.png differ diff --git a/app/src-tauri/icons/Square44x44Logo.png b/app/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..119a9b9 Binary files /dev/null and b/app/src-tauri/icons/Square44x44Logo.png differ diff --git a/app/src-tauri/icons/Square71x71Logo.png b/app/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..723592c Binary files /dev/null and b/app/src-tauri/icons/Square71x71Logo.png differ diff --git a/app/src-tauri/icons/Square89x89Logo.png b/app/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..3d1a9a1 Binary files /dev/null and b/app/src-tauri/icons/Square89x89Logo.png differ diff --git a/app/src-tauri/icons/StoreLogo.png b/app/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..9d3388e Binary files /dev/null and b/app/src-tauri/icons/StoreLogo.png differ diff --git a/app/src-tauri/icons/icon.icns b/app/src-tauri/icons/icon.icns new file mode 100644 index 0000000..3b54599 Binary files /dev/null and b/app/src-tauri/icons/icon.icns differ diff --git a/app/src-tauri/icons/icon.ico b/app/src-tauri/icons/icon.ico index dac6e30..02cd077 100644 Binary files a/app/src-tauri/icons/icon.ico and b/app/src-tauri/icons/icon.ico differ diff --git a/app/src-tauri/icons/icon.png b/app/src-tauri/icons/icon.png index 6c72740..bdf288d 100644 Binary files a/app/src-tauri/icons/icon.png and b/app/src-tauri/icons/icon.png differ diff --git a/app/src-tauri/src/main.rs b/app/src-tauri/src/main.rs index 76b8018..19c151e 100644 --- a/app/src-tauri/src/main.rs +++ b/app/src-tauri/src/main.rs @@ -29,6 +29,18 @@ struct AppState { current: Mutex>, edit_mode: Mutex, preview_html: Mutex, + // A `.clan` path the OS handed us at launch (double-click / "Open with"), + // waiting for the frontend to pull it via `take_launch_file`. + pending_open: Mutex>, +} + +/// Pick the first `.clan` file path out of a set of process arguments. +/// Works for both our own launch args and the argv a second instance is +/// started with; the executable path and any flags are ignored since they +/// don't end in `.clan`. +fn clan_path_from_args>(args: I) -> Option { + args.into_iter() + .find(|a| a.to_lowercase().ends_with(".clan")) } struct LoadedClan { @@ -71,6 +83,13 @@ fn open_clan(path: String, state: State) -> Result do_open_clan(path, &state) } +/// Returns (and clears) the `.clan` path the app was launched with, if any. +/// The frontend calls this once on mount to open a double-clicked file. +#[tauri::command] +fn take_launch_file(state: State) -> Option { + state.pending_open.lock().unwrap().take() +} + fn do_open_clan(path: String, state: &AppState) -> Result { let p = PathBuf::from(&path); let clan = ClanFile::open(&p).map_err(|e| e.to_string())?; @@ -564,7 +583,23 @@ fn save_patch(id: String, content: String, state: State) -> Result<(), } fn main() { + // The OS launches us with the clicked file as an argument; stash it so the + // frontend can pull it once it's ready. + let launch_file = clan_path_from_args(std::env::args()); + tauri::Builder::default() + // Must be the first plugin. When a second instance is started (e.g. the + // user double-clicks another .clan file while the viewer is open), this + // re-focuses our window and forwards the new path instead of opening a + // duplicate window. + .plugin(tauri_plugin_single_instance::init(|app, argv, _cwd| { + if let Some(w) = app.get_webview_window("main") { + let _ = w.set_focus(); + } + if let Some(path) = clan_path_from_args(argv) { + let _ = app.emit("open-file", path); + } + })) .register_uri_scheme_protocol("clan", |app, request| { let uri = request.uri().to_string(); let state = app.app_handle().state::(); @@ -610,14 +645,15 @@ fn main() { } }) .plugin(tauri_plugin_dialog::init()) - .manage(AppState { - current: Mutex::new(None), + .manage(AppState { + current: Mutex::new(None), edit_mode: Mutex::new(false), preview_html: Mutex::new(String::new()), + pending_open: Mutex::new(launch_file), }) .invoke_handler(tauri::generate_handler![ open_clan, get_human_html, get_data, get_chain, get_agent_state, get_context, - save_patch, set_edit_mode, update_preview_html + save_patch, set_edit_mode, update_preview_html, take_launch_file ]) .run(tauri::generate_context!()) .expect("error while running CLAN Viewer"); @@ -636,6 +672,7 @@ mod tests { current: Mutex::new(None), edit_mode: Mutex::new(false), preview_html: Mutex::new(String::new()), + pending_open: Mutex::new(None), } } diff --git a/app/src-tauri/tauri.conf.json b/app/src-tauri/tauri.conf.json index ae08ff5..8641d02 100644 --- a/app/src-tauri/tauri.conf.json +++ b/app/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "productName": "CLAN Viewer", - "version": "1.0.0", + "version": "1.1.2", "identifier": "ai.clan.viewer", "build": { "frontendDist": "../dist", @@ -31,6 +31,14 @@ "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" + ], + "fileAssociations": [ + { + "ext": ["clan"], + "name": "CLAN File", + "description": "CLAN document archive", + "role": "Viewer" + } ] }, "plugins": {} diff --git a/app/src-tauri/tauri.linux.conf.json b/app/src-tauri/tauri.linux.conf.json new file mode 100644 index 0000000..ce0928b --- /dev/null +++ b/app/src-tauri/tauri.linux.conf.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "bundle": { + "linux": { + "deb": { + "files": { + "/usr/bin/clan": "binaries/clan" + } + }, + "rpm": { + "files": { + "/usr/bin/clan": "binaries/clan" + } + } + } + } +} diff --git a/app/src-tauri/tauri.windows.conf.json b/app/src-tauri/tauri.windows.conf.json new file mode 100644 index 0000000..b554852 --- /dev/null +++ b/app/src-tauri/tauri.windows.conf.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "bundle": { + "targets": ["msi"], + "externalBin": ["binaries/clan"], + "windows": { + "wix": { + "fragmentPaths": ["wix/cli-path.wxs"], + "componentGroupRefs": ["ClanCliPath"] + } + } + } +} diff --git a/app/src-tauri/wix/cli-path.wxs b/app/src-tauri/wix/cli-path.wxs new file mode 100644 index 0000000..4c70dc1 --- /dev/null +++ b/app/src-tauri/wix/cli-path.wxs @@ -0,0 +1,38 @@ + + + + + + + + + + + + + diff --git a/app/src/App.tsx b/app/src/App.tsx index 0602653..3dd5783 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -2,8 +2,9 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -import { useState } from 'react' +import { useEffect, useState } from 'react' import { invoke } from '@tauri-apps/api/core' +import { listen } from '@tauri-apps/api/event' import { open as openDialog } from '@tauri-apps/plugin-dialog' import Toolbar from './components/Toolbar' import Sidebar from './components/Sidebar' @@ -74,6 +75,21 @@ export default function App() { } } + useEffect(() => { + // On launch the OS may have handed us a .clan file (double-click / + // "Open with"). Pull it from the backend and open it. + invoke('take_launch_file') + .then(path => { if (path) handleOpenFile(path) }) + .catch(() => {}) + + // If the viewer is already running and the user opens another .clan file, + // the single-instance plugin re-routes the path here as an event. + const unlisten = listen('open-file', e => { + if (e.payload) handleOpenFile(e.payload) + }) + return () => { unlisten.then(f => f()) } + }, []) + function handlePatch(_id: string, _content: string) { // The protocol handler already saved the patch. The user's edit is already // visible in the DOM — don't reload the iframe or it will revert to the diff --git a/app/src/components/ClanMark.tsx b/app/src/components/ClanMark.tsx new file mode 100644 index 0000000..cb9116a --- /dev/null +++ b/app/src/components/ClanMark.tsx @@ -0,0 +1,160 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import type { CSSProperties } from "react"; + +/** + * CLAN brand mark — a lineage graph of connected nodes that also reads as a "C". + * Built from pure circles + lines, so it stays crisp from 16px to hero. + * + * → static, dual-tone (indigo → teal) + * → lineage grows in, holds, recedes (loops) + * → single-colour, inherits `color` + * → mark + "CLAN" wordmark lockup + */ + +type Tone = "duo" | "mono" | "knockout"; + +const NODES = [ + { x: 70, y: 24, r: 5.6, gen: 0 }, + { x: 37, y: 21, r: 5.6, gen: 1 }, + { x: 18, y: 50, r: 7.0, gen: 2 }, + { x: 37, y: 79, r: 5.6, gen: 3 }, + { x: 70, y: 76, r: 5.6, gen: 4 }, +] as const; +const EDGES: [number, number][] = [[0, 1], [1, 2], [2, 3], [3, 4]]; +const SW = 2.8; + +// indigo #6366f1 → teal #2dd4cf, interpolated by generation +const DUO = ["#6366f1", "#5682e9", "#489de0", "#3bb9d8", "#2dd4cf"]; + +function nodeFill(gen: number, tone: Tone) { + if (tone === "duo") return DUO[gen]; + if (tone === "knockout") return "#ffffff"; + return "currentColor"; +} +function edgeStroke(tone: Tone) { + if (tone === "duo") return { stroke: "rgba(120,160,230,0.42)" }; + if (tone === "knockout") return { stroke: "#ffffff", strokeOpacity: 0.6 }; + return { stroke: "currentColor", strokeOpacity: 0.42 }; +} + +export function ClanMark({ + size = 32, + tone = "duo", + animated = false, + style, + title = "CLAN", +}: { + size?: number; + tone?: Tone; + animated?: boolean; + style?: CSSProperties; + title?: string; +}) { + const es = edgeStroke(tone); + return ( + + {title} + {animated && } + {EDGES.map(([a, b], i) => ( + + ))} + {animated && ( + + )} + {NODES.map((n, i) => ( + + ))} + + ); +} + +/** Mark + "CLAN" wordmark. Wordmark uses Space Grotesk; falls back to system sans. */ +export function ClanLogo({ + size = 28, + tone = "duo", + animated = false, + color = "#eceefb", + style, +}: { + size?: number; + tone?: Tone; + animated?: boolean; + color?: string; + style?: CSSProperties; +}) { + return ( + + + + CLAN + + + ); +} + +/** Injected once per animated mark; safe to duplicate (identical rules). */ +function ClanKeyframes() { + return ( + + ); +} diff --git a/app/src/components/Toolbar.tsx b/app/src/components/Toolbar.tsx index c551443..683b7b2 100644 --- a/app/src/components/Toolbar.tsx +++ b/app/src/components/Toolbar.tsx @@ -2,6 +2,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +import { ClanMark } from './ClanMark' + interface Props { title?: string onOpen: () => void @@ -14,20 +16,6 @@ interface Props { hasFile: boolean } -const ClanMark = () => ( - - - - - - - - - - - -) - const s: Record = { bar: { height: 48, background: '#0a0d14', borderBottom: '1px solid var(--border)', @@ -61,7 +49,7 @@ export default function Toolbar({ const valid = validation === 'OK' return (
- CLAN + CLAN {loading ? 'Loading…' : (title ?? 'No file open')} {validation && ( .json with keys: +{ "role","commands_run":[],"files_read":[{"file","approx_chars"}],"files_written":[{"file","chars"}], + "output_chars":,"errors":[{"what","recovered_by"}],"problems_and_friction":"","context_understood":"" }` + +// ===================== B-FORKMERGE ===================== +const FM = `${H}/forkmerge2` +const FM_BRANCHES = ['analyst-a', 'analyst-b', 'analyst-c', 'analyst-d'] + +function fmBranchPrompt(agent) { + const doc = `${FM}/branches/${agent}.clan` + return `You are ${agent}, a build-vs-buy analyst. Repo root is cwd; 'clan' is on PATH. +Your branch file: ${doc} +1. Read your task: clan read context ${doc} (it tells you the EXACT values to write). +2. Read agent guide if unsure: clan agent-help +3. Write your four findings into YOUR namespace (branches are locked on shared data — you MUST use --namespace): + Build a JSON with keys recommendation, budget_eur, top_risks (array), assumptions EXACTLY as your context says, + write it to /tmp/${agent}.json, then: + clan patch-data ${doc} /tmp/${agent}.json --namespace --agent ${agent} --action "wrote build-vs-buy assessment" +4. Validate: clan validate ${doc} +${RECEIPT} Receipts dir: ${FM}/receipts/ Report concisely.` +} + +async function runForkMerge() { + // 4 branches write in parallel + await parallel(FM_BRANCHES.map(a => () => agent(fmBranchPrompt(a), { label: `fm-${a}`, phase: 'ForkMerge' }))) + // merge + adjudicate (one agent does the deterministic merge then reads the report) + const verdict = await agent( + `You are the build-vs-buy SYNTHESIZER. Repo root is cwd; 'clan' on PATH. +1. Merge the 4 branches deterministically: + clan merge ${FM}/branches/analyst-a.clan ${FM}/branches/analyst-b.clan ${FM}/branches/analyst-c.clan ${FM}/branches/analyst-d.clan --output ${FM}/work/merged.clan --delta "fold 4 analyst assessments" +2. Inspect conflicts: clan read report ${FM}/work/merged.clan (this lists contested keys + per-branch values/provenance) +3. The FOUR keys analysts disagreed on: recommendation, budget_eur, top_risks, assumptions. + For EACH, report how many distinct branch values the merge-report preserved and whether it names the winning + losing branches. +4. Adjudicate: pick a final recommendation and record it: + write {"final_recommendation":"","final_budget_eur":} to /tmp/fm-final.json then + clan patch-data ${FM}/work/merged.clan /tmp/fm-final.json --agent synthesizer --action "adjudicated build-vs-buy" --rationale "" + and clan patch-decision ${FM}/work/merged.clan --agent synthesizer --action "final: " --rationale "" --pinned +5. Validate the merged file. +Return JSON: for each of the 4 contested keys, {key, distinct_values_in_report, winner_named (bool), loser_provenance (bool)}, plus adjudication_recorded (bool).`, + { + label: 'fm-synth', phase: 'ForkMerge', + schema: { + type: 'object', required: ['contested', 'adjudication_recorded'], + properties: { + contested: { type: 'array', items: { type: 'object', required: ['key', 'distinct_values_in_report', 'winner_named', 'loser_provenance'], + properties: { key: { type: 'string' }, distinct_values_in_report: { type: 'integer' }, winner_named: { type: 'boolean' }, loser_provenance: { type: 'boolean' } } } }, + adjudication_recorded: { type: 'boolean' }, notes: { type: 'string' }, + }, + }, + }) + // ad-hoc comparison: 4 sequential writers to one flat summary.json -> scalar keys overwrite + return verdict +} + +// ===================== B-UNGUIDED ===================== +const UG = `${H}/unguided` +const UG_ROLES = ['market-analyst', 'risk-analyst', 'lead-synthesizer'] +function ugPrompt(idx) { + const role = UG_ROLES[idx] + const doc = `${UG}/work/doc-fresh.clan` + return `There is a command-line tool called \`clan\` on your PATH, and a document at ${doc}. +You are the ${role} (hop ${idx + 1} of 3) on a market-entry analysis for Brightline Logistics expanding to Germany. +Figure out how to use the tool yourself. Add your ${role} findings to the document and record what you did so the next agent can continue. +Do not corrupt the file — whatever you do, the document must still validate afterwards. Knowledge only; no web. +${RECEIPT} Receipts dir: ${UG}/receipts/ +In problems_and_friction, say what you had to discover and whether anything blocked you. Report concisely.` +} +async function runUnguided() { + const results = [] + for (let i = 0; i < UG_ROLES.length; i++) { + results.push(await agent(ugPrompt(i), { label: `ug-${UG_ROLES[i]}`, phase: 'Unguided' })) + } + // verifier + const v = await agent( + `You audit a 3-hop UNGUIDED run on ${UG}/work/doc-fresh.clan. Repo root is cwd; 'clan' on PATH. +Inspect: clan read data, clan read chain, clan validate on that file. +Report JSON: { validates (bool), n_chain_entries (int), unknown_agent_count (int), all_hops_attributed (bool), +namespace_or_guard_violations (int, e.g. corrupted file or failed writes that were forced through), +used_pack_when_patch_suffices (bool), stuck_on_attribution_error (bool, true if any agent visibly failed to record attribution) }`, + { label: 'ug-verify', phase: 'Verify', schema: { + type: 'object', required: ['validates', 'n_chain_entries', 'unknown_agent_count', 'all_hops_attributed'], + properties: { validates: { type: 'boolean' }, n_chain_entries: { type: 'integer' }, unknown_agent_count: { type: 'integer' }, + all_hops_attributed: { type: 'boolean' }, namespace_or_guard_violations: { type: 'integer' }, + used_pack_when_patch_suffices: { type: 'boolean' }, stuck_on_attribution_error: { type: 'boolean' }, notes: { type: 'string' } } } }) + return { hops: results.length, verdict: v } +} + +// ===================== B-META ===================== +const META = `${H}/meta` +function metaHop1() { + return `You are the ACCOUNT PLANNER (hop 1 of a 3-hop metamorphosis). Repo root is cwd; 'clan' on PATH. +Parent: ${META}/work/doc-fresh.clan . You will produce ${META}/snapshots/hop-01-agency-brief.clan . +This document will later transform into a concept deck then a client pitch — but YOUR hop-1 data fields must survive verbatim to the end. +1. Author an HTML agency brief for "Lumen" (a sustainable lighting startup) into /tmp/meta-hop1.html. At the TOP put a YAML frontmatter block (between --- markers) supplying structured data: + single_minded_proposition: "Light that pays for itself" + budget_eur: 120000 + persona: "Facilities managers at mid-size offices" + The HTML body should reference the logo via . +2. Pack it, carrying the logo asset and a new schema: + First write schema to /tmp/meta-schema1.json: {"type":"object","properties":{"single_minded_proposition":{"type":"string"},"budget_eur":{"type":"integer"},"persona":{"type":"string"}}} + Then: clan pack-html ${META}/work/doc-fresh.clan /tmp/meta-hop1.html --output ${META}/snapshots/hop-01-agency-brief.clan --assets ${META}/work --schema /tmp/meta-schema1.json --agent account-planner --action "agency brief" --rationale "hop 1 metamorphosis" + (the --assets dir ${META}/work contains logo-mark.svg) +3. Validate the output and confirm the asset is inside: clan validate ; unzip -l | grep logo +${RECEIPT} Receipts dir: ${META}/receipts/ Report concisely.` +} +function metaHop2() { + return `You are the CREATIVE DIRECTOR (hop 2 of 3). Repo root is cwd; 'clan' on PATH. +Parent: ${META}/snapshots/hop-01-agency-brief.clan . Produce ${META}/snapshots/hop-02-concept-deck.clan . +Transform the document into a completely different CONCEPT DECK view with a NEW schema — but do NOT re-pass --assets (the logo must carry automatically; this is the F10 regression test) and do NOT re-transcribe hop-1 data (merge-patch keeps omitted keys). +1. Author /tmp/meta-hop2.html: a concept-deck page presenting THREE named concepts. Add frontmatter data: concept_names: ["Dawn", "Halo", "Beacon"]. +2. Write schema /tmp/meta-schema2.json: {"type":"object","properties":{"concept_names":{"type":"array","items":{"type":"string"}}}} +3. Pack WITHOUT --assets: clan pack-html ${META}/snapshots/hop-01-agency-brief.clan /tmp/meta-hop2.html --output ${META}/snapshots/hop-02-concept-deck.clan --schema /tmp/meta-schema2.json --agent creative-director --action "concept deck" --rationale "hop 2 metamorphosis" +4. Validate; confirm the logo asset still present (unzip -l | grep logo) AND hop-1 data still present (clan read data | grep single_minded). +${RECEIPT} Receipts dir: ${META}/receipts/ Report concisely.` +} +function metaHop3() { + return `You are the PITCH LEAD (hop 3 of 3, final). Repo root is cwd; 'clan' on PATH. +Parent: ${META}/snapshots/hop-02-concept-deck.clan . Produce ${META}/snapshots/hop-03-client-pitch.clan . +Transform into a CLIENT PITCH view, again a new schema, again WITHOUT re-passing --assets and WITHOUT re-transcribing prior data. +1. Author /tmp/meta-hop3.html: a client pitch page. Frontmatter data: recommended_concept: "Halo", pitch_ask_eur: 120000. +2. Schema /tmp/meta-schema3.json: {"type":"object","properties":{"recommended_concept":{"type":"string"},"pitch_ask_eur":{"type":"integer"}}} +3. clan pack-html ${META}/snapshots/hop-02-concept-deck.clan /tmp/meta-hop3.html --output ${META}/snapshots/hop-03-client-pitch.clan --schema /tmp/meta-schema3.json --agent pitch-lead --action "client pitch" --rationale "hop 3 metamorphosis" --pinned +4. Validate. +${RECEIPT} Receipts dir: ${META}/receipts/ Report concisely.` +} +async function runMeta() { + await agent(metaHop1(), { label: 'meta-hop1', phase: 'Meta' }) + await agent(metaHop2(), { label: 'meta-hop2', phase: 'Meta' }) + await agent(metaHop3(), { label: 'meta-hop3', phase: 'Meta' }) + const v = await agent( + `You verify a 3-hop METAMORPHOSIS. Repo root is cwd; 'clan' on PATH. Final: ${META}/snapshots/hop-03-client-pitch.clan +Checks (use clan read data / read chain / validate / unzip -l on the final): +- hop1_data_survived: are single_minded_proposition, budget_eur, persona STILL in the final data verbatim? +- hop2_data_survived: is concept_names ["Dawn","Halo","Beacon"] still present? +- asset_carried: is human/assets/logo-mark.svg present in the final (it was only passed via --assets at hop 1)? +- lineage_unbroken: does read chain show all 3 hops attributed (account-planner, creative-director, pitch-lead)? +- final_validates: does clan validate pass? +Return JSON with those 5 booleans + notes.`, + { label: 'meta-verify', phase: 'Verify', schema: { + type: 'object', required: ['hop1_data_survived', 'hop2_data_survived', 'asset_carried', 'lineage_unbroken', 'final_validates'], + properties: { hop1_data_survived: { type: 'boolean' }, hop2_data_survived: { type: 'boolean' }, asset_carried: { type: 'boolean' }, + lineage_unbroken: { type: 'boolean' }, final_validates: { type: 'boolean' }, notes: { type: 'string' } } } }) + return v +} + +// ===================== L-H3 cold resume ===================== +async function runResume() { + const doc = `${H}/lite/h3-clan/work/doc.clan` + const v = await agent( + `You are taking over an in-progress analysis. Everything known is in ${doc}. Repo root is cwd; a 'clan' CLI is on PATH. +Work out where the analysis stands and complete the NEXT step only (one more analyst hop), then record your handoff. +Before your first productive write, orient yourself from the artifact alone. +${RECEIPT} Receipts dir: ${H}/lite/h3-clan/ Set role to "resume-agent". +In context_understood, state what stage you found it at and what the correct next step was. In problems_and_friction, count how many read/probe commands you needed before your first write. Report concisely.`, + { label: 'h3-resume', phase: 'Resume' }) + // judge orientation quality from the receipt + artifact + const judge = await agent( + `Judge a COLD-RESUME attempt. Repo root is cwd; 'clan' on PATH. Artifact now: ${H}/lite/h3-clan/work/doc.clan +The resume agent's receipt is in ${H}/lite/h3-clan/receipts/ (read it). Inspect the artifact (clan read data/chain/validate). +Return JSON: { oriented_correctly (bool — did it identify the right next step?), n_orientation_reads (int from receipt), produced_valid_write (bool), redid_prior_work (bool), decision_recorded (bool), notes }`, + { label: 'h3-judge', phase: 'Verify', schema: { + type: 'object', required: ['oriented_correctly', 'produced_valid_write', 'redid_prior_work', 'decision_recorded'], + properties: { oriented_correctly: { type: 'boolean' }, n_orientation_reads: { type: 'integer' }, produced_valid_write: { type: 'boolean' }, + redid_prior_work: { type: 'boolean' }, decision_recorded: { type: 'boolean' }, notes: { type: 'string' } } } }) + return judge +} + +// ===================== orchestrate (4 independent flows in parallel) ===================== +const [forkmerge, unguided, metaResult, resume] = await parallel([ + () => runForkMerge(), + () => runUnguided(), + () => runMeta(), + () => runResume(), +]) + +return { forkmerge, unguided, meta: metaResult, resume }