arkaos 2.22.0 → 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 (66) 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/auto_documentor.py +127 -12
  12. package/core/cognition/capture/__pycache__/collector.cpython-313.pyc +0 -0
  13. package/core/cognition/capture/collector.py +10 -3
  14. package/core/cognition/scheduler/__pycache__/daemon.cpython-313.pyc +0 -0
  15. package/core/cognition/scheduler/daemon.py +5 -0
  16. package/core/jobs/__pycache__/auto_doc_worker.cpython-313.pyc +0 -0
  17. package/core/jobs/auto_doc_worker.py +5 -3
  18. package/core/obsidian/__pycache__/writer.cpython-313.pyc +0 -0
  19. package/core/obsidian/writer.py +5 -4
  20. package/core/runtime/__pycache__/__init__.cpython-313.pyc +0 -0
  21. package/core/runtime/__pycache__/base.cpython-313.pyc +0 -0
  22. package/core/runtime/__pycache__/claude_code.cpython-313.pyc +0 -0
  23. package/core/runtime/__pycache__/codex_cli.cpython-313.pyc +0 -0
  24. package/core/runtime/__pycache__/cursor.cpython-313.pyc +0 -0
  25. package/core/runtime/__pycache__/gemini_cli.cpython-313.pyc +0 -0
  26. package/core/runtime/__pycache__/llm_cost_telemetry.cpython-313.pyc +0 -0
  27. package/core/runtime/__pycache__/llm_cost_telemetry_cli.cpython-313.pyc +0 -0
  28. package/core/runtime/__pycache__/llm_provider.cpython-313.pyc +0 -0
  29. package/core/runtime/__pycache__/path_resolver.cpython-313.pyc +0 -0
  30. package/core/runtime/__pycache__/pricing.cpython-313.pyc +0 -0
  31. package/core/runtime/claude_code.py +22 -16
  32. package/core/runtime/codex_cli.py +23 -6
  33. package/core/runtime/gemini_cli.py +135 -11
  34. package/core/runtime/llm_provider.py +14 -9
  35. package/core/runtime/path_resolver.py +202 -0
  36. package/core/shared/__init__.py +6 -0
  37. package/core/shared/__pycache__/__init__.cpython-313.pyc +0 -0
  38. package/core/shared/__pycache__/safe_session_id.cpython-313.pyc +0 -0
  39. package/core/shared/safe_session_id.py +41 -0
  40. package/core/specs/SPEC-installer-cross-os.md +227 -0
  41. package/core/specs/SPEC-paths-portability.md +209 -0
  42. package/core/synapse/__pycache__/kb_cache.cpython-313.pyc +0 -0
  43. package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
  44. package/core/synapse/kb_cache.py +7 -6
  45. package/core/synapse/layers.py +7 -0
  46. package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
  47. package/core/workflow/__pycache__/marker_cache.cpython-313.pyc +0 -0
  48. package/core/workflow/__pycache__/research_gate.cpython-313.pyc +0 -0
  49. package/core/workflow/flow_enforcer.py +6 -14
  50. package/core/workflow/marker_cache.py +6 -8
  51. package/core/workflow/research_gate.py +11 -9
  52. package/departments/dev/skills/scaffold/SKILL.md +4 -4
  53. package/departments/kb/skills/knowledge/SKILL.md +1 -1
  54. package/departments/ops/skills/operations/SKILL.md +1 -1
  55. package/installer/cli.js +2 -1
  56. package/installer/doctor.js +48 -0
  57. package/installer/index.js +26 -1
  58. package/installer/migrate-user-data.js +17 -1
  59. package/installer/migrations/v3_path_schema.js +109 -0
  60. package/installer/package-manager.js +191 -0
  61. package/installer/path-resolver.js +124 -0
  62. package/installer/system-tools.js +243 -0
  63. package/installer/update.js +24 -3
  64. package/knowledge/obsidian-config.json +1 -1
  65. package/package.json +1 -1
  66. package/pyproject.toml +1 -1
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  npx arkaos install
9
9
  ```
10
10
 
11
- [![npm](https://img.shields.io/npm/v/arkaos)](https://www.npmjs.com/package/arkaos) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![Tests](https://img.shields.io/badge/tests-1993%20passing-brightgreen)]()
11
+ [![npm](https://img.shields.io/npm/v/arkaos)](https://www.npmjs.com/package/arkaos) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![Tests](https://img.shields.io/badge/tests-3025%20passing-brightgreen)]()
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 # Fresh install (auto-detects runtime)
375
- npx arkaos update # Update to latest version
376
- npx arkaos migrate # Migrate from v1
377
- npx arkaos doctor # Health check
378
- npx arkaos dashboard # Start monitoring dashboard
379
- npx arkaos keys # Manage API keys
380
- npx arkaos uninstall # Remove ArkaOS
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 (1,993 tests) and Quality Gate review.
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.22.0
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) | `/Users/andreagroferreira/AIProjects/purz-comfyui-workflows` |
23
- | **lora_tester** | CLI Tool + Gallery | Python 3.8+ (stdlib) + Vanilla HTML/JS | `/Users/andreagroferreira/AIProjects/lora_tester` |
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: `/Users/andreagroferreira/AIProjects/lora_tester/.claude/skills/comfy_local/skill.md`
11
- 2. Check available workflows: `ls /Users/andreagroferreira/AIProjects/purz-comfyui-workflows/`
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 /Users/andreagroferreira/AIProjects/purz-comfyui-workflows && git log --oneline -5
324
- cd /Users/andreagroferreira/AIProjects/lora_tester && git log --oneline -5
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: `/Users/andreagroferreira/Documents/Personal/Projects/WizardingCode Internal/ComfyUI/`
406
+ All documentation: `${VAULT_PATH}/Projects/ComfyUI/`
407
407
 
408
408
  ```
