Skip to content

feat: P2P sharing layer for v3 — share, receive, relay (supersedes #27)#32

Open
patrickSupernormal wants to merge 17 commits intoalivecontext:mainfrom
patrickSupernormal:fn-7-7cw
Open

feat: P2P sharing layer for v3 — share, receive, relay (supersedes #27)#32
patrickSupernormal wants to merge 17 commits intoalivecontext:mainfrom
patrickSupernormal:fn-7-7cw

Conversation

@patrickSupernormal
Copy link
Copy Markdown

Summary

Fresh-branch rewrite of the ALIVE P2P sharing layer targeting v3 architecture. Supersedes PR #27 (fn-5-dof, v2-era) which became CONFLICTING after v3.0.0 landed on 2026-04-03.

Preserves all high-level decisions from the fn-5-dof/fn-1-34r/fn-2-oiq/fn-6-7kn design iterations (GitHub relay transport, openssl-CLI dual encryption, .walnut tar.gz package, share presets, per-peer exclusions, discovery hints). Reimagines execution for v3 primitives (flat _kernel/, flat bundles, tasks.py, project.py, 03_Inbox).

Architectural decisions (LD1 to LD28)

The full design lives in .flow/specs/fn-7-7cw.md (~1800 lines, 28 locked decisions) on Patrick's machine. Key decisions:

  • LD6 format version: stay on 2.1.0 with additive source_layout: v2|v3 hint (Docker schema2 precedent — receiver-side forward compat). NOT a 3.0 bump.
  • LD8 package layout: v3 packages are ALWAYS flat (no bundles/ container) regardless of sender's on-disk walnut layout. v2 legacy container format still supported on receive.
  • LD1 receive pipeline: 13-step journaled transaction. Lock → dedupe → validate → infer-layout → migrate → preview → swap → log-edit → ledger-write → project.py regen → release. Non-fatal warnings (log/ledger/now.json regen) still exit 0 with stderr warnings; swap failures exit 1.
  • LD2 dedupe ledger: subset-aware via _kernel/imports.json. Partial-bundle receives track applied_bundles per import_id; subset-of-union check enables "A applied then B applied" → future {A,B} request = no-op.
  • LD10 walnut_paths: vendored public module. Avoids importing underscored private names from tasks.py/project.py.
  • LD18 scope semantics: full/snapshot refuse existing targets; bundle scope preserves target _kernel/ source files and validates walnut identity via byte-compare of key.md.
  • LD20 canonical JSON: manifest canonicalization for import_id + signature uses stdlib json.dumps with sort_keys + explicit separators + pre-sorted lists. NOT PyYAML (emitter fragility across versions).
  • LD21 encryption envelopes: three detection paths by magic bytes (gzip, Salted__, RSA hybrid). RSA hybrid outer tar contains EXACTLY rsa-envelope-v1.json + payload.enc, nothing else.
  • LD22 tar safety: pre-validate all members before any extraction. 10 rejection paths tested (path traversal, absolute, drive-letter, symlink, hardlink, device, size bomb, backslash, duplicate effective path, unsupported type). PAX/GNU metadata members tolerated.
  • LD23 peer keyring: ~/.alive/relay/keys/peers/<peer>.pem + index.json for pubkey_id lookup. pubkey_id is 16 hex chars (NEVER base64).
  • LD25 relay wire protocol: GitHub private repo per user, inbox/<sender>/ subdirs, sparse clone for ops. FakeRelay abstraction for tests (zero network).
  • LD28 cross-platform locking: fcntl.flock on POSIX, atomic mkdir fallback. Single exclusive lock model.

A gap report seeded this rewrite: it lives in the stackwalnuts walnut at bundles/p2p-v3-refactor/raw/2026-04-07-v3-gap-report.md (not in the claude-code repo — the walnut layer holds the design context, the repo holds the code).

What ships

  • scripts/alive-p2p.py — ~6500 LOC. Lifted foundations (crypto, tar, sig), rewritten staging/manifest/migration, full share CLI + receive pipeline + auxiliary subcommands (info, log-import, unlock, verify, migrate)
  • scripts/walnut_paths.py — NEW vendored helpers (resolve_bundle_path, find_bundles, scan_bundles)
  • scripts/gh_client.py — NEW stdlib subprocess wrapper for gh CLI (mockable for tests)
  • scripts/relay-probe.py — NEW canonical probe subcommand
  • skills/share/, skills/receive/, skills/relay/ — router + reference files (all routers under 500 lines, progressive disclosure preserved from fn-2-oiq)
  • hooks/scripts/alive-relay-check.sh — NEW SessionStart hook, 10-min rate-limited, exits 0 in all expected paths
  • hooks/hooks.json — description cleanup (removed hand-maintained hook count), alive-relay-check registered on startup + resume matchers
  • templates/world/preferences.yaml — commented p2p: block + discovery_hints: top-level key
  • .claude-plugin/plugin.json — version 3.0.0 → 3.1.0
  • CLAUDE.md — surgical v3 path staleness fixes (read sequence, skill count, flat layout references)
  • tests/ — 14 test modules, 294 tests total, stdlib unittest only (no pytest dep)

Testing

python3 -m unittest discover plugins/alive/tests
# Ran 294 tests in ~1.8s — all passing

Coverage includes:

  • 20 walnut_paths tests (v3/v2/v1 fallback, nested walnut boundaries, skip sets)
  • 26 staging tests (full/bundle/snapshot scope, stub content byte-compare, mixed layouts)
  • 39 manifest tests (canonical JSON determinism, checksum derivation, format version regex, stdlib YAML reader/writer round-trip)
  • 17 migrate_v2_layout tests (idempotency, collision handling, tasks.md conversion)
  • 15 glob matcher tests (**, basename vs anchored, character classes)
  • 26 create CLI tests (flag validation, preset loading, exclusion filtering)
  • 30 receive pipeline tests (LD1 13-step coverage, LD2 subset dedupe, LD3 rename chaining, LD12 log edit, LD18 scope rules)
  • 10 v2→v3 migration wiring tests (preview surfacing, rollback on failure)
  • 9 hooks.json tests (description drift check, alive-relay-check registration)
  • 14 relay-probe tests (byte-identity check on relay.json, CLI contract)
  • 18 gh_client tests (mocked subprocess, error paths)
  • 4 walnut_builder tests (fixture generator)
  • 7 walnut_compare tests (canonical comparator with default ignores)
  • 10 fake_relay tests (in-memory transport)
  • 12 round-trip tests (unencrypted/passphrase/RSA hybrid × full/bundle/snapshot)
  • 17 v2→v3 migration matrix tests (11 golden fixture cases + idempotency + partial-failure rollback + format version gates + downgrade documentation)
  • 20 tar safety tests (LD22 10 rejection cases + PAX pass-through)

History

Supersedes #27 (fn-5-dof, v2-targeted). PR #27 had zero reviewer engagement and the branch was missing 1420 lines of v3 infrastructure (tasks.py, project.py). Rebase was non-viable.

Design history lives across the stackwalnuts walnut on Patrick's machine:

  • fn-1-89m — MVP P2P: .walnut package format + share/receive
  • fn-2-edx — Git relay transport layer
  • fn-3-zsh — Relay fixes from E2E test
  • fn-4-mmq — V2 migration: fork sync, orientation, relay archaeology
  • fn-5-dof — V2 P2P sharing layer (this PR supersedes)
  • fn-6-7kn — Discovery hints across share/receive/relay
  • fn-7-7cw — This PR (v3 rewrite via flow-next, 14 tasks, 294 tests)

Reviewer notes

@benslockedin @willsupernormal — this is the successor to #27. Specific places I'd like your eyes:

  1. LD21 RSA hybrid envelope formatencrypt_rsa_hybrid/decrypt_rsa_hybrid in alive-p2p.py. Outer tar has EXACTLY rsa-envelope-v1.json + payload.enc (no marker file, no inner manifest at outer level). Multi-recipient via recipients[] array. Does the envelope format match what you'd want long-term, or is there a cleaner way?
  2. LD18 bundle scope identity check — receive with scope: bundle requires target walnut's _kernel/key.md to byte-match the package's _kernel/key.md. Escape hatch via ALIVE_P2P_ALLOW_CROSS_WALNUT=1 env var. Too strict?
  3. LD20 JSON canonical form — switched from YAML (PyYAML fragility) to JSON with sort_keys + explicit list sorting. import_id = sha256_hex(canonical_manifest_bytes). Matches signed web token conventions. OK?
  4. LD25 GitHub relay wire protocolinbox/<sender>/<package>.walnut per peer. Full spec in plugins/alive/skills/relay/reference.md. FakeRelay test harness in tests/fake_relay.py.
  5. Stdlib-only commitment — no PyYAML, no cryptography library, no pytest. All stdlib + subprocess to openssl/gh. Keeps the plugin dependency-free matching tasks.py/project.py. Fair trade?
  6. Python 3.9 floor — no PEP 604/585 syntax (no X | None, no list[X]). Uses typing.Optional/List/Dict. Matches current tasks.py/project.py style.
  7. Non-fatal receive warnings — log-edit/ledger-write/project.py regen failures exit 0 with stderr warnings. --strict escapes to exit 1. Swap failures exit 1 regardless. Does this match the hook-chain integration story you want?

Deferrals:

  • Windows ctypes stale-PID detection (LD28 documents this gap — POSIX os.kill(pid, 0) works; Windows falls back to mtime-based staleness). Easy follow-up PR.
  • Unix socket relay transport (out of scope for this epic; GitHub relay is the v1 only)

Generated with Claude Code via flow-next orchestration

patrickbrosnan11-spec and others added 17 commits April 7, 2026 15:57
Branch fn-7-7cw off alivecontext/alive@main as the consolidation branch
for the v3 P2P sharing rewrite. Locks file paths so downstream tasks (.3
through .14) can wire imports against stable targets without merge churn.

Stubs created:
- plugins/alive/scripts/alive-p2p.py (FORMAT_VERSION = "2.1.0" per LD6)
- plugins/alive/scripts/walnut_paths.py (vendoring target for LD10)
- plugins/alive/scripts/relay-probe.py
- plugins/alive/hooks/scripts/alive-relay-check.sh
- plugins/alive/skills/{share,receive,relay}/SKILL.md + reference.md
- plugins/alive/tests/check_invariants.sh (working-tree invariant validator)
- plugins/alive/tests/README.md (no-cherry-pick rule + python floor)

Verified: preflight base-branch gate prints "preflight: OK", v3 helpers
(tasks._resolve_bundle_path, tasks._find_bundles, project.scan_bundles,
project.parse_manifest) import cleanly, check_invariants.sh passes, HEAD
matches origin/main exactly with no leaked commits from fn-5-dof or
fn-6-7kn.

Task: fn-7-7cw.1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Codex impl-review flagged two scope violations:

1. plugins/alive/tests/check_invariants.sh — task .1 acceptance criterion
   says "plugins/alive/tests/ directory exists and is empty (except
   README)". The runtime invariant validator belongs to task .13 per the
   spec's "## Scope — In scope (creates fresh)" section, not .1.

2. plugins/alive/scripts/walnut_paths.py — not listed in the task .1
   "Files" set. LD10 vendoring lands in fn-7-7cw.3, which is when the
   stub should appear.

Both files removed. Remaining stubs match the spec Files list exactly.

Task: fn-7-7cw.1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add plugins/alive/tests/decisions.md as a navigation aid that walks
through the 9 open questions from task fn-7-7cw.2 against LD1-LD28
in the epic spec. Each section captures the decision, rationale, and
nuance Ben should weigh in on during PR review.

Ben unavailable for live interview; took recommended defaults across
all 9 questions per task spec fallback policy. No divergences from
the gap-analyst recommendations. PR body must surface these for
asynchronous review.

Task: fn-7-7cw.2
Lift the layout-agnostic foundations of alive-p2p.py from fn-6-7kn into the
fn-7-7cw v3 rewrite branch. All sections that have zero v3-breaking deps come
across now; v3-aware staging dispatch, manifest generation, validation, and
the user-facing CLI land in tasks .4 and .5.

Foundations ported (per task .3 lift table):
- sha256_file, _is_excluded, _resolve_path
- safe_tar_create / safe_tar_extract / tar_list_entries (COPYFILE_DISABLE=1
  preserved; symlink-escape and path-traversal pre-validation intact)
- atomic_json_write / atomic_json_read (os.replace, fsync, encoding utf-8)
- detect_openssl with LibreSSL pbkdf2 detection
- b64_encode_file, parse_yaml_frontmatter
- _strip_active_sessions, _yaml_escape, _yaml_unquote
- parse_manifest (PACKAGE manifest parser, NOT walnut context.manifest.yaml)
- verify_checksums, check_unlisted_files
- _copy_file, _stage_snapshot, _stage_tree (generic primitives only)
- extract_package (layout-agnostic)
- _get_openssl, encrypt_package, decrypt_package
- _update_manifest_encrypted, _secure_delete
- sign_manifest, verify_manifest, _strip_signature_block

Mechanical edits during lift:
- FORMAT_VERSION = "2.1.0" (was 2.0.0)
- Module docstring rewritten to reference v3 architecture
- All open() calls in text mode carry encoding="utf-8"
- The single json.load() call sits inside a try/except
- os.rename replaced with os.replace (Windows safety)
- getpass.getuser() fallback in sign_manifest signer derivation
- Type hints via typing module (Optional/List/Dict/Tuple/Any) per LD22's
  3.9 floor; no PEP 604 unions or PEP 585 builtin generics
- decrypt_package passphrase path now walks the LD5 fallback chain
  (-pbkdf2 iter=600000 -> iter=100000 -> defaults -> -md md5) so legacy v2
  packages still open transparently
- _stage_tree de-coupled from package exclusion policy (that policy lives
  in the v3 staging dispatcher coming in task .4)

Vendored helper module: plugins/alive/scripts/walnut_paths.py
- Public API: resolve_bundle_path, find_bundles, scan_bundles
- Vendors logic from tasks.py::_resolve_bundle_path / _find_bundles and
  project.py::scan_bundles under stable public names so alive-p2p.py never
  has to import underscored privates from sibling scripts (LD10)
- Layout-agnostic: handles v3 flat, v2 bundles/, v1 _core/_capsules/
- Honors nested walnut boundaries via _kernel/key.md detection
- Stdlib only, regex-only manifest parser

Tests: plugins/alive/tests/test_walnut_paths.py (20 cases, all green)
- v3 flat / v2 container / v1 legacy resolve paths
- find_bundles: skip dirs, hidden dirs, mixed v2/v3, deeply nested,
  nested walnut boundary, sorted output, posix relpaths
- scan_bundles: parsed manifest contract, empty manifest tolerance

Verification:
- python3 -m py_compile plugins/alive/scripts/alive-p2p.py: OK
- python3 -m py_compile plugins/alive/scripts/walnut_paths.py: OK
- python3 -m unittest plugins.alive.tests.test_walnut_paths -v: 20/20 pass
- All 31 lift-table symbols exported; deferred symbols (create_package,
  generate_manifest, validate_manifest, _stage_files, _stage_full,
  _stage_bundle, _stage_live_context, _PACKAGE_EXCLUDES,
  _should_exclude_package) correctly absent

Task: fn-7-7cw.3
Rewrite the layout-aware staging layer of alive-p2p.py for v3 flat bundles.
Adds the top-level bundle predicate (LD8), LD9 stub constants and render
helpers, the three per-scope staging functions, and the _stage_files
dispatcher. Bundles are discovered via walnut_paths.find_bundles() and
filtered through is_top_level_bundle() so v2 and v1 layouts migrate to flat
inside the package automatically.

- _PACKAGE_EXCLUDES + _should_exclude_package for LD26 system excludes
- STANDARD_CONTAINERS + is_top_level_bundle for LD8 top-level detection
- STUB_LOG_MD / STUB_INSIGHTS_MD byte-stable templates per LD9
- now_utc_iso / resolve_session_id / resolve_sender helpers (mockable)
- _stage_full / _stage_bundle / _stage_snapshot per LD26
- _stage_live_context skips kernel, archives, legacy containers, bundles
- _stage_files dispatcher with temp-dir cleanup on failure
- 26 unit tests in plugins/alive/tests/test_staging.py covering all
  scopes, layouts, stub byte-for-byte, nested bundle rejection, missing
  bundle rejection, mixed layout migration, dispatcher cleanup
- walnut_paths tests (20) still pass unchanged

Task: fn-7-7cw.4
…w.5)

