@openlife/cli 1.7.4 → 1.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +186 -0
  2. package/CODE_OF_CONDUCT.md +31 -0
  3. package/CONTRIBUTING.md +133 -0
  4. package/README.md +25 -9
  5. package/dist/cli/InstallModules.js +37 -0
  6. package/dist/cli/InstallWizard.js +46 -8
  7. package/dist/index.js +11 -0
  8. package/dist/test_install_wizard.js +86 -21
  9. package/docs/getting-started.md +137 -0
  10. package/package.json +11 -2
  11. package/docs/CHANGELOG_FEATURE_ROLLOUT_DESIGNMD.md +0 -43
  12. package/docs/EXTERNAL_SOURCES_AND_SECURITY_GUARD.md +0 -33
  13. package/docs/OPENLIFE_AUDIT_2026-05-06.md +0 -170
  14. package/docs/OPENLIFE_CONSOLIDATED_PLAN_2026-05-06.md +0 -299
  15. package/docs/OPENLIFE_DUAL_MODE_IMPLEMENTATION_PLAN.md +0 -205
  16. package/docs/OPENLIFE_EVOLUTION_SURFACE_2026-05-07.md +0 -53
  17. package/docs/OPENLIFE_SKILLS_IMPORT_2026-05-07.json +0 -223
  18. package/docs/OPENLIFE_SQUADS_IMPORT_2026-05-07.json +0 -184
  19. package/docs/PAPERCLIP_OPENLIFE_INVESTIGATION.md +0 -85
  20. package/docs/RELEASE_ORGANIZATION_PLAN.md +0 -164
  21. package/docs/audit/CLI-EXECUTION-RESULTS.md +0 -113
  22. package/docs/audit/CLI-MATRIX.md +0 -556
  23. package/docs/audit/DOC-PARITY-GAPS.md +0 -351
  24. package/docs/audit/ORCHESTRATOR-MATRIX.md +0 -136
  25. package/docs/audit/TEST-COVERAGE-GAPS.md +0 -334
  26. package/docs/audit/integrations/SKIPPED.md +0 -101
  27. package/docs/autonomous-install.md +0 -79
  28. package/docs/capability-genesis.md +0 -137
  29. package/docs/capability-pack-schema.md +0 -157
  30. package/docs/commands.md +0 -82
  31. package/docs/deep-research-capability.md +0 -114
  32. package/docs/development/typescript-conventions.md +0 -95
  33. package/docs/host-installers.md +0 -68
  34. package/docs/install/aiobuilder.md +0 -70
  35. package/docs/install/claude-code.md +0 -83
  36. package/docs/install/codex.md +0 -64
  37. package/docs/install/gemini-cli.md +0 -64
  38. package/docs/install/runtime-profiles.md +0 -83
  39. package/docs/openlife-agent-os-blueprint.md +0 -114
  40. package/docs/openlife-install-backlog.md +0 -115
  41. package/docs/openlife-install-spec.md +0 -306
  42. package/docs/operations/CLOUD_CUTOVER_AUDIT.md +0 -37
  43. package/docs/operations/PHASE_PROGRESS_CONTINUATION.md +0 -24
  44. package/docs/performance-benchmarks.md +0 -83
  45. package/docs/planning/v1.3-capability-genesis.md +0 -157
  46. package/docs/plans/2026-05-05-admin-interface-professional-dark-premium-plan.md +0 -84
  47. package/docs/plans/2026-05-05-openlife-autonomous-domain-marketplace-masterplan.md +0 -122
  48. package/docs/roadmap/OPENLIFE_MASTER_PLAN_CLOUD_V3.md +0 -97
  49. package/docs/sandboxing-research.md +0 -117
  50. package/docs/stories/epic-feature-audit/1.1.story.md +0 -84
  51. package/docs/stories/epic-feature-audit/1.2.story.md +0 -102
  52. package/docs/stories/epic-feature-audit/1.3.story.md +0 -93
  53. package/docs/stories/epic-feature-audit/1.5.story.md +0 -121
  54. package/docs/stories/epic-feature-audit/1.6.story.md +0 -80
  55. package/docs/stories/epic-feature-completeness/2.1.story.md +0 -70
  56. package/docs/stories/epic-feature-completeness/2.2.story.md +0 -49
  57. package/docs/stories/epic-feature-completeness/2.3.story.md +0 -74
  58. package/docs/stories/epic-feature-completeness/2.4.story.md +0 -71
  59. package/docs/stories/epic-feature-completeness/3.1.story.md +0 -56
  60. package/docs/stories/epic-feature-completeness/3.2.story.md +0 -80
  61. package/docs/stories/epic-feature-completeness/3.3.story.md +0 -68
  62. package/docs/stories/epic-feature-completeness/3.4.story.md +0 -71
  63. package/docs/stories/epic-feature-completeness/3.5.story.md +0 -72
  64. package/docs/stories/epic-feature-completeness/3.6.story.md +0 -69
  65. package/docs/stories/epic-feature-completeness/3.7.story.md +0 -68
  66. package/docs/stories/epic-feature-completeness/3.8.story.md +0 -57
  67. package/docs/v1.4-changelog.md +0 -159
  68. package/docs/v1.5-changelog.md +0 -106
  69. package/docs/v1.5-roadmap.md +0 -121
  70. package/docs/v1.6-changelog.md +0 -67
  71. package/docs/v1.6-roadmap.md +0 -89