409
- WizardingCode Internal/ComfyUI/
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 ~/Documents/Personal/Projects/WizardingCode Internal/ArkaOS/
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
- "~/Documents/Personal/Projects/WizardingCode Internal/ArkaOS/Knowledge Base"
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
- `~/Documents/Personal/Projects/WizardingCode Internal/ArkaOS/Dreaming/YYYY-MM-DD.md`
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 ~/Documents/Personal/Projects/WizardingCode Internal/ArkaOS/
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
- "~/Documents/Personal/Projects/WizardingCode Internal/ArkaOS/Knowledge Base"
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
- `~/Documents/Personal/Projects/WizardingCode Internal/ArkaOS/Dreaming/YYYY-MM-DD.md`
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
- `~/Documents/Personal/Projects/WizardingCode Internal/ArkaOS/Research/YYYY-MM-DD.md`
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
@@ -21,16 +21,28 @@ from __future__ import annotations
21
21
 
22
22
  import json
23
23
  import re
24
+ from contextlib import contextmanager
24
25
  from dataclasses import dataclass, field
26
+ from datetime import datetime, timezone
25
27
  from pathlib import Path
26
28
  from typing import Iterable
27
29
 
28
30
  from core.obsidian import cataloger as _cataloger
29
31
  from core.obsidian import relator as _relator
30
32
  from core.obsidian.writer import ObsidianWriter
33
+ from core.shared import safe_session_id as _safe_session_id_module
31
34
 
35
+ try:
36
+ import fcntl # POSIX only
37
+ _HAS_FLOCK = True
38
+ except ImportError:
39
+ _HAS_FLOCK = False
32
40
 
33
- SAFE_SESSION_ID_RE = re.compile(r"^[A-Za-z0-9._-]{1,128}$")
41
+
42
+ # Re-export for backward compatibility with any external importers.
43
+ SAFE_SESSION_ID_RE = _safe_session_id_module.SAFE_SESSION_ID_RE
44
+
45
+ AUTO_DOC_TELEMETRY_PATH = Path.home() / ".arkaos" / "telemetry" / "auto_doc.jsonl"
34
46
 
35
47
  _URL_RE = re.compile(r"https?://[^\s\)\]\"']+")
36
48
  _FILE_PATH_RE = re.compile(r"(?:^|[\s`'])(/[A-Za-z0-9_./\-]+\.[A-Za-z0-9]+)")