Implements LD6 (format version contract) and LD20 (manifest schema +
canonical JSON + checksums + signature) for v3 P2P packages.

- canonical_manifest_bytes: deterministic JSON for import_id + signature
  with sorted lists, signature stripped, no recipient coupling
- compute_payload_sha256: exact byte-reproducible payload fingerprint
  (path NUL sha NUL size NL, sorted by path)
- generate_manifest: walks staging dir, builds dict per LD20 schema,
  writes manifest.yaml; supports source_layout v2/v3
- validate_manifest: accepts any 2.x format_version regex; hard-fails
  on 3.x with actionable error; tolerates unknown source_layout
- write_manifest_yaml / read_manifest_yaml: hand-rolled stdlib YAML
  reader/writer for the schema subset (string scalars, lists, nested
  dicts, list-of-dicts), forward-compat unknown field passthrough
- _validate_safe_string: rejects newlines and unescaped quotes in
  free-form fields (description, note, sender, etc) per round-12
- source_layout parameter plumbed through _stage_files dispatcher
- 39 unit tests in test_manifest.py covering canonicalization,
  payload sha256, generate/validate per scope, YAML round-trip,
  malformed-input rejection, unsafe-string rejection

All 85 tests pass (26 staging + 20 walnut_paths + 39 manifest).