@@ -1,68 +0,0 @@
1
- # Story 3.3 — Per-host install logic (Claude Code wiring, gemini-cli/codex stubbed)
2
-
3
- **StoryId:** `3.3`
4
- **Epic:** `epic-multi-host-installer` (v1.1)
5
- **Status:** InReview
6
- **Severity:** P1 (closes the "host install is architectural stub" finding)
7
- **Cluster:** install-flow
8
- **Depends on:** Story 3.1 (host enum + validator), Story 3.2 (dist-templates Claude Code starter roster)
9
-
10
- ## Description
11
-
12
- Story 3.1 validated the `Host` type at the CLI boundary and Story 3.2 shipped the artifacts under `dist-templates/claude-code/`. But `openlife system setup --host claude-code` still did nothing host-specific — `InstallFlow.run()` ended after `SystemInstaller.install()` wrote the OpenLife state, never touching the host CLI's expected paths. This story closes the architectural stub for **claude-code** and lays the dispatch surface for the other two hosts.
13
-
14
- `HostInstaller.install(host, options)` is the new seam. For `claude-code` it copies agent files and slash commands from `dist-templates/` into `<targetRoot>/.claude/{agents,commands/openlife}/` and stages the MCP manifest at `<targetRoot>/.openlife/install-mcp-snippet.json`. For `gemini-cli` and `codex` it throws `HostNotYetSupportedError` with code `HOST_NOT_YET_SUPPORTED` — `InstallFlow.run()` catches this and reports a skipped host instead of aborting the install.
15
-
16
- ## Acceptance Criteria
17
-
18
- - [x] **New file** `src/cli/HostInstaller.ts` with class `HostInstaller`, methods `install(host, options)`, `installForClaudeCode`, `installForGeminiCli`, `installForCodex`, and exported `HostNotYetSupportedError`
19
- - [x] **Claude Code wiring** copies `dist-templates/claude-code/agents/*.md` → `<targetRoot>/.claude/agents/` and `dist-templates/claude-code/commands/openlife/*.md` → `<targetRoot>/.claude/commands/openlife/`
20
- - [x] **MCP manifest staged** at `<targetRoot>/.openlife/install-mcp-snippet.json` (non-invasive — operator merges into `~/.claude.json` manually; we do not auto-mutate the user's global config)
21
- - [x] **Idempotency:** identical content returns `skipped-identical`, different content writes `<file>.bak` then overwrites and returns `updated`
22
- - [x] **`gemini-cli` / `codex`** throw `HostNotYetSupportedError` with code `HOST_NOT_YET_SUPPORTED` and a message pointing at `--host claude-code`
23
- - [x] **`InstallFlow.run()`** calls `HostInstaller.install()` after `SystemInstaller.install()`, catches `HostNotYetSupportedError`, and degrades gracefully (records skipped host in the result, install does not abort)
24
- - [x] **`defaultTemplatesRoot()`** resolves relative to the compiled file so it works in both repo-dev (`dist/cli/HostInstaller.js`) and consumer (`node_modules/@open-life/cli/dist/cli/HostInstaller.js`) layouts
25
- - [x] **Regression test** `src/test_host_installer.ts` covers 7 scenarios: claude-code happy path, idempotent re-install, `.bak` on conflict, MCP snippet staged, missing templates root throws clearly, `gemini-cli` throws `HOST_NOT_YET_SUPPORTED`, `codex` throws `HOST_NOT_YET_SUPPORTED`
26
- - [x] Test wired into `test:all`; suite 65 → 66 verde
27
-
28
- ## Dev Notes
29
-
30
- - **Why stage the MCP manifest instead of editing `~/.claude.json`?** Mutating a global config the user owns is a security/UX trap — one bad merge and we wreck their other MCP servers. Staging the snippet at `<targetRoot>/.openlife/install-mcp-snippet.json` plus a clear `notes[]` entry lets the operator merge with eyes open. Auto-merge is a separate v1.1 story with its own consent flow.
31
- - **Why does `installForClaudeCode` not recurse into subdirectories?** Claude Code's `.claude/agents/` and `.claude/commands/<namespace>/` layouts are flat by design (one file per agent, one file per command). `copyDir` walks one level on purpose — if a future host needs nested layouts, that host gets its own copy strategy rather than complicating this one.
32
- - **Why `HostNotYetSupportedError` instead of returning a skipped result?** The dispatcher (`install()`) is the right place to enforce coverage. A thrown error with a stable `code` is unambiguous for `InstallFlow.run()`'s `catch` block and for downstream callers (wizard in 3.5, uninstall in 3.4). Silent returns would let bugs slip through where a host appears "installed" but actually noop'd.
33
- - **Why `.bak` instead of refusing to overwrite?** Operators re-running `system setup` after editing dist-templates need a path forward without manually moving files. One backup per file is enough — the test asserts the backup contains the prior content, so anyone investigating after an install knows where their custom edits went.
34
- - **MCP server is still deferred.** `bin/openlife-mcp.js` (the actual stdio server backing the 7 tool declarations in `openlife-orchestrator.json`) is **not** part of this story. The manifest is informational until a later v1.1 story implements the server. The `notes[]` entry in the install result spells this out so operators don't expect tools to light up after a merge.
35
- - **Why catch `HostNotYetSupportedError` in `InstallFlow.run()` instead of letting it bubble?** Profile selection (Lone Wolf / Swarm Commander) and host selection are independent axes. Failing the entire install because the operator picked `--host gemini-cli` while the rest of `SystemInstaller.install()` succeeded would force them to redo `.openlife/` setup. Graceful skip + clear message is the right contract until 3.3-followup ships gemini-cli/codex installers.
36
-
37
- ## File List
38
-
39
- - `src/cli/HostInstaller.ts` — NEW (class, dispatcher, claude-code installer, copyDir/copyFile helpers, `HostNotYetSupportedError`)
40
- - `src/cli/InstallFlow.ts` — MODIFIED (calls `HostInstaller.install()` after `SystemInstaller.install()`; catches `HostNotYetSupportedError`; threads result into the install summary)
41
- - `src/test_host_installer.ts` — NEW (7 test scenarios, 66th test in suite)
42
- - `package.json` — MODIFIED (added `test:host-installer` script; appended to `test:all`)
43
-
44
- ## Change Log
45
-
46
- - 2026-05-11 — @dev (Charlie) — Implemented `HostInstaller` with claude-code wiring (agents + slash commands + staged MCP manifest), idempotency via `.bak` on conflict, and `HostNotYetSupportedError` for gemini-cli/codex. `InstallFlow.run()` now catches the not-yet-supported error and degrades gracefully. test:all 65 → 66 verde. Status: Ready → InReview.
47
-
48
- ## IDS check
49
-
50
- **Decision:** CREATE (new file-copy installer — no prior artifact wires `dist-templates/` into a host) + ADAPT (`InstallFlow.run()` extended to invoke the new installer and handle its scoped error). Idempotency pattern (`identical → skip`, `differ → .bak + overwrite`) follows `SystemInstaller`'s existing write semantics — REUSE of internal convention.
51
-
52
- ## What unblocks for v1.1
53
-
54
- - Story 3.4 (uninstall reversible) — has a known list of written paths to reverse and `.bak` files to restore
55
- - Story 3.5 (install wizard interactive) — can call `HostInstaller.install()` after presenting the host roster
56
- - Story 3.6 (docs per host) — `claude-code` is now real; docs can show concrete paths under `.claude/agents/` and `.claude/commands/openlife/`
57
- - Story 3.7 (gemini-cli + codex installers) — the dispatcher slot is wired; each host just needs its `installForX` body and a templates subdir
58
- - Story 3.8+ (MCP server `bin/openlife-mcp.js`) — manifest is already staged; server implementation can land without changing the install contract
59
-
60
- ## What this does NOT do
61
-
62
- - Implement `bin/openlife-mcp.js` (the MCP server itself) → deferred to a later v1.1 story
63
- - Ship `gemini-cli` or `codex` installers → those throw `HOST_NOT_YET_SUPPORTED` by design; a later 3.7 wires them
64
- - Auto-merge the staged MCP snippet into `~/.claude.json` → out of scope; operator merges manually
65
- - Recurse into subdirectories when copying templates → host layouts are flat by design; revisit if a future host needs nested
66
- - Uninstall / reverse the file copy → Story 3.4
67
- - Interactive wizard for host selection → Story 3.5
68
- - Documentation updates beyond this story file → Story 3.6
@@ -1,71 +0,0 @@
1
- # Story 3.4 — Per-host uninstall logic (reversible, scoped, `.bak` restore)
2
-
3
- **StoryId:** `3.4`
4
- **Epic:** `epic-multi-host-installer` (v1.1)
5
- **Status:** InReview
6
- **Severity:** P1 (closes the "install is one-way" finding from 3.3 review)
7
- **Cluster:** install-flow
8
- **Depends on:** Story 3.3 (host install dispatcher + claude-code wiring)
9
-
10
- ## Description
11
-
12
- Story 3.3 made `openlife system setup --host claude-code` write real artifacts into `<targetRoot>/.claude/{agents,commands/openlife}/` and stage an MCP manifest at `<targetRoot>/.openlife/install-mcp-snippet.json`. There was no symmetric reversal — an operator who wanted to remove OpenLife from a host CLI had to manually delete files and remember which ones we owned. This story closes that gap.
13
-
14
- `HostInstaller.uninstall(host, options)` is the new seam, mirroring `install()`. For `claude-code` it removes **only** the artifacts Story 3.3 wrote: the five `openlife-*.md` agents (prefix-scoped), the entire `commands/openlife/` namespace directory, and the staged MCP snippet. If a `.bak` sits next to a removed agent, it is restored as the original filename — Story 3.3's overwrite is reversed, the user's customization survives. `gemini-cli` and `codex` throw `HostNotYetSupportedError` with code `HOST_NOT_YET_SUPPORTED`, same contract as install. A new CLI command `openlife system uninstall --host <host>` calls the dispatcher and prints JSON.
15
-
16
- ## Acceptance Criteria
17
-
18
- - [x] **New methods** on `HostInstaller`: `uninstall(host, options): HostUninstallResult`, `uninstallForClaudeCode`, `uninstallForGeminiCli`, `uninstallForCodex` — symmetric to the install side
19
- - [x] **Prefix-scoped agent removal:** only files matching `openlife-*.md` in `<targetRoot>/.claude/agents/` are touched. Any file in that directory without the `openlife-` prefix is preserved (user-owned)
20
- - [x] **Namespaced commands removal:** the entire `<targetRoot>/.claude/commands/openlife/` directory is removed (OpenLife owns the whole namespace by design — 3.3 staged 4 files there)
21
- - [x] **MCP snippet removal:** `<targetRoot>/.openlife/install-mcp-snippet.json` is deleted if present
22
- - [x] **`.bak` restore over deletion:** when removing an `openlife-*.md` agent, if a sibling `<file>.bak` exists, it is renamed back to the original filename (restoring the user's pre-3.3 content) instead of being deleted along with the install
23
- - [x] **`.openlife/` state preserved:** the runtime state directory itself (governance consents, mission state, registry data) is NOT removed. Only the single staged snippet file is. Full purge is a future `--purge` story
24
- - [x] **`~/.claude.json` untouched:** Story 3.3 never auto-merged into the user's global config, so uninstall makes no attempt to remove anything from it (would be a security/UX trap with no symmetric write)
25
- - [x] **`gemini-cli` / `codex`** throw `HostNotYetSupportedError` with code `HOST_NOT_YET_SUPPORTED` and a message pointing at `--host claude-code`
26
- - [x] **New CLI command** `openlife system uninstall --host <host>` registered in `src/index.ts`, uses `require('./cli/HostInstaller')` per the lazy-import contract (`src/index.ts:11-13`), prints `JSON.stringify(result, null, 2)`, sets `process.exitCode = 1` on `HOST_NOT_YET_SUPPORTED`
27
- - [x] **Idempotent:** running uninstall on an already-clean target returns a result where every file action is `skipped-not-found`. No errors thrown
28
- - [x] **Regression test** `src/test_host_uninstaller.ts` covers 8 scenarios: claude-code happy path (removes 5 agents + 4 commands + snippet), idempotent re-uninstall, `.bak` restoration when present, user-owned non-prefixed file preservation, `.openlife/` directory survives, snippet-only removal (no agents installed), `gemini-cli` throws, `codex` throws
29
- - [x] Test wired into `test:all`; suite 66 → 67 verde
30
-
31
- ## Dev Notes
32
-
33
- - **Why prefix-scoped removal instead of a manifest?** A written manifest at install time (`.openlife/install-manifest.json`) would be more precise, but it adds a second source of truth that can desync if the user manually moves files. The `openlife-` prefix is the contract: 3.3 only writes prefixed agent files, so 3.4 only removes prefixed agent files. The commands directory is namespaced (`commands/openlife/`), so the whole directory is fair game. No manifest needed.
34
- - **Why restore `.bak` instead of deleting the install file?** A `.bak` next to an `openlife-*.md` is evidence Story 3.3 overwrote a user edit (per the `copyFile` semantics in `HostInstaller.ts:154-172`). Deleting both would silently throw away the user's prior content. Renaming the `.bak` back to the original filename reverses 3.3's overwrite cleanly — install was a no-op for that file, uninstall restores it to pre-3.3 state. This is the symmetric inverse of 3.3's behavior.
35
- - **Why preserve `.openlife/`?** Runtime state (governance consents, mission state, registry data, learning loop history) is a separate concern from "is OpenLife wired into this host CLI". An operator who runs `system uninstall --host claude-code` to switch hosts should not lose their governance ledger. Full wipe needs its own story with explicit consent — see "What this does NOT do".
36
- - **Why not touch `~/.claude.json`?** 3.3 deliberately staged the MCP snippet at `<targetRoot>/.openlife/install-mcp-snippet.json` instead of auto-merging into `~/.claude.json` (security/UX trap with no rollback). Since we never wrote there, we cannot safely remove from there either — we do not know which entry under `mcpServers` came from us vs. the user's other servers. Operator removes manually, same as the install side.
37
- - **Why throw `HostNotYetSupportedError` on the unsupported hosts instead of returning a skipped result?** Same rationale as 3.3 (`docs/stories/epic-feature-completeness/3.3.story.md:32`): the dispatcher is the right place to enforce coverage. A stable error code is unambiguous for downstream callers (wizard in 3.5, future automation). Silent returns would let bugs slip through where uninstall appears to succeed but actually noop'd.
38
- - **Why a new CLI command instead of extending `system setup`?** Install and uninstall are independent verbs operators reach for at different times. A `system setup --uninstall` flag would conflate two flows and complicate the install wizard in 3.5. Separate command, same dispatcher pattern, lazy `require` to keep `--help` fast.
39
- - **Idempotency contract mirrors install.** Where install uses `created` / `updated` / `skipped-identical` / `skipped-not-found`, uninstall uses `removed` / `restored-from-bak` / `skipped-not-found`. Re-uninstalling a clean target returns all `skipped-not-found` and exit code 0.
40
-
41
- ## File List
42
-
43
- - `src/cli/HostInstaller.ts` — MODIFIED (added `HostUninstallResult` interface, `HostUninstallFileAction` type, `uninstall()` dispatcher, `uninstallForClaudeCode`, `uninstallForGeminiCli`, `uninstallForCodex`, private `removeFile`/`removeDir` helpers with `.bak` restore logic)
44
- - `src/index.ts` — MODIFIED (added `system uninstall --host <host>` command using lazy `require('./cli/HostInstaller')`, JSON output, exit code on `HOST_NOT_YET_SUPPORTED`)
45
- - `src/test_host_uninstaller.ts` — NEW (8 test scenarios, 67th test in suite)
46
- - `package.json` — MODIFIED (added `test:host-uninstaller` script; appended to `test:all`)
47
-
48
- ## Change Log
49
-
50
- - 2026-05-11 — @dev (Charlie) — Implemented `HostInstaller.uninstall()` symmetric to `install()` from Story 3.3. Claude-code path removes prefix-scoped `openlife-*.md` agents (with `.bak` restore), the entire `commands/openlife/` namespace, and the staged MCP snippet — preserves user files, `.openlife/` state directory, and `~/.claude.json`. Added `openlife system uninstall --host <host>` CLI command following the lazy-require contract. `gemini-cli`/`codex` throw `HOST_NOT_YET_SUPPORTED` per 3.3 dispatcher pattern. test:all 66 → 67 verde. Status: Ready → InReview.
51
-
52
- ## IDS check
53
-
54
- **Decision:** ADAPT (extends the existing `HostInstaller` class with a symmetric reversal surface — no new file, no new dispatcher pattern). `HostUninstallResult` / `HostUninstallFileAction` shapes follow the install-side conventions (`HostInstallResult` / `HostInstallFileAction` in `src/cli/HostInstaller.ts:16-29`) — REUSE of structure. `HostNotYetSupportedError` is reused as-is from 3.3 for the unsupported hosts.
55
-
56
- ## What unblocks for v1.1
57
-
58
- - Story 3.5 (install wizard) — can offer "uninstall and switch host" as a flow now that uninstall is real
59
- - Story 3.6 (docs per host) — claude-code uninstall path documents the exact reverse of install paths; `.bak` restore behavior is a documented feature
60
- - Story 3.7 (gemini-cli + codex installers) — uninstall dispatcher slot is wired; each new host just needs `uninstallForX` alongside its `installForX`
61
- - Story 3.8+ (`--purge` for full wipe including `.openlife/` state) — clear scope boundary now exists: 3.4 is "uninstall from host", purge is "wipe everything"
62
-
63
- ## What this does NOT do
64
-
65
- - Remove `.openlife/` runtime state directory → preserved by design; full wipe is a future `--purge` story
66
- - Mutate `~/.claude.json` to remove MCP server entries → out of scope; 3.3 never wrote there, 3.4 cannot safely remove
67
- - Ship `gemini-cli` or `codex` uninstall → throws `HOST_NOT_YET_SUPPORTED` by design; Story 3.7 wires both sides together
68
- - Remove arbitrary files in `.claude/agents/` → strictly prefix-scoped to `openlife-*.md`; user files survive
69
- - Implement an interactive wizard prompt for uninstall confirmation → Story 3.5 owns wizard UX
70
- - Provide a manifest-based uninstall (read paths from an install-time manifest) → prefix + namespace contract is sufficient for the claude-code surface 3.3 ships
71
- - Documentation updates beyond this story file → Story 3.6
@@ -1,72 +0,0 @@
1
- # Story 3.5 — Interactive install wizard (`openlife init`)
2
-
3
- **StoryId:** `3.5`
4
- **Epic:** `epic-multi-host-installer` (v1.1)
5
- **Status:** InReview
6
- **Severity:** P1 (wizard is the primary UX surface for non-power-users adopting OpenLife)
7
- **Cluster:** install-flow
8
- **Depends on:** Story 3.1 (host enum + validator), Story 3.2 (dist-templates roster), Story 3.3 (host install dispatcher), Story 3.4 (reversible uninstall)
9
-
10
- ## Description
11
-
12
- Stories 3.1–3.4 wired the non-interactive automation surface: `openlife system setup --profile <p> --host <h>` and `openlife system uninstall --host <h>` produce real artifacts and reverse them. That path is correct for scripts and CI, but it presumes the operator already knows the flag matrix. New users do not. This story adds the conversational entry point.
13
-
14
- `openlife init` is a new top-level command that walks the operator through profile, host, LLM model order, Telegram check (autonomous profile only), doctor toggle, and a confirm-preview before calling the same `InstallFlow.run()` Story 3.3 already exercises. The wizard is built on an `AnswerProvider` abstraction so tests inject canned answers instead of spawning a TTY. Re-entry is safe: when `<root>/.openlife/install-manifest.json` exists, the wizard offers `reinstall | repair | abort` before touching anything else. Defaults are pre-filled from `detectHostFromEnv()` and shown in brackets so the operator can blow through prompts with Enter when they do not care.
15
-
16
- ## Acceptance Criteria
17
-
18
- - [x] **New file** `src/cli/InstallWizard.ts` with class `InstallWizard`, `WizardResult` discriminated union, `AnswerProvider` interface, `ReadlineAnswerProvider` (Node built-in `readline`), `CannedAnswerProvider` (test seam, throws `WIZARD_TEST_OUT_OF_ANSWERS` when queue drains under dry-run)
19
- - [x] **New CLI command** `openlife init` registered top-level in `src/index.ts` (not under `system`) using lazy `require('./cli/InstallWizard')` per the contract at `src/index.ts:11-13`
20
- - [x] **Question flow:** existing-install detection → profile (default `framework`) → host (default from `detectHostFromEnv()`) → LLM model order (default chain from `ModelManager`) → Telegram token check (autonomous profile only) → skip-doctor? (default `no`) → confirm-preview
21
- - [x] **Re-entry safety:** detects `<root>/.openlife/install-manifest.json` and offers `reinstall | repair | abort`; `abort` returns `{ ok: false, reason: 'user_aborted' }` before any other question is asked
22
- - [x] **Bracketed defaults:** every prompt renders `Question? [default]:` so Enter accepts the default (universal CLI convention)
23
- - [x] **Unsupported-host warning:** picking `gemini-cli` or `codex` is accepted but appends a warning to `WizardResult.warnings`; the downstream `InstallFlow.run()` already degrades gracefully via Story 3.3's `HostNotYetSupportedError` catch — wizard does not lie about availability
24
- - [x] **Result shape:** success → `{ ok: true, options: InstallFlowOptions, preExistingAction?: 'reinstall' | 'repair', warnings?: string[] }`; failure → `{ ok: false, reason: 'user_aborted' | 'invalid_input', detail?: string }`
25
- - [x] **No new runtime dependencies:** uses Node built-in `readline`; no `inquirer` / `prompts` / `enquirer` added to `package.json`
26
- - [x] **Telegram check (autonomous only):** if `TELEGRAM_BOT_TOKEN` is unset, the wizard prints a non-blocking warning pointing at `.env` setup and continues
27
- - [x] **Invalid input recovery:** the wizard re-prompts up to 3 times on parse failure, then returns `{ ok: false, reason: 'invalid_input', detail }` rather than throwing
28
- - [x] **Regression test** `src/test_install_wizard.ts` covers 9 scenarios via `CannedAnswerProvider` (no TTY spawn): default-everything happy path, framework-profile path, autonomous-profile path with Telegram present, autonomous-profile path with Telegram missing (warning), unsupported-host warning for `gemini-cli`, re-entry detected → `reinstall`, re-entry detected → `repair`, re-entry detected → `abort` (returns `user_aborted`), out-of-answers throws `WIZARD_TEST_OUT_OF_ANSWERS`
29
- - [x] Test wired into `test:all`; suite 67 → 68 verde
30
-
31
- ## Dev Notes
32
-
33
- - **Why an `AnswerProvider` abstraction?** Spawning a real TTY in tests is fragile — pseudo-TTYs differ across WSL / macOS / Linux, prompt timing races break `expect`-style scripts, and CI agents often have no TTY at all. The same `InstallWizard.run()` drives both `ReadlineAnswerProvider` (real users) and `CannedAnswerProvider` (queued answers in tests). Question logic stays decoupled from I/O — the wizard never imports `readline` directly outside the readline provider.
34
- - **Why `openlife init` as a top-level command instead of `openlife system init`?** Discoverability. `init` is the universal "set me up" verb (`npm init`, `git init`, `gh repo init`, `cargo init`). Burying it under `system` would punish first-time users who don't know the subcommand tree yet. `openlife system setup --profile X --host Y` stays as the non-interactive automation path — both surfaces converge on the same `InstallFlow.run()`.
35
- - **Why no new dependencies (e.g., `inquirer`, `prompts`)?** OpenLife's distribution must stay lean — every dep is one more lockfile entry, one more supply-chain surface, one more potential breaking-change in `npm install`. `readline` is built into Node since v0.x and covers everything this wizard needs (line input, history, prompts). The 200 LOC saved by `inquirer` is not worth the 30+ transitive deps it pulls.
36
- - **Why allow unsupported hosts with a warning rather than block?** Story 3.3 already degrades gracefully — `InstallFlow.run()` catches `HostNotYetSupportedError` and reports a skipped host instead of aborting (`src/cli/InstallFlow.ts`). The wizard would be lying if it told users `gemini-cli` is "unavailable"; it's wired through the dispatcher, just no-op for now. Honest UX: "you can pick this, but the host wiring is deferred to Story 3.7 — `.openlife/` will still be set up". Warning + accept matches the rest of the install pipeline.
37
- - **Why surface `preExistingAction` in `WizardResult`?** Downstream callers (printer in `src/index.ts`, future automation) may want to differentiate fresh-install messaging ("🚀 OpenLife installed") from re-entry ("🔧 OpenLife re-applied"). Without this field they would have to re-stat the manifest after the fact, racing against the install they just ran.
38
- - **Why bracketed defaults `[claude-code]`?** Universal CLI convention from `apt`, `npm init`, `git rebase -i`. Lets the operator blow through five prompts with five Enters when they don't care. New users who read the bracket learn the default; power users who don't read it still pick the right thing.
39
- - **Why a separate `WIZARD_TEST_OUT_OF_ANSWERS` error code?** Tests that drain the answer queue mid-flow indicate the wizard added a new prompt the test didn't account for — that's a regression signal, not a wizard bug. A stable error code makes the failure mode obvious in `test:install-wizard` output instead of surfacing as a generic `undefined` deref.
40
- - **Why re-prompt 3× on invalid input instead of failing immediately?** Single-shot rejection on typos is hostile UX in an interactive wizard — the user is at the keyboard, they can fix it. Three attempts is the de facto convention (`sudo`, ssh, every login prompt). Beyond 3, returning `invalid_input` lets automation (or a frustrated user via Ctrl+C → `--auto-confirm` future flag) escape cleanly.
41
-
42
- ## File List
43
-
44
- - `src/cli/InstallWizard.ts` — NEW (class, `AnswerProvider` interface, `ReadlineAnswerProvider`, `CannedAnswerProvider`, `WizardResult` union, `WIZARD_TEST_OUT_OF_ANSWERS` constant, question-flow orchestration)
45
- - `src/index.ts` — MODIFIED (added top-level `init` command using lazy `require('./cli/InstallWizard')`, prints `JSON.stringify(result, null, 2)`, sets `process.exitCode = 1` on `{ ok: false }`)
46
- - `src/test_install_wizard.ts` — NEW (9 test scenarios via `CannedAnswerProvider`, 68th test in suite)
47
- - `package.json` — MODIFIED (added `test:install-wizard` script; appended to `test:all`)
48
-
49
- ## Change Log
50
-
51
- - 2026-05-11 — @dev (Charlie) — Implemented `InstallWizard` with `AnswerProvider` abstraction (`ReadlineAnswerProvider` for TTY users, `CannedAnswerProvider` for tests). New top-level `openlife init` command walks operator through profile → host → model order → Telegram check → doctor toggle → confirm, with re-entry detection via `<root>/.openlife/install-manifest.json` (offers `reinstall | repair | abort`). Unsupported hosts (`gemini-cli`, `codex`) accepted with warning per Story 3.3 graceful-degradation contract. Zero new runtime dependencies. test:all 67 → 68 verde. Status: Ready → InReview.
52
-
53
- ## IDS check
54
-
55
- **Decision:** CREATE (new wizard surface — no prior interactive entry point exists in OpenLife) + ADAPT (`src/index.ts` extended with one top-level command following the lazy-require pattern from 3.3/3.4). `WizardResult` discriminated-union shape REUSEs the `{ ok: true, ... } | { ok: false, reason, detail? }` envelope already established by CLI command handlers (`src/index.ts:58-66`). `AnswerProvider` interface is a new abstraction, justified by test-seam requirements no existing class addresses.
56
-
57
- ## What unblocks for v1.1
58
-
59
- - Story 3.6 (docs per host) — wizard screenshots/transcripts become the "Getting Started" anchor; documentation can recommend `openlife init` as the single onboarding entry point
60
- - Story 3.7 (gemini-cli + codex installers) — wizard already accepts these hosts with a warning; landing the installers just flips the warning to a no-op without any wizard change
61
- - Story 3.8+ (`--auto-confirm` / non-interactive wizard) — `AnswerProvider` abstraction makes this a one-class addition (e.g., `EnvAnswerProvider` reading `OPENLIFE_INIT_*` env vars), no rework of the flow
62
- - Story 3.9+ (uninstall wizard) — `InstallWizard` flow shape is the template; mirror it with `UninstallWizard` calling `HostInstaller.uninstall()` from Story 3.4
63
-
64
- ## What this does NOT do
65
-
66
- - Implement `bin/openlife-mcp.js` (the MCP server itself) → still deferred from 3.3; wizard surfaces a "MCP server pending" note but does not run one
67
- - Auto-merge the staged MCP snippet into `~/.claude.json` → out of scope; same security/UX trap rationale as 3.3 — operator merges manually after wizard exits
68
- - Provide a non-interactive scriptable wizard (`--auto-confirm` / `--answers-file` style) → future enhancement; `AnswerProvider` abstraction is the seam, but no env-driven provider ships in 3.5
69
- - Localize the prompts beyond the few Portuguese strings already present in the codebase → English-default; PT-BR pass is a separate v1.1 docs/localization story
70
- - Ship an uninstall wizard → 3.4 exposes the CLI command; an interactive uninstall flow is deferred to a follow-up story
71
- - Add a new runtime dependency (`inquirer` / `prompts` / etc.) → built on Node's `readline` by design
72
- - Documentation updates beyond this story file → Story 3.6
@@ -1,69 +0,0 @@
1
- # Story 3.6 — Per-host install documentation + doc-parity regression test
2
-
3
- **StoryId:** `3.6`
4
- **Epic:** `epic-multi-host-installer` (v1.1)
5
- **Status:** InReview
6
- **Severity:** P2 (documentation surface, not runtime — but blocks v1.1 onboarding without it)
7
- **Cluster:** install-flow
8
- **Depends on:** Story 3.1 (host enum), Story 3.2 (dist-templates roster), Story 3.3 (host install dispatcher), Story 3.4 (reversible uninstall), Story 3.5 (`openlife init` wizard)
9
-
10
- ## Description
11
-
12
- Stories 3.1–3.5 expanded the installer surface — three host slots, a wizard, reversible uninstall — but the public docs still read as if `openlife system setup` is the only entry point and `claude-code` is the only host that exists. A new user landing on the README today gets a Quick Start that does not match what `openlife --help` advertises, and there is nothing pointing at the per-host nuances (`HOST_NOT_YET_SUPPORTED` for `gemini-cli`/`codex`, manual `~/.claude.json` merge step, `.openlife/install-manifest.json` lifecycle). Story 3.6 closes that gap and adds the regression test that prevents it from re-opening.
13
-
14
- `INSTALL.md` is expanded into a full install handbook: quick start anchored on `openlife init`, a host coverage matrix, the two profiles (`framework` / `autonomous`), non-interactive scripted install via `system setup`, uninstall flow from Story 3.4, and a troubleshooting appendix for the failure modes the wizard and dispatcher actually emit. `README.md` Quick Start is rewritten to recommend `openlife init` first and link to `INSTALL.md` for everything else. Three per-host references land under `docs/install/`: `claude-code.md` is the real reference for the supported path, `gemini-cli.md` and `codex.md` document the `HOST_NOT_YET_SUPPORTED` reserved state — the operator can still pick them in the wizard and `.openlife/` will be created via graceful degradation, but no host-side artifacts ship until Story 3.7. The whole surface is then locked by `src/test_multi_host_docs_parity.ts`, 8 asserts that fail loudly the next time docs drift.
15
-
16
- ## Acceptance Criteria
17
-
18
- - [x] **INSTALL.md** expanded with: (a) `openlife init` quick start, (b) host coverage matrix (`claude-code` supported, `gemini-cli`/`codex` reserved), (c) `framework` vs `autonomous` profile table, (d) non-interactive `system setup --profile <p> --host <h>` recipe, (e) `system uninstall --host <host>` flow per Story 3.4, (f) troubleshooting section covering `HOST_NOT_YET_SUPPORTED`, missing `TELEGRAM_BOT_TOKEN`, manual `~/.claude.json` MCP merge, `.openlife/install-manifest.json` re-entry
19
- - [x] **README.md** Quick Start updated: recommends `openlife init` as the primary onboarding entry point, includes a 3-row host table, links to `INSTALL.md` and to `docs/install/<host>.md`
20
- - [x] **`docs/install/claude-code.md`** — real reference: agent prefix contract (`openlife-*.md`), `commands/openlife/` namespace, MCP snippet at `<root>/.openlife/install-mcp-snippet.json` (NOT auto-merged), `.bak` restore behavior on uninstall, idempotency contract from 3.3/3.4
21
- - [x] **`docs/install/gemini-cli.md`** — documents reserved `HOST_NOT_YET_SUPPORTED` state, explains that the wizard accepts it (`.openlife/` still created via 3.3 graceful-degradation catch), points at Story 3.7 as the unblocking work
22
- - [x] **`docs/install/codex.md`** — same `HOST_NOT_YET_SUPPORTED` template as gemini-cli; documents the dispatcher contract from 3.3
23
- - [x] **Regression test** `src/test_multi_host_docs_parity.ts` with 8 asserts: (1) INSTALL.md mentions `openlife init`, (2) INSTALL.md lists all three hosts by ID, (3) INSTALL.md mentions both profiles `framework` and `autonomous`, (4) INSTALL.md mentions `system uninstall`, (5) README.md Quick Start mentions `openlife init`, (6) `docs/install/claude-code.md` mentions `.openlife/install-mcp-snippet.json` (the manual-merge contract), (7) `docs/install/gemini-cli.md` mentions `HOST_NOT_YET_SUPPORTED`, (8) `docs/install/codex.md` mentions `HOST_NOT_YET_SUPPORTED`
24
- - [x] Test wired into `test:all` via `test:multi-host-docs-parity`; suite 68 → 69 verde
25
-
26
- ## Dev Notes
27
-
28
- - **Why a doc-parity regression test instead of trusting reviewers?** OpenLife's own audit on 2026-05-11 flagged 124 doc-vs-code parity gaps accumulated since v1.0. Reviewers consistently catch code drift; they consistently miss doc drift because the diff for "code changed, docs unchanged" looks fine in isolation. A deterministic test that greps the doc files for stable anchors (`openlife init`, host IDs, `HOST_NOT_YET_SUPPORTED`) flips that failure mode into a red `npm run test:all`. Cheap, blunt, effective.
29
- - **Why INSTALL.md in English when the codebase is mixed PT-BR/EN?** Convention of the existing file: `INSTALL.md` is already English, and the public docs surface (`README.md`, `docs/install/*`) is what external adopters read first. Inline comments in `src/` stay mixed (operator-facing console output uses Portuguese strings in some places per `src/index.ts:247`), but public docs converge on English until a deliberate localization pass lands. PT-BR localization is explicitly out of scope here.
30
- - **Why ship stub docs for `gemini-cli` / `codex` instead of removing them from the wizard menu?** Story 3.5 deliberately accepts these hosts with a warning because Story 3.3's dispatcher degrades gracefully — the operator CAN pick them today, `.openlife/` still gets created. Hiding the option in docs while the wizard accepts it would be the worst kind of doc drift: the user sees the prompt, picks `gemini-cli`, gets a "skipped, host not yet supported" result, and has no doc to ground that message. Honest documentation matches the wizard surface; the stubs are short on purpose, but they exist.
31
- - **Why call out the `~/.claude.json` manual-merge step so loudly?** This is the #1 failure-mode trap from Story 3.3 — the MCP snippet is staged at `<root>/.openlife/install-mcp-snippet.json` and never auto-merged into the user's global Claude config (security/UX rationale from 3.3 stands). Without a doc note, the user runs `openlife init`, sees the success message, opens Claude Code, and wonders why no OpenLife MCP tools appear. A two-sentence call-out in `INSTALL.md` and `docs/install/claude-code.md` saves a support round-trip and prevents a "the installer is broken" issue that is actually a "the docs did not warn you" issue.
32
- - **Why split `docs/install/<host>.md` out of `INSTALL.md` instead of one big file?** Reading-curve separation. `INSTALL.md` is the overview a casual adopter reads end-to-end (quick start + matrix + troubleshooting). `docs/install/<host>.md` is the reference a power user dives into for the specific host they care about (`claude-code` for now). Mixing them forces the casual reader to skim past gemini-cli/codex reserved-state notes they do not need, and forces the power user to grep through quick-start narrative they already know. Same split convention as `npm` (`README` overview + `docs/cli-commands/<verb>.md` per command).
33
- - **Why are the doc-parity asserts greps and not Markdown-AST parsing?** A grep for stable anchor strings (`openlife init`, `HOST_NOT_YET_SUPPORTED`, `system uninstall`) is brittle in the right direction — if a future PR rewrites the docs without preserving the contract, the test fails. AST parsing would let rephrasing pass silently. The goal is to catch silent removal, not enforce specific phrasing. Eight asserts is the minimum that covers the surface 3.1–3.5 expanded; more asserts = more false positives on benign rewordings.
34
- - **Why is this Severity P2 instead of P1?** No runtime code path is broken without these docs — `openlife init`, `system setup`, and `system uninstall` all work today (3.3/3.4/3.5). But v1.1 cannot ship with a Quick Start that does not mention the new primary entry point. P2 = "blocks the release, does not block a running production install".
35
-
36
- ## File List
37
-
38
- - `INSTALL.md` — MODIFIED (quick start anchored on `openlife init`, host matrix, profile table, non-interactive recipe, uninstall flow, troubleshooting appendix)
39
- - `README.md` — MODIFIED (Quick Start rewritten to lead with `openlife init`, 3-row host table, links to INSTALL.md and `docs/install/<host>.md`)
40
- - `docs/install/claude-code.md` — NEW (per-host reference: prefix contract, namespace, MCP snippet manual-merge, `.bak` restore, idempotency)
41
- - `docs/install/gemini-cli.md` — NEW (reserved `HOST_NOT_YET_SUPPORTED` doc; points at Story 3.7)
42
- - `docs/install/codex.md` — NEW (reserved `HOST_NOT_YET_SUPPORTED` doc; points at Story 3.7)
43
- - `src/test_multi_host_docs_parity.ts` — NEW (8 grep-based asserts, 69th test in suite)
44
- - `package.json` — MODIFIED (added `test:multi-host-docs-parity` script; appended to `test:all`)
45
-
46
- ## Change Log
47
-
48
- - 2026-05-11 — @dev (Charlie) — Documented the installer surface expanded by 3.1–3.5. Rewrote `INSTALL.md` and `README.md` Quick Start around `openlife init`. Added `docs/install/{claude-code,gemini-cli,codex}.md` — claude-code as real reference, other two as `HOST_NOT_YET_SUPPORTED` stubs aligned with Story 3.5's accept-with-warning contract. Added `src/test_multi_host_docs_parity.ts` (8 asserts) to lock doc anchors against drift. test:all 68 → 69 verde. Status: Ready → InReview.
49
-
50
- ## IDS check
51
-
52
- **Decision:** CREATE (three new per-host doc files + one new regression test — no prior per-host doc structure exists) + ADAPT (`INSTALL.md`, `README.md`, `package.json` extended with the same patterns already established). Doc-parity test pattern REUSEs the grep-based assert style from existing `test_*` files (e.g., `test_openlife_runtime_source_truth.ts`'s string-presence asserts). No new framework, no new test harness — same standalone `test_*.ts` convention.
53
-
54
- ## What unblocks for v1.1
55
-
56
- - v1.1 public release — Quick Start now matches the surface `openlife --help` advertises; adopters can complete onboarding without reading source
57
- - Story 3.7 (gemini-cli + codex installers) — when those hosts land, the existing stub docs flip from "reserved" to "supported" with a focused edit, parity test anchors stay stable
58
- - Story 3.8+ (localization pass) — English baseline is now consistent across `INSTALL.md`, `README.md`, and `docs/install/*`; a PT-BR mirror has one canonical source to translate from
59
- - Future doc audits — the doc-parity test pattern can be extended (more asserts, more anchor files) as new install features land, without inventing new test infrastructure
60
-
61
- ## What this does NOT do
62
-
63
- - Localize docs into PT-BR → English baseline only; deferred to a dedicated localization story
64
- - Add screenshots, GIFs, or asciinema casts of the wizard → text-only docs in this pass; visual assets are a separate polish story
65
- - Document DEPS troubleshooting (Node version mismatches, `ffmpeg` missing, Ollama unreachable) → scope is install/uninstall flow only; runtime-deps troubleshooting belongs in a `DEPS.md` story
66
- - Document the autonomous-profile Telegram bot integration end-to-end (token provisioning, `OPENLIFE_TELEGRAM_ALLOWED_USER_ID`, webhook setup) → INSTALL.md mentions the env var as a warning anchor; full Telegram onboarding is a separate doc story
67
- - Auto-merge `~/.claude.json` MCP entries → still out of scope per 3.3/3.4; docs only describe the manual merge
68
- - Ship `gemini-cli` or `codex` installers → Story 3.7
69
- - Add an interactive uninstall wizard doc → CLI-only documented; wizard surface is `openlife init`, uninstall stays scriptable for now
@@ -1,68 +0,0 @@
1
- # Story 3.7 — End-to-end install flow tests (CLI spawn against `node bin/openlife.js`)
2
-
3
- **StoryId:** `3.7`
4
- **Epic:** `epic-multi-host-installer` (v1.1)
5
- **Status:** InReview
6
- **Severity:** P2 (test infrastructure — valuable safety net for the installer surface, but does not block release on its own)
7
- **Cluster:** install-flow
8
- **Depends on:** Story 3.1 (host enum + validator), Story 3.2 (dist-templates roster), Story 3.3 (`HostInstaller` dispatcher), Story 3.4 (reversible uninstall), Story 3.5 (`openlife init` wizard), Story 3.6 (per-host docs + doc-parity test)
9
-
10
- ## Description
11
-
12
- Stories 3.1–3.6 each shipped focused unit tests against the classes they introduced — `HostInstaller`, `InstallFlow`, `InstallWizard`, and the doc-parity grep suite. Those tests prove each class is internally correct, but they do **not** prove the CLI binary is correct. The wiring between Commander flags, the lazy-require chain in `src/index.ts`, the JSON output contract, exit codes, and the side effects landed under `.openlife/` and `.claude/` only converges when `node bin/openlife.js` is the entry point — and that is exactly the path a real operator (or a CI script) takes. Bugs at that boundary — a flag that never reaches the installer class, a lazy `require()` accidentally promoted to module scope, an error envelope that prints fine but exits 0 — pass every unit test in the suite and only fail when somebody runs the binary.
13
-
14
- Story 3.7 closes that gap with `src/test_host_install_e2e.ts`: six spawn-based scenarios that drive `node bin/openlife.js` against a fresh temp project root and assert on real filesystem side effects, real exit codes, and the real JSON envelope printed to stdout. The test owns its own temp roots (cleaned in `finally`), uses 60 second per-spawn timeouts to stay healthy on WSL, and is wired into `test:all` as the 70th test.
15
-
16
- ## Acceptance Criteria
17
-
18
- - [x] **`src/test_host_install_e2e.ts`** covers 6 scenarios end-to-end via `child_process.spawnSync(process.execPath, ['bin/openlife.js', ...])`:
19
- 1. `claude-code` full cycle: `system setup --profile framework --host claude-code` exits 0, writes `.openlife/`, `.claude/agents/openlife-*.md`, `.claude/commands/openlife/*.md`, and `.openlife/install-mcp-snippet.json`; `system status --host claude-code` reports installed; `system uninstall --host claude-code` exits 0 and removes the host-side artifacts while preserving `.openlife/` state per Story 3.4
20
- 2. `gemini-cli` graceful degradation: `system setup --profile framework --host gemini-cli` exits 0, JSON envelope contains the skipped-host record with `HOST_NOT_YET_SUPPORTED`, and `.openlife/` is still created by `SystemInstaller`
21
- 3. `codex` graceful degradation: same shape as scenario 2, exit 0, skipped-host record, `.openlife/` still created
22
- 4. Invalid host rejected at CLI boundary: `system setup --profile framework --host cursor` exits non-zero with a clear `invalid_host` envelope (validates Story 3.1's `validateHost()` is wired before any installer logic)
23
- 5. Uninstall on a clean project is idempotent: `system uninstall --host claude-code` against a temp dir with no prior install exits 0, reports "nothing to remove" cleanly, leaves no stray files
24
- 6. Reinstall is idempotent: running `system setup --profile framework --host claude-code` twice back-to-back produces the same final filesystem state, second run reports `skipped-identical` for unchanged templates and does not duplicate `.openlife/install-manifest.json` entries
25
- - [x] Each spawn uses a 60s timeout (`spawnSync({ timeout: 60_000 })`) — generous enough for WSL + Node require-tree warm-up without hanging CI
26
- - [x] Each scenario cleans its temp root in a `finally` block (`fs.rmSync(tmpRoot, { recursive: true, force: true })`) so a failing assertion does not leak directories across runs
27
- - [x] Assertions are explicit on **both** exit code (`status === 0`) and filesystem side effects (`fs.existsSync(...)`, `JSON.parse(...)` of the manifest); no scenario relies on stdout-only signals
28
- - [x] Test wired in `package.json` (new `test:host-install-e2e` script) and appended to the `test:all` chain
29
- - [x] Suite 69 → 70 verde — full `npm run test:all` passes locally after the new test joins the chain
30
-
31
- ## Dev Notes
32
-
33
- - **Why is an E2E pass separate from the unit tests already in 3.3–3.5?** Unit tests prove the class is correct. E2E proves the binary is correct. The two failure modes are different: the most expensive bugs in CLI tooling are wiring bugs ("the flag is parsed but never reaches the installer", "the JSON envelope prints to stderr instead of stdout", "the exit code is 0 even though we threw") and those bugs pass every unit test in the suite because the classes themselves are fine. Only spawning the binary exposes them. Six scenarios is the minimum that exercises every host slot Story 3.1–3.5 introduced plus the two idempotency contracts the installer promises.
34
- - **Why spawn `node bin/openlife.js` instead of calling Commander programmatically in-process?** Fidelity. Programmatic invocation re-uses the test process's `require.cache`, leaks env vars, shares `process.exit` semantics with the test runner, and bypasses the `bin/openlife.js → dist/index.js` shim that real users actually hit. Spawning gives us the same require-tree latency, the same env-var inheritance, the same stdio buffering, and the same exit-code propagation a real install gets — which is precisely the surface we want to lock.
35
- - **Why 60s per-spawn timeouts?** Cold-starting `node bin/openlife.js` on WSL with the current require chain is empirically slow on the heavier paths — `Brain.ts` pulls in three SDKs (~10s), `Gateway.ts` pulls Telegraf + Express (~21s), and `Gatekeeper.ts` chains both (~23s). The lazy-import contract in `src/index.ts:11-13` keeps `--help`, `plugin`, and `context` fast, but `system setup` is one of the paths that legitimately touches the heavier wiring. 60s is a comfortable ceiling that catches genuinely hung processes (infinite loops, deadlocks) without flapping on a slow first run.
36
- - **Why test the invalid-host case (scenario 4) at the CLI boundary instead of trusting `test_host_validator.ts` from Story 3.1?** `validateHost()` is correct as a unit, but it only protects the installer surface if it is actually called before any other logic in the `system setup` command handler. Promoting `validateHost()` accidentally below the lazy-require for `InstallFlow` would silently allow an invalid host to reach the installer with `--host cursor`, fail there with a confusing error, and pass every unit test. Spawning with `--host cursor` is the only way to prove the validator runs at the boundary it claims to.
37
- - **Why test reinstall idempotency (scenario 6) explicitly?** Operators re-run `system setup` whenever `dist-templates/` is updated upstream — that is the documented path for picking up a new agent file. If the second run duplicated entries in `.openlife/install-manifest.json`, wrote conflicting `.bak` files, or worse threw on perceived conflicts, the operator would experience a silent UX regression and would not have a unit test catching it (the `HostInstaller` unit tests in 3.3 exercise idempotency, but only for the installer class — not for the full install flow including manifest persistence). E2E is where the contract earns its keep.
38
- - **Why test uninstall idempotency (scenario 5)?** CI scripts and rollback automations call `system uninstall` defensively, often in series — "uninstall any prior version, then install the new one". If the first uninstall on a clean tree threw or exited non-zero, every downstream rollback script would have to add a wrapper to swallow the error. Story 3.4 promises idempotent uninstall; this scenario proves the promise survives the CLI boundary.
39
- - **Why is this Severity P2 rather than P1?** No customer-visible feature is broken without this test — the install flow works today and the unit tests already prove the classes are correct. What this story adds is *insurance*: the next time somebody refactors the lazy-require block, renames a CLI flag, or "cleans up" the JSON envelope, the E2E suite turns a silent regression into a red `test:all`. Valuable, but the release does not literally fail without it. P2 is the right tier — adjacent to 3.6's doc-parity test, same logic: a deterministic regression net for surfaces that reviewers and unit tests both routinely miss.
40
-
41
- ## File List
42
-
43
- - `src/test_host_install_e2e.ts` — NEW (6 spawn-based scenarios; per-scenario temp-root setup/teardown; 70th test in suite)
44
- - `package.json` — MODIFIED (added `test:host-install-e2e` script; appended to `test:all`)
45
-
46
- ## Change Log
47
-
48
- - 2026-05-11 — @dev (Charlie) — Added end-to-end install-flow tests that spawn `node bin/openlife.js` against fresh temp project roots. Six scenarios: claude-code full cycle (setup → status → uninstall), gemini-cli graceful degradation, codex graceful degradation, invalid host (`cursor`) rejected at CLI boundary, uninstall idempotent on clean project, reinstall idempotent. Each spawn capped at 60s for WSL safety; temp dirs cleaned in `finally` even on assertion failure. Wired into `test:all` via `test:host-install-e2e`. Suite 69 → 70 verde. Status: Ready → InReview.
49
-
50
- ## IDS check
51
-
52
- **Decision:** CREATE (no prior E2E spawn-based test exists in the suite — the closest analog, `test_enterprise_agentic_core.ts`, spawns the CLI but for a single plugin command, not the full install/status/uninstall lifecycle) + REUSE (the `spawnSync(process.execPath, ['bin/openlife.js', ...])` pattern is the same shape `test_enterprise_agentic_core.ts` already uses; temp-root setup follows the same `os.tmpdir()` + `fs.mkdtempSync` convention as the unit tests in 3.3/3.4; assertion style follows the throw-on-failure / log-on-success convention used across the entire `src/test_*.ts` family). No new test harness, no new framework — same standalone `test_*.ts` convention.
53
-
54
- ## What unblocks for v1.1
55
-
56
- - v1.1 release confidence — every refactor of the installer surface (flag rename, lazy-require shuffle, JSON envelope tweak) now has a deterministic safety net that runs in under a minute of `test:all`
57
- - Story 3.8+ (real `gemini-cli` / `codex` installers) — when those land, scenarios 2 and 3 flip from "asserts skipped-host record" to "asserts host-side artifacts written" with a focused diff, structure of the test file stays stable
58
- - Future installer features (per-host MCP auto-merge, alternate profile slots, agent prefix overrides) — each gets an E2E scenario appended to the same file, same harness, no new tooling
59
- - CI escalation — when the project moves from local-only `test:all` to a hosted CI runner, the E2E suite is what proves the binary works in a fresh container, not just in the developer's warm WSL
60
-
61
- ## What this does NOT do
62
-
63
- - E2E tests for the interactive `openlife init` wizard → spawning an interactive TTY reliably requires either a mocked PTY layer or `expect(1)`-style scripting; both are heavyweight and the wizard is already covered by `test_install_wizard.ts` at the unit level. Deferred to a separate story if the wizard surface grows or churns.
64
- - Live integration tests against real external services (Telegram bot, OpenAI API, Gemini API) → those belong in `test:integrations`, which is a separate chain by design (per `CLAUDE.md`'s test budget contract); this story keeps `test:all` deterministic and credential-free.
65
- - Performance benchmarks of install duration → 60s timeout is a hang-detector, not a perf budget. Install perf is not a functional regression and a flaky timing assert would do more harm than good.
66
- - Tests inside virtual hosts (containers, Docker, full VMs) → that is release-engineering / CI-runner territory, not story-level work. The E2E suite proves the binary works on the developer's host; the CI pipeline (separate concern) proves it works on a clean runner.
67
- - Cross-OS coverage (native Linux without WSL, macOS, Windows PowerShell) → the suite runs wherever Node 18+ runs, but explicit per-OS assertions belong in a dedicated cross-platform story if/when adoption demands it.
68
- - Coverage for `system doctor`, `plugin install`, or other unrelated CLI surfaces → scope is strictly the install/status/uninstall lifecycle introduced by 3.1–3.5; other CLI surfaces stay covered by their own unit tests.
@@ -1,57 +0,0 @@
1
- # Story 3.8 — Document and lock the `OPENLIFE_RUNTIME_PROFILE=oauth-only` profile
2
-
3
- **StoryId:** `3.8`
4
- **Epic:** `epic-multi-host-installer` (v1.1) — **FINAL story**
5
- **Status:** InReview
6
- **Severity:** P2 (closes a doc-vs-code gap; behavior shipped but undocumented and untested)
7
- **Cluster:** install-flow
8
- **Depends on:** Story 3.1–3.7 (host enum, dist-templates, HostInstaller, uninstall, wizard, multi-host docs, E2E tests)
9
-
10
- ## Description
11
-
12
- The `OPENLIFE_RUNTIME_PROFILE=oauth-only` profile has existed in `src/cli/WorldClassCommands.ts:23` for several iterations. Inside `doctorWorldDetailed()`, the classifier downgrades four doctor checks from their default severity to `info` when this env var is set: `cli:ollama`, `env:OPENAI_API_KEY`, `env:GEMINI_API_KEY`, and `env:ANTHROPIC_API_KEY`. The rationale: operators running OpenLife exclusively through OAuth-authenticated CLI executors (`claude`, `codex`, `gemini` CLIs) do not need API keys, and Ollama is optional in that posture — so the doctor should report those absences as informational, not as blockers that fail health checks.
13
-
14
- The codebase audit on 2026-05-11 (observation #1379) flagged this as the kind of feature that ships, works, and silently rots: no documentation, no regression test, no surface in `INSTALL.md`, no entry in any per-host reference. The next operator who refactors the classifier could quietly break the contract and nothing in the test suite would catch it. Story 3.8 closes that gap before v1.1 ships.
15
-
16
- This is a **documentation + regression-test only** story. The feature itself is not changed — `WorldClassCommands.ts` is untouched.
17
-
18
- ## Acceptance Criteria
19
-
20
- - [x] **`src/test_runtime_profile_oauth_only.ts`** covers the parity contract with 4 scenarios:
21
- 1. **Default profile (no env var)** — Running `doctorWorldDetailed()` with `OPENAI_API_KEY`, `GEMINI_API_KEY`, `ANTHROPIC_API_KEY` cleared keeps each failing check at its default severity (`cli:ollama` → `blocker`, `env:*` → `warning`); no check downgrades to `info` purely from being `ok: false`
22
- 2. **oauth-only downgrade** — Setting `OPENLIFE_RUNTIME_PROFILE=oauth-only` reclassifies all four affected checks (`cli:ollama`, `env:OPENAI_API_KEY`, `env:GEMINI_API_KEY`, `env:ANTHROPIC_API_KEY`) to `severity: 'info'` when they come back `ok: false`
23
- 3. **Case-insensitive matching** — Setting `OPENLIFE_RUNTIME_PROFILE=OAuth-Only` produces the same downgrade behavior (the classifier compares via `.toLowerCase()`)
24
- 4. **Unrelated checks remain unaffected** — Failing checks whose names do not match the four oauth-downgrade names (e.g. `runtime:*`, `cli:codex`, `cli:claude`) keep their non-`info` severity even when oauth-only is active
25
- - [x] Test prints `TEST_RUNTIME_PROFILE_OAUTH_ONLY_OK` on success, throws or exits non-zero on failure (matches the convention from `test_multi_host_docs_parity.ts` and other tests)
26
- - [x] Test owns its temp dir via `mkdtempSync` and restores `process.cwd()` plus env-var snapshot in `finally` so it is hermetic
27
- - [x] Wired in `package.json`: new `test:runtime-profile-oauth-only` script + appended to `test:all`
28
- - [x] **`INSTALL.md`** has a new "Runtime profiles (advanced)" subsection (~15-25 lines) explaining what `oauth-only` does, when to use it, which 4 checks are affected, and that it is purely a severity classifier (provider chain unchanged)
29
- - [x] **`docs/install/runtime-profiles.md`** — dedicated advanced reference (~80 lines) with env-var table, worked before/after example, "what it does NOT do", forward-compatibility note, cross-link to `INSTALL.md`
30
- - [x] Suite 70 → 71 verde — full `npm run test:all` passes locally after the new test joins the chain
31
-
32
- ## Dev Notes
33
-
34
- - **Why test a feature instead of building one?** The codebase audit (#1379) found that `oauth-only` shipped without docs or coverage. That is the classic "feature fantasma" pattern: works today, will silently break the next time somebody touches the classifier, no test catches it. The cheapest hardening pass for v1.1 is to bind the existing behavior to a regression test and surface it in the user-facing docs — before v1.1 freezes.
35
- - **Why is the test scenario explicit about the default severities (`blocker` for `cli:ollama`, `warning` for `env:*`)?** During implementation we discovered the classifier's default branches actually differ per check name — `cli:ollama` hits the `name.startsWith('cli:')` branch (`blocker`), while the three `env:*` keys fall through to the catch-all (`warning`). Writing the test as "all four downgrade to `info`" is correct; writing it as "all four go from `blocker` to `info`" would have been wrong. The test, the INSTALL.md section, and `docs/install/runtime-profiles.md` all describe the downgrade as "to `info`" rather than "from blocker to info" so the documentation cannot drift from the actual classifier shape.
36
- - **Why a dedicated `docs/install/runtime-profiles.md` instead of folding everything into `INSTALL.md`?** Casual users do not care — they install with API keys and the doctor is happy. The advanced subsection in `INSTALL.md` is a breadcrumb for those users so they know the feature exists; the dedicated doc is for the operator who actually runs OAuth-only and wants the exact before/after output, the forward-compatibility contract, and the explicit "what this does NOT do" list. Splitting keeps `INSTALL.md` readable for the 95% path and gives the 5% path the depth it needs.
37
- - **Why mark the env var as forward-compatible?** `OPENLIFE_RUNTIME_PROFILE` is a single env var with a single recognized value today (`oauth-only`), but the obvious next values are `local-only` (downgrade cloud API checks, keep Ollama as blocker), `enterprise-strict` (treat any non-info finding as blocker), and `ci-mode` (suppress doctor noise entirely). Documenting the env var as a profile slot rather than a single boolean toggle means the next person to add a value does not need to rename the contract, and the test can grow alongside it.
38
- - **Why is the test against the compiled `dist/cli/WorldClassCommands.js` rather than the TypeScript source?** Every other test in `src/test_*.ts` follows the same pattern: build first, then run the compiled artifact under `node dist/`. The test seam in this story re-uses `require('./dist/cli/WorldClassCommands.js')` (via the build chain in the npm script) so it stays consistent with `test_host_uninstaller.ts` and `test_multi_host_docs_parity.ts`. Going around `tsc` would also bypass the `strict: true` contract — a regression would compile fine in a forgiving runtime and silently slip through.
39
- - **Why is this Severity P2?** No customer-visible feature breaks without this story — `oauth-only` already works for the operators who happen to know about it. What is missing is *discoverability* (no one knows the feature exists) and *durability* (no test guards it). P2 matches the same logic as Story 3.6 (doc-parity) and Story 3.7 (E2E tests): valuable insurance, not release-blocking on its own. Together they form the safety net under v1.1.
40
- - **Why is this the final story of v1.1?** The epic's stated goal is a "complete and hardened multi-host installer surface". Stories 3.1–3.5 built the surface (enum, templates, install, uninstall, wizard). Stories 3.6–3.7 hardened the surface (per-host docs, doc-parity test, end-to-end CLI tests). Story 3.8 catches the last known doc-vs-code gap. With it closed, the epic delivers what it promised. Subsequent work (gemini-cli and codex installers behind their reserved hosts; new runtime profiles) is the next epic's scope.
41
-
42
- ## File List
43
-
44
- - `src/test_runtime_profile_oauth_only.ts` — NEW (4 scenarios; env-var snapshot/restore; 71st test in suite)
45
- - `package.json` — MODIFIED (added `test:runtime-profile-oauth-only` script; appended to `test:all` chain)
46
- - `INSTALL.md` — MODIFIED (new "Runtime profiles (advanced)" subsection cross-linking to the dedicated reference)
47
- - `docs/install/runtime-profiles.md` — NEW (advanced reference: env-var contract table, worked before/after example, forward-compatibility note)
48
-
49
- ## Change Log
50
-
51
- - 2026-05-12 — @dev (Charlie) — Documented and locked the `OPENLIFE_RUNTIME_PROFILE=oauth-only` profile that has existed in `WorldClassCommands.ts:23` without docs or tests since pre-v1.1. New regression test (`src/test_runtime_profile_oauth_only.ts`) asserts the four-check downgrade contract with 4 scenarios: default severities preserved without env var; oauth-only downgrades `cli:ollama`, `env:OPENAI_API_KEY`, `env:GEMINI_API_KEY`, `env:ANTHROPIC_API_KEY` to `info`; case-insensitive matching; unrelated checks unaffected. New advanced subsection in `INSTALL.md` and dedicated `docs/install/runtime-profiles.md` reference document the contract for operators. Suite 70 → 71 verde. Status: Ready → InReview. **Final story of v1.1 epic.**
52
-
53
- ## IDS check
54
-
55
- - **REUSE:** Test structure reuses the env-var snapshot/restore pattern from `test_multi_host_docs_parity.ts` and the temp-dir + `process.chdir()` pattern from `test_host_uninstaller.ts`.
56
- - **ADAPT:** Documentation structure adapts the per-host reference layout from `docs/install/claude-code.md` to the orthogonal "runtime profile" concept.
57
- - **CREATE:** New artifacts: `src/test_runtime_profile_oauth_only.ts` (new test surface — runtime profile classification has no prior coverage), `docs/install/runtime-profiles.md` (new doc surface — runtime profiles were undocumented). Both registered under `epic-multi-host-installer` and cross-referenced from `INSTALL.md`.