---
phase: 35-provenance-foundation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - plugins/autonomous-loop/scripts/provenance-lib.sh
  - plugins/autonomous-loop/tests/test-provenance.sh
autonomous: true
requirements:
  - PROV-01
  - PROV-02
  - PROV-03
  - PROV-04

must_haves:
  truths:
    - "PROV-01: emit_provenance writes one valid JSONL line per call to <state_dir>/provenance.jsonl with all schema-required fields"
    - "PROV-02: emit_provenance also writes the same line to ~/.claude/loops/global-provenance.jsonl; rotate_global_provenance moves oldest 5000 lines to global-provenance.<unixts>.jsonl.gz when file exceeds 10000 lines"
    - "PROV-03: schema_version=1 enforced; required fields ts_iso, ts_us, event, loop_id, agent, session_id, cwd_observed, cwd_bound, registry_generation, owner_pid_before, owner_pid_after, reason, decision, schema_version present in every line"
    - "PROV-04: emit_provenance is callable as a primitive — caller controls when state mutations happen; lib does NOT touch registry.json or heartbeat.json (decoupling enforced by absence of imports/sources)"
    - "test-provenance.sh exists and exits 0 with all 5 test cases passing: happy path, concurrent writes (10 parallel processes), rotation at 10k, missing state_dir graceful degradation, schema validation"
    - "All scripts use set -euo pipefail and follow autonomous-loop CLAUDE.md conventions (Pitfall #4 atomic mktemp+mv pattern)"
  artifacts:
    - path: "plugins/autonomous-loop/scripts/provenance-lib.sh"
      provides: "emit_provenance, rotate_global_provenance functions"
      contains: "schema_version=1"
    - path: "plugins/autonomous-loop/tests/test-provenance.sh"
      provides: "5-case test suite for provenance-lib.sh"
      contains: "test_concurrent_writes"
  key_links:
    - "https://github.com/anthropics/claude-code/issues/47018 — context for why hook-stdin binding (consumed in P36) is needed"
---

<objective>
Introduce an append-only provenance ledger primitive (`provenance-lib.sh`) that all subsequent v4.10.0 phases (36, 37, 38) will consume. Every state mutation in the autonomous-loop plugin will write a typed event to `<state_dir>/provenance.jsonl` with a mirror to `~/.claude/loops/global-provenance.jsonl` (rotated at 10k lines). No registry, heartbeat, or hook code is changed in this plan — only the new shared library plus tests.

Purpose: Create the auditing primitive other phases consume.
Output: One new library script + one new test script. No behavioral change yet.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/REQUIREMENTS.md
@.planning/phases/35-provenance-foundation/35-CONTEXT.md
@plugins/autonomous-loop/CLAUDE.md
</context>

<tasks>

<task type="auto">
  <name>Task 1: Author provenance-lib.sh</name>
  <files>plugins/autonomous-loop/scripts/provenance-lib.sh</files>
  <read_first>
    - plugins/autonomous-loop/scripts/registry-lib.sh
    - plugins/autonomous-loop/scripts/state-lib.sh
    - plugins/autonomous-loop/scripts/notifications-lib.sh
    - plugins/autonomous-loop/CLAUDE.md
  </read_first>
  <action>
Create `plugins/autonomous-loop/scripts/provenance-lib.sh`. Mirror the style of existing lib scripts (`set -euo pipefail`, function-level docstrings, exit code conventions, fd-9 flock for cross-process serialization).

**Required exports:**