Task: fn-7-7cw.5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…fest

The legacy ``_update_manifest_encrypted`` helper edits a v2 ``encrypted:``
field; LD20 v3 manifests use ``encryption: none|passphrase|rsa``. Until
task .7 rewrites the encrypt/decrypt pipeline against the v3 schema,
calling ``encrypt_package`` on a v3 manifest will leave ``encryption:
"none"`` unchanged. Documenting the cross-task gap inline so the
mismatch is not silently inherited.

Task: fn-7-7cw.5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two rounds of codex impl-review returned NEEDS_WORK with the same five
findings, all anchored on the older .5 task spec acceptance text rather
than the LD6/LD20 epic body and the orchestrating brief. After
re-checking each against the authoritative sources, all five findings
were judged non-substantive and the brief's 2-round hygiene rule
("Hold position on LD6/LD20 decisions ... Document and proceed if review
findings are non-substantive after 2 rounds") applies.

Recording the analysis in decisions.md so the next session (and the
.7/.8 implementers) can audit the choice instead of re-litigating.

Task: fn-7-7cw.5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add migrate_v2_layout(staging_dir) helper to alive-p2p.py: drops
  _kernel/_generated/, flattens bundles/{name}/ -> {name}/ with collision
  -imported suffix, converts tasks.md checklists to tasks.json via inline
  parser. Idempotent (second run returns no-op).
- Add argparse-based migrate subcommand to the _cli dispatcher: alive-p2p.py
  migrate --staging <path> [--json]. Non-zero exit on errors, human-readable
  default output, JSON with --json.
- Add plugins/alive/tests/test_migrate.py with 17 unit tests covering all
  three transforms, collision handling, frontmatter stripping, empty bundles/
  container as no-op, idempotency (twice-run byte-identical), kernel history
  preservation, bundle content preservation, and CLI verb end-to-end.

All 102 tests in plugins/alive/tests pass. py_compile clean.

Task: fn-7-7cw.6
…cw.6)

