# Plan: Generic Lifecycle Hooks for Commit-Scoped `.swival` Sync

## Objective

Add a generic lifecycle-hook mechanism that lets Swival run a user-defined command at startup and exit, with enough Git and run metadata to support commit-scoped Hugging Face Bucket workflows without hard-coding any HF-specific storage logic into Swival core.

## Why This Is Needed

- Swival currently persists important state only to the local `.swival/` tree:
  - history in [`swival/agent.py`](/Users/j/src/swival/swival/agent.py#L401)
  - memory in [`swival/agent.py`](/Users/j/src/swival/swival/agent.py#L392)
  - continue files in [`swival/continue_here.py`](/Users/j/src/swival/swival/continue_here.py#L42)
  - large tool output in [`swival/tools.py`](/Users/j/src/swival/swival/tools.py#L1932)
- Swival already has two nearby external-command precedents:
  - config-resolved command strings such as `reviewer` and `llm_filter` in [`swival/config.py`](/Users/j/src/swival/swival/config.py#L288)
  - shell-split subprocess execution for reviewers in [`swival/agent.py`](/Users/j/src/swival/swival/agent.py#L2586)
- We discussed a Hugging Face Bucket workflow where `.swival/` is synced to remote object storage, possibly with one logical namespace per `HEAD` commit. The right core change is not “teach Swival about Buckets”; it is “teach Swival how to call a user hook with deterministic project metadata.”

## Locked Design Decisions

- The feature is **generic**, not Hugging Face-specific.
- Swival runs **one configured command** for both lifecycle events:
  - `startup`
  - `exit`
- The command is invoked as:
  - `<configured command> startup <base_dir>`
  - `<configured command> exit <base_dir>`
- The hook receives:
  - inherited environment
  - additional `SWIVAL_*` environment variables
- `cwd` is always `base_dir`.
- `startup` runs **after config and Git discovery**, but **before** Swival reads `.swival/memory`, `.swival/continue.md`, or other resumable local state.
- `exit` runs in a `finally` path **after** Swival writes history/report/continue artifacts.
- Phase 1 is **CLI-focused**:
  - one-shot mode
  - REPL mode
- Phase 1 also includes **Session parity**:
  - `Session._setup()` runs `startup` before `build_system_prompt()`
  - `Session.run()` / `Session.ask()` run `exit` after local artifacts are written
- Phase 1 explicitly excludes:
  - `--reviewer-mode`
  - `--serve` A2A server mode
  - first-class HF SDK integration
  - built-in bucket GC or bucket naming policy
- The initial behavior is **fail-open by default**. A failing hook should not break normal Swival runs unless the user opts into fail-closed behavior.
- Phase 1 contract is **environment-variable-first**. A structured stdin manifest is optional and deferred unless it falls out trivially during implementation.

## Non-Goals

- No new storage backend abstraction in core.
- No direct `huggingface_hub` dependency in Swival.
- No support for `hf://` paths in `read_file`, `write_file`, or other built-in tools.
- No built-in “delete all project buckets” command in core.
- No per-turn hook execution. This is process/session lifecycle only.

## Proposed User Surface

### CLI / Config / Session API

Add these settings:

- `--lifecycle-command COMMAND`
- `--lifecycle-timeout SECONDS`
- `--lifecycle-fail-closed`
- `--no-lifecycle`

Matching config keys:

- `lifecycle_command = "./scripts/swival-artifacts"`
- `lifecycle_timeout = 300`
- `lifecycle_fail_closed = false`
- `no_lifecycle = false`

Matching `Session` kwargs:

- `lifecycle_command: str | None = None`
- `lifecycle_timeout: int = 300`
- `lifecycle_fail_closed: bool = False`
- `lifecycle_enabled: bool = True`

Notes:

- Keep naming parallel to existing config style.
- `COMMAND` follows the same shell-split / path-like resolution rules as `reviewer` and `llm_filter`.
- `--no-lifecycle` is the kill switch for nested or automated invocations.

## Execution Contract

### Command Resolution

Reuse existing config behavior:

- Parse command strings with `shlex.split`.
- Resolve only path-like first tokens against the config file directory, like [`swival/config.py`](/Users/j/src/swival/swival/config.py#L288).
- Validate executability at startup using logic parallel to [`_validate_external_command()`](/Users/j/src/swival/swival/agent.py#L3955).

### Process Invocation

The hook runner should:

- construct `argv = shlex.split(command) + [event, base_dir]`
- run with `cwd=base_dir`
- capture `stdout` and `stderr`
- return `(exit_code, stdout, stderr, duration)`

Recommended runtime behavior:

- `stdout` is ignored by default except for verbose diagnostics and report logging
- `stderr` is surfaced in verbose mode on failure
- timeout returns a lifecycle error result but does not raise
- no `shell=True`; lifecycle commands follow the same subprocess model as `reviewer` and `llm_filter`

### Failure Semantics

- Default: fail-open
  - nonzero exit, timeout, or spawn failure logs a warning and continues
- Optional: fail-closed
  - same failures raise `AgentError` before the run proceeds (`startup`) or before process exit completes (`exit`)

## Git Metadata Contract

Add a helper module, e.g. [`swival/lifecycle.py`](/Users/j/src/swival/swival/lifecycle.py), that discovers project identity once and reuses it for both events.

### Metadata to Compute

- `repo_root`
  - from `git rev-parse --show-toplevel`
- `head_sha`
  - from `git rev-parse HEAD`
- `is_dirty`
  - from `git diff-index --quiet HEAD --` or equivalent
- `origin_url`
  - from `git config --get remote.origin.url`
- `normalized_remote`
  - canonicalized so SSH/HTTPS forms collapse to the same ID where possible
- `project_rel`
  - relative path from `repo_root` to `base_dir`
- `repo_hash`
  - deterministic hash of normalized repo identity
- `project_hash`
  - deterministic hash of normalized repo identity + `project_rel`

### Behavior Outside Git

If `base_dir` is not inside a Git repo:

- set `SWIVAL_GIT_PRESENT=0`
- leave Git-specific fields empty in env

Do not fail the run just because Git metadata is unavailable.

## Hook Environment Variables

Populate these env vars when available:

- `SWIVAL_HOOK_EVENT`
- `SWIVAL_BASE_DIR`
- `SWIVAL_SWIVAL_DIR`
- `SWIVAL_PROVIDER`
- `SWIVAL_MODEL`
- `SWIVAL_GIT_PRESENT`
- `SWIVAL_REPO_ROOT`
- `SWIVAL_PROJECT_REL`
- `SWIVAL_GIT_HEAD`
- `SWIVAL_GIT_DIRTY`
- `SWIVAL_GIT_REMOTE`
- `SWIVAL_REPO_HASH`
- `SWIVAL_PROJECT_HASH`
- `SWIVAL_REPORT`
- `SWIVAL_OUTCOME`
- `SWIVAL_EXIT_CODE`

Rules:

- `SWIVAL_REPORT`, `SWIVAL_OUTCOME`, and `SWIVAL_EXIT_CODE` are `exit`-only.
- `SWIVAL_MODEL` is populated only after provider resolution succeeds.
- Empty/unknown values should be omitted or set consistently; do not mix styles across fields.

## Optional JSON Manifest

Environment variables are the phase 1 contract. A structured stdin manifest is useful, but not required to ship the feature.

If it falls out trivially during implementation, it can be included as an additional convenience. If not, defer it to phase 2.

If implemented later, it should:

- be passed on `stdin`
- be versioned
- mirror the `SWIVAL_*` environment variables rather than adding new semantics
- stay small and descriptive, not conversational-state-bearing

Illustrative shape:

```json
{
  "version": 1,
  "event": "startup",
  "base_dir": "/repo/services/api",
  "swival_dir": "/repo/services/api/.swival",
  "provider": "openrouter",
  "model": "z-ai/glm-5",
  "git": {
    "present": true,
    "repo_root": "/repo",
    "project_rel": "services/api",
    "head": "9f3c1e7a...",
    "dirty": false,
    "origin_url": "git@github.com:org/repo.git",
    "normalized_remote": "github.com/org/repo",
    "repo_hash": "a1b2c3d4e5f6",
    "project_hash": "deadbeefcafe"
  },
  "result": null
}
```

If present, `exit` would add:

- `result.outcome`
- `result.exit_code`
- `result.report_path`

## Runtime Ordering

### Startup

The startup hook must run after:

- config loading
- CLI/config precedence merge
- provider/model resolution
- Git metadata discovery
- local `.swival/` directory creation, if needed

It must run before:

- memory loading into the system prompt
- continue-file resume checks
- REPL startup that depends on local `.swival/` content

This ordering is the whole point of the feature: a startup hook must be able to hydrate `.swival/` before Swival consumes it.

### Exit

The exit hook must run after:

- final answer is produced or run outcome is known
- `.swival/HISTORY.md` append finishes
- report file is written, if configured
- continue file is written on interruption paths

This requires an outer `try/finally` integration point in the main CLI path.

## HF Buckets Reference Workflow

The docs should include one opinionated example script, but Swival core must remain generic.

### Recommended Reference Layout

The reference example should use:

- one stable bucket per repo identity
- one project-specific prefix inside that bucket
- one commit-specific prefix below the project prefix

Example naming policy for the sample script:

- bucket name: `swival-<repo_hash>`
- prefix: `<project_rel>/heads/<head_sha>/`

The sample hook can then sync local `.swival/` to:

```text
hf://buckets/<namespace>/swival-<repo_hash>/<project_rel>/heads/<head_sha>/
```

This keeps bucket count bounded, isolates monorepo subprojects via `project_rel`, and still gives commit-scoped remote state.

- startup: remote bucket -> local `.swival/`
- exit: local `.swival/` -> remote bucket

with explicit include/exclude patterns.

### What to Sync

Reference script should sync:

- `memory/**`
- `continue.md`
- `reports/**` or named report files

Reference script should not sync:

- `cache.db`
- `trash/**`
- lock files
- transient `cmd_output_*` by default

### Project-Wide Purge Strategy

Because the reference layout uses one stable repo bucket, project- and repo-wide cleanup is much simpler.

Do not implement this in Swival core in Phase 1. Instead, document a companion script strategy:

1. derive `repo_hash`
2. delete the whole repo bucket when you want to purge all Swival state for that repo
3. or remove only `<project_rel>/` when you want to purge one subproject in a monorepo

Optional hardening for the sample script:

- maintain a metadata file under the stable bucket if you want richer cleanup/reporting later

This keeps cleanup simple without pushing provider-specific GC logic into Swival itself.

## Code Changes

### New Module

- [`swival/lifecycle.py`](/Users/j/src/swival/swival/lifecycle.py)
  - Git metadata discovery
  - environment payload construction
  - hook subprocess execution
  - helper to format diagnostics/report events

### Config

- [`swival/config.py`](/Users/j/src/swival/swival/config.py)
  - add schema/defaults for lifecycle settings
  - resolve `lifecycle_command` like `reviewer`
  - thread settings into `args_to_session_kwargs()` / `config_to_session_kwargs()`
  - update generated config template

### Session API

- [`swival/session.py`](/Users/j/src/swival/swival/session.py)
  - add constructor kwargs
  - store resolved lifecycle settings
  - run startup hook before `build_system_prompt()`
  - run exit hook after `run()` / `ask()` local writes

### Main CLI Path

- [`swival/agent.py`](/Users/j/src/swival/swival/agent.py)
  - argparse flags
  - startup validation of lifecycle command
  - startup hook call before `.swival` state is consumed
  - exit hook call in outer `finally`
  - skip hooks in `--reviewer-mode` and `--serve`

### Reporting

- [`swival/report.py`](/Users/j/src/swival/swival/report.py)
  - record lifecycle events or summary metadata
  - include hook failures/durations when hooks are configured

### Docs

- [`docs.md/customization.md`](/Users/j/src/swival/docs.md/customization.md)
- [`docs.md/usage.md`](/Users/j/src/swival/docs.md/usage.md)
- likely new section in [`docs.md/reports.md`](/Users/j/src/swival/docs.md/reports.md)

## Interaction With Existing Features

### Reviewer / Self-Review

- Do not run lifecycle hooks in `--reviewer-mode`.
- Do not mirror lifecycle settings into self-review subprocesses.
- This avoids nested remote sync and surprising side effects.

### REPL

- Run `startup` once when the REPL process starts.
- Run `exit` once on REPL termination.
- Do not run hooks on `/clear`, `/continue`, or per prompt.

### A2A Serve

- Exclude from Phase 1.
- Session-per-context hooks would need different semantics and can be addressed later.

### AgentFS / Sandbox

- Hooks should run in the same process context as Swival.
- Document that a startup hook using external CLIs may itself need appropriate command/network access in the user’s environment.

## Testing Plan

### Unit Tests

- Config parsing:
  - lifecycle keys accepted
  - command resolution matches `reviewer` behavior
  - `args_to_session_kwargs()` threads values correctly
- Git metadata helper:
  - inside repo with origin
  - inside repo without origin
  - outside repo
  - nested project path computes `project_rel`
- Optional manifest builder, if implemented:
  - startup shape
  - exit shape
  - optional fields omitted correctly

### Integration Tests

- Startup ordering:
  - hook writes `.swival/memory/MEMORY.md`
  - later startup code sees and loads it
- Continue hydration:
  - hook writes `.swival/continue.md`
  - run observes it
- Exit ordering:
  - hook sees report path and existing report file
- Failure handling:
  - fail-open warning path
  - fail-closed abort path
- REPL:
  - hook runs once on start and once on exit
- Skip modes:
  - reviewer mode does not invoke hooks
  - serve mode does not invoke hooks

### Regression Checks

- Existing reviewer command behavior unchanged
- Existing config path resolution unchanged
- Normal runs without lifecycle settings behave exactly as before

## Documentation Deliverables

1. Generic lifecycle-hook docs:
   - command format
   - env vars
   - optional stdin manifest shape, if implemented later
   - failure semantics
2. HF Buckets example:
   - sample hook script
   - include/exclude recommendations
   - stable bucket + project/commit prefix naming example
3. Project purge example:
   - delete repo bucket or remove project prefix
   - optional metadata-file hardening

## Risks and Mitigations

- Risk: startup hook runs too late and misses memory/continue hydration.
  - Mitigation: add explicit ordering tests around startup consumption points.
- Risk: nested subprocesses trigger remote sync loops.
  - Mitigation: disable hooks in reviewer mode and do not mirror them into self-review.
- Risk: stable-bucket layout still accumulates stale commit prefixes over time.
  - Mitigation: keep commit scoping in prefixes and document repo/project purge strategies.
- Risk: lifecycle command becomes a second ad hoc integration system.
  - Mitigation: keep one command, two events, stable env contract, no special-case HF code.
- Risk: users need structured input beyond env vars.
  - Mitigation: keep env vars as the phase 1 contract and add a versioned stdin manifest only if the implementation cost stays low.

## Implementation Phases

### Phase 1: Generic Hook Infrastructure

1. Add config / CLI / Session surfaces.
2. Implement lifecycle runner + Git metadata helper.
3. Wire startup and exit hooks into CLI flow before `build_system_prompt()` and in an outer `finally`.
4. Wire startup and exit hooks into `Session` with the same ordering guarantees.
5. Add diagnostics and report metadata.
6. Document contract and sample HF script.

### Phase 2: A2A Parity

1. Define lifecycle semantics for long-lived sessions and A2A context sessions.
2. Add parity tests.

### Phase 3: Optional Structured Manifest + HF Helper Scripts

1. If still useful, add a versioned stdin manifest that mirrors the env contract.
2. Add an example `scripts/swival-hfcd` helper outside core package or in docs examples.
3. Include sample sync logic.
4. Include repo/project purge helper for the stable-bucket layout.

## Acceptance Criteria

- [ ] A user can configure one lifecycle command and have Swival invoke it on startup and exit.
- [ ] The hook receives stable Git/project metadata sufficient to derive commit-scoped remote storage paths.
- [ ] Startup hooks can hydrate `.swival/` before memory/continue state is consumed.
- [ ] Exit hooks can see report/outcome metadata after local artifacts are written.
- [ ] Hook configuration follows existing Swival command-resolution conventions.
- [ ] `Session` has the same lifecycle-hook semantics as CLI runs.
- [ ] Runs without lifecycle configuration remain unchanged.
- [ ] Docs include a complete HF Bucket example using a stable repo bucket plus project/commit prefixes.
- [ ] Docs include a repo/project purge strategy for that layout.
