arkaos 2.22.1 → 2.25.0

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 (48) hide show
  1. package/README.md +105 -9
  2. package/VERSION +1 -1
  3. package/arka/SKILL.md +1 -0
  4. package/arka/skills/comfyui/SKILL.md +2 -2
  5. package/arka/skills/comfyui/references/workflows.md +6 -6
  6. package/arka/skills/costs/SKILL.md +11 -0
  7. package/config/cognition/prompts/dreaming.md +3 -3
  8. package/config/cognition/prompts/research.md +4 -4
  9. package/config/hooks/user-prompt-submit.sh +6 -1
  10. package/core/cognition/__pycache__/auto_documentor.cpython-313.pyc +0 -0
  11. package/core/cognition/capture/__pycache__/collector.cpython-313.pyc +0 -0
  12. package/core/cognition/capture/collector.py +10 -3
  13. package/core/cognition/scheduler/__pycache__/daemon.cpython-313.pyc +0 -0
  14. package/core/cognition/scheduler/daemon.py +5 -0
  15. package/core/jobs/__pycache__/auto_doc_worker.cpython-313.pyc +0 -0
  16. package/core/obsidian/__pycache__/writer.cpython-313.pyc +0 -0
  17. package/core/obsidian/writer.py +5 -4
  18. package/core/runtime/__pycache__/base.cpython-313.pyc +0 -0
  19. package/core/runtime/__pycache__/claude_code.cpython-313.pyc +0 -0
  20. package/core/runtime/__pycache__/codex_cli.cpython-313.pyc +0 -0
  21. package/core/runtime/__pycache__/gemini_cli.cpython-313.pyc +0 -0
  22. package/core/runtime/__pycache__/llm_provider.cpython-313.pyc +0 -0
  23. package/core/runtime/__pycache__/path_resolver.cpython-313.pyc +0 -0
  24. package/core/runtime/path_resolver.py +202 -0
  25. package/core/shared/__pycache__/__init__.cpython-313.pyc +0 -0
  26. package/core/shared/__pycache__/safe_session_id.cpython-313.pyc +0 -0
  27. package/core/specs/SPEC-installer-cross-os.md +227 -0
  28. package/core/specs/SPEC-paths-portability.md +209 -0
  29. package/core/synapse/__pycache__/kb_cache.cpython-313.pyc +0 -0
  30. package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
  31. package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
  32. package/core/workflow/__pycache__/marker_cache.cpython-313.pyc +0 -0
  33. package/core/workflow/__pycache__/research_gate.cpython-313.pyc +0 -0
  34. package/departments/dev/skills/scaffold/SKILL.md +4 -4
  35. package/departments/kb/skills/knowledge/SKILL.md +1 -1
  36. package/departments/ops/skills/operations/SKILL.md +1 -1
  37. package/installer/cli.js +2 -1
  38. package/installer/doctor.js +48 -0
  39. package/installer/index.js +26 -1
  40. package/installer/migrate-user-data.js +17 -1
  41. package/installer/migrations/v3_path_schema.js +109 -0
  42. package/installer/package-manager.js +191 -0
  43. package/installer/path-resolver.js +124 -0
  44. package/installer/system-tools.js +243 -0
  45. package/installer/update.js +24 -3
  46. package/knowledge/obsidian-config.json +1 -1
  47. package/package.json +1 -1
  48. package/pyproject.toml +1 -1