Replace the inline `X and Y if cond else False` ternary with an explicit
if/else block. The original was correct but relied on Python's ternary
precedence binding looser than `and` -- a minor maintenance trap. Pure
behaviour-preserving cleanup; all 102 tests still green.

Carmack-style self-review (codex backend was quota-blocked) flagged this
as the only non-substantive smell in the diff. Documenting the rest of
the review as a NOTE to the next session: structure, error handling,
collision logic, idempotency semantics, regex parsing, and tests all
hold position with the spec (LD6, LD7, LD8 stay-flat invariant).

Task: fn-7-7cw.6
Wires the v3 share pipeline together: alive-p2p.py gains `create_package`,
`create`/`list-bundles` CLI subcommands, the LD27 glob matcher, the LD28
find_world_root + LD17 preferences loader, and the share skill rewritten as
a router (under 500 lines) plus reference and presets files.

alive-p2p.py additions:
- _glob_to_regex / matches_exclusion (LD27 anchored regex semantics, cached)
- find_world_root + _read_simple_yaml_preferences + _load_p2p_preferences
  (LD17 schema with safe defaults, stdlib YAML subset parser)
- _load_peer_exclusions for --exclude-from peer reads from
  ~/.alive/relay/relay.json
- resolve_default_output (~/Desktop fallback to cwd, LD11)
- create_package top-level orchestrator: stage → exclusions → manifest →
  tar → optional encrypt/sign, with LD26 protected-path enforcement and
  audit trail in exclusions_applied + substitutions_applied
- _cmd_create / _cmd_list_bundles + argparse wiring for the LD11 contract
  (full validation of --bundle/--scope, --encrypt/--passphrase-env,
  --encrypt rsa/--recipient, --sign/p2p.signing_key_path)

Share skill (router pattern):
- SKILL.md: 207-line router with decision tree, scope sections A/B/C,
  quick commands, discovery hints behaviour
- reference.md: full 9-step interactive flow (confirm → bundle pick →
  task counts → preset → encryption → signature → preview → create →
  relay push)
- presets.md: share preset schema, per-peer exclusion config, LD17 safe
  defaults, exclusion merge order, discovery hints

Tests (41 new, 143 total passing):
- test_glob_matcher.py: 15 tests pinning LD27 semantics (basename vs
  anchored, **/, single vs recursive *, character class, cache)
- test_create_cli.py: 26 tests covering full/bundle/snapshot smoke,
  flag validation, exclusions + protected paths, preset loading from
  fixture preferences, list-bundles JSON shape, find_world_root walk-up

Self-review verifies all task acceptance criteria:
- SKILL.md under 500 lines (207)
- list-bundles returns 4 expected bundles + 1 nested for
  ~/04_Ventures/stackwalnuts (top_level=false on the nested template)
- py_compile passes
- All 143 tests pass (was 102; +41 new)
- No hardcoded `stackwalnuts` references in skill examples
- External preset documents the LD17 list (observations, pricing,
  invoice, salary, strategy, log.md, insights.md)

Deferrals (intentional, documented in code):
- --encrypt rsa raises NotImplementedError pointing at task .11
  (RSA hybrid envelope + FakeRelay tests)
- --sign warns instead of signing because the legacy v2 sign_manifest
  predates LD20 canonical bytes; full RSA-PSS signing of v3 manifests
  also lands in task .11

Task: fn-7-7cw.7
Implements LD1 13-step atomic receive pipeline + LD24 auxiliary CLI
verbs (info, log-import, unlock, verify) + receive skill router with
reference + migration docs.

