docs(plan): v1.3 phase 1 plans (9 candidates, 3 waves, 1 acceptance gate)

9 ACCEPT candidates from the v1.3 IDEATE pass (REQ-INB-28..34,
36, 37) decompose into 3 waves of 8 PRs / 8 commits:

Wave 1 (P0 closures):
- T1.1 (1 PR, 2 files): fix(docs+scripts) — close cto-inboxos
  owner drift in docs/gitea-actions.md:95 +
  scripts/setup_gitea_secrets.sh:4,14,18,28 (REQ-INB-28 +
  REQ-INB-37, same P0 class as v1.1 sweep commits 7c20d5d +
  d530921; also closes v1.1 P1-2 carryover)
- T1.2 (1 PR, 1 file): ci(ci-pr) — wire 6 v1.1 offline tests
  into ci-pr fast-tests job (REQ-INB-34; test_papra_link_parity
  uses continue-on-error per v1.0/v1.1 deferred-runtime
  precedent)

Wave 2 (doc-hygiene, 5 PRs):
- T2.1 (XS): docs(runbook §footer) — next-review pointer
  v1.1 → v1.3 (REQ-INB-29)
- T2.2 (XS): docs(runbook §4.5) — alerting forward-pointer
  v1.2 → v1.3 (REQ-INB-30)
- T2.3 (XS): docs(papra-write-policy) — test count 13 → 15
  (REQ-INB-31)
- T2.4 (S): docs(extraction-contract) — refresh 4 stale
  pipelines/common.py: line refs to post-v1.1 positions
  (REQ-INB-32)
- T2.5 (XS): docs(runbook §6) — renumber 6.1 → 6.2 → 6.3 →
  6.4 (REQ-INB-36, closes v1.1 P1-1)

Wave 3 (env-file drift, 1 PR):
- T3.1 (S): chore(env) — add 7 missing workflow tunables
  to .env.example (REQ-INB-33: OLLAMA_API_KEY,
  INBOXOS_LLM_BACKEND, PAPRA_DRY_RUN, PAPRA_BODY_WRITE_MODULES,
  PAPRA_METADATA_ONLY_MODULES, MBOX_BUCKET, OPENCODE_NPM_VERSION)

3 DEFER candidates (REQ-INB-35 .gitignore opencode.json,
REQ-INB-38 apply_schemas.py papra_index --check, REQ-INB-39
test_papra_link_parity imapclient shim) are filed for v1.4+
in .ciagent/inboxos/ideation/v1.3-candidates.md §9.

Total: 8 PRs / 8 commits / 9 unique file touches. Net diff
≈ 30-50 lines. 0 new code, 0 new tests, 0 schema changes,
0 new dependencies. v1.3 is the smallest source-tree delta
since v1.0 close.

---ci---
project: inboxos
phase: 1
milestone: v1.3
status: plan
waves:
  wave_1:
    name: "P0 closures (cto-inboxos + CI test wiring)"
    tasks: [T1.1, T1.2]
    prs: 2
    files: 3
  wave_2:
    name: "Doc-hygiene sweep (5 candidates)"
    tasks: [T2.1, T2.2, T2.3, T2.4, T2.5]
    prs: 5
    files: 3
  wave_3:
    name: "Env-file drift closure"
    tasks: [T3.1]
    prs: 1
    files: 1
totals:
  tasks: 8
  prs: 8
  commits: 8
  unique_files: 9
  new_code: 0
  new_tests: 0
  schema_changes: 0
  new_dependencies: 0
candidates_accepted: [REQ-INB-28, REQ-INB-29, REQ-INB-30, REQ-INB-31, REQ-INB-32, REQ-INB-33, REQ-INB-34, REQ-INB-36, REQ-INB-37]
candidates_deferred: [REQ-INB-35, REQ-INB-38, REQ-INB-39]
candidates_rejected: []
backlog_closures:
  v1_1_P1_1: REQ-INB-36
  v1_1_P1_2: REQ-INB-37
