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.
- package/README.md +105 -9
- package/VERSION +1 -1
- package/arka/SKILL.md +1 -0
- package/arka/skills/comfyui/SKILL.md +2 -2
- package/arka/skills/comfyui/references/workflows.md +6 -6
- package/arka/skills/costs/SKILL.md +11 -0
- package/config/cognition/prompts/dreaming.md +3 -3
- package/config/cognition/prompts/research.md +4 -4
- package/config/hooks/user-prompt-submit.sh +6 -1
- package/core/cognition/__pycache__/auto_documentor.cpython-313.pyc +0 -0
- package/core/cognition/capture/__pycache__/collector.cpython-313.pyc +0 -0
- package/core/cognition/capture/collector.py +10 -3
- package/core/cognition/scheduler/__pycache__/daemon.cpython-313.pyc +0 -0
- package/core/cognition/scheduler/daemon.py +5 -0
- package/core/jobs/__pycache__/auto_doc_worker.cpython-313.pyc +0 -0
- package/core/obsidian/__pycache__/writer.cpython-313.pyc +0 -0
- package/core/obsidian/writer.py +5 -4
- package/core/runtime/__pycache__/base.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/claude_code.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/codex_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/gemini_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/llm_provider.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/path_resolver.cpython-313.pyc +0 -0
- package/core/runtime/path_resolver.py +202 -0
- package/core/shared/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/shared/__pycache__/safe_session_id.cpython-313.pyc +0 -0
- package/core/specs/SPEC-installer-cross-os.md +227 -0
- package/core/specs/SPEC-paths-portability.md +209 -0
- package/core/synapse/__pycache__/kb_cache.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/marker_cache.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/research_gate.cpython-313.pyc +0 -0
- package/departments/dev/skills/scaffold/SKILL.md +4 -4
- package/departments/kb/skills/knowledge/SKILL.md +1 -1
- package/departments/ops/skills/operations/SKILL.md +1 -1
- package/installer/cli.js +2 -1
- package/installer/doctor.js +48 -0
- package/installer/index.js +26 -1
- package/installer/migrate-user-data.js +17 -1
- package/installer/migrations/v3_path_schema.js +109 -0
- package/installer/package-manager.js +191 -0
- package/installer/path-resolver.js +124 -0
- package/installer/system-tools.js +243 -0
- package/installer/update.js +24 -3
- package/knowledge/obsidian-config.json +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
npx arkaos install
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
[](https://www.npmjs.com/package/arkaos) [](LICENSE) [](https://www.npmjs.com/package/arkaos) [](LICENSE) []()
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -228,6 +228,84 @@ arkaos scheduler logs # View logs
|
|
|
228
228
|
|
|
229
229
|
---
|
|
230
230
|
|
|
231
|
+
## Intelligence Loop (v2.21.0+)
|
|
232
|
+
|
|
233
|
+
Three-layer system that makes ArkaOS consult its brain before going external, and write every session's learnings back to the vault automatically. The system compounds daily — tomorrow's ArkaOS is smarter than today's.
|
|
234
|
+
|
|
235
|
+
### Synapse L2.5 — KB Context Injection (v2.21.0)
|
|
236
|
+
|
|
237
|
+
On every user prompt, ArkaOS runs semantic search against the Obsidian vault and injects the top relevant notes (with wikilinks and excerpts) as context **before the model starts planning**. The agent sees what the vault already knows, naturally — no hard gate, no friction.
|
|
238
|
+
|
|
239
|
+
- Top 3–5 notes by similarity
|
|
240
|
+
- Jaccard fallback when vector store absent
|
|
241
|
+
- Feature flag `synapse.l25KbContext` default `true`
|
|
242
|
+
- Kill switch `ARKA_BYPASS_L25=1`
|
|
243
|
+
|
|
244
|
+
### Research Gate — Natural Nudge (v2.21.0)
|
|
245
|
+
|
|
246
|
+
When an agent tries `Context7`, `WebSearch`, `WebFetch`, or `Firecrawl` without consulting Obsidian first:
|
|
247
|
+
|
|
248
|
+
- **First attempt** → natural PT-PT nudge listing top 3 vault hits. Allows the call.
|
|
249
|
+
- **Second attempt in same turn** → deny. The nudge wasn't enough.
|
|
250
|
+
|
|
251
|
+
Feature flag `hooks.kbFirst` default `false` in v2.21.0 — dormant until operator flips. Kill switch `ARKA_BYPASS_KB_FIRST=1` (audited).
|
|
252
|
+
|
|
253
|
+
### Auto-Documentor + Cataloger + Relator (v2.21.0)
|
|
254
|
+
|
|
255
|
+
After every approved session, an async worker writes the learnings back to the vault:
|
|
256
|
+
|
|
257
|
+
1. **Extract** — parses the transcript for sources consulted, decisions made, deliverables produced
|
|
258
|
+
2. **Classify** — cataloger chooses the right taxonomic home (code pattern, persona, client strategy, marketing test, ADR, research finding, framework, or session fallback)
|
|
259
|
+
3. **Write** — creates the note at the correct vault path with frontmatter + tags
|
|
260
|
+
4. **Relate** — relator finds semantically similar existing notes, creates bidirectional `[[wikilinks]]`, updates MOCs
|
|
261
|
+
|
|
262
|
+
The vault becomes a relational graph that grows with every session.
|
|
263
|
+
|
|
264
|
+
### LLM Agnostic Auto-Documentor (v2.22.0)
|
|
265
|
+
|
|
266
|
+
The synthesis LLM call is runtime-agnostic. **Zero model hardcoding.** Whatever runtime you have configured decides the model:
|
|
267
|
+
|
|
268
|
+
| Provider | Default | Model decided by |
|
|
269
|
+
|---|---|---|
|
|
270
|
+
| `subagent` | ✅ | Active runtime's headless CLI (`claude -p`, `gemini -p …`) |
|
|
271
|
+
| `anthropic-direct` | — | `ANTHROPIC_MODEL` env var; no code default |
|
|
272
|
+
| `stub` | tests | Template fallback |
|
|
273
|
+
|
|
274
|
+
Fallback chain `subagent → anthropic-direct → stub` never raises. Prompt caching ON by default (5-min TTL `cache_control: ephemeral` on system block).
|
|
275
|
+
|
|
276
|
+
### Budget Telemetry + `/arka costs` (v2.22.0)
|
|
277
|
+
|
|
278
|
+
Every LLM call appends to `~/.arkaos/telemetry/llm-cost.jsonl` with tokens, cache hits, and estimated cost. Per [ADR-011](docs/adr/), this is **visibility only — never blocks**.
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
/arka costs # today (default)
|
|
282
|
+
/arka costs week # last 7 days
|
|
283
|
+
/arka costs month # last 30 days
|
|
284
|
+
/arka costs sessions # top 10 expensive sessions
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Soft advisory when a single session exceeds $5 equivalent. No hard caps.
|
|
288
|
+
|
|
289
|
+
### Flow Marker v2 (v2.21.0)
|
|
290
|
+
|
|
291
|
+
The constitutional 13-phase flow enforcement (binding in v2.20) got a turn-scoped marker cache, eliminating the cross-turn false positives that triggered on subagent dispatch and short user continuations. ADR-compliant: cache accelerates ALLOW decisions only; the transcript remains authoritative for DENY.
|
|
292
|
+
|
|
293
|
+
### Polish & Consolidation (v2.22.1)
|
|
294
|
+
|
|
295
|
+
- `core/shared/safe_session_id.py` — single source of truth for the path-traversal / injection allowlist, deduped across 6 modules (100% coverage, 33 security-contract tests)
|
|
296
|
+
- Gemini CLI headless live (`gemini -p "…" --output-format json`)
|
|
297
|
+
- `_MAX_FALLBACK_NOTES = 2000` bound on Synapse L2.5 degraded-mode scan
|
|
298
|
+
- Auto-documentor coverage lifted 93% → 98%
|
|
299
|
+
- Template synthesiser now mirrors LLM prompt structure (Key Facts → Decisions → Sources)
|
|
300
|
+
|
|
301
|
+
### ADRs
|
|
302
|
+
|
|
303
|
+
- [`2026-04-20-flow-marker-v2.md`](docs/adr/2026-04-20-flow-marker-v2.md) — turn-scoped cache amendment
|
|
304
|
+
- [`2026-04-20-kb-first-intelligence.md`](docs/adr/2026-04-20-kb-first-intelligence.md) — Synapse L2.5 + gate + auto-documentor
|
|
305
|
+
- [`2026-04-20-llm-agnostic.md`](docs/adr/2026-04-20-llm-agnostic.md) — runtime-agnostic LLM wiring
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
231
309
|
## Ecosystem Management
|
|
232
310
|
|
|
233
311
|
ArkaOS manages client projects as **ecosystems** — groups of related projects with dedicated squads.
|
|
@@ -370,21 +448,39 @@ Full documentation is available on the **[GitHub Wiki](https://github.com/andrea
|
|
|
370
448
|
|
|
371
449
|
## CLI Reference
|
|
372
450
|
|
|
451
|
+
**Installer (terminal):**
|
|
452
|
+
|
|
373
453
|
```bash
|
|
374
|
-
npx arkaos install
|
|
375
|
-
npx arkaos update
|
|
376
|
-
npx arkaos migrate
|
|
377
|
-
npx arkaos doctor
|
|
378
|
-
npx arkaos dashboard
|
|
379
|
-
npx arkaos keys
|
|
380
|
-
npx arkaos uninstall
|
|
454
|
+
npx arkaos install # Fresh install (auto-detects runtime)
|
|
455
|
+
npx arkaos@latest update # Update core + hooks to latest version
|
|
456
|
+
npx arkaos migrate # Migrate from v1
|
|
457
|
+
npx arkaos doctor # Health check
|
|
458
|
+
npx arkaos dashboard # Start monitoring dashboard
|
|
459
|
+
npx arkaos keys # Manage API keys
|
|
460
|
+
npx arkaos uninstall # Remove ArkaOS
|
|
381
461
|
```
|
|
382
462
|
|
|
463
|
+
**In-session commands (inside Claude Code / Codex / Gemini / Cursor):**
|
|
464
|
+
|
|
465
|
+
```
|
|
466
|
+
/arka update # Sync all project configs after core update
|
|
467
|
+
/arka status # System health + LLM costs (24h)
|
|
468
|
+
/arka costs [today|week|month|all|sessions] # LLM cost visibility
|
|
469
|
+
/arka index # (Re)index Obsidian vault into vector store
|
|
470
|
+
/arka search <query> # Semantic search in knowledge base
|
|
471
|
+
/arka standup # Daily standup (projects, blockers, updates)
|
|
472
|
+
/arka onboard <path> # Onboard an existing project
|
|
473
|
+
/arka conclave # Personal AI advisory board
|
|
474
|
+
/do <description> # Universal routing — natural language to department
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Department commands: `/dev`, `/mkt`, `/brand`, `/fin`, `/strat`, `/ecom`, `/kb`, `/ops`, `/pm`, `/saas`, `/landing`, `/content`, `/community`, `/sales`, `/lead`, `/org`.
|
|
478
|
+
|
|
383
479
|
---
|
|
384
480
|
|
|
385
481
|
## Contributing
|
|
386
482
|
|
|
387
|
-
See [CONTRIBUTING.md](.github/CONTRIBUTING.md). PRs welcome — all changes require passing the full test suite (
|
|
483
|
+
See [CONTRIBUTING.md](.github/CONTRIBUTING.md). PRs welcome — all changes require passing the full test suite (3,025 tests) and Quality Gate review.
|
|
388
484
|
|
|
389
485
|
## License
|
|
390
486
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.25.0
|
package/arka/SKILL.md
CHANGED
|
@@ -122,6 +122,7 @@ violation (squad-routing, arka-supremacy, spec-driven, mandatory-qa).
|
|
|
122
122
|
| `/arka search <query>` | Semantic search in knowledge base |
|
|
123
123
|
| `/arka keys` | Manage API keys (OpenAI, Google, fal.ai) |
|
|
124
124
|
| `/arka personas` | Manage AI personas (create, clone to agent) |
|
|
125
|
+
| `/arka resume <PR_URL>` | Re-enter the Claude Code session that produced a PR (GitHub / GitLab / Bitbucket). Wraps the native `/resume` from Claude Code 2.1.122+. Useful with `arka-spec` and `arka-release` archaeology. |
|
|
125
126
|
| `/do <description>` | Universal routing — natural language to department command |
|
|
126
127
|
|
|
127
128
|
## Universal Orchestrator (/do)
|
|
@@ -19,8 +19,8 @@ Full-spectrum AI media production platform. From ComfyUI node engineering to Hol
|
|
|
19
19
|
|
|
20
20
|
| Project | Type | Stack | Path |
|
|
21
21
|
|---------|------|-------|------|
|
|
22
|
-
| **purz-comfyui-workflows** | Workflow Collection | ComfyUI JSON (7 workflows, 110+ nodes) |
|
|
23
|
-
| **lora_tester** | CLI Tool + Gallery | Python 3.8+ (stdlib) + Vanilla HTML/JS |
|
|
22
|
+
| **purz-comfyui-workflows** | Workflow Collection | ComfyUI JSON (7 workflows, 110+ nodes) | `${ARKA_OS_REPOS}/purz-comfyui-workflows` |
|
|
23
|
+
| **lora_tester** | CLI Tool + Gallery | Python 3.8+ (stdlib) + Vanilla HTML/JS | `${ARKA_OS_REPOS}/lora_tester` |
|
|
24
24
|
|
|
25
25
|
## Squads
|
|
26
26
|
|
|
@@ -7,8 +7,8 @@ Referenced from SKILL.md. Read only when needed.
|
|
|
7
7
|
For EVERY `/comfyui` request, follow this workflow:
|
|
8
8
|
|
|
9
9
|
### Phase 1 — Context Loading
|
|
10
|
-
1. Read ComfyUI skill reference:
|
|
11
|
-
2. Check available workflows: `ls /
|
|
10
|
+
1. Read ComfyUI skill reference: `${ARKA_OS_REPOS}/lora_tester/.claude/skills/comfy_local/skill.md`
|
|
11
|
+
2. Check available workflows: `ls ${ARKA_OS_REPOS}/purz-comfyui-workflows/`
|
|
12
12
|
3. Check ComfyUI server: `curl -s http://localhost:8188/system_stats 2>/dev/null`
|
|
13
13
|
4. Load project context for affected repos
|
|
14
14
|
5. Identify which squad(s) the request needs
|
|
@@ -320,8 +320,8 @@ while pending:
|
|
|
320
320
|
curl -s http://localhost:8188/system_stats 2>/dev/null
|
|
321
321
|
|
|
322
322
|
# Project status
|
|
323
|
-
cd /
|
|
324
|
-
cd /
|
|
323
|
+
cd ${ARKA_OS_REPOS}/purz-comfyui-workflows && git log --oneline -5
|
|
324
|
+
cd ${ARKA_OS_REPOS}/lora_tester && git log --oneline -5
|
|
325
325
|
```
|
|
326
326
|
|
|
327
327
|
Present as:
|
|
@@ -403,10 +403,10 @@ When Iris (Node Researcher) investigates custom nodes:
|
|
|
403
403
|
|
|
404
404
|
## Obsidian Output
|
|
405
405
|
|
|
406
|
-
All documentation:
|
|
406
|
+
All documentation: `${VAULT_PATH}/Projects/ComfyUI/`
|
|
407
407
|
|
|
408
408
|
```
|
|
409
|
-
|
|
409
|
+
Projects/ComfyUI/
|
|
410
410
|
├── Home.md <- Ecosystem overview
|
|
411
411
|
├── Workflows/
|
|
412
412
|
│ ├── LTX-2 T2V.md
|
|
@@ -55,6 +55,17 @@ Source:
|
|
|
55
55
|
One JSONL line per LLM call, written by every provider adapter.
|
|
56
56
|
Malformed lines are skipped and counted, never raised.
|
|
57
57
|
|
|
58
|
+
## Cost-tuning levers
|
|
59
|
+
|
|
60
|
+
Outside this skill's scope but worth surfacing alongside the summary:
|
|
61
|
+
|
|
62
|
+
| Lever | Where | Effect |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| `ARKAOS_LLM_PROVIDER` | env | Switch between Anthropic, OpenAI, Gemini adapters per the multi-LLM router. |
|
|
65
|
+
| `ANTHROPIC_BEDROCK_SERVICE_TIER` | env (Claude Code 2.1.122+) | `default` / `flex` / `priority`. Only relevant when routing through AWS Bedrock — `flex` cuts cost ~50 % on non-urgent workloads at the price of higher tail latency. |
|
|
66
|
+
| Agent `model:` field | `departments/*/agents/*.yaml` | Per-agent override per the model-routing matrix in CLAUDE.md. Mechanical roles default to haiku, C-suite to opus. |
|
|
67
|
+
| Quality Gate model | constitution rule (NON-NEGOTIABLE) | Marta / Eduardo / Francisca always run on opus regardless of the cost ceiling. |
|
|
68
|
+
|
|
58
69
|
## Non-negotiables
|
|
59
70
|
|
|
60
71
|
1. Read-only. This skill never edits state.
|
|
@@ -8,7 +8,7 @@ You are ArkaOS performing your nightly Dreaming session. Your job is to review e
|
|
|
8
8
|
- Read any file from any project
|
|
9
9
|
- Read git logs and diffs
|
|
10
10
|
- Search the web (WebSearch, Firecrawl)
|
|
11
|
-
- Write to Obsidian vault at
|
|
11
|
+
- Write to Obsidian vault at ${VAULT_PATH}/Projects/ArkaOS/
|
|
12
12
|
- Write to ~/.arkaos/ (captures, insights, logs, knowledge)
|
|
13
13
|
- Use browser for research
|
|
14
14
|
- Read online documentation
|
|
@@ -81,7 +81,7 @@ from core.cognition.memory.writer import DualWriter
|
|
|
81
81
|
|
|
82
82
|
writer = DualWriter(
|
|
83
83
|
obsidian_base=os.path.expanduser(
|
|
84
|
-
"
|
|
84
|
+
"${VAULT_PATH}/Projects/ArkaOS/Knowledge Base"
|
|
85
85
|
),
|
|
86
86
|
vector_db_path=os.path.expanduser("~/.arkaos/knowledge.db"),
|
|
87
87
|
)
|
|
@@ -113,7 +113,7 @@ Use `DualWriter.write()` for each KnowledgeEntry. This automatically writes to b
|
|
|
113
113
|
## Phase 6: Report + Evolution Metrics
|
|
114
114
|
|
|
115
115
|
Write daily report to Obsidian:
|
|
116
|
-
|
|
116
|
+
`${VAULT_PATH}/Projects/ArkaOS/Dreaming/YYYY-MM-DD.md`
|
|
117
117
|
|
|
118
118
|
Format:
|
|
119
119
|
```markdown
|
|
@@ -8,7 +8,7 @@ You are ArkaOS performing your daily Research session. Your job is to stay curre
|
|
|
8
8
|
- Read any file from any project
|
|
9
9
|
- Read git logs
|
|
10
10
|
- Search the web extensively (WebSearch, Firecrawl)
|
|
11
|
-
- Write to Obsidian vault at
|
|
11
|
+
- Write to Obsidian vault at ${VAULT_PATH}/Projects/ArkaOS/
|
|
12
12
|
- Write to ~/.arkaos/ (insights, logs, knowledge, profiles)
|
|
13
13
|
- Use browser for deep research
|
|
14
14
|
- Read online documentation, blogs, changelogs, GitHub releases
|
|
@@ -94,7 +94,7 @@ from core.cognition.memory.writer import DualWriter
|
|
|
94
94
|
|
|
95
95
|
writer = DualWriter(
|
|
96
96
|
obsidian_base=os.path.expanduser(
|
|
97
|
-
"
|
|
97
|
+
"${VAULT_PATH}/Projects/ArkaOS/Knowledge Base"
|
|
98
98
|
),
|
|
99
99
|
vector_db_path=os.path.expanduser("~/.arkaos/knowledge.db"),
|
|
100
100
|
)
|
|
@@ -114,7 +114,7 @@ writer.write(entry)
|
|
|
114
114
|
## Phase 5: Cross-Reference with Dreaming
|
|
115
115
|
|
|
116
116
|
1. Read tonight's Dreaming report (if it exists):
|
|
117
|
-
|
|
117
|
+
`${VAULT_PATH}/Projects/ArkaOS/Dreaming/YYYY-MM-DD.md`
|
|
118
118
|
|
|
119
119
|
2. Read pending insights from `~/.arkaos/insights.db`:
|
|
120
120
|
```python
|
|
@@ -145,7 +145,7 @@ store.save(insight)
|
|
|
145
145
|
## Phase 6: Intelligence Briefing
|
|
146
146
|
|
|
147
147
|
Write daily briefing to Obsidian:
|
|
148
|
-
|
|
148
|
+
`${VAULT_PATH}/Projects/ArkaOS/Research/YYYY-MM-DD.md`
|
|
149
149
|
|
|
150
150
|
Format:
|
|
151
151
|
```markdown
|
|
@@ -368,7 +368,12 @@ else
|
|
|
368
368
|
fi
|
|
369
369
|
|
|
370
370
|
# ─── Metrics ─────────────────────────────────────────────────────────────
|
|
371
|
+
# Count @-mentions in the prompt for at_mention_frequency telemetry. Mirrors
|
|
372
|
+
# the claude_code.at_mention OpenTelemetry event introduced in Claude Code
|
|
373
|
+
# 2.1.122, but works on any version because we count from the same input
|
|
374
|
+
# the hook already receives.
|
|
375
|
+
at_mentions=$(printf '%s' "$user_input" | grep -oE '(^|[[:space:]])@[A-Za-z0-9_./-]+' | wc -l | tr -d ' ')
|
|
371
376
|
elapsed=$(_hook_ms)
|
|
372
377
|
if [ "$elapsed" -gt 0 ] 2>/dev/null; then
|
|
373
|
-
echo "{\"hook\":\"user-prompt-submit-v2\",\"ms\":$elapsed}" >> "$CACHE_DIR/hook-metrics.jsonl" 2>/dev/null
|
|
378
|
+
echo "{\"hook\":\"user-prompt-submit-v2\",\"ms\":$elapsed,\"at_mentions\":${at_mentions:-0}}" >> "$CACHE_DIR/hook-metrics.jsonl" 2>/dev/null
|
|
374
379
|
fi
|
|
Binary file
|
|
Binary file
|
|
@@ -7,13 +7,20 @@ from datetime import datetime, timezone
|
|
|
7
7
|
|
|
8
8
|
from core.cognition.capture.store import CaptureStore
|
|
9
9
|
from core.cognition.memory.schemas import RawCapture
|
|
10
|
+
from core.runtime.path_resolver import ProfileMissingError, project_root_regex
|
|
11
|
+
|
|
12
|
+
_FALLBACK_REGEX = re.compile(
|
|
13
|
+
r"((?:/Users|/home)/\S+/(?:Herd|Work|AIProjects|code|repos))[/\\]([^\s/\\]+)"
|
|
14
|
+
)
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
def _detect_project(digest: str) -> tuple[str, str]:
|
|
13
18
|
"""Try to detect project name and path from digest content."""
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
try:
|
|
20
|
+
regex = project_root_regex()
|
|
21
|
+
except ProfileMissingError:
|
|
22
|
+
regex = _FALLBACK_REGEX
|
|
23
|
+
path_match = regex.search(digest)
|
|
17
24
|
if path_match:
|
|
18
25
|
return path_match.group(2).rstrip("/"), path_match.group(1).rstrip("/")
|
|
19
26
|
return "unknown", os.getcwd()
|
|
Binary file
|
|
@@ -144,6 +144,11 @@ class ArkaScheduler:
|
|
|
144
144
|
claude_bin = self._resolve_claude_binary()
|
|
145
145
|
prompt_path = os.path.expanduser(schedule.prompt_file)
|
|
146
146
|
prompt_content = Path(prompt_path).read_text(encoding="utf-8")
|
|
147
|
+
try:
|
|
148
|
+
from core.runtime.path_resolver import resolve
|
|
149
|
+
prompt_content = resolve(prompt_content)
|
|
150
|
+
except Exception:
|
|
151
|
+
pass # fall back to raw template if profile unavailable
|
|
147
152
|
return [claude_bin, "-p", prompt_content, "--dangerously-skip-permissions"]
|
|
148
153
|
|
|
149
154
|
# ------------------------------------------------------------------
|
|
Binary file
|
|
Binary file
|
package/core/obsidian/writer.py
CHANGED
|
@@ -130,14 +130,15 @@ class ObsidianWriter:
|
|
|
130
130
|
|
|
131
131
|
if config_path.exists():
|
|
132
132
|
try:
|
|
133
|
+
from core.runtime.path_resolver import resolve
|
|
133
134
|
config = json.loads(config_path.read_text())
|
|
134
|
-
vault = config.get("vault_path", "")
|
|
135
|
-
if vault and Path(vault).exists():
|
|
135
|
+
vault = resolve(config.get("vault_path", ""))
|
|
136
|
+
if vault and not vault.startswith("${") and Path(vault).exists():
|
|
136
137
|
return Path(vault)
|
|
137
|
-
except (json.JSONDecodeError, OSError):
|
|
138
|
+
except (json.JSONDecodeError, OSError, Exception):
|
|
138
139
|
pass
|
|
139
140
|
|
|
140
|
-
# 3. Environment variable
|
|
141
|
+
# 3. Environment variable (legacy fallback only — path_resolver handles ARKAOS_VAULT_PATH)
|
|
141
142
|
env_vault = os.environ.get("ARKAOS_VAULT", "")
|
|
142
143
|
if env_vault and Path(env_vault).exists():
|
|
143
144
|
return Path(env_vault)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Template-token path resolver for ArkaOS.
|
|
2
|
+
|
|
3
|
+
Resolves user-specific paths from ``~/.arkaos/profile.json`` and environment
|
|
4
|
+
variables, so source files can use neutral tokens like ``${VAULT_PATH}`` and
|
|
5
|
+
``${ARKA_OS_REPOS}`` instead of hardcoded absolute paths.
|
|
6
|
+
|
|
7
|
+
Token precedence (highest → lowest):
|
|
8
|
+
1. Environment variable (e.g. ``ARKAOS_VAULT_PATH``)
|
|
9
|
+
2. profile.json field (e.g. ``vaultPath``)
|
|
10
|
+
3. For ``${GIT_HOST}`` only: hardcoded default ``github.com``
|
|
11
|
+
|
|
12
|
+
If ``~/.arkaos/profile.json`` is missing or unparseable and an
|
|
13
|
+
unconfigured token is requested, ``ProfileMissingError`` is raised
|
|
14
|
+
with a clear remediation message. Unknown tokens pass through
|
|
15
|
+
unchanged so that prompt strings containing ``${SOME_BASH_VAR}`` for
|
|
16
|
+
the agent are preserved.
|
|
17
|
+
|
|
18
|
+
See ``core/specs/SPEC-paths-portability.md`` for the full contract.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from core.runtime.user_paths import user_data_root
|
|
30
|
+
|
|
31
|
+
_PROFILE_FILENAME = "profile.json"
|
|
32
|
+
_TOKEN_PATTERN = re.compile(r"\$\{([A-Z_]+)\}")
|
|
33
|
+
|
|
34
|
+
_DEFAULT_PROJECT_ROOTS = ["~/Herd", "~/Work", "~/AIProjects"]
|
|
35
|
+
_DEFAULT_REPOS_ROOT = "~/AIProjects"
|
|
36
|
+
_DEFAULT_GIT_HOST = "github.com"
|
|
37
|
+
|
|
38
|
+
_PROFILE_CACHE: "ProfileV3 | None" = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ProfileMissingError(RuntimeError):
|
|
42
|
+
"""Raised when ``~/.arkaos/profile.json`` cannot be loaded."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class ProfileV3:
|
|
47
|
+
"""In-memory representation of profile.json v3."""
|
|
48
|
+
|
|
49
|
+
version: str
|
|
50
|
+
vault_path: str
|
|
51
|
+
repos_root: str
|
|
52
|
+
project_roots: list[str] = field(default_factory=list)
|
|
53
|
+
raw: dict = field(default_factory=dict)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def profile_path() -> Path:
|
|
57
|
+
"""Absolute path to ``~/.arkaos/profile.json``."""
|
|
58
|
+
return user_data_root() / _PROFILE_FILENAME
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def load_profile(*, refresh: bool = False) -> ProfileV3:
|
|
62
|
+
"""Load and validate profile.json.
|
|
63
|
+
|
|
64
|
+
Cached per process; pass ``refresh=True`` to force a re-read.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ProfileMissingError: file absent, unreadable, or unparseable JSON.
|
|
68
|
+
"""
|
|
69
|
+
global _PROFILE_CACHE
|
|
70
|
+
if _PROFILE_CACHE is not None and not refresh:
|
|
71
|
+
return _PROFILE_CACHE
|
|
72
|
+
|
|
73
|
+
path = profile_path()
|
|
74
|
+
if not path.exists():
|
|
75
|
+
raise ProfileMissingError(
|
|
76
|
+
f"~/.arkaos/profile.json not found at {path}. "
|
|
77
|
+
"Run /arka setup to configure ArkaOS paths."
|
|
78
|
+
)
|
|
79
|
+
try:
|
|
80
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
81
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
82
|
+
raise ProfileMissingError(
|
|
83
|
+
f"~/.arkaos/profile.json could not be parsed ({exc}). "
|
|
84
|
+
"Run /arka setup to repair, or restore from a .bak file."
|
|
85
|
+
) from exc
|
|
86
|
+
|
|
87
|
+
_PROFILE_CACHE = _project_from_raw(raw)
|
|
88
|
+
return _PROFILE_CACHE
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def reset_cache() -> None:
|
|
92
|
+
"""Drop the cached profile (for tests)."""
|
|
93
|
+
global _PROFILE_CACHE
|
|
94
|
+
_PROFILE_CACHE = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def resolve(template: str) -> str:
|
|
98
|
+
"""Substitute known ``${VAR}`` tokens in *template*.
|
|
99
|
+
|
|
100
|
+
Tokens recognised:
|
|
101
|
+
- ``${VAULT_PATH}`` → ``ARKAOS_VAULT_PATH`` env or ``profile.vaultPath``
|
|
102
|
+
- ``${ARKA_OS_REPOS}`` → ``ARKAOS_REPOS_ROOT`` env or ``profile.reposRoot``
|
|
103
|
+
- ``${PROJECT_ROOTS}`` → ``os.pathsep``-joined profile.projectRoots
|
|
104
|
+
- ``${GIT_HOST}`` → ``ARKAOS_GIT_HOST`` env, default ``github.com``
|
|
105
|
+
- ``${HOME}`` → ``os.path.expanduser("~")``
|
|
106
|
+
|
|
107
|
+
Unknown tokens pass through unchanged.
|
|
108
|
+
"""
|
|
109
|
+
return _TOKEN_PATTERN.sub(_resolve_token_match, template)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def resolve_dict(obj):
|
|
113
|
+
"""Recursively resolve every string value in nested dicts/lists."""
|
|
114
|
+
if isinstance(obj, str):
|
|
115
|
+
return resolve(obj)
|
|
116
|
+
if isinstance(obj, dict):
|
|
117
|
+
return {k: resolve_dict(v) for k, v in obj.items()}
|
|
118
|
+
if isinstance(obj, list):
|
|
119
|
+
return [resolve_dict(item) for item in obj]
|
|
120
|
+
return obj
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def project_root_regex() -> re.Pattern[str]:
|
|
124
|
+
"""Compile a regex matching paths under any configured project root.
|
|
125
|
+
|
|
126
|
+
Used by ``core/cognition/capture/collector.py`` to detect project paths
|
|
127
|
+
inside captured session digests across macOS, Linux and Windows.
|
|
128
|
+
"""
|
|
129
|
+
profile = load_profile()
|
|
130
|
+
roots = [os.path.expanduser(r) for r in profile.project_roots] or [
|
|
131
|
+
os.path.expanduser(r) for r in _DEFAULT_PROJECT_ROOTS
|
|
132
|
+
]
|
|
133
|
+
alternation = "|".join(re.escape(r.rstrip("/").rstrip("\\")) for r in roots)
|
|
134
|
+
return re.compile(rf"({alternation})[/\\]([^\s/\\]+)")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _resolve_token_match(match: re.Match[str]) -> str:
|
|
138
|
+
return _resolve_token(match.group(1), match.group(0))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _resolve_token(name: str, original: str) -> str:
|
|
142
|
+
if name == "HOME":
|
|
143
|
+
return os.path.expanduser("~")
|
|
144
|
+
if name == "GIT_HOST":
|
|
145
|
+
return _env_or("ARKAOS_GIT_HOST", _DEFAULT_GIT_HOST)
|
|
146
|
+
if name == "VAULT_PATH":
|
|
147
|
+
env_val = _env_or("ARKAOS_VAULT_PATH", _env_or("ARKAOS_VAULT", ""))
|
|
148
|
+
return env_val or load_profile().vault_path
|
|
149
|
+
if name == "ARKA_OS_REPOS":
|
|
150
|
+
env_val = _env_or("ARKAOS_REPOS_ROOT", "")
|
|
151
|
+
return env_val or load_profile().repos_root
|
|
152
|
+
if name == "PROJECT_ROOTS":
|
|
153
|
+
env_val = _env_or("ARKAOS_PROJECT_ROOTS", "")
|
|
154
|
+
if env_val:
|
|
155
|
+
return env_val
|
|
156
|
+
return os.pathsep.join(load_profile().project_roots)
|
|
157
|
+
return original
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _env_or(name: str, fallback: str) -> str:
|
|
161
|
+
value = os.environ.get(name, "")
|
|
162
|
+
return value if value else fallback
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _project_from_raw(raw: dict) -> ProfileV3:
|
|
166
|
+
vault_path = raw.get("vaultPath") or raw.get("vault_path") or ""
|
|
167
|
+
if not vault_path:
|
|
168
|
+
raise ProfileMissingError(
|
|
169
|
+
"profile.json has no vaultPath. Run /arka setup to configure it."
|
|
170
|
+
)
|
|
171
|
+
repos_root = (
|
|
172
|
+
raw.get("reposRoot")
|
|
173
|
+
or raw.get("repos_root")
|
|
174
|
+
or _DEFAULT_REPOS_ROOT
|
|
175
|
+
)
|
|
176
|
+
project_roots = list(raw.get("projectRoots") or [])
|
|
177
|
+
if not project_roots:
|
|
178
|
+
project_roots = _derive_project_roots(raw.get("projectsDir", ""))
|
|
179
|
+
return ProfileV3(
|
|
180
|
+
version=str(raw.get("version", "2")),
|
|
181
|
+
vault_path=vault_path,
|
|
182
|
+
repos_root=repos_root,
|
|
183
|
+
project_roots=project_roots,
|
|
184
|
+
raw=raw,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _derive_project_roots(projects_dir_text: str) -> list[str]:
|
|
189
|
+
"""Best-effort parse of the legacy free-text ``projectsDir`` field.
|
|
190
|
+
|
|
191
|
+
Looks for absolute paths on macOS/Linux/Windows. Falls back to
|
|
192
|
+
``_DEFAULT_PROJECT_ROOTS`` when no path can be extracted. This keeps
|
|
193
|
+
the ~20K legacy users functional until ``npx arkaos update`` rewrites
|
|
194
|
+
their profile.json with the new ``projectRoots`` field.
|
|
195
|
+
"""
|
|
196
|
+
if not projects_dir_text:
|
|
197
|
+
return list(_DEFAULT_PROJECT_ROOTS)
|
|
198
|
+
posix = r"(?:/Users|/home)/\S+?/(?:Herd|Work|AIProjects|code|repos)"
|
|
199
|
+
windows = r"[A-Z]:\\Users\\[^\s\\]+\\(?:Herd|Work|AIProjects|code|repos)"
|
|
200
|
+
pattern = re.compile(rf"({posix}|{windows})")
|
|
201
|
+
found = pattern.findall(projects_dir_text)
|
|
202
|
+
return found or list(_DEFAULT_PROJECT_ROOTS)
|
|
Binary file
|
|
Binary file
|