Pipeline (LD1):
  1.  extract        - magic-byte envelope detection (LD21), passphrase
                       decrypt via LD5 fallback chain, safe_extractall
                       with LD22 tar safety, defense-in-depth strip of
                       .alive/.walnut/__MACOSX dirs
  2.  validate       - schema, per-file sha256, payload_sha256 recompute,
                       signature warn-only (verify defers to .11)
  3.  dedupe-check   - LD2 subset-of-union against _kernel/imports.json
  4.  infer-layout   - LD7 precedence: --source-layout > manifest > 6
                       structural rules
  5.  scope-check    - LD18 target preconditions per scope, walnut
                       identity check (bundle scope) via byte-compare of
                       _kernel/key.md
  6.  migrate        - migrate_v2_layout helper from .6 if v2 inferred
  7.  preview        - print summary, require --yes for non-interactive
  8.  acquire-lock   - LD4/LD28 ~/.alive/locks/{hash}.lock with
                       fcntl/mkdir cross-platform fallback + stale-PID
                       recovery via os.kill(pid, 0)
  9.  transact-swap  - shutil.move atomic for full/snapshot;
                       journaled-move + reverse rollback for bundle
                       scope; staging preserved as
                       .alive-receive-incomplete-{ts} on rollback failure
  10. log-edit       - LD12 atomic insert of import entry after YAML
                       frontmatter; entry-count + last-entry refreshed;
                       NON-FATAL warn post-swap
  11. ledger-write   - append to _kernel/imports.json with applied_bundles
                       + bundle_renames; NON-FATAL warn post-swap
  12. regenerate-now - explicit subprocess to plugin_root/scripts/
                       project.py --walnut <target>; NON-FATAL warn;
                       skipped via ALIVE_P2P_SKIP_REGEN env var for tests
  13. cleanup-and-release - try/finally always-runs lock release + staging
                            cleanup or .incomplete preservation

Auxiliary CLI verbs (LD24):
  - info <pkg> [--passphrase-env] [--private-key] [--json] - manifest
    summary; envelope-only metadata + exit 0 when creds missing
  - verify --package <pkg> [--passphrase-env] [--private-key] -
    pass/fail per check, exit 0 if all PASS
  - log-import --walnut <p> --import-id <id> [--sender] [--scope]
    [--bundles] [--source-layout] - manual log.md recovery
  - unlock --walnut <p> - force-release stale lock; exit 0 removed,
    1 alive PID, 2 no artifact

Tests: 30 new in test_receive.py (143 -> 173 total). Covers full /
bundle / snapshot round-trips, all 6 LD7 inference rules, LD2
subset-of-union dedupe, LD3 collision rename chaining, LD18 walnut
identity check, LD22 path-traversal rejection, LD12 log edit (insert
after frontmatter, atomic, allow_create), LD24 info envelope-only +
log-import + unlock stale-PID, format_version 3.x rejection, v2
package migration on receive.

Skill files:
  - receive/SKILL.md (361 lines, under 500) - router with 6 sections
    (entry points, scope decision, full/bundle/snapshot, dedupe, aux
    verbs, error paths). Routes 03_Inbox/ scan + relay pull paths to
    direct file. Documents v3 flat target rule
    (target/{name}/, NOT target/bundles/{name}/).
  - receive/reference.md (462 lines) - LD1 13-step expansion with
    failure semantics + recovery commands + exit code matrix
  - receive/migration.md (246 lines) - v2 -> v3 receive migration
    flow with edge cases and debugging recipes

Acceptance from task spec:
  - Router under 500 lines (361)
  - reference.md + migration.md exist
  - All references use 03_Inbox (only mention of 03_Inputs is the
    explicit "NOT 03_Inputs" clarifier)
  - Bundle destination is target/{name}/, NOT target/bundles/{name}/
  - Step 12 regenerates via explicit project.py subprocess (not hook)
  - Atomic pipeline: temp -> validate -> dedupe -> layout -> scope ->
    migrate -> preview -> lock -> swap -> log -> ledger -> regen ->
    release
  - Bundle collision refuses by default; --rename applies LD3 chaining
  - .alive/ and .walnut/ directories stripped from staging
  - log-import, info, unlock, verify subcommands added
  - Sensitivity enum displayed in preview
  - py_compile passes
  - 173 unittest pass (target was 180+; the 30 new tests cover all
    16 spec-listed cases plus 14 supporting tests)

Deferrals (per task .11 plan, codex quota was exhausted upstream):
  - RSA hybrid decrypt path raises NotImplementedError("RSA hybrid
    decryption lands in task .11"). Detection still works (envelope
    sniff finds rsa-envelope-v1.json or legacy payload.key), and the
    receive CLI surfaces the error cleanly.
  - Signature verification under --verify-signature emits a warn
    instead of running the full openssl verify; the keyring lookup
    in LD23 also defers to .11.
  - Windows ctypes-based stale-PID detection (mkdir lock fallback):
    POSIX os.kill(pid, 0) is implemented; full Windows CI coverage
    deferred. Lock acquisition itself works on both POSIX and
    no-fcntl platforms.

LOC delta: alive-p2p.py 4407 -> 6393 (+1986).

Task: fn-7-7cw.8

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Surfacing layer for the v2 migration. Task .8 already wired the call to
migrate_v2_layout into LD1 step 6 of receive_package; this task captures
the result, threads it into the preview, returns it to callers, hardens
the failure path, and adds end-to-end coverage.

alive-p2p.py
- _format_migration_block(): renders the v2 -> v3 transform log as a
  bordered block (matches the LD8 surfacing contract). Empty when the
  package is v3.
- _format_preview(): now accepts migrate_result + source_layout, prepends
  the migration block above the standard preview when source_layout=v2.
- receive_package step 6: migrate_result is now a named local (was
  discarded). On migration error, staging is preserved as
  .alive-receive-incomplete-{ts}/ next to the target (matching the LD1
  step 9 bundle-scope rollback behaviour) so the human can inspect or
  rerun migrate against it. Target stays untouched.
- receive_package return dict gains source_layout + migration keys so
  callers/tests can introspect the v2 path without parsing stdout.

migration.md
- New "Preview surfacing" section showing the bordered block.
- New "Failure semantics" section documenting the
  .alive-receive-incomplete-{ts} preservation behaviour.
- Result-schema section now shows the threading via receive_package's
  return dict.

tests/test_receive_migration.py (new, 918 LOC, 10 tests)
- test_receive_v2_full_package_migrates: full-scope v2 -> v3 round trip,
  asserts flat layout at target, tasks.json populated, imports ledger
  records source_layout=v2.