---/ci---
This commit is contained in:
2026-06-08 23:20:57 +00:00
parent d2f3bd561b
commit bc7280f239
+334
View File
@@ -0,0 +1,334 @@
# InboxOS v1.3 — Phase 1 Closure Plan (Codebase-Drift Sweep)
**Project:** inboxos
**Milestone:** v1.3
**Phase:** 1 — v1.3 Ideation → Plan closure (9 ACCEPT candidates, 3 DEFER)
**Status:** plan (spec complete; execution pending CPO promotion of candidates to real requirements)
**Plan owner:** ci-planner
> The 9 ACCEPT candidates (REQ-INB-28 through REQ-INB-34, REQ-INB-36, REQ-INB-37) originate from the codebase-drift IDEATE pass recorded in `.ciagent/inboxos/ideation/v1.3-candidates.md`. They are **[CANDIDATE]** until the CPO authors the matching entries in `docs/requirements.md` (CPO-owned, per board mandate `b49cfb7`).
>
> This plan covers the 9 ACCEPT candidates and defers the 3 DEFER candidates (REQ-INB-35, REQ-INB-38, REQ-INB-39) to v1.4+. **No code lands in this plan beyond doc fixes, env-file entries, a 1-line shell-script owner default, and a CI test-wiring extension**; v1.3 is the cleanest milestone since v1.0 and the surface area is small per task. The plan is the integrator's contract: each task is a single PR (or a single pair for the cto-inboxos closure) that compiles, passes CI, and moves the project measurably toward the v1.3 acceptance gate.
---
## How to read this plan
- **Wave 1 (P0)** closes the 2 P0 candidates: the cto-inboxos owner drift that the v1.1 sweep missed (`docs/gitea-actions.md:95`, `scripts/setup_gitea_secrets.sh:4,14,18,28`) and the 6 v1.1 offline tests not wired into the `ci-pr.yml` merge gate. Both P0 candidates can land as 2 PRs (the cto-inboxos pair is 1 PR; the test wiring is 1 PR). No persona fan-out; the cto-inboxos pair is devops-engineer (gitea-actions.md is in their territory) + security-engineer (setup_gitea_secrets.sh is shell-secret territory); the test wiring is devops-engineer alone.
- **Wave 2 (doc-hygiene)** closes the 5 doc-hygiene candidates: the runbook footer "Next review" pointer (REQ-INB-29), the runbook §4.5 alerting forward-pointer (REQ-INB-30), the papra-write-policy.md test count (REQ-INB-31), the extraction-contract.md 4 stale line refs (REQ-INB-32), and the runbook §6 ordering renumber (REQ-INB-36). All 5 are XS/S effort; the lead-developer is the primary owner because the changes span 3 docs that no single persona owns end-to-end.
- **Wave 3 (env-file)** closes the .env.example 7-entry addition (REQ-INB-33) — a single S-effort task, devops-engineer primary (env contract), security-engineer review (the new entries include 3 PII-related knobs: `PAPRA_DRY_RUN`, `PAPRA_BODY_WRITE_MODULES`, `PAPRA_METADATA_ONLY_MODULES`).
- The DEFER candidates (REQ-INB-35, 38, 39) are NOT in this plan. They are filed for v1.4+ in `.ciagent/inboxos/ideation/v1.3-candidates.md §9`.
- Each task lists a `must_haves` array — the concrete artifacts a reviewer must see before the task is closed.
- **CPO-owned files (`docs/requirements.md`, `docs/roadmap.md`) are NOT edited by any task in this plan.** The plan reads from them (the REQ-INB IDs in `must_haves`) but never writes to them. The CPO is the only role authorised to promote `[CANDIDATE]` to a real requirement.
- The plan is the integrator's contract for the EXECUTE phases that follow. The Wave 1 + Wave 2 + Wave 3 commits each carry a well-formed `---ci---` block with `project: inboxos`, `phase: 1`, `milestone: v1.3`, `status: execute` / `verify` (see `.ciagent/inboxos/CI-LOG.md` for the block format).
- **Commit convention** follows the v1.1 / v1.2 pattern: each task is 1 commit (or 1 commit per file if the task touches > 5 files, per the runbook §9.1 row 5 cap). The cto-inboxos pair (REQ-INB-28 + REQ-INB-37) is 1 commit (2 files; same risk class as the v1.1 P0 sweep commits `7c20d5d` and `d530921`).
---
## Persona assignments
| Persona | Active in v1.3 | Tasks |
|---------|----------------|-------|
| lead-developer | yes (coordination + cross-cutting doc) | T2.1, T2.2, T2.3, T2.4, T2.5 |
| devops-engineer | yes (env contract, runbook, gitea-actions.md, ci-pr.yml) | T1.1, T1.2, T3.1 |
| security-engineer | yes (PII knobs review, setup_gitea_secrets.sh shell-secret) | review for T1.1, T3.1 |
| backend-engineer | yes (territory respected; no Wave 1 task) | review for T1.2 |
| data-engineer | yes (territory respected; no Wave 1 task) | not invoked in v1.3 |
| ml-engineer | yes (territory respected; no Wave 1 task) | not invoked in v1.3 |
| frontend-engineer | DEACTIVATED (no React/TSX in repo) | n/a |
Cross-persona reviews are explicit (`needs review from:`). Territory enforcement is `warn` per `.ciagent/config.json`.
---
## Wave 1 — P0 closures (cto-inboxos + CI test wiring)
> Wave 1 has 2 tasks that land as 2 PRs. The cto-inboxos pair (T1.1) is 1 PR with 2 file touches; the CI test wiring (T1.2) is 1 PR with 1 file touch. Both P0 candidates must land before the v1.3 audit; the v1.1 audit's PASS depended on the v1.1 P0 sweep, and the v1.3 audit's PASS will depend on this wave.
---
### T1.1 — Close the cto-inboxos owner drift in `docs/gitea-actions.md` + `scripts/setup_gitea_secrets.sh` (REQ-INB-28 + REQ-INB-37) **[doc + shell]**
- **Refs:** REQ-INB-28 (gitea-actions.md cto-inboxos), REQ-INB-37 (setup_gitea_secrets.sh cto-inboxos), REQ-INB-6 (operations runbook), REQ-INB-10 (self-hosted PII boundary)
- **Files:** `docs/gitea-actions.md` (line 95), `scripts/setup_gitea_secrets.sh` (lines 4, 14, 18, 28)
- **Persona:** devops-engineer (primary; gitea-actions.md is in their territory), security-engineer (review of the shell-script owner default; co-owns REQ-INB-10)
- **needs review from:** security-engineer, lead-developer
- **Diff size:** small (1 line in gitea-actions.md, 4 lines in setup_gitea_secrets.sh; total 5 lines + 2 header comment fixes)
- **Disposition note:** this is the same P0 class as the v1.1 auto-applied P0 sweep (commits `7c20d5d` and `d530921`). The v1.1 sweep missed `docs/gitea-actions.md` (last touched at Phase 4 / INB-33, before the org migration) and `scripts/setup_gitea_secrets.sh` (pre-existing at the v1.0 close, not in the v1.1 diff). Both fixes use the same env-var-resolved default pattern as the v1.1 P0 sweep (`${GITEA_OWNER:-jc-capital}`).
- **must_haves:**
- `docs/gitea-actions.md:95` (the verify-runner `curl` example) replaces the hardcoded `https://git.cloudinit.dev/api/v1/repos/cto-inboxos/inboxos/actions/runners` with `https://git.cloudinit.dev/api/v1/repos/${GITEA_OWNER:-jc-capital}/inboxos/actions/runners` (or, if the doc style prefers literal-then-env-var, replace with `https://git.cloudinit.dev/api/v1/repos/jc-capital/inboxos/actions/runners` and add a parenthetical `(or whatever value of GITEA_OWNER your org uses; the example shows the canonical jc-capital migration target)`). The implementer picks the shape; either way the literal `cto-inboxos` is gone.
- `docs/gitea-actions.md` is verified to have zero remaining `cto-inboxos` references: `grep -n cto-inboxos docs/gitea-actions.md` returns empty.
- `scripts/setup_gitea_secrets.sh:4,14` (the 2 header comments) replace `cto-inboxos` with `${GITEA_OWNER:-jc-capital}` (or `jc-capital` + a parenthetical explaining the env-var override). The header style is preserved (3-line banner with `# ===` / `# scripts/setup_gitea_secrets.sh — ...` / `# ===`).
- `scripts/setup_gitea_secrets.sh:18` (the line `GITEA_OWNER="${GITEA_OWNER:-cto-inboxos}"`) replaces `cto-inboxos` with `jc-capital` so the default is the new owner. The env-var override path (`GITEA_OWNER=...` exported by the operator) remains intact.
- `scripts/setup_gitea_secrets.sh:28` (the final header comment with the example) replaces `cto-inboxos` with `${GITEA_OWNER:-jc-capital}` (same env-var-resolved pattern as lines 4, 14).
- `scripts/setup_gitea_secrets.sh` is verified to have zero remaining `cto-inboxos` references: `grep -n cto-inboxos scripts/setup_gitea_secrets.sh` returns empty.
- The script is still `chmod +x` (verified by `ls -la scripts/setup_gitea_secrets.sh`); the `set -euo pipefail` discipline is unchanged.
- `git log` shows one commit: `fix(docs+scripts): close cto-inboxos owner drift (REQ-INB-28 + REQ-INB-37)`. The commit touches exactly 2 files (gitea-actions.md, setup_gitea_secrets.sh).
---
### T1.2 — Wire the 6 v1.1 offline tests into the `ci-pr.yml` merge gate (REQ-INB-34) **[ci]**
- **Refs:** REQ-INB-34 (CI test wiring), REQ-INB-8 (PR CI)
- **Files:** `.gitea/workflows/ci-pr.yml` (the `fast-tests` job, lines 63-99)
- **Persona:** devops-engineer (primary; the ci-pr workflow YAML is in their territory)
- **needs review from:** lead-developer
- **Diff size:** small (6 new `python pipelines/_tests/test_X_offline.py` invocations + a short heredoc-style batch wrapper, ≈ 25-35 lines added to the workflow)
- **Disposition note:** this is a P0 by the merge-gate-stays-clean standard. The 6 v1.1 offline tests all pass locally (v1.1 verify report records 30/30), but a PR could regress `papra_index_upsert`, `papra_decide`, the metrics emitter, the secret-rotation helper, the Papra-link parity, or the backup/restore contract without the merge gate noticing. The fix shape is identical to the existing 3 fast-tests at `ci-pr.yml:91-99` and the existing historical-pipeline offline fast-subset at `ci-pr.yml:106-134`.
- **must_haves:**
- `.gitea/workflows/ci-pr.yml` `fast-tests` job gains 6 new `run: python pipelines/_tests/test_X_offline.py` steps, one per test file. The 6 files are:
1. `pipelines/_tests/test_papra_decide_offline.py`
2. `pipelines/_tests/test_papra_index_offline.py`
3. `pipelines/_tests/test_papra_link_parity_offline.py` (deferred-runtime per v1.0 / v1.1 precedent; the import chain will fail in CI, so this step must `continue-on-error: true` and emit a warning, not a hard failure)
4. `pipelines/_tests/test_metrics_offline.py`
5. `pipelines/_tests/test_restore_offline.py`
6. `pipelines/_tests/test_rotate_secrets_offline.py`
- The 6 new steps are sequenced **after** the existing 3 fast-tests (gitea_request, mbox_stream, live_pipeline) and **before** the historical-pipeline fast-subset heredoc at lines 106-134. The order is documented in a 1-line comment: `# v1.1 offline tests (REQ-INB-34)`.
- The `test_papra_link_parity_offline.py` step uses `continue-on-error: true` (the deferred-runtime disposition per v1.0 P-7 / v1.1 P2-2: the test transitively imports `pipelines.live_pipeline` which requires `imapclient`, a runtime-only dep). A trailing comment notes: `deferred-runtime per v1.0 / v1.1 precedent; runtime dep imapclient not in test fixture`.
- The other 5 tests are wired as hard-fail steps (the v1.1 verify report records them as 30/30 passing locally, so they should pass in CI too; the offline-test host installs the deps via the same `pip install` block at `ci-pr.yml:78-87`).
- `pip install` step at `ci-pr.yml:78-87` is verified to include all deps required by the 5 hard-fail tests. The 6 tests' import surfaces are: `tenacity`, `boto3`, `requests`, `imapclient` (only the deferred-runtime one). `imapclient` may already be in the install list (verify) or may need to be added (the deferred-runtime test would still fail; that's the point of `continue-on-error: true`).
- The fast-tests job's timeout (the `timeout-minutes:` if any) is verified to accommodate the 5 new hard-fail tests. The existing 3 + new 5 = 8 tests; each is < 30s locally per the v1.1 verify report. A 5-minute total budget is the v1.0 / v1.1 precedent; if the workflow currently has no explicit timeout, no edit is needed.
- `.gitea/workflows/ci-pr.yml` is verified to remain valid YAML: `python -c "import yaml; yaml.safe_load(open('.gitea/workflows/ci-pr.yml'))"` returns no error.
- `git log` shows one commit: `ci(ci-pr): wire 6 v1.1 offline tests into fast-tests job (REQ-INB-34)`. The commit touches exactly 1 file (ci-pr.yml).
---
## Wave 2 — Doc-hygiene sweep (5 candidates, 1 PR each)
> Wave 2 has 5 tasks that land as 5 PRs. All 5 are XS or S effort, all 5 are pure doc edits. The lead-developer is the primary owner for all 5 because the changes span 3 docs (`docs/operations-runbook.md`, `docs/extraction-contract.md`, `docs/papra-write-policy.md`) that no single persona owns end-to-end; the lead-developer's territory is `**/*.md` and `.ciagent/inboxos/**` per `PERSONAS.md`. devops-engineer is the reviewer for the runbook and papra-write-policy edits (their territory); ml-engineer is the reviewer for the extraction-contract.md line-ref refresh (their territory).
---
### T2.1 — Update the runbook footer "Next review" pointer to v1.3 (REQ-INB-29) **[doc]**
- **Refs:** REQ-INB-29 (footer pointer), REQ-INB-6 (operations runbook)
- **Files:** `docs/operations-runbook.md` (line 823, the footer)
- **Persona:** lead-developer (primary, owns `**/*.md` territory)
- **needs review from:** devops-engineer
- **Diff size:** XS (1 line, 1 line replaced with 1 line)
- **must_haves:**
- `docs/operations-runbook.md:823` (the footer line) replaces `*End of runbook. Next review: after the v1.1 ship (2026-06-08, milestone \`milestone/v1.1-initial\`), or when the next failure mode is added to §5. Last verified against the source files cited inline; any divergence is a bug in this document — fix the doc, do not just edit around it.*` with `*End of runbook. Next review: after the v1.3 ship (2026-06-08, milestone \`milestone/v1.3-initial\`), or when the next failure mode is added to §5. Last verified against the source files cited inline; any divergence is a bug in this document — fix the doc, do not just edit around it.*`. The 1-line change is the substring `v1.1 ship` → `v1.3 ship` and `milestone/v1.1-initial` → `milestone/v1.3-initial`.
- The rest of the footer (the trailing sentence about divergence) is preserved verbatim.
- `git log` shows one commit: `docs(runbook §footer): next-review pointer v1.1 → v1.3 (REQ-INB-29)`.
---
### T2.2 — Update the runbook §4.5 alerting forward-pointer (REQ-INB-30) **[doc]**
- **Refs:** REQ-INB-30 (alerting forward-pointer), REQ-INB-6 (operations runbook), REQ-INB-23 (structured metrics)
- **Files:** `docs/operations-runbook.md` (lines 285-289, the §4.5 paragraph that mentions "alert layer is the next v1.2 deliverable")
- **Persona:** lead-developer (primary, owns `**/*.md` territory)
- **needs review from:** devops-engineer
- **Diff size:** XS (4 lines replaced with 4 lines)
- **must_haves:**
- `docs/operations-runbook.md:285-289` (the §4.5 paragraph) updates the "alert layer is the next v1.2 deliverable" claim to reflect v1.3 reality: v1.2 was a 0/1 disposition (no alerting deliverable); v1.3 is doc-hygiene, not observability. The replacement text is: "Alerting remains **out of scope** for v1.3. v1.2 closed on a 0/1 ideation disposition (no candidates accepted) per `.ciagent/inboxos/CI-LOG.md`, so the v1.1 forward-pointer to a v1.2 alerting deliverable is now stale. The alert layer is the next v1.x candidate cycle to re-evaluate; in the meantime the runbook §4 observability surface is logs-only, with `pipelines/_metrics/emit.py` emitting a per-run `metrics.jsonl` (REQ-INB-23) for offline postmortem."
- The §4.5 paragraph as a whole remains in the same place (after §4.4) and the rest of §4.5 is unchanged.
- `git log` shows one commit: `docs(runbook §4.5): refresh alerting forward-pointer (REQ-INB-30)`.
---
### T2.3 — Correct the `papra-write-policy.md:192` test count (REQ-INB-31) **[doc]**
- **Refs:** REQ-INB-31 (test count), REQ-INB-9 (Papra dry-run), REQ-INB-20 (Papra gate)
- **Files:** `docs/papra-write-policy.md` (lines 192, 202)
- **Persona:** lead-developer (primary, owns `**/*.md` territory)
- **needs review from:** devops-engineer (papra-write-policy.md is in their territory per PERSONAS.md)
- **Diff size:** XS (2 line replacements: 13 → 15)
- **must_haves:**
- `docs/papra-write-policy.md:192` (the line ``pipelines/_tests/test_papra_decide_offline.py` covers 13 cases:`) replaces `13 cases` with `15 cases`.
- `docs/papra-write-policy.md:202` (the line `All 13 cases pass on the test environment.`) replaces `13 cases` with `15 cases`.
- The 2 new test cases referenced (added by the v1.1 EXECUTE stage) are: `test_papra_dry_run_override_wins` and `test_emit_papra_write_gate_event_logs_three_fields`, both in `pipelines/_tests/test_papra_decide_offline.py`. The doc does not need to list them by name (the doc is about the policy, not the test inventory); the count update is sufficient.
- `git log` shows one commit: `docs(papra-write-policy): test count 13 → 15 (REQ-INB-31)`.
---
### T2.4 — Refresh the 4 stale `pipelines/common.py:` line refs in `extraction-contract.md` (REQ-INB-32) **[doc]**
- **Refs:** REQ-INB-32 (line-ref refresh), REQ-INB-4 (LLM extraction contract), REQ-INB-14 (prompt-as-code)
- **Files:** `docs/extraction-contract.md` (lines 40, 98, 102, 161)
- **Persona:** lead-developer (primary, owns `**/*.md` territory)
- **needs review from:** ml-engineer (extraction-contract.md is in their territory per PERSONAS.md)
- **Diff size:** S (4 line-ref substrings replaced)
- **must_haves:**
- `docs/extraction-contract.md:40` (the cite ``INBOXOS_LLM_MODEL` env var (see `pipelines/common.py:467-468`).``) replaces `467-468` with the current `LLMConfig.from_env` default-pin line (verified: `pipelines/common.py:533-534` contains the `gemma4:e4b` default; the doc's `467-468` cite is now stale by the v1.1 line-shuffle). The new cite is `pipelines/common.py:533-534`. (If the v1.1 EXECUTE stage added lines to `LLMConfig`, the implementer verifies the post-v1.1 line range and updates accordingly; the lead-developer can `git log -L :from_env:pipelines/common.py` to find the canonical cite.)
- `docs/extraction-contract.md:98` (the cite ``LLMConfig.from_env()` (see `pipelines/common.py:414-479`).``) replaces `414-479` with the current `LLMConfig.from_env` class range (verified: `class LLMConfig` starts at `pipelines/common.py:492` and `from_env` is the first method). The new cite is `pipelines/common.py:492-540` (or whatever range the post-v1.1 diff puts the class at).
- `docs/extraction-contract.md:102` (the cite `pipelines/common.py::llm_run_command` and `_ollama_call_one_model,`) updates the `_ollama_call_one_model` line ref. The new cite is `pipelines/common.py:636-660` (or whatever range the function occupies post-v1.1).
- `docs/extraction-contract.md:161` (the cite ``opencode` backend in `pipelines/common.py:378-407` is documented``) replaces `378-407` with the current `opencode_run_command` range (verified: `def opencode_run_command` starts at `pipelines/common.py:440` and the docstring extends to ≈ line 487). The new cite is `pipelines/common.py:440-487` (or whatever range the post-v1.1 diff puts the function at).
- All 4 cites are verified post-edit by `grep -n 'pipelines/common.py:' docs/extraction-contract.md` returning 0 hits to the stale ranges (467-468, 414-479, 605-624 [if cited], 378-407) and ≥ 1 hit each to the new ranges.
- The contract semantics (model pin, prompt path, JSON parse) are unchanged — only the line refs are updated. The implementer does not modify the surrounding prose.
- `git log` shows one commit: `docs(extraction-contract): refresh 4 stale pipelines/common.py line refs (REQ-INB-32)`.
---
### T2.5 — Renumber the runbook §6 sections to restore monotonic ordering (REQ-INB-36) **[doc]**
- **Refs:** REQ-INB-36 (§6 renumber), REQ-INB-6 (operations runbook)
- **Files:** `docs/operations-runbook.md` (lines 474, 476, 508, 558, 565 — the §6 section headings and their anchors)
- **Persona:** lead-developer (primary, owns `**/*.md` territory)
- **needs review from:** devops-engineer (runbook is in their territory per PERSONAS.md)
- **Diff size:** XS (4 heading renumbers + any in-doc anchor/cross-ref updates)
- **must_haves:**
- `docs/operations-runbook.md:508` (the line `### 6.4 Secret rotation (REQ-INB-21)`) renumbers to `### 6.2 Secret rotation (REQ-INB-21)`. The current §6.2 (line 558) and §6.3 (line 565) become §6.3 and §6.4 respectively.
- `docs/operations-runbook.md:558` (the current §6.2 "Why we never commit secrets") renumbers to `### 6.3 Why we never commit secrets`.
- `docs/operations-runbook.md:565` (the current §6.3 "Data-sovereignty posture") renumbers to `### 6.4 Data-sovereignty posture`.
- The current §6.1 (line 476, "Secret inventory") is unchanged.
- Any in-doc cross-references to the old §6.4 / §6.2 / §6.3 numbers (the runbook may have footer or appendix cites) are updated to the new numbering. The implementer `grep -n '§6\.[234]' docs/operations-runbook.md` to find all cross-refs and update them.
- The §6.4 footer (if any) is verified not to exist (the runbook structure has no §6.4 footer — the renumber is a heading-level edit only).
- `git log` shows one commit: `docs(runbook §6): renumber to 6.1 → 6.2 → 6.3 → 6.4 (REQ-INB-36, v1.1 P1-1 closure)`.
---
## Wave 3 — Env-file drift closure (1 candidate, 1 PR)
> Wave 3 has 1 task that lands as 1 PR. The 7 new `.env.example` entries are S effort (one file, 7 lines + a section comment). devops-engineer is the primary owner (the env contract is in their territory per PERSONAS.md); security-engineer is the reviewer (the 3 PII-related knobs require a security sign-off).
---
### T3.1 — Add 7 missing workflow env vars to `.env.example` (REQ-INB-33) **[doc/env]**
- **Refs:** REQ-INB-33 (env-file drift), REQ-INB-6 (operations runbook), REQ-INB-9 (Papra dry-run), REQ-INB-11 (single pinned LLM model)
- **Files:** `.env.example` (a new "Workflow tunables" section between the existing `IMAP_*` block and the `# NOCODB_*` block, or at the end of the file with a section comment — the implementer picks the placement; the v1.1 `INBOXOS_LLM_MODEL` lines are at 96-99 of `.env.example` and the new section is best placed adjacent to that)
- **Persona:** devops-engineer (primary, owns the env contract)
- **needs review from:** security-engineer (PII-related knobs: `PAPRA_DRY_RUN`, `PAPRA_BODY_WRITE_MODULES`, `PAPRA_METADATA_ONLY_MODULES`)
- **Diff size:** S (1 file, ≈ 15 lines added — 7 entries + 3-4 section comment lines + 4 explanatory comments)
- **must_haves:**
- `.env.example` gains 7 commented-out entries with the workflow-baked defaults documented:
1. `# OLLAMA_API_KEY=""` — empty by default (Ollama local is unauthenticated; the entry is for FRP-tunneled or remote Ollama where the operator has set a key).
2. `# INBOXOS_LLM_BACKEND=ollama` — default backend; the v1.0 production path is Ollama. The `opencode` backend is wired in `pipelines/common.py::opencode_run_command` but documented as "kept for offline tests and future opencode versions" (per the docstring at `common.py:455-461`); operators should not flip the default to `opencode` without reading the docstring.
3. `# PAPRA_DRY_RUN=true` — the v1.0 + v1.1 + v1.2 default; `false` is the Phase 6 / future-v1.x live-write gate per `docs/papra-write-policy.md`.
4. `# PAPRA_BODY_WRITE_MODULES=crm,finance,legal` — modules whose bodies (not just attachments) get written to Papra on the post-classify pass; verified against the workflow default in `.gitea/workflows/live-pipeline.yml` and `historical-pipeline.yml`.
5. `# PAPRA_METADATA_ONLY_MODULES=intel,promotions` — modules whose Papra write is metadata-only (no body upload); verified against the workflow default.
6. `# MBOX_BUCKET=` — historical pipeline's S3 bucket for the `.mbox` source; defaults to `$SEAWEEDFS_S3_BUCKET` if unset. The entry is intentionally empty (the default falls through to `SEAWEEDFS_S3_BUCKET`).
7. `# OPENCODE_NPM_VERSION=latest` — historical pipeline's opencode CLI version (the `opencode` backend is offline-tests-only in production, but the env var is still referenced by `pipelines/common.py`); default is `latest` per the workflow env at `historical-pipeline.yml`.
- The 7 entries are grouped under a section comment: `# --- Workflow tunables (REQ-INB-33) --- ... # --- end Workflow tunables ---`. The section is placed between the existing `INBOXOS_LLM_MODEL` block (lines 96-99) and the `NOCODB_*` block, OR at the end of the file (the implementer picks the placement; either is acceptable).
- Each entry has a 1-line comment explaining the default and the consequence of changing it (per the v1.0 `INBOXOS_LLM_MODEL` precedent at `.env.example:96-99` which the v1.1 EXECUTE stage updated at commit `6b05e8d`).
- `.env.example` is verified not to leak any real secret values: `grep -E 'OLLAMA_API_KEY=|NOCODB_API_TOKEN=|GITEA_PASS=|IMAP_PASSWORD=' .env.example` returns only commented-out or empty-string assignments. The v1.0 secret-scan precedent is honored.
- `.env.example` is verified not to introduce any literal `cto-inboxos` references (the v1.0 → v1.1 → v1.3 migration is complete): `grep -n cto-inboxos .env.example` returns empty.
- The `INBOXOS_LLM_MODEL` / `INBOXOS_LLM_MODEL_EXTRACT` block at lines 96-99 is left unchanged (REQ-INB-15 / v1.1 P1-5 closure, commit `6b05e8d`).
- `git log` shows one commit: `chore(env): add 7 workflow tunables to .env.example (REQ-INB-33)`. The commit touches exactly 1 file (.env.example).
---
## Cross-waves
### Within-wave file-share note
- **Wave 1**: T1.1 touches `docs/gitea-actions.md` and `scripts/setup_gitea_secrets.sh`; T1.2 touches `.gitea/workflows/ci-pr.yml`. No file overlap. The 2 PRs are independent.
- **Wave 2**: T2.1, T2.2, T2.5 all touch `docs/operations-runbook.md`. **They must be sequenced as 3 separate PRs** (one per task) so the v1.3 audit can attribute each diff to one candidate. The 3 PRs are independent in code, but the runbook audit will check the final file is internally consistent. The runbook §footer (T2.1), §4.5 (T2.2), and §6 renumber (T2.5) touch disjoint line ranges. T2.3 (`docs/papra-write-policy.md`) and T2.4 (`docs/extraction-contract.md`) touch different files; no overlap.
- **Wave 3**: T3.1 touches `.env.example`. No file overlap with Wave 1 or Wave 2.
### Vertical-slice check
Each Wave 1 + Wave 2 + Wave 3 task is a single-PR vertical slice: a doc/env/CI edit that compiles (no `.py` changes), passes the ci-pr merge gate (with T1.2's 5 new hard-fail tests), and moves the project measurably toward the v1.3 acceptance gate. No cross-slice dependency: each task is a stand-alone commit that can be reviewed and merged independently.
### Must-have check
- **Wave 1 (2 tasks, 2 PRs):** T1.1 closes the cto-inboxos drift in 2 files; T1.2 wires 6 v1.1 offline tests into the ci-pr merge gate. Both are P0 closures and must land before the v1.3 audit.
- **Wave 2 (5 tasks, 5 PRs):** T2.1 + T2.2 + T2.3 + T2.4 + T2.5 close 5 doc-hygiene items. All 5 are XS or S effort; the runbook footer, runbook §4.5, papra-write-policy test count, extraction-contract line refs, and runbook §6 renumber.
- **Wave 3 (1 task, 1 PR):** T3.1 closes the .env.example drift with 7 new entries.
- **Total: 8 tasks, 8 PRs, 8 commits (or 8 commits + 1 commit for the cto-inboxos pair; T1.1 is 1 commit touching 2 files).**
### Milestone-level must-have
The v1.3 acceptance gate is the 9/12 disposition from the v1.3 IDEATE pass (9 ACCEPT candidates close cleanly, 3 DEFER candidates are filed for v1.4+ in `v1.3-candidates.md §9`). The gate is met when:
- All 8 tasks land as 8 PRs with 8 well-formed `---ci---` blocks (8 execute + 1 verify = 9 total status blocks for the phase)
- The 2 P0 candidates (T1.1 cto-inboxos closure, T1.2 ci-pr test wiring) land **before** the verify pass — the verify report's structural-layer check includes "no `cto-inboxos` references in `docs/` or `scripts/`" and "ci-pr `fast-tests` job includes all 6 v1.1 offline tests"
- The 7 doc-hygiene / env-file candidates (T2.1, T2.2, T2.3, T2.4, T2.5, T3.1) land with line-ref verification (the v1.3 verify report grep-asserts the line-refs in `extraction-contract.md` and the §6 ordering in `docs/operations-runbook.md`)
- The 3 DEFER candidates (REQ-INB-35, 38, 39) are explicitly filed in `v1.3-candidates.md §9` for v1.4+ — the verify report confirms the file mentions them
- `.ciagent/inboxos/REQUIREMENTS.md` v1.3 section is unchanged at close (the 12 candidates remain `[CANDIDATE]`; the CPO-owned promotion to a real requirement is a CPO action, not an EXECUTE action)
- `docs/requirements.md` and `docs/roadmap.md` are byte-identical to the v1.2 close (CPO-owned, not modified by EXECUTE)
---
## Wave 4 — NFR (no code delta)
> Wave 4 is the empty wave. The v1.3 milestone produces no NFR-only chore commit; the EXECUTE stage lands the 8 tasks in Waves 1-3 directly. If the lead-developer finds a small chore commit is needed (e.g. to record the "no code delta" decision for the audit), the chore commit lands in EXECUTE and is the 9th commit, not a separate wave.
---
## Requirement coverage
| REQ-ID | v1.3 status | Plan task | Disposition |
|--------|-------------|-----------|-------------|
| REQ-INB-28 | `[CANDIDATE]` | T1.1 | ACCEPT (P0) |
| REQ-INB-29 | `[CANDIDATE]` | T2.1 | ACCEPT (XS) |
| REQ-INB-30 | `[CANDIDATE]` | T2.2 | ACCEPT (XS) |
| REQ-INB-31 | `[CANDIDATE]` | T2.3 | ACCEPT (XS) |
| REQ-INB-32 | `[CANDIDATE]` | T2.4 | ACCEPT (S) |
| REQ-INB-33 | `[CANDIDATE]` | T3.1 | ACCEPT (S) |
| REQ-INB-34 | `[CANDIDATE]` | T1.2 | ACCEPT (P0) |
| REQ-INB-35 | `[CANDIDATE]` | (defer to v1.4+) | DEFER |
| REQ-INB-36 | `[CANDIDATE]` | T2.5 | ACCEPT (XS, closes v1.1 P1-1) |
| REQ-INB-37 | `[CANDIDATE]` | T1.1 | ACCEPT (P0, closes v1.1 P1-2) |
| REQ-INB-38 | `[CANDIDATE]` | (defer to v1.4+) | DEFER |
| REQ-INB-39 | `[CANDIDATE]` | (defer to v1.4+) | DEFER |
| REQ-INB-26 (v1.2 carry) | `[REJECTED — out of domain]` | n/a | unchanged from v1.2 |
| REQ-INB-1..25 (v1.0 + v1.1) | `[SHIPPED]` / `[EXECUTED]` | n/a | unchanged |
**Coverage ratio:** 9/12 ACCEPT, 3/12 DEFER, 0/12 REJECT. **v1.1 backlog closure:** P1-1 closed by REQ-INB-36, P1-2 closed by REQ-INB-37. P2-1 and P2-2 remain in the v1.4+ backlog.
---
## Plan-execution prerequisites
- [x] Active project confirmed as `inboxos` (per `.ciagent/config.json` `active_projects[0]`)
- [x] Active milestone confirmed as `v1.3`
- [x] Research commit recorded: `66ee903` (stack unchanged, no PIT items blocking; 3 PITs absorbed into the candidate set)
- [x] Ideate commit recorded: `946b744` (12 candidates, 9 ACCEPT)
- [x] Requirements registration commit recorded: `d2f3bd5` (REQUIREMENTS.md v1.3 section, 12 candidates)
- [x] No paperclip monorepo ad-hoc changes (operator directive honored)
- [x] Gitea issue queue is empty (CTO closed INB-183 + INB-193; verified)
- [x] Cross-references verified: REQ-INB-28, 29, 30, 31, 32, 33, 34, 36, 37 in `REQUIREMENTS.md` v1.3 section
- [x] PERSONAS.md territory is current (v1.0 research, no v1.3 changes needed; the devops-engineer + security-engineer + lead-developer + ml-engineer territories cover all 8 tasks)
---
## Plan deliverables
This plan produces:
- **8 PRs** (1 per task: T1.1, T1.2, T2.1, T2.2, T2.3, T2.4, T2.5, T3.1)
- **8 commits** (T1.1 is 1 commit touching 2 files; the other 7 are 1 commit per task)
- **8 file touches** (3 unique files in T1.1, 1 file in T1.2, 4 unique files in Wave 2, 1 file in Wave 3 = 9 unique files; the runbook is touched 3 times across 3 different line ranges, the rest once)
- **0 new code** (no `.py` changes; no new NocoDB tables; no new shell scripts)
- **0 new tests** (the 6 v1.1 offline tests are wired into ci-pr, not new)
- **0 schema changes**
- **0 new dependencies** (no `requirements.txt` change)
The plan's net diff is **≈ 30-50 lines across 9 files**, dominated by the runbook footer / §4.5 / §6 renumber (T2.1 + T2.2 + T2.5) and the .env.example 7-entry addition (T3.1). The v1.3 milestone is the smallest source-tree change since v1.0 close.
---
## Risk register
| Risk | Likelihood | Impact | Mitigation |
|------|------------|--------|------------|
| `pipelines/common.py` line refs in `extraction-contract.md` (T2.4) drift again between the v1.3 plan and the v1.3 execute | medium | low | The implementer runs `grep -n 'pipelines/common.py:' docs/extraction-contract.md` after each T2.4 edit and verifies the new ranges point to the canonical class/function start lines. If the v1.3 EXECUTE stage also touches `pipelines/common.py` (it should not, per the v1.3 plan's "0 new code" deliverable), the line-ref refresh is a re-run task. |
| T1.2's `test_papra_link_parity_offline.py` import chain fails in CI for an unexpected reason (not just the `imapclient` dep) | low | medium | The step uses `continue-on-error: true`; the failure is logged but does not block the merge. A trailing comment in the workflow YAML notes the v1.0 / v1.1 precedent. If the test fails for a non-imapclient reason, the failure is filed in the v1.4+ backlog. |
| The runbook §6 renumber (T2.5) breaks a downstream cross-reference (the v1.0 / v1.1 docs may have anchor links to `§6.4` that the v1.3 plan does not enumerate) | low | low | The implementer `grep -rn '§6\.[234]' docs/` to find all cross-refs and updates them. The v1.3 verify report's structural check includes "no §6.4 cross-refs point at the old `Secret rotation` position." |
| T1.1's cto-inboxos pair misses a third file (the v1.0 / v1.1 sweep may have left a stray reference in `docker/`, `pipelines/`, or `docs/schemas.md`) | low | low | The v1.3 verify report's structural check includes "no `cto-inboxos` references in `docs/`, `pipelines/`, `docker/`, `scripts/`, `.gitea/workflows/`, `.env.example`, or `.opencode/commands/`" — if a stray reference is found, the verify pass flags it for a P0 follow-up commit before the milestone closes. |
| The 7 new `.env.example` entries (T3.1) include a default that has drifted from the actual workflow env (e.g. `PAPRA_BODY_WRITE_MODULES` was changed in a workflow patch that did not update the doc) | low | low | The implementer verifies each new entry's default against the canonical source (the workflow YAML's `env:` block for the relevant var) before adding it. The v1.3 verify report grep-asserts the 7 new entries match the workflow defaults. |
| The 5-REQ doc-hygiene sweep in Wave 2 (T2.1..T2.5) is rejected by a reviewer who wants the docs to be promoted to a real requirement (not [CANDIDATE]) before landing the doc edits | low | medium | The v1.1 EXECUTE stage followed the same pattern (REQ-INB-15..18 are [CANDIDATE] in `REQUIREMENTS.md` and the doc edits landed without promotion). The v1.3 plan honors the same pattern. The CPO may promote at any time without blocking the EXECUTE stage. |
---
## Hand-off
- **EXECUTE phase:** runs on the 9 ACCEPT candidate set. Produces 8 PRs / 8 commits on `phase/01-v1.3-execute`. The execution order within `phase/01-v1.3-execute` is: T1.1 first (cto-inboxos closure is a 1-commit PR; lowest risk), then T1.2 (CI test wiring, 1-commit PR; needs the cto-inboxos closure to land first so the ci-pr merge gate runs against the post-closure state), then T2.1..T2.5 (5 doc-hygiene PRs; can be ordered in any sequence; recommended order is T2.5 → T2.4 → T2.3 → T2.2 → T2.1 to land the most material doc edits first), then T3.1 (env-file PR; can be last). The total is 8 PRs.
- **VERIFY phase:** verifies the 9 ACCEPT candidates close cleanly — `grep -n cto-inboxos docs/ scripts/ .env.example pipelines/ docker/ .gitea/workflows/ .opencode/commands/ opencode.json` returns empty (T1.1); `ci-pr.yml` fast-tests job includes all 6 v1.1 offline tests (T1.2); `extraction-contract.md` line refs are post-v1.1 (T2.4); `docs/operations-runbook.md:823` says v1.3 (T2.1); `docs/operations-runbook.md:285-289` says "v1.3" not "v1.2" (T2.2); `docs/papra-write-policy.md:192,202` say 15 cases not 13 (T2.3); `docs/operations-runbook.md` §6 is `6.1 → 6.2 → 6.3 → 6.4` (T2.5); `.env.example` has the 7 new entries (T3.1). The 4 verification layers (structural, behavioral, security, quality) are documented in `.ciagent/inboxos/verify/v1.3-PHASE1-REPORT.md`.
- **REVIEW phase:** multi-persona code review across the 8 PRs. The review scope is the diff (≈ 30-50 lines across 9 files) and the 6 new ci-pr steps. All 6 personas participate (frontend-engineer is deactivated; 5 active personas + lead-developer). The review verdict is PASS-with-NITS (the v1.1 and v1.2 review verdicts); the v1.3 P0 sweep has no P0 fixes because the 2 P0 candidates (T1.1, T1.2) are pre-reviewed by the EXECUTE-stage impl.
- **AUDIT phase:** verify project health. The audit reconstruction pass: 8 EXECUTE commits + 1 verify + 1 review (× 2 if review produces a P0 fix) = 10-11 status blocks on `milestone/v1.3-initial`. The file-discipline pass: all 12 v1.3 candidate files present in `REQUIREMENTS.md` v1.3 section; all 8 plan tasks have a `must_haves` array; the 3 DEFER candidates are in `v1.3-candidates.md §9`; the 5 re-verifications (T2-8, T2-9, T2-10, CP-2, CP-3) are in `v1.3-candidates.md §4` and §5. The commit-discipline pass: 8 EXECUTE commits have well-formed `---ci---` blocks; all 8 follow `<type>(<scope>):`; max files-per-commit observed (T1.1 is 2 files; T2.1..T2.5 + T3.1 are 1 file each; T1.2 is 1 file). The CPO-files pass: `docs/requirements.md` and `docs/roadmap.md` byte-identical to v1.2 close. The audit verdict is PASS (the v1.1 and v1.2 audit verdicts).
- **COMPLETE phase:** closes the v1.3 inboxos milestone on the 9/12 disposition. Tag `v1.3.0`. Release notes record the 9-candidate closure + 3-candidate defer. Merge to `main` per the v1.1 / v1.2 pattern: FF-merge from `phase/01-v1.3-execute` to `milestone/v1.3-initial` to `main`, push tag, push milestone branch, create Gitea release. The v1.3.0 release notes title is "v1.3 — Codebase-Drift Sweep (9 candidates, 3 deferred)" and the body lists the 9 ACCEPT + 3 DEFER + 0 REJECT.
---
## Files updated by this plan
- **New:** `.ciagent/inboxos/plans/01-PLAN-v1.3.md` (this file)
- **Filename choice:** the v1.1 plan lives at `plans/01-PLAN.md` and `plans/01-PLAN-CHECK.md` (committed in `78e42ee`); the v1.2 plan lives at `plans/01-PLAN-v1.2.md` (committed in `8e10c22`); the v1.3 plan is written to `plans/01-PLAN-v1.3.md` to avoid an in-place overwrite of the v1.1 / v1.2 audit trail. The `01-PLAN-CHECK.md` companion is not duplicated for v1.3 — the plan-checker verdict for this plan is recorded inline in §"Milestone-level must-have" and the EXECUTE / VERIFY commits that follow.
- **Untouched:** all inboxos code (no `.py` changes), test, schema, and doc files (the v1.3 EXECUTE stage will touch 9 files: `docs/gitea-actions.md`, `scripts/setup_gitea_secrets.sh`, `.gitea/workflows/ci-pr.yml`, `docs/operations-runbook.md` (3 disjoint line ranges), `docs/extraction-contract.md`, `docs/papra-write-policy.md`, `.env.example`).
- **Untouched:** `.ciagent/inboxos/plans/01-PLAN.md` (v1.1 plan, preserved)
- **Untouched:** `.ciagent/inboxos/plans/01-PLAN-CHECK.md` (v1.1 plan-check, preserved)
- **Untouched:** `.ciagent/inboxos/plans/01-PLAN-v1.2.md` (v1.2 plan, preserved)
- **Untouched:** `.ciagent/inboxos/plans/05-PLAN.md` (v1.0 phase 5 plan, preserved)