@@ -0,0 +1,227 @@
1
+ ---
2
+ type: spec
3
+ status: approved
4
+ feature: installer-cross-os
5
+ project: ArkaOS
6
+ version: 2.24.0
7
+ pr: PR2 of 6 (Conclave roadmap 2026-05-13)
8
+ branch: feature/v2.24.0-installer-cross-os
9
+ date_created: 2026-05-13
10
+ tags: [spec, arkaos, installer, cross-os, v2.24.0, conclave-pr2]
11
+ ---
12
+
13
+ # SPEC: Installer Cross-OS Completo (v2.24.0)
14
+
15
+ ## Overview
16
+
17
+ **Problem.** ArkaOS installer is ~65 % cross-OS complete. Obsidian is never auto-installed (only path-detected). Node.js is never validated. `install.sh` hardcodes the GitHub repo URL on line 25 and only scans macOS paths for Obsidian vaults on lines 625–640.
18
+
19
+ **Goal.** Deliver a full cross-OS install flow (macOS / Linux / Windows) that auto-installs what it can without sudo, reports what needs sudo with copy-paste-ready commands, and falls back gracefully when no package manager is available.
20
+
21
+ **Actors.** End users running `npx arkaos install` on fresh macOS / Linux / Windows machines; CI pipelines running headless.
22
+
23
+ ## Scope
24
+
25
+ **In scope (PR2 / v2.24.0):**
26
+
27
+ | Item | Purpose |
28
+ |---|---|
29
+ | new `installer/package-manager.js` | brew / winget / snap / apt / choco detection + install helpers |
30
+ | new `installer/system-tools.js` | `ensureSystemTools()` phase: Obsidian, Node.js ≥ 20, Python ≥ 3.12 |
31
+ | modify `installer/index.js` | call `ensureSystemTools()` before venv creation |
32
+ | modify `installer/update.js` | re-run `ensureSystemTools()` on `npx arkaos update` |
33
+ | modify `installer/cli.js` | new `npx arkaos doctor` subcommand + `--no-system` flag |
34
+ | new `installer/doctor.js` | diagnose-only report (table format, exit codes 0/1/2) |
35
+ | modify `install.sh` line 25 | `ARKAOS_REPO_URL` env override (default canonical URL) |
36
+ | modify `install.sh` lines 625-640 | add Linux + Windows vault detection |
37
+ | modify `installer/index.js` dashboard step | `pnpm install` / `npm ci` in `dashboard/` if Node ≥ 20 |
38
+ | new `tests/installer/test_package_manager.mjs` | ≥ 10 Node tests |
39
+ | new `tests/installer/test_system_tools.mjs` | ≥ 5 Node tests |
40
+
41
+ **Out of scope:**
42
+
43
+ - Auto-update of system tools when newer versions exist (just detect + report).
44
+ - Flatpak / AppImage Obsidian variants on Linux (defer to PR6 cleanup if user demand).
45
+ - Interactive sudo prompt (André chose "print command for manual run" — headless-safe).
46
+ - Aggressive auto-install (André chose hybrid: install what can be installed without sudo; report rest).
47
+
48
+ ## Acceptance Criteria
49
+
50
+ 1. **Fresh macOS install.** Given a Mac without Obsidian, when `npx arkaos install` runs, then `brew install --cask obsidian` is invoked (or printed if brew missing) and Obsidian binary is detected on completion.
51
+
52
+ 2. **Fresh Ubuntu install.** Given Ubuntu without Obsidian / Node, when `npx arkaos install` runs, then the installer prints copy-paste commands like:
53
+ ```
54
+ To finish install, run:
55
+ sudo apt update && sudo apt install -y nodejs
56
+ sudo snap install obsidian --classic
57
+ Then re-run: npx arkaos install
58
+ ```
59
+ and exits with code 0 (informational, not a failure).
60
+
61
+ 3. **Fresh Windows 11.** Given Windows without Obsidian / Node, when `npx arkaos install` runs, then it invokes `winget install --id Obsidian.Obsidian --silent` (and similar for Node), or prints the command if elevation is denied.
62
+
63
+ 4. **`npx arkaos doctor`.** Given any state, when `npx arkaos doctor` runs, then it prints a table with Obsidian / Node / Python rows showing installed/version/location/action, with exit code 0 (all OK), 1 (missing), or 2 (version mismatch).
64
+
65
+ 5. **`install.sh` GitHub URL configurable.** Given `ARKAOS_REPO_URL=https://gitlab.example.com/x/y.git` is set, when `install.sh` runs in curl|bash mode, then it clones from the custom URL.
66
+
67
+ 6. **`install.sh` Linux vault detection.** Given Ubuntu user with vault at `~/.local/share/obsidian/vault` OR `~/Documents/Obsidian`, when `install.sh` runs vault auto-detection, then both candidate paths are scanned.
68
+
69
+ 7. **`install.sh` Windows vault detection.** Given Git Bash on Windows with vault at `$USERPROFILE/Documents/Obsidian`, when `install.sh` runs vault auto-detection, then the path is scanned.
70
+
71
+ 8. **`--no-system` flag.** Given `npx arkaos install --no-system`, when installer runs, then `ensureSystemTools()` is bypassed (legacy install-only flow preserved for CI).
72
+
73
+ 9. **Idempotent.** Given `ensureSystemTools()` has already installed everything, when it runs again, then no install commands fire, no side effects, exit success.
74
+
75
+ 10. **Regression-free.** Given PR2 merges, when full pytest suite runs, then all 3049 tests still pass. New Node tests add ≥ 15 cases for package-manager + system-tools.
76
+
77
+ 11. **shellcheck clean.** Given the updated `install.sh`, when `shellcheck install.sh` runs, then zero new warnings vs baseline.
78
+
79
+ ## Data Model
80
+
81
+ N/A — pure installer logic, no persisted schema changes.
82
+
83
+ ## API Contracts
84
+
85
+ ### `installer/package-manager.js`
86
+
87
+ ```javascript
88
+ /**
89
+ * Detect the highest-priority package manager available on this host.
90
+ *
91
+ * Priority order:
92
+ * macOS: brew
93
+ * Linux: apt > snap (apt for Node, snap for Obsidian classic-confined)
94
+ * Windows: winget > choco
95
+ *
96
+ * Returns the canonical name string or null when nothing is available.
97
+ */
98
+ export function detectPackageManager(): string | null
99
+
100
+ /**
101
+ * Install a single package via the detected (or specified) manager.
102
+ * Never invokes sudo automatically. When sudo is required, returns
103
+ * the command for the caller to print and the user to run.
104
+ */
105
+ export function installViaPackageManager(
106
+ pkg: string,
107
+ options?: { manager?: string, fallbackUrl?: string }
108
+ ): {
109
+ success: boolean,
110
+ command: string, // command that ran or should run
111
+ needsSudo: boolean,
112
+ installed: boolean, // did we actually install in this call?
113
+ fallbackOpened?: boolean, // did we open the download URL?
114
+ error?: string,
115
+ }
116
+
117
+ /** Format an array of missing-package commands into copy-paste text. */
118
+ export function formatSudoInstructions(missing: SudoCommand[]): string
119
+ ```
120
+
121
+ ### `installer/system-tools.js`
122
+
123
+ ```javascript
124
+ export type ToolStatus = {
125
+ name: "obsidian" | "node" | "python",
126
+ installed: boolean,
127
+ version?: string,
128
+ location?: string,
129
+ needsAction: "none" | "install" | "upgrade",
130
+ suggestedCommand?: string,
131
+ needsSudo?: boolean,
132
+ }
133
+
134
+ /**
135
+ * Check each required system tool. Install what can be installed without
136
+ * sudo. Collect commands for tools that need sudo into `sudoCommands`
137
+ * so the caller can print them to the user.
138
+ *
139
+ * Idempotent: tools already installed at sufficient version are no-ops.
140
+ */
141
+ export function ensureSystemTools(options?: {
142
+ skipSystem?: boolean,
143
+ dryRun?: boolean,
144
+ }): {
145
+ obsidian: ToolStatus,
146
+ node: ToolStatus,
147
+ python: ToolStatus,
148
+ sudoCommands: string[],
149
+ }
150
+
151
+ export function checkObsidian(): ToolStatus
152
+ export function checkNode(): ToolStatus
153
+ export function checkPython(): ToolStatus
154
+ ```
155
+
156
+ ### `installer/doctor.js`
157
+
158
+ ```javascript
159
+ /**
160
+ * `npx arkaos doctor` — diagnose-only report. Never installs.
161
+ * Returns exit code 0 (all OK), 1 (missing tool), 2 (version mismatch).
162
+ */
163
+ export function runDoctor(): number
164
+ ```
165
+
166
+ ### CLI integration
167
+
168
+ `installer/cli.js` gains:
169
+ - `npx arkaos doctor` subcommand → calls `runDoctor()`
170
+ - `--no-system` global flag → sets `options.skipSystem = true` for `ensureSystemTools()`
171
+
172
+ ## Edge Cases
173
+
174
+ 1. **brew not installed on macOS** → print Homebrew install one-liner, exit 0 with informational message.
175
+ 2. **snap available but `obsidian` not in store** (older Ubuntu) → fall back to `https://obsidian.md/download` opened in browser, or printed for headless.
176
+ 3. **Windows < 1809 without winget** → choco fallback, or printed download URL.
177
+ 4. **Node installed but version < 20** → `needsAction: "upgrade"`, suggested command per OS.
178
+ 5. **Multiple Python versions present** → prefer 3.12+, recommend `pyenv` if mixed.
179
+ 6. **User installed Obsidian via Flatpak** → detect via `flatpak list md.obsidian` (best-effort), accept as installed.
180
+ 7. **Concurrent installer runs** → checks read filesystem only; install commands themselves are idempotent (apt/brew/winget handle already-installed packages gracefully).
181
+ 8. **curl|bash mode** → `install.sh` clones repo first; `ARKAOS_REPO_URL` env wins over hardcoded default; supports private fork redirection.
182
+ 9. **Sudo denied** on Windows winget → print the command and ask user to re-run from an elevated prompt; do not loop or retry.
183
+ 10. **Headless CI** → `--no-system` flag skips entirely; `arka doctor` exit codes drive CI decisions.
184
+ 11. **Network failure during install** → catch, report, exit with copy-paste recovery command.
185
+
186
+ ## Test Scenarios
187
+
188
+ | # | Scenario | Type | Expected |
189
+ |---|---|---|---|
190
+ | 1 | `detectPackageManager` on macOS w/ brew | Unit | `"brew"` |
191
+ | 2 | `detectPackageManager` on Ubuntu w/ apt+snap | Unit | `"apt"` (then `"snap"` for Obsidian-specific calls) |
192
+ | 3 | `detectPackageManager` on Windows w/ winget | Unit | `"winget"` |
193
+ | 4 | `detectPackageManager` on Linux w/ none | Unit | `null` |
194
+ | 5 | `checkObsidian` when binary in PATH | Unit | `installed: true, location: <path>` |
195
+ | 6 | `checkObsidian` when binary missing | Unit | `installed: false, needsAction: "install"` |
196
+ | 7 | `checkNode` when version < 20 | Unit | `needsAction: "upgrade"` |
197
+ | 8 | `checkNode` when version ≥ 20 | Unit | `needsAction: "none"` |
198
+ | 9 | `checkPython` when 3.11 only | Unit | `needsAction: "upgrade"` |
199
+ | 10 | `ensureSystemTools` w/ all 3 missing | Integration | `sudoCommands` array populated |
200
+ | 11 | `ensureSystemTools` idempotent | Integration | second call: no commands |
201
+ | 12 | `runDoctor` exit codes | Integration | 0 / 1 / 2 verified |
202
+ | 13 | `install.sh` with `ARKAOS_REPO_URL` override | Manual / shell | clones from custom URL |
203
+ | 14 | `install.sh` Linux vault detection | Manual / shell | finds `~/.local/share/obsidian/...` |
204
+ | 15 | `install.sh` Windows vault detection | Manual / shell | finds `$USERPROFILE/Documents/Obsidian` |
205
+ | 16 | `--no-system` flag bypass | Integration | `ensureSystemTools` not called |
206
+ | 17 | full pytest regression | Integration | 3049 still pass |
207
+
208
+ ## Dependencies
209
+
210
+ - `installer/platform.js` (existing) — `IS_WINDOWS`, `CMD_FINDER`, `PYTHON_CMD`.
211
+ - `installer/path-resolver.js` (PR1) — for any path template substitution.
212
+ - `installer/python-resolver.js` (existing) — Python detection logic.
213
+ - Node.js `node:child_process` for `execSync` (already used).
214
+ - No new npm deps.
215
+
216
+ ## Quality Gate Criteria
217
+
218
+ - **Eduardo:** all error / status messages read naturally in EN; copy-paste sudo commands are accurate and OS-specific; no AI clichés.
219
+ - **Francisca:** package-manager detection covers macOS/Ubuntu/Debian/Fedora/Arch/Windows 10+/11; `ensureSystemTools()` is fully idempotent; no shell injection vectors in execSync calls; shellcheck clean on install.sh.
220
+ - **Marta:** verified on 3 synthetic environments — macOS with brew, Ubuntu without snap, Windows with winget — each producing the right printed commands and side effects.
221
+
222
+ ## References
223
+
224
+ - Plan: `~/.arkaos/plans/2026-05-13-arkaos-next-level-conclave.md` (PR2 section)
225
+ - Memory: [[project_next_level_conclave]]
226
+ - Existing: [[SPEC-paths-portability]] (PR1, dependency), [[platform.js]] (platform abstraction)
227
+ - HIGH items inherited from PR1 audit: `install.sh:25`, `install.sh:625-640`
@@ -0,0 +1,209 @@
1
+ ---
2
+ type: spec
3
+ status: approved
4
+ feature: paths-portability
5
+ project: ArkaOS
6
+ version: 2.23.0
7
+ pr: PR1 of 6 (Conclave roadmap 2026-05-13)
8
+ branch: feature/v2.23.0-paths-portability
9
+ date_created: 2026-05-13
10
+ date_updated: 2026-05-13
11
+ tags: [spec, arkaos, paths, portability, refactor, v2.23.0, conclave-pr1]
12
+ ---
13
+
14
+ # SPEC: Paths Portability (v2.23.0)
15
+
16
+ ## Overview
17
+
18
+ **Problem.** ArkaOS source contains hardcoded user-specific paths (`/Users/andreagroferreira/...`) across SKILL.md files, JSON configs, Python regexes, and cognitive prompts. These break for the **~20K users** currently running ArkaOS with different home directories and OS layouts.
19
+
20
+ **Goal.** Replace 7 CRITICAL hardcoded paths with profile-driven templates resolved at runtime, with **non-breaking auto-migration** for the existing user base.
21
+
22
+ **Actors.** ArkaOS core (skill loader, cognition collector, installer migration), end users (via `~/.arkaos/profile.json`).
23
+
24
+ ## Scope
25
+
26
+ **In scope (PR1 / v2.23.0):**
27
+
28
+ | # | File | Fix |
29
+ |---|---|---|
30
+ | 1 | `knowledge/obsidian-config.json:6` | `vault_path` value resolved at load via `path_resolver.resolve_dict` (profile.vaultPath) |
31
+ | 2 | `core/cognition/capture/collector.py:15-17` | Regex generated from `profile.projectRoots`; fallback `os.getcwd()` |
32
+ | 3 | `departments/kb/skills/knowledge/SKILL.md:25` | `${VAULT_PATH}` template |
33
+ | 4 | `departments/ops/skills/operations/SKILL.md:59` | `${VAULT_PATH}` template |
34
+ | 5 | `arka/skills/comfyui/SKILL.md:22-23` + `references/workflows.md` | `${ARKA_OS_REPOS}/{purz-comfyui-workflows,lora_tester}` |
35
+ | 6 | `config/cognition/prompts/dreaming.md` | `${VAULT_PATH}` template |
36
+ | 7 | `config/cognition/prompts/research.md` | `${VAULT_PATH}` template |
37
+ | 8 | `departments/dev/skills/scaffold/SKILL.md:19` | `${GIT_HOST}` template, default `github.com` HTTPS |
38
+ | — | new `core/runtime/path_resolver.py` | `resolve()`, `resolve_dict()`, `load_profile()` |
39
+ | — | new `installer/migrations/v3_path_schema.js` | Auto-migrate `projectsDir` text → `projectRoots` list + `reposRoot` |
40
+ | — | skill loader extension | Substitute `${VAR}` tokens at SKILL.md load |
41
+ | — | new `tests/python/test_path_resolver.py` | ≥ 15 tests |
42
+
43
+ **Out of scope (deferred):**
44
+
45
+ - `install.sh:25, 631-634` (GitHub URL hardcode + Mac-only Obsidian detection) → **PR2 v2.24.0** (cross-OS installer is the natural home for OS-aware logic)
46
+ - 50+ docs/superpowers/plans hardcoded paths (LOW priority, examples only) → **PR6 v2.28.0** cleanup pass
47
+ - Vision bridge migration (test_vision_bridge.py + scripts/tools/vision/*) → file was discarded; not relevant to PR1
48
+ - Migration of installed user skills under `~/.claude/skills/arka/` — installer rewrites these from source on every update; fixing source covers it
49
+
50
+ ## Acceptance Criteria
51
+
52
+ 1. **Zero CRITICAL grep hits.** Given a fresh checkout of `feature/v2.23.0-paths-portability`, when `grep -rn "/Users/andreagroferreira" core/ arka/ installer/ config/ departments/ knowledge/ --exclude-dir=node_modules --exclude-dir=__pycache__ --exclude-dir=.git --exclude-dir=docs` runs, then it returns **0 CRITICAL hits**. Hits inside `docs/superpowers/plans/` are tracked but acceptable (PR6 cleanup).
53
+
54
+ 2. **Synthetic install works end-to-end.** Given `$HOME=/tmp/testuser` and `~/.arkaos/profile.json` containing `{"version":"3","vaultPath":"/tmp/testuser/vault","reposRoot":"/tmp/testuser/repos","projectRoots":["/tmp/testuser/code"]}`, when every SKILL.md is loaded, then `${VAULT_PATH}`, `${ARKA_OS_REPOS}` and `${GIT_HOST}` resolve to the configured values with zero `${` leakage.
55
+
56
+ 3. **20K-user safe migration.** Given a legacy profile.json missing `projectRoots` and `reposRoot`, when `npx arkaos@latest update` runs, then:
57
+ - `projectRoots` is derived from parsing `projectsDir` text (regex extracts absolute paths).
58
+ - If parsing finds 0 paths, sensible defaults are applied (`["~/Herd","~/Work","~/AIProjects"]`).
59
+ - `reposRoot` defaults to `~/AIProjects` (or first match containing `AIProjects` if any).
60
+ - `version` bumps to `"3"`, `migrated_at` timestamp added.
61
+ - A `.bak-<timestamp>` backup is written first.
62
+ - `[arka:migrated] profile.json schema v2 → v3` logged.
63
+
64
+ 4. **Hard error on truly missing profile.** Given `~/.arkaos/profile.json` is absent or unparseable JSON, when any `path_resolver` call runs, then `ProfileMissingError("Run /arka setup to configure ArkaOS paths")` is raised. No silent fallback.
65
+
66
+ 5. **Env-var override.** Given `ARKAOS_VAULT_PATH=/tmp/override` is set, when `${VAULT_PATH}` is resolved, then the env var wins over profile. Same for `ARKAOS_REPOS_ROOT`, `ARKAOS_PROJECT_ROOTS`, `ARKAOS_GIT_HOST`. Empty string is treated as unset.
67
+
68
+ 6. **Regression-free.** Given PR1 is merged, when full pytest suite runs, then all existing 542+ tests pass and ≥ 15 new path_resolver tests pass. Coverage ≥ 80%.
69
+
70
+ 7. **Migration idempotent.** Given migration ran once, when it runs again, then profile.json is unchanged (no version re-bump, no second backup).
71
+
72
+ ## Data Model
73
+
74
+ | Field (new in profile.json v3) | Type | Default | Source |
75
+ |---|---|---|---|
76
+ | `version` | `"3"` (str) | — | bumped by migration |
77
+ | `projectRoots` | `list[str]` | `["~/Herd","~/Work","~/AIProjects"]` | parsed from `projectsDir` text on migration |
78
+ | `reposRoot` | `str` | `"~/AIProjects"` | matches first path containing `AIProjects`, else default |
79
+ | `migrated_at` | ISO8601 `str` | — | written at migration time |
80
+
81
+ **Backwards-compat fields kept verbatim:** `language`, `market`, `role`, `company`, `projectsDir` (kept for human reference), `vaultPath`, `created`, `updated`.
82
+
83
+ ## API Contracts
84
+
85
+ ### `core/runtime/path_resolver.py`
86
+
87
+ ```python
88
+ class ProfileMissingError(RuntimeError):
89
+ """Raised when ~/.arkaos/profile.json is absent or unparseable."""
90
+
91
+ @dataclass
92
+ class ProfileV3:
93
+ version: str
94
+ vault_path: str # ${VAULT_PATH}
95
+ repos_root: str # ${ARKA_OS_REPOS}
96
+ project_roots: list[str] # ${PROJECT_ROOTS}
97
+ raw: dict # full profile.json for forward-compat
98
+
99
+ def load_profile() -> ProfileV3:
100
+ """Load and cache ~/.arkaos/profile.json. Raises ProfileMissingError."""
101
+
102
+ def resolve(template: str) -> str:
103
+ """Substitute known tokens. Unknown tokens pass through unchanged.
104
+
105
+ Tokens:
106
+ ${VAULT_PATH} -> ARKAOS_VAULT_PATH env or profile.vaultPath
107
+ ${ARKA_OS_REPOS} -> ARKAOS_REPOS_ROOT env or profile.reposRoot
108
+ ${PROJECT_ROOTS} -> os.pathsep-joined profile.projectRoots
109
+ ${GIT_HOST} -> ARKAOS_GIT_HOST env or "github.com"
110
+ ${HOME} -> os.path.expanduser("~")
111
+ """
112
+
113
+ def resolve_dict(d: dict) -> dict:
114
+ """Recursively resolve string values in a JSON-shaped dict."""
115
+
116
+ def project_root_regex() -> re.Pattern:
117
+ """Compile regex from profile.projectRoots for collector.py."""
118
+ ```
119
+
120
+ ### Skill loader extension
121
+
122
+ Find existing skill loader (likely `core/agents/loader.py` or `arka/skills/.../loader`); extend:
123
+
124
+ ```python
125
+ def load_skill_markdown(path: Path) -> str:
126
+ return path_resolver.resolve(path.read_text())
127
+ ```
128
+
129
+ ### Migration script `installer/migrations/v3_path_schema.js`
130
+
131
+ ```javascript
132
+ function migrateV3(profile) {
133
+ if (profile.version === "3" || profile.projectRoots) return profile;
134
+ const roots = parseProjectsDirText(profile.projectsDir);
135
+ const reposRoot = roots.find(r => r.includes("AIProjects")) || "~/AIProjects";
136
+ return { ...profile, version: "3", projectRoots: roots,
137
+ reposRoot, migrated_at: new Date().toISOString() };
138
+ }
139
+ ```
140
+
141
+ Wired into `installer/migrate-user-data.js` (existing framework).
142
+
143
+ ## Edge Cases
144
+
145
+ 1. **`projectsDir` text is empty / null** → defaults applied (no error).
146
+ 2. **`vaultPath` empty string** → treated as missing → `ProfileMissingError`.
147
+ 3. **SKILL.md contains literal `${SOME_BASH_VAR}` for agent instructions** → resolver only touches the 5 known tokens; unknown tokens pass through.
148
+ 4. **Windows path separators in `${PROJECT_ROOTS}`** → uses `os.pathsep` (`;` on Windows, `:` on Unix). Internal storage is POSIX `/`.
149
+ 5. **Digest path from foreign OS** (cross-machine Obsidian sync) → `_detect_project` falls back to `os.getcwd()` if no `projectRoots` match (existing behaviour).
150
+ 6. **Concurrent calls during migration** → migration uses temp-file + atomic rename; safe.
151
+ 7. **User edits profile.json manually mid-session** → load_profile is cached per-process; re-import to refresh. Documented.
152
+ 8. **Migration corrupts profile** → `.bak-<timestamp>` rollback file always exists.
153
+
154
+ ## Test Scenarios
155
+
156
+ | # | Scenario | Type | Expected |
157
+ |---|---|---|---|
158
+ | 1 | `resolve("${VAULT_PATH}/foo")` with `vaultPath="/v"` | Unit | `"/v/foo"` |
159
+ | 2 | `resolve("${HOME}/bar")` | Unit | `"$HOME/bar"` expanded |
160
+ | 3 | `resolve("${UNKNOWN}/baz")` | Unit | `"${UNKNOWN}/baz"` unchanged |
161
+ | 4 | `resolve` with absent profile.json | Unit | `ProfileMissingError` |
162
+ | 5 | `resolve` with `ARKAOS_VAULT_PATH=/x` | Unit | env wins over profile |
163
+ | 6 | `resolve_dict` on obsidian-config.json fixture | Unit | All string values resolved |
164
+ | 7 | `_detect_project` with `projectRoots=["~/Herd"]` | Unit | matches path under `~/Herd` |
165
+ | 8 | `_detect_project` on Windows-style path | Unit | matches with `\` separator |
166
+ | 9 | `load_skill_markdown` on kb SKILL.md | Integration | `${VAULT_PATH}` substituted |
167
+ | 10 | full pytest suite | Regression | all 542+ tests pass |
168
+ | 11 | `migrateV3` on legacy profile (v2 schema) | Integration | new fields added, old kept, .bak written |
169
+ | 12 | `migrateV3` idempotent (second run no-op) | Integration | profile unchanged, no second .bak |
170
+ | 13 | Grep audit (CRITICAL paths) | Audit | 0 hits in core/ arka/ installer/ config/ departments/ knowledge/ |
171
+ | 14 | `scaffold SKILL.md ${GIT_HOST}` | Integration | `github.com` default |
172
+ | 15 | `comfyui SKILL.md ${ARKA_OS_REPOS}` | Integration | substituted correctly |
173
+ | 16 | `migrateV3` with empty `projectsDir` | Unit | defaults applied, no crash |
174
+ | 17 | env vars with empty string values | Unit | treated as unset, profile wins |
175
+
176
+ ## Dependencies
177
+
178
+ - No external deps. Reuses existing `installer/migrate-user-data.js` framework + `core/specs/` Living Specs engine.
179
+ - New Python module only depends on stdlib (`json`, `os`, `pathlib`, `re`, `dataclasses`).
180
+
181
+ ## Migration Plan for 20K Users
182
+
183
+ `npx arkaos@latest update` runs `migrate-user-data.js` which includes the new `migrateV3` step. Flow:
184
+
185
+ 1. Read `~/.arkaos/profile.json`. If absent → skip migration (user must run `/arka setup`).
186
+ 2. If `version === "3"` or `projectRoots` exists → no-op (idempotent).
187
+ 3. Otherwise:
188
+ - Write backup: `~/.arkaos/profile.json.bak-<unix-timestamp>`.
189
+ - Parse `projectsDir` text with `/((?:\/Users|\/home|[A-Z]:\\Users)\/\S+?\/(?:Herd|Work|AIProjects|code|repos))/g`.
190
+ - Apply defaults if parsing yields 0 results.
191
+ - Atomic write: temp file + `fs.rename`.
192
+ - Log `[arka:migrated] profile.json schema v2 → v3 ({N} roots, repos=...)` to `~/.arkaos/logs/migrate.log`.
193
+
194
+ **Rollback procedure (documented in installer):** restore `.bak-<timestamp>` over profile.json.
195
+
196
+ ## Quality Gate Criteria
197
+
198
+ Marta (CQO) requires APPROVED from all three reviewers:
199
+
200
+ - **Eduardo (Copy):** All SKILL.md files read naturally after substitution — no `${` leakage in human-facing prose. Error messages clear and actionable.
201
+ - **Francisca (Tech):** path_resolver handles all 8 edge cases. Migration is atomic + idempotent + reversible. 17 test scenarios all green. No crash on corrupt profile.
202
+ - **Marta (CQO):** Migration verified non-breaking by simulating 5 different legacy profile shapes (André's current one + 4 synthetic variants representing common 20K-user variations).
203
+
204
+ ## References
205
+
206
+ - Plan: `~/.arkaos/plans/2026-05-13-arkaos-next-level-conclave.md` (PR1 section)
207
+ - Memory: [[project_next_level_conclave]]
208
+ - KB: [[2026-04-29-claude-code-2-1-122-and-2-1-123]] (Claude Code features unblocked by 2.1.122)
209
+ - Existing Specs in this project: [[SPEC-cognitive-layer-feedback-loop]], [[SPEC-wave1-single-source-of-truth]]
@@ -16,7 +16,7 @@ Create new projects from real git repositories with full automation: dependencie
16
16
 
17
17
  | Command | Git Repository | Stack |
18
18
  |---------|---------------|-------|
19
- | `/dev scaffold laravel <name>` | `git@andreagroferreira:andreagroferreira/laravel-starter-kit.git` | Laravel + Herd |
19
+ | `/dev scaffold laravel <name>` | `https://${GIT_HOST}/laravel/laravel.git` (override with `ARKAOS_LARAVEL_STARTER_REPO` env) | Laravel + Herd |
20
20
  | `/dev scaffold nuxt-saas <name>` | `https://github.com/nuxt-ui-templates/dashboard.git` | Nuxt 3 Dashboard |
21
21
  | `/dev scaffold nuxt-landing <name>` | `https://github.com/nuxt-ui-templates/landing.git` | Nuxt 3 Landing |
22
22
  | `/dev scaffold nuxt-docs <name>` | `https://github.com/nuxt-ui-templates/docs.git` | Nuxt 3 Docs |
@@ -136,7 +136,7 @@ cp PROJECT.md "$ARKA_OS/projects/<name>/PROJECT.md"
136
136
 
137
137
  Create pages in the Obsidian vault:
138
138
 
139
- **Main page:** `Documents/Personal/Projects/<name>/Home.md`
139
+ **Main page:** `${VAULT_PATH}/Projects/<name>/Home.md`
140
140
  ```markdown
141
141
  ---
142
142
  type: project
@@ -165,7 +165,7 @@ tags:
165
165
  - *Part of the [[Projects MOC]]*
166
166
  ```
167
167
 
168
- **Architecture page:** `Documents/Personal/Projects/<name>/Architecture/decisions.md`
168
+ **Architecture page:** `${VAULT_PATH}/Projects/<name>/Architecture/decisions.md`
169
169
  ```markdown
170
170
  ---
171
171
  type: adr-log
@@ -242,7 +242,7 @@ Both directories get their respective dependencies installed, and the full-stack
242
242
 
243
243
  ## Error Handling
244
244
 
245
- - If `git clone` fails: check SSH keys (`git@andreagroferreira:` for private repos)
245
+ - If `git clone` fails: check SSH keys / repo access permissions for the configured `${GIT_HOST}`
246
246
  - If `composer install` fails: check PHP version (`php -v`, need 8.3+)
247
247
  - If `pnpm install` fails: check Node version (`node -v`, need 18+)
248
248
  - If `herd link` fails: check Herd is installed and running
@@ -22,7 +22,7 @@ Dynamic knowledge acquisition and management system. Learn from YouTube videos,
22
22
 
23
23
  ## Obsidian Configuration
24
24
 
25
- - **Vault:** `/Users/andreagroferreira/Documents/Personal`
25
+ - **Vault:** `${VAULT_PATH}`
26
26
  - **Config:** Read `knowledge/obsidian-config.json` for full path/convention details
27
27
  - **MCP:** Use Obsidian MCP when available, fallback to direct file Write
28
28
  - **Conventions:** YAML frontmatter, wikilinks `[[]]`, MOC references, kebab-case tags
@@ -56,7 +56,7 @@ Every phase transition announced to the user.
56
56
 
57
57
  ## Obsidian Output
58
58
 
59
- Vault root: `/Users/andreagroferreira/Documents/Personal`
59
+ Vault root: `${VAULT_PATH}`
60
60
 
61
61
  | Content | Path |
62
62
  |---------|------|
package/installer/cli.js CHANGED
@@ -19,6 +19,7 @@ const { values, positionals } = parseArgs({
19
19
  runtime: { type: "string", short: "r" },
20
20
  path: { type: "string", short: "p" },
21
21
  force: { type: "boolean", short: "f" },
22
+ "no-system": { type: "boolean" },
22
23
  },
23
24
  allowPositionals: true,
24
25
  strict: false,
@@ -74,7 +75,7 @@ async function main() {
74
75
  switch (command) {
75
76
  case "install":
76
77
  const runtime = values.runtime || await detectRuntime();
77
- await install({ runtime, path: values.path, force: values.force });
78
+ await install({ runtime, path: values.path, force: values.force, skipSystem: values["no-system"] });
78
79
  break;
79
80
 
80
81
  case "init": {
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
4
4
  import { execSync } from "node:child_process";
5
5
  import { getArkaosPython, getVenvPython, canImportCore, getRepoRoot } from "./python-resolver.js";
6
6
  import { IS_WINDOWS, HOOK_EXT, CMD_FINDER } from "./platform.js";
7
+ import { checkNode, checkObsidian } from "./system-tools.js";
7
8
 
8
9
  const INSTALL_DIR = join(homedir(), ".arkaos");
9
10
 
@@ -124,6 +125,53 @@ const checks = [
124
125
  check: () => existsSync(join(INSTALL_DIR, "schedules.yaml")),
125
126
  fix: () => "Run: npx arkaos@latest update (deploys scheduler)",
126
127
  },
128
+ {
129
+ name: "obsidian",
130
+ description: "Obsidian app installed",
131
+ severity: "warn",
132
+ check: () => checkObsidian().installed,
133
+ fix: () => {
134
+ const s = checkObsidian();
135
+ if (s.suggestedCommand) return `Run: ${s.suggestedCommand}`;
136
+ return `Install Obsidian from ${s.fallbackUrl || "https://obsidian.md/download"}`;
137
+ },
138
+ },
139
+ {
140
+ name: "node",
141
+ description: "Node.js 20+ available",
142
+ severity: "warn",
143
+ check: () => checkNode().needsAction === "none",
144
+ fix: () => {
145
+ const s = checkNode();
146
+ if (s.suggestedCommand) return `Run: ${s.suggestedCommand}`;
147
+ return `Install Node.js 20+ from ${s.fallbackUrl || "https://nodejs.org/en/download"}`;
148
+ },
149
+ },
150
+ {
151
+ name: "claude-code-version",
152
+ description: "Claude Code 2.1.122+ (ToolSearch late-binding + hooks isolation)",
153
+ severity: "warn",
154
+ check: () => {
155
+ if (!commandExists("claude")) return true; // no claude binary = not applicable
156
+ try {
157
+ const out = execSync("claude --version 2>&1", {
158
+ stdio: "pipe",
159
+ }).toString().trim();
160
+ const m = out.match(/(\d+)\.(\d+)\.(\d+)/);
161
+ if (!m) return false;
162
+ const [, maj, min, patch] = m.map(Number);
163
+ // 2.1.122 minimum
164
+ if (maj > 2) return true;
165
+ if (maj < 2) return false;
166
+ if (min > 1) return true;
167
+ if (min < 1) return false;
168
+ return patch >= 122;
169
+ } catch {
170
+ return false;
171
+ }
172
+ },
173
+ fix: () => "Upgrade Claude Code: npm install -g @anthropic-ai/claude-code@latest",
174
+ },
127
175
  ];
128
176
 
129
177
  // ─── Windows-only checks ───────────────────────────────────────────────
@@ -12,7 +12,7 @@ const __dirname = dirname(__filename);
12
12
  const ARKAOS_ROOT = resolve(__dirname, "..");
13
13
  const VERSION = JSON.parse(readFileSync(join(ARKAOS_ROOT, "package.json"), "utf-8")).version;
14
14
 
15
- export async function install({ runtime, path, force }) {
15
+ export async function install({ runtime, path, force, skipSystem }) {
16
16
  const startTime = Date.now();
17
17
  const config = getRuntimeConfig(runtime);
18
18
  const isUpgrade = existsSync(join(path || join(homedir(), ".arkaos"), "install-manifest.json"));
@@ -72,6 +72,31 @@ export async function install({ runtime, path, force }) {
72
72
  ok("No v1 installation found");
73
73
  }
74
74
 
75
+ // ═══ Step 2.5: Ensure system tools (Obsidian, Node ≥ 20, Python ≥ 3.12) ═══
76
+ // Wrapped in try/catch so a regression in system-tools.js cannot break a
77
+ // fresh `npx arkaos install` for the ~20K user base. We degrade to a warn
78
+ // and proceed to venv creation rather than exiting non-zero.
79
+ if (!skipSystem) {
80
+ try {
81
+ const { ensureSystemTools } = await import("./system-tools.js");
82
+ const { formatSudoInstructions } = await import("./package-manager.js");
83
+ const sys = ensureSystemTools({ skipSystem: false });
84
+ if (sys.sudoCommands && sys.sudoCommands.length > 0) {
85
+ console.log(formatSudoInstructions(sys.sudoCommands));
86
+ }
87
+ for (const tool of [sys.obsidian, sys.node]) {
88
+ if (!tool) continue;
89
+ if (tool.justInstalled) ok(`${tool.name} installed`);
90
+ else if (tool.needsAction === "none") ok(`${tool.name} ready`);
91
+ else warn(`${tool.name} ${tool.needsAction} — see commands above`);
92
+ }
93
+ } catch (err) {
94
+ warn(`System tool check failed: ${err.message}. Continuing without it.`);
95
+ }
96
+ } else {
97
+ warn("System tool checks skipped (--no-system)");
98
+ }
99
+
75
100
  // ═══ Step 3: Check Python + create venv ═══
76
101
  step(3, 14, "Checking Python 3.11+ and creating virtual environment...");
77
102
  const systemPython = findSystemPython();
@@ -2,6 +2,8 @@ import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
 
5
+ import { migrateProfileSchemaV3 } from "./migrations/v3_path_schema.js";
6
+
5
7
  const LEGACY_SKILLS_ROOT = join(homedir(), ".claude", "skills", "arka");
6
8
  const USER_DATA_ROOT = join(homedir(), ".arkaos");
7
9
 
@@ -28,8 +30,22 @@ export function migrateUserData({ dryRun = false } = {}) {
28
30
  const skipped = [...projectsResult.skipped, ...ecosystemsResult.skipped];
29
31
  const conflicts = [...projectsResult.conflicts, ...ecosystemsResult.conflicts];
30
32
 
33
+ let profileSchemaResult = null;
34
+ if (!dryRun) {
35
+ try {
36
+ profileSchemaResult = migrateProfileSchemaV3();
37
+ if (profileSchemaResult?.migrated) {
38
+ moved.push(
39
+ `profile.json schema v2 → v3 (${profileSchemaResult.projectRoots.length} roots)`
40
+ );
41
+ }
42
+ } catch (err) {
43
+ conflicts.push(`profile schema v3 migration failed: ${err.message}`);
44
+ }
45
+ }
46
+
31
47
  const logPath = writeReport({ moved, skipped, conflicts, dryRun });
32
- return { moved, skipped, conflicts, logPath };
48
+ return { moved, skipped, conflicts, logPath, profileSchemaResult };
33
49
  }
34
50
 
35
51
  export function printMigrationReport(result) {