@@ -58,8 +70,8 @@ _SYSTEM_PROMPT = (
58
70
  "(150-300 words) summarising the session. Structure: short intro, "
59
71
  "then markdown sections for Key Facts, Decisions, and Sources. "
60
72
  "Preserve every URL and file path verbatim. Use Obsidian wikilinks "
61
- "([[Topic]]) for reusable concepts. No preamble, no sign-off, no "
62
- "meta commentary about the model or prompt. Output only markdown."
73
+ "([[Topic]]) for reusable concepts. Do not include preamble, sign-off, "
74
+ "or meta commentary about the model or prompt. Output only markdown."
63
75
  )
64
76
 
65
77
 
@@ -329,18 +341,52 @@ def _build_synthesis_prompt(learning: Learning) -> str:
329
341
  return "\n".join(lines)
330
342
 
331
343
 
344
+ def _extract_key_facts(learning: Learning, limit: int = 5) -> list[str]:
345
+ """Pull 3-5 bullet candidates from the learning content.
346
+
347
+ Used by the template fallback so both the LLM and template paths
348
+ produce a ``## Key Facts`` section in the same order as
349
+ ``_SYSTEM_PROMPT`` requires.
350
+ """
351
+ text = (learning.content or "").strip()
352
+ if not text:
353
+ return []
354
+ paragraphs = [p.strip() for p in re.split(r"\n\s*\n", text) if p.strip()]
355
+ bullets: list[str] = []
356
+ for para in paragraphs:
357
+ for raw in para.splitlines():
358
+ line = raw.strip().lstrip("-*• ").strip()
359
+ # Skip markdown section headings; they aren't facts.
360
+ if not line or line.startswith("#"):
361
+ continue
362
+ if line.startswith(">") or line.startswith("`"):
363
+ continue
364
+ if len(line) < 8:
365
+ continue
366
+ bullets.append(line[:240])
367
+ if len(bullets) >= limit:
368
+ return bullets
369
+ if len(bullets) >= limit:
370
+ break
371
+ return bullets
372
+
373
+
332
374
  def _template_synthesize(learning: Learning) -> str:
333
- parts = [f"# {learning.topic}", ""]
375
+ # Section order mirrors _SYSTEM_PROMPT: Key Facts → Decisions →
376
+ # Sources. Keeping both synthesis paths aligned means downstream
377
+ # consumers (MOC generation, relator) never branch on provider.
378
+ parts: list[str] = [f"# {learning.topic}", ""]
334
379
  parts.append(f"> {_AUTO_DOC_SUFFIX}.")
335
380
  parts.append("")
336
381
  if learning.content.strip():
337
382
  parts.append(learning.content.strip())
338
383
  parts.append("")
339
- if learning.sources:
340
- parts.append("## Sources")
384
+ key_facts = _extract_key_facts(learning)
385
+ if key_facts:
386
+ parts.append("## Key Facts")
341
387
  parts.append("")
342
- for src in learning.sources[:20]:
343
- parts.append(f"- {src}")
388
+ for fact in key_facts:
389
+ parts.append(f"- {fact}")
344
390
  parts.append("")
345
391
  if learning.decisions:
346
392
  parts.append("## Decisions")
@@ -348,6 +394,12 @@ def _template_synthesize(learning: Learning) -> str:
348
394
  for dec in learning.decisions[:10]:
349
395
  parts.append(f"- {dec}")
350
396
  parts.append("")
397
+ if learning.sources:
398
+ parts.append("## Sources")
399
+ parts.append("")
400
+ for src in learning.sources[:20]:
401
+ parts.append(f"- {src}")
402
+ parts.append("")
351
403
  return "\n".join(parts).rstrip() + "\n"
352
404
 
353
405
 