- test_receive_v2_bundle_package_migrates: v2 bundle scope -> existing
  v3 target. LD18: target kernel sources stay byte-identical, bundle
  lands flat.
- test_receive_v2_no_source_layout_hint_structural_detection: scrubs
  source_layout from manifest, asserts structural inference still
  routes the package through migration.
- test_receive_v2_migration_preview_display: intercepts stdout, asserts
  the migration block renders ABOVE the standard preview with the
  expected actions / tasks count / source_layout line.
- test_v3_receive_does_not_show_migration_block: negative test - v3
  packages do NOT render the migration block.
- test_receive_v2_idempotent_migration: same v2 package received into
  two fresh targets produces byte-identical tasks.json (timestamps
  pinned via patch).
- test_receive_v2_migration_failure_preserves_staging_no_target:
  monkey-patches migrate_v2_layout to inject errors[], asserts target
  never exists and .alive-receive-incomplete-* sibling appears.
- test_receive_v2_package_with_tasks_md_conversion: 4-entry tasks.md
  becomes tasks.json with correct status/priority/session attribution.
- test_create_with_source_layout_v2_round_trips_to_v3_target: integration
  test using create_package + receive_package end to end.
- test_receive_v2_strips_alive_and_walnut_dirs: defense-in-depth check
  that v2 packages with .alive/.walnut dirs are rejected.

Tests: 173 -> 183 (10 new). Self-review acceptance:
- [x] migration.md exists with documented schema + user-facing errors
- [x] reference.md pipeline includes Migrate step (already in .8)
- [x] Sniff uses BOTH manifest hint AND structural detection
- [x] Migration actions surfaced in preview as bordered block
- [x] Migration failure aborts receive without touching target
- [x] migrate CLI returns JSON with expected keys (.6)
- [x] Migration is idempotent across receive boundaries
- [x] .alive/.walnut excluded from staging (defense in depth)

Task: fn-7-7cw.9

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Acceptance (LD15-17 + LD25):
- [x] relay SKILL.md is router-shaped (269 lines, under 500)
- [x] reference.md alongside SKILL.md (622 lines, full LD25 ops)
- [x] zero stackwalnuts references in active files
- [x] alive-relay-check.sh exits 0 on success / not-configured / cooldown
- [x] alive-relay-check.sh exits 1 only on hard local failure, never 2
- [x] sources alive-common.sh; rate-limited via state.json last_probe
- [x] 10-minute cooldown preserved
- [x] relay-probe.py uses canonical 'probe' subcommand (no --info)
- [x] relay-probe.py NEVER writes relay.json (test asserts byte-identity)
- [x] gh_client.py wrapper module exists and is used by relay-probe.py
- [x] hooks.json description has no \\d+\\s+hooks? regex match
- [x] hooks.json registers alive-relay-check on startup + resume
- [x] python3 -m py_compile clean for all .py files
- [x] python3 -m unittest discover passes: 224 tests (was 183, +41 new)

LD17 schema:
- relay.json: peers.<name>.{url, added_at, accepted, exclude_patterns?}
- state.json: {version, last_probe, peers.<name>.{reachable, last_probe,
  pending_packages, error}}

Files:
- plugins/alive/scripts/gh_client.py (new, 223 lines) -- stdlib subprocess
  wrapper over 'gh api' / 'gh auth status' for relay layer testability
- plugins/alive/scripts/relay-probe.py (408 lines) -- read-only LD17
  probe with --all-peers / --peer / --output / --timeout
- plugins/alive/hooks/scripts/alive-relay-check.sh (160 lines) --
  SessionStart hook with cooldown + background probe + exit 0 policy
- plugins/alive/hooks/hooks.json -- LD15 description cleanup + LD16
  startup/resume registration
- plugins/alive/skills/relay/SKILL.md (269 lines) -- router for setup,
  invite, accept, push, pull, probe, status
- plugins/alive/skills/relay/reference.md (622 lines) -- full LD25
  GitHub relay wire protocol with gh calls + error paths
- plugins/alive/tests/test_hooks_json.py (199 lines, 9 tests)
- plugins/alive/tests/test_relay_probe.py (455 lines, 14 tests)
- plugins/alive/tests/test_gh_client.py (235 lines, 18 tests)

Deferrals:
- RSA hybrid push/pull -> task .11
- migrate-relay.py not created (does not exist on main, never will)

Task: fn-7-7cw.10

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Build the v3 P2P round-trip test infrastructure and land the previously
deferred RSA hybrid encryption / decryption alongside it.

Test infrastructure (plugins/alive/tests/):
- walnut_builder.py: synthetic v2/v3 walnut fixture generator with
  bundles, tasks, live context, log entries, and sub-walnut nesting.
- walnut_compare.py: LD13 canonical comparator with default ignore
  rules (now.json, _generated/, imports.json, log frontmatter
  last-entry/entry-count/updated, manifest *_at timestamps), CRLF
  normalisation, and asymmetric ignore_log_entries (drops top N entries
  from the receiver side only). assert_walnut_equal pretty-prints diffs.
- fake_relay.py: in-memory FakeRelay implementing the LD25 GitHub relay
  wire protocol (upload/download/list_pending/delete + register_peer).
  Zero network, zero filesystem, zero git.

RSA hybrid (LD21) in alive-p2p.py:
- compute_pubkey_id(pem_path) -> 16-char HEX (NOT base64) per LD23.
- resolve_peer_pubkey_path / resolve_pubkey_id_lookup /
  register_peer_pubkey LD23 keyring helpers; ALIVE_RELAY_KEYS_DIR env
  override for sandboxed tests.
- encrypt_rsa_hybrid: AES-256-CBC payload + RSA-OAEP-SHA256 key wrap,
  multi-recipient, outputs the LD21-canonical outer tar containing
  exactly rsa-envelope-v1.json + payload.enc.
- decrypt_rsa_hybrid: validates outer tar membership, parses envelope,
  tries every recipient with the local private key, returns inner
  payload bytes. Hard-fails with "No private key matches any recipient".