1. `emit_provenance <loop_id> <event> [field=value ...]`
   - Resolves `state_dir` from registry via `read_registry_entry "$loop_id"` (graceful degrade: if registry has no entry, write only to global mirror).
   - Computes `ts_iso` (ISO 8601 UTC with millisecond precision: `date -u +"%Y-%m-%dT%H:%M:%S.000Z"`; use Python or %3N if available, fall back to ".000Z" suffix).
   - Computes `ts_us` via `now_us` from state-lib.sh OR inline `python3 -c "import time; print(int(time.time()*1_000_000))"`.
   - Auto-detects `agent` from `${_PROV_AGENT:-$(basename "${BASH_SOURCE[1]:-$0}")}` (caller can override via `_PROV_AGENT`).
   - Validates `event` against allow-list (strings only, no validation against the enum — keep it open for v2; just ensure it's non-empty and shell-safe).
   - Constructs JSON line via `jq -n --arg k v ... '{...}'` with all schema fields. Missing optional fields default to `null` (jq emits as JSON null).
   - Writes to TWO destinations atomically:
     - `<state_dir>/provenance.jsonl` (append; if state_dir doesn't exist, skip silently — not an error)
     - `~/.claude/loops/global-provenance.jsonl` (append, with flock fd 9 on the file itself — serializes parallel writers)
   - Append uses `>>` after acquiring lock; for state_dir append, use `flock -x` on the file too (avoid torn lines under concurrent writes from different agents).
   - Returns 0 always (graceful — provenance failure must NEVER block a hook or tool call).

2. `rotate_global_provenance`
   - Idempotent. Reads line count of `~/.claude/loops/global-provenance.jsonl`.
   - If count > 10000: with flock, move oldest 5000 lines to `~/.claude/loops/global-provenance.<unixts>.jsonl`, gzip the archive, leave the newest 5000 in place.
   - Use `head -n 5000` + `tail -n +5001` pattern via mktemp + mv (atomic).
   - Returns 0 on success, 1 only on filesystem errors.

**Schema enforcement:**

Every line MUST include these keys (use `jq -n` with `--arg`/`--argjson` for each):

```
ts_iso, ts_us, event, loop_id, agent, session_id,
cwd_observed, cwd_bound, registry_generation,
owner_pid_before, owner_pid_after, reason, decision,
schema_version
```

`schema_version` is always integer `1`. Caller-provided fields that aren't in the schema are silently ignored (forward-compat; future schemas can promote them to first-class).

**Atomicity pattern (from CLAUDE.md Pitfall #4):**

```bash
LOCK_FD=9
exec 9>>"$global_path.lock"  # lock file co-located with target
flock -x $LOCK_FD
echo "$json_line" >> "$global_path"
flock -u $LOCK_FD
```

Per-loop ledger uses the same pattern with `state_dir/.provenance.lock`.

**Non-goals for this plan:**

- Do NOT modify registry-lib.sh, state-lib.sh, hook-install-lib.sh, waker.sh, heartbeat-tick.sh, or any skill SKILL.md.
- Do NOT add `emit_provenance` calls into existing code paths — that's Phase 36/37/38's job.
- Do NOT touch `~/.claude/settings.json`.
  </action>
  </task>

<task type="auto">
  <name>Task 2: Author test-provenance.sh</name>
  <files>plugins/autonomous-loop/tests/test-provenance.sh</files>
  <read_first>
    - plugins/autonomous-loop/tests/test-registry-write.sh
    - plugins/autonomous-loop/tests/test-heartbeat-hook.sh
    - plugins/autonomous-loop/scripts/provenance-lib.sh
  </read_first>
  <action>
Create `plugins/autonomous-loop/tests/test-provenance.sh`. Match the style of existing test scripts (bash-only, no bats; per-test isolation via `mktemp -d`; HOME override for global mirror).

**Five test cases:**

1. `test_happy_path` — single emit_provenance call writes one valid JSONL line to both per-loop ledger and global mirror; both lines parse as JSON; both have schema_version=1; all required fields present.

2. `test_concurrent_writes` — fork 10 background processes each calling emit_provenance with a unique reason; wait for all; assert per-loop ledger has exactly 10 lines AND every line parses as JSON (no torn lines from race) AND every reason value is present exactly once.

3. `test_rotation` — pre-populate global mirror with 10001 lines (use `seq 1 10001 | jq -R '. | {n: tonumber, schema_version: 1}'` style); call rotate_global_provenance; assert: archived file `global-provenance.*.jsonl.gz` exists with 5000 lines (after gunzip); current global-provenance.jsonl has 5001 lines.

4. `test_missing_state_dir` — call emit_provenance with a loop_id that has no registry entry (or pointing to a deleted state_dir); assert: function returns 0 (graceful), global mirror still has the line, per-loop ledger was NOT created.

5. `test_schema_validation` — call emit_provenance with event="bind_first" loop_id="aaaaaaaaaaaa" and various optional fields; jq-validate that the emitted line has every required field key (use `jq 'has("ts_iso") and has("schema_version") and ...'`).

**Test harness conventions:**

- Each test sets `TEST_HOME=$(mktemp -d)` and `export HOME="$TEST_HOME"`.
- Source provenance-lib.sh once at the top; functions reset state via `mkdir -p "$HOME/.claude/loops"`.
- Use `assert_eq`, `assert_file_lines`, `assert_jq_truthy` helpers (define inline; don't introduce a framework).
- Print `PASS test_name` or `FAIL test_name: <reason>`; exit 1 if any FAIL.
- Final line: `All tests passed.` on success.

Make the script executable (`chmod +x`).
</action>
</task>

<task type="auto">
  <name>Task 3: Run tests; commit on green</name>
  <files></files>
  <read_first></read_first>
  <action>
1. Make both new scripts executable: `chmod +x plugins/autonomous-loop/scripts/provenance-lib.sh plugins/autonomous-loop/tests/test-provenance.sh`.
2. Run shellcheck on both: `shellcheck plugins/autonomous-loop/scripts/provenance-lib.sh plugins/autonomous-loop/tests/test-provenance.sh` — must exit 0 (warnings allowed, errors fail the plan).
3. Run the test script: `bash plugins/autonomous-loop/tests/test-provenance.sh` — must exit 0 with `All tests passed.` on the last line.
4. Run plugin validation: `bun scripts/validate-plugins.mjs` — must succeed.
5. Stage only the two new files: `git add plugins/autonomous-loop/scripts/provenance-lib.sh plugins/autonomous-loop/tests/test-provenance.sh`.
6. Commit with message:
```
feat(autonomous-loop): add provenance-lib.sh foundation (PROV-01..04)

Append-only schema-versioned ledger primitive that subsequent v4.10.0
phases consume. Per-loop file at <state_dir>/provenance.jsonl plus
10k-rotated global mirror at ~/.claude/loops/global-provenance.jsonl.

- emit_provenance(loop_id, event, fields...) — atomic dual-write with
  flock fd 9; resolves state_dir from registry; graceful degrade if
  registry has no entry (writes only to global mirror).
- rotate_global_provenance — idempotent; archives oldest 5000 lines to
  gzipped <ts>.jsonl.gz when global mirror exceeds 10k.
- schema_version=1 with required fields: ts_iso, ts_us, event, loop_id,
  agent, session_id, cwd_observed, cwd_bound, registry_generation,
  owner_pid_before, owner_pid_after, reason, decision, schema_version.
- Per-line atomic via mktemp+mv-style flock pattern (CLAUDE.md Pitfall #4).
- Returns 0 on all paths so callers in hooks/wakers never block.

Tests: 5 cases (happy path, 10-process concurrent writes, rotation,
missing state_dir, schema validation) — all pass under bash + shellcheck.

No existing code paths modified yet; emit_provenance calls land in
Phase 36 (binding) / Phase 37 (waker) / Phase 38 (doctor).

Refs: PROV-01, PROV-02, PROV-03, PROV-04

```
7. If shellcheck or tests fail: do NOT commit. Surface the failure with file/line and request user input on whether to adjust the plan or re-attempt.
  </action>
</task>

</tasks>

<verification>
After all tasks complete:

1. `bash plugins/autonomous-loop/tests/test-provenance.sh` exits 0
2. `shellcheck` on both new files exits 0
3. `git log -1 --oneline` shows the new commit
4. `git diff HEAD~1 HEAD --stat` shows exactly 2 files changed (provenance-lib.sh + test-provenance.sh) with reasonable line counts (~150-250 LOC for lib, ~150-250 LOC for tests)
5. `jq empty <(echo '<sample line from a test run>')` succeeds — schema is valid JSON
</verification>
```