@@ -391,13 +443,78 @@ def _document_one(
391
443
  meta.setdefault("auto_documented", True)
392
444
  try:
393
445
  plan = _cataloger.plan(body, meta)
394
- except ValueError:
446
+ except ValueError as exc:
447
+ _log_auto_doc_event(
448
+ session_id=session_id,
449
+ event="classification-failed",
450
+ topic=learning.topic,
451
+ reason=str(exc),
452
+ )
453
+ return None
454
+ if plan is None:
455
+ _log_auto_doc_event(
456
+ session_id=session_id,
457
+ event="succeeded-empty",
458
+ topic=learning.topic,
459
+ reason="cataloger returned no plan",
460
+ )
395
461
  return None
396
462
  note_path = _cataloger.execute(plan, body, writer)
397
463
  _relate_note(note_path, body, vault_path, plan)
464
+ _log_auto_doc_event(
465
+ session_id=session_id,
466
+ event="succeeded-wrote-note",
467
+ topic=learning.topic,
468
+ reason=str(note_path),
469
+ )
398
470
  return note_path
399
471
 
400
472
 
473
+ @contextmanager
474
+ def _locked_append(path: Path):
475
+ """Append to ``path`` under an exclusive advisory lock (POSIX flock).
476
+
477
+ Mirrors the pattern in ``core/workflow/flow_enforcer._locked_append``
478
+ — see that module for the platform-fallback rationale.
479
+ """
480
+ path.parent.mkdir(parents=True, exist_ok=True)
481
+ fh = path.open("a", encoding="utf-8")
482
+ try:
483
+ if _HAS_FLOCK:
484
+ fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
485
+ yield fh
486
+ finally:
487
+ if _HAS_FLOCK:
488
+ try:
489
+ fcntl.flock(fh.fileno(), fcntl.LOCK_UN)
490
+ except OSError:
491
+ pass
492
+ fh.close()
493
+
494
+
495
+ def _log_auto_doc_event(
496
+ *,
497
+ session_id: str,
498
+ event: str,
499
+ topic: str,
500
+ reason: str,
501
+ ) -> None:
502
+ """Append a structured auto-doc telemetry entry, degrade silently."""
503
+ entry = {
504
+ "ts": datetime.now(timezone.utc).isoformat(),
505
+ "session_id": session_id,
506
+ "event": event,
507
+ "topic": topic[:120],
508
+ "reason": reason[:240],
509
+ }
510
+ try:
511
+ with _locked_append(AUTO_DOC_TELEMETRY_PATH) as fh:
512
+ fh.write(json.dumps(entry) + "\n")
513
+ except OSError:
514
+ # Telemetry failures must never break the doc job.
515
+ return
516
+
517
+
401
518
  def _relate_note(note_path: Path, body: str, vault_path: Path, plan) -> None:
402
519
  try:
403
520
  related = _relator.find_related(
@@ -428,6 +545,4 @@ def _append_related_block(note_path: Path, related) -> None:
428
545
 
429
546
 
430
547
  def _safe_session_id(session_id: str) -> bool:
431
- if not isinstance(session_id, str) or not session_id:
432
- return False
433
- return bool(SAFE_SESSION_ID_RE.match(session_id))
548
+ return _safe_session_id_module.safe_session_id(session_id) is not None
@@ -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
- path_match = re.search(
15
- r"(/Users/\S+/(?:Herd|Work|AIProjects)/([^\s/]+))", digest
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()
@@ -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
  # ------------------------------------------------------------------
@@ -28,7 +28,6 @@ from __future__ import annotations
28
28
  import argparse
29
29
  import json
30
30
  import os
31
- import re
32
31
  import sys
33
32
  import time
34
33
  import uuid
@@ -36,9 +35,12 @@ from datetime import datetime, timezone
36
35
  from pathlib import Path
37
36
  from typing import Optional
38
37
 
38
+ from core.shared import safe_session_id as _safe_session_id_module
39
+
39
40
 
40
41
  MAX_ATTEMPTS = 3
41
- SAFE_SESSION_ID_RE = re.compile(r"^[A-Za-z0-9._-]{1,128}$")
42
+ # Re-export for backward compatibility with any external importers.
43
+ SAFE_SESSION_ID_RE = _safe_session_id_module.SAFE_SESSION_ID_RE
42
44
  _QUEUE_SUBDIRS = ("pending", "processing", "completed", "failed")
43
45
 
44
46
 
@@ -77,7 +79,7 @@ def enqueue_job(
77
79
  """Write a pending job file. Returns the job id."""
78
80
  root = queue_root or _queue_root()
79
81
  _ensure_queue(root)
80
- safe = session_id if SAFE_SESSION_ID_RE.match(session_id or "") else "unknown"
82
+ safe = _safe_session_id_module.safe_session_id(session_id or "") or "unknown"
81
83
  job_id = f"{int(time.time())}-{uuid.uuid4().hex[:12]}"
82
84
  payload = {
83
85
  "job_id": job_id,
@@ -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)