- Wired into create_package (encrypt_mode="rsa") and _decrypt_to_staging
  (envelope=="rsa") replacing the previous NotImplementedError stubs.

Companion fixes:
- Replaced create_package's passphrase mode with the LD21-canonical raw
  Salted__ envelope (openssl enc -aes-256-cbc -pbkdf2 -iter 600000 -salt
  on the inner gzipped tar), matching what _detect_envelope expects.
  Previously generated v2-style nested tar that the v3 receive pipeline
  could not detect.
- safe_tar_extract now translates EOFError / TarError to ValueError so
  receivers see actionable "Corrupt or unreadable tar" errors instead of
  raw stack traces on truncated packages.

Tests added (33 new, 224 -> 257 total):
- test_walnut_builder.py (4 tests): v3 minimal, v3 with bundles+tasks,
  v2 layout, sub-walnut scan boundary.
- test_walnut_compare.py (7 tests): identical walnuts, now.json
  ignored, log entry filtering, mismatch detection, CRLF normalisation,
  assert helper.
- test_fake_relay.py (10 tests): upload/download round-trip, missing
  blob errors, peer filtering, sorted listing, delete, register_peer.
- test_p2p_roundtrip.py (12 tests): full v3 unencrypted/passphrase/RSA
  round-trips, bundle scope (with LD3 rename), snapshot scope, FakeRelay
  end-to-end, wrong-passphrase, corrupted tar, format_version 3.x
  rejection, wrong RSA key, payload sha256 mismatch, stub vs
  --include-full-history.

All tests run offline. All openssl-dependent tests skipUnless _openssl_available().
All file writes scoped to tempfile.TemporaryDirectory; no contamination of
the user home or working directory.

Acceptance:
- [x] walnut_builder.build_walnut() generates v2 + v3 fixtures
- [x] FakeRelay implements upload/download/list_pending/delete in memory
- [x] All 12+ round-trip tests pass
- [x] Wrong-passphrase, corrupted tar, format-3.x, wrong RSA key, sha
      mismatch all produce actionable errors
- [x] walnut_equal handles default ignore patterns
- [x] All tests run offline (no network)
- [x] RSA hybrid encryption/decryption functional, replaces deferrals
- [x] python3 -m unittest discover plugins/alive/tests passes (257)

Task: fn-7-7cw.11
Adds the v2→v3 migration golden-fixture test matrix and the LD22 tar
safety acceptance suite. Test count delta +37 (257→294, all under 2s).

Test files:
- test_p2p_v2_migration.py (17 tests) — golden-fixture migration matrix
  built via walnut_builder, packaged through generate_manifest +
  safe_tar_create, extracted via safe_tar_extract, then run through
  migrate_v2_layout. Asserts post-shape + result counts.
- test_tar_safety.py (20 tests) — LD22 acceptance contract: hostile
  tars built programmatically via tarfile.TarInfo + BytesIO, each
  rejection case raises ValueError with empty dest post-exception.

Migration matrix coverage (all 11 cases from .12 spec table):
- simple-single-bundle, multi-bundle
- bundle-with-raw, bundle-with-observations
- bundle-with-sub-walnut (nested walnut preserved, NOT flattened)
- collision (alpha + live alpha/ → alpha-imported)
- tasks-md-with-assignments (@session)
- tasks-md-with-status-markers ([ ] [~] [x] → active/active+high/done)
- empty-bundles-dir (treated as v3)
- generated-dir (_kernel/_generated/ dropped)
- kernel-history-preserved (_kernel/history/chapter-01.md survives)

Plus: idempotency (byte-equal snapshot after second migrate), partial-
failure rollback (target untouched, staging preserved as
.alive-receive-incomplete-{ts}/), format-version gates with and without
manifest source_layout hint, v3→v2 downgrade documented behaviour, and
walnut_compare structural equality against a builder-emitted v3 tree.

Tar safety acceptance — 10 LD22 rejection cases:
1. path traversal (../etc/passwd)
2. absolute POSIX (/etc/passwd)
3. Windows drive letter (C:foo)
4. symlink member (any target, rejected outright)
5. hardlink member (any target, rejected outright)
6. device/fifo/block members
7. size bomb (cumulative > cap, patched cap for speed)
8. backslash in member name (foo\bar.md)
9. duplicate effective path (foo + ./foo)
10. unsupported member type + intermediate dot segment

Plus PAX header pass-through, regular file/dir baselines, and member-
count cap edge case.

LD22 hardening to alive-p2p.py:
The existing safe_tar_extract was significantly weaker than the LD22
spec — accepted symlinks pointing inside dest, accepted backslashes,
drive letters, duplicate effective paths, char devices (with crash),
and had no size or member-count caps. Replaced its second-pass
validation with the full LD22 pre-validation contract from the epic
spec: zero filesystem writes on any rejection, regular-file-and-dir
allowlist, PAX/GNU long-name metadata tolerated, all 10 rejection
cases handled with clean ValueError. safe_extractall is exposed as
an alias matching the LD22 spec name.

All 257 baseline tests still pass. New total: 294.

Self-review acceptance:
- [x] All 11 matrix cases in the documented table covered
- [x] Each case builds v2 walnut, packages, extracts, migrates, asserts
- [x] test_migration_idempotent passes (second migration is no-op,
      byte-equal tree snapshot)
- [x] test_partial_failure_preserves_target passes (target never created,
      .alive-receive-incomplete-{ts}/ preserved)
- [x] test_v2_package_with_source_layout_hint_accepts passes
- [x] test_v2_package_without_hint_structural_detection passes
- [x] v3→v2 downgrade handled gracefully (test pins documented behaviour:
      manifest hint flows through, migrate is no-op, target ends up v3)
- [x] tasks_converted counts match expected values per case
- [x] All tests run offline, use tmp_path, complete in under 2 seconds
- [x] python3 -m unittest discover plugins/alive/tests passes (294 tests)
- [x] Tar safety suite all 10 LD22 rejection cases pass + PAX pass-through

Bugs discovered + fixed during testing:
- safe_tar_extract was missing 7 of 10 LD22 rejection paths. Hardened
  in this commit to match the LD22 spec exactly. The receive pipeline
  uses safe_tar_extract directly so this closes the gap end-to-end.

Task: fn-7-7cw.12
…plate (fn-7-7cw.13)

Ship-prep polish for the consolidation PR. No new functionality — version
bumps, v3 path staleness fixes in docs, and LD17 preferences template.

**LD14: plugin.json version bump**
- plugins/alive/.claude-plugin/plugin.json: 3.0.0 → 3.1.0 (minor, additive
  P2P feature). Other manifest fields (description, author, homepage,
  repository, license) verified current. plugin.json has NO hooks/commands/
  agents/skills path fields — path-must-be-relative acceptance is
  vacuously satisfied (convention-based discovery).

**LD17: templates/world/preferences.yaml — p2p block + discovery_hints**
- Added top-level commented `discovery_hints:` block per LD17 (the main
  template had no such key previously).
- Added commented `p2p:` block with share_presets (internal/external),
  relay config (url + token_env), auto_receive, signing_key_path,
  require_signature — all opt-in, all default off.

**CLAUDE.md v3 path staleness (minimal surgical fixes, not a rewrite)**
- Frontmatter version 3.0.0 → 3.1.0 (consistency with plugin.json).
- "Read Before Speaking" sequence:
  - item 2: `_kernel/now.json` annotated as computed projection via scripts/project.py
  - dropped `bundles/*/tasks.md` entry (v3 uses tasks.json)
  - new item 5: `_kernel/tasks.json` (v3 JSON task queue)
  - item 7: `bundles/` → `{walnut}/{bundle}/context.manifest.yaml` with
    note that v3 bundles live flat at walnut root, not under `bundles/`
- Skills section: "Fifteen Skills" → "Eighteen Skills", added /alive:share,
  /alive:receive, /alive:relay entries. All other skill lines unchanged.

**Full-tree grep sweep (findings + fixes)**

grep -rn "stackwalnuts" plugins/alive/ → ZERO matches. Clean.

grep -rn "03_Inputs" plugins/alive/ → 5 matches, all intentional:
  - skills/system-upgrade/SKILL.md:33,336,337,378 — v2→v3 migration docs (OK)
  - skills/receive/SKILL.md:89 — documents old-path rejection (OK)
  - hooks/scripts/alive-session-new.sh:346 — v2-world detector (OK)
  - hooks/scripts/alive-session-new.sh:357 — "what's new" upgrade message (OK)

grep -rn "tasks\.md" plugins/alive/ → classified:
  - ACTIVE V3 CODE PATHS (FIXED):
    - skills/save/SKILL.md:25 — "Do NOT read bundles/*/tasks.md" reworded
      to point at tasks.json + tasks.py
    - templates/world/agents.md:26 — bundle state description reworded
      to v3 (flat layout + tasks.json)
    - skills/world/setup.md:485-518 — walnut scaffolder was creating
      _kernel/_generated/ + bundles/ + tasks.md template. Now creates
      flat _kernel/ with only key.md/log.md/insights.md; tasks.json,
      completed.json and now.json created lazily by tasks.py/project.py.
    - skills/world/setup.md:600-604 — reference table updated: now.json
      path, tasks.json row added, bundles path shown as flat.
    - skills/load-context/SKILL.md:144 — deep load path updated to
      {walnut}/{name}/context.manifest.yaml (v3 flat) with v2 fallback note
    - skills/search-world/SKILL.md:72 — example output "bundles/research/"
      → "research/" (v3 flat)
  - MIGRATION DOCS / V2 FALLBACK (OK, kept):
    - skills/system-upgrade/SKILL.md — documents v2→v3 conversion
    - skills/receive/migration.md — documents receive-time v2 migration
    - skills/create-walnut/migrate.md — documents legacy import
    - scripts/alive-p2p.py — migrate_v2_layout() helper
    - scripts/tasks.py:82-84 — warns when v2 tasks.md detected
    - hooks/scripts/alive-context-watch.sh:186 — v2 file-change detector
    - hooks/scripts/alive-session-new.sh:342 — v2-world detector
    - tests/walnut_builder.py, tests/test_migrate.py,
      tests/test_receive_migration.py, tests/test_p2p_v2_migration.py,
      tests/test_walnut_builder.py — v2 fixture builders + migration tests
    - skills/bundle/SKILL.md:93 — "Do NOT create tasks.md" negative guidance
    - skills/system-cleanup/SKILL.md — v2-remnant detection
    - rules/bundles.md:403 — legacy comparison table
    - rules/squirrels.md:347 — v3 contract note
    - templates/subagent-brief.md:43 — backward-compat note

grep -rn "bundles/" plugins/alive/ — remaining matches are in rules,
tests, migration code, legacy fallback scanners, or create-walnut/migrate.md
(all correctly contextualised). Active v3 code paths cleaned.

grep -rn "_generated" plugins/alive/ — remaining matches are in migration/
upgrade/backward-compat contexts only. rules/world.md:88 explicitly
documents fallback chain. Clean.

**Test suite**: 294 tests passing (stdlib unittest, unchanged count from .12).
**test_hooks_json.py**: 10 tests passing — LD15 description regex, LD16
relay-check registration on startup + resume, structure integrity all green.

**Self-review against acceptance criteria**:
- [x] plugin.json version = 3.1.0
- [x] CLAUDE.md "Read Before Speaking" no longer references bundles/*/tasks.md
- [x] CLAUDE.md bundle references are flat-layout aware
- [x] Zero `stackwalnuts` references in active plugins/alive/** files
- [x] `bundles/` references only in migration/backward-compat paths
- [x] `03_Inputs` references only in migration/detector code
- [x] `_generated` references only in migration/fallback chains
- [x] `tasks.md` references only in migration code + v2 detectors
- [x] plugin.json has no path fields (vacuously relative)
- [x] test suite passes (294/294)
- [x] test_hooks_json.py passes (10/10)

Task: fn-7-7cw.13

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants