arkaos 4.20.0 → 4.22.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 (80) hide show
  1. package/README.md +1 -1
  2. package/VERSION +1 -1
  3. package/arka/SKILL.md +2 -1
  4. package/config/claude-agents/marta-cqo.md +9 -0
  5. package/config/standards/{claude-md-overlays → stack-rules}/laravel.md +11 -0
  6. package/config/standards/{claude-md-overlays → stack-rules}/node.md +10 -1
  7. package/config/standards/{claude-md-overlays → stack-rules}/nuxt.md +12 -1
  8. package/config/standards/stack-rules/php.md +13 -0
  9. package/config/standards/{claude-md-overlays → stack-rules}/python.md +5 -0
  10. package/config/standards/stack-rules/react.md +14 -0
  11. package/config/standards/stack-rules/vue.md +13 -0
  12. package/core/cognition/evolve.py +299 -0
  13. package/core/cognition/evolve_cli.py +55 -0
  14. package/core/governance/stop_lint.py +266 -0
  15. package/core/governance/stop_lint_telemetry.py +132 -0
  16. package/core/governance/tool_loop_check.py +111 -0
  17. package/core/hooks/stop.py +73 -22
  18. package/core/runtime/pricing.py +53 -19
  19. package/core/synapse/layers.py +49 -476
  20. package/core/synapse/layers_base.py +99 -0
  21. package/core/synapse/layers_kb.py +395 -0
  22. package/core/sync/content_syncer.py +79 -10
  23. package/dashboard/app/components/AgentDependencyGraph.vue +5 -5
  24. package/dashboard/app/components/AgentEditDrawer.vue +120 -52
  25. package/dashboard/app/components/AgentSuggestionsCard.vue +4 -2
  26. package/dashboard/app/components/ConfirmDialog.vue +2 -2
  27. package/dashboard/app/components/DashboardState.vue +5 -3
  28. package/dashboard/app/components/GlobalSearch.vue +11 -7
  29. package/dashboard/app/components/KeyboardShortcutsHelp.vue +8 -6
  30. package/dashboard/app/components/KnowledgeSourcesList.vue +13 -9
  31. package/dashboard/app/components/MarkdownEditor.vue +1 -1
  32. package/dashboard/app/components/NotificationsBell.vue +8 -4
  33. package/dashboard/app/components/OnboardingTour.vue +24 -11
  34. package/dashboard/app/components/PersonaCloneDialog.vue +15 -8
  35. package/dashboard/app/components/PersonaWizard.vue +105 -72
  36. package/dashboard/app/components/SidebarFavoritesWidget.vue +5 -5
  37. package/dashboard/app/components/SidebarStatsWidget.vue +3 -1
  38. package/dashboard/app/components/Terminal.vue +9 -8
  39. package/dashboard/app/components/TextDiff.vue +27 -15
  40. package/dashboard/app/composables/useActivityFeed.ts +3 -3
  41. package/dashboard/app/composables/useApi.ts +4 -2
  42. package/dashboard/app/composables/useConfirmDialog.ts +1 -1
  43. package/dashboard/app/composables/useFavorites.ts +8 -8
  44. package/dashboard/app/composables/useTerminalThemes.ts +13 -13
  45. package/dashboard/app/composables/useThemeColor.ts +7 -4
  46. package/dashboard/app/pages/agents/[id].vue +319 -159
  47. package/dashboard/app/pages/agents/compare.vue +152 -52
  48. package/dashboard/app/pages/agents/index.vue +68 -55
  49. package/dashboard/app/pages/agents/new.vue +125 -58
  50. package/dashboard/app/pages/audit.vue +13 -6
  51. package/dashboard/app/pages/budget.vue +20 -18
  52. package/dashboard/app/pages/commands.vue +26 -22
  53. package/dashboard/app/pages/departments/[dept].vue +67 -31
  54. package/dashboard/app/pages/departments/compare.vue +85 -27
  55. package/dashboard/app/pages/departments/index.vue +31 -7
  56. package/dashboard/app/pages/health.vue +19 -12
  57. package/dashboard/app/pages/knowledge/index.vue +190 -78
  58. package/dashboard/app/pages/models.vue +74 -28
  59. package/dashboard/app/pages/personas/[id].vue +322 -219
  60. package/dashboard/app/pages/personas/archetypes.vue +20 -6
  61. package/dashboard/app/pages/personas/compare-with-agent.vue +109 -39
  62. package/dashboard/app/pages/personas/compare.vue +196 -124
  63. package/dashboard/app/pages/personas/index.vue +67 -60
  64. package/dashboard/app/pages/personas/new.vue +1 -1
  65. package/dashboard/app/pages/settings.vue +1 -0
  66. package/dashboard/app/pages/tasks.vue +80 -38
  67. package/dashboard/app/pages/trash.vue +11 -9
  68. package/dashboard/app/pages/workflows.vue +29 -19
  69. package/dashboard/nuxt.config.ts +2 -1
  70. package/dashboard/package.json +5 -0
  71. package/departments/kb/skills/doc-extraction/SKILL.md +99 -0
  72. package/departments/kb/skills/doc-redaction/SKILL.md +103 -0
  73. package/departments/kb/skills/knowledge-ops/SKILL.md +88 -0
  74. package/departments/ops/skills/github-ops/SKILL.md +89 -0
  75. package/departments/ops/skills/terminal-ops/SKILL.md +83 -0
  76. package/installer/index.js +1 -1
  77. package/knowledge/commands-registry.json +16 -3
  78. package/knowledge/skills-manifest.json +66 -1
  79. package/package.json +1 -1
  80. package/pyproject.toml +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **The Operating System for AI Agent Teams.**
4
4
 
5
- 86 agents. 17 departments. 287 skills. Enterprise frameworks. Multi-runtime. One install.
5
+ 86 agents. 17 departments. 292 skills. Enterprise frameworks. Multi-runtime. One install.
6
6
 
7
7
  ```bash
8
8
  npx arkaos install
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.20.0
1
+ 4.22.0
package/arka/SKILL.md CHANGED
@@ -25,7 +25,7 @@ measures citation compliance per turn.
25
25
  # ArkaOS — Main Orchestrator
26
26
 
27
27
  > **The Operating System for AI Agent Teams**
28
- > 86 agents. 17 departments. 287 skills. Multi-runtime. Dashboard. Knowledge RAG.
28
+ > 86 agents. 17 departments. 292 skills. Multi-runtime. Dashboard. Knowledge RAG.
29
29
 
30
30
  ## ⛔ Evidence flow — 4 gates (NON-NEGOTIABLE)
31
31
 
@@ -175,6 +175,7 @@ violation (squad-routing, arka-supremacy, spec-driven, mandatory-qa).
175
175
  | `/arka mcps [period]` | MCP usage — aggregates the PostToolUse MCP telemetry (`~/.arkaos/telemetry/mcp-usage.jsonl`) by day/week/month/all. Shows total calls, servers in use, top servers and top tools. Shells out to `~/.arkaos/bin/arka-py -m core.runtime.mcp_telemetry_cli <period>`. |
176
176
  | `/arka compliance [period]` | Behavior compliance summary (PR29 v2.48.0) — aggregates stop-hook telemetry by day/week/month/all. Shows rates for the four contracts: closing marker, `[arka:meta]` tag, KB citation pass, sycophancy clean. Shells out to `~/.arkaos/bin/arka-py -m core.governance.compliance_telemetry_cli <period>`. |
177
177
  | `/arka reorganize [--since-days N]` | Dreaming → Agent reorganizer. Aggregates recent KB pattern/anti-pattern/lesson artifacts (default last 7 days) into a markdown proposal at `~/.arkaos/reorganize-proposals/<date>.md`. **Propose-only** — never modifies agent YAMLs. Sanitizes client identifiers from titles and body excerpts; drops `tags:` field entirely to prevent project-name leaks. **Auto-fires on session start when today's proposal is missing** (PR24 v2.46.0 stale-aware trigger, 30s timeout, background). Shells out to `~/.arkaos/bin/arka-py -m core.cognition.reorganizer_cli [--since-days N] [--dry-run]`. |
178
+ | `/arka evolve [--min-projects N] [--min-confidence X]` | Instinct evolution. Ingests accumulated cross-project signals (`~/.arkaos/gotchas.json`) into the InsightStore as deterministic instincts (confidence in the [0.3, 0.9] band from occurrence count) and writes a markdown proposal at `~/.arkaos/evolve-proposals/<date>.md` with cross-project promotion candidates (`InsightStore.promotable`) and the strongest instincts. **Propose-only** — nothing is promoted or modified; LLM-free (Dreaming owns vault clustering). Redacts client identifiers; never renders project names. Shells out to `~/.arkaos/bin/arka-py -m core.cognition.evolve_cli [--min-projects N] [--min-confidence X] [--dry-run]`. |
178
179
  | `/arka standup` | Daily standup (projects, priorities, blockers, updates) |
179
180
  | `/arka monitor` | System health monitoring |
180
181
  | `/arka onboard <path>` | Onboard an existing project into ArkaOS |
@@ -41,6 +41,15 @@ evidence report, never from model size.
41
41
  justification in notes; otherwise REJECTED.
42
42
  4. Record the outcome via `core.governance.review_workflow` passing
43
43
  `evidence_overall` — it raises on APPROVED-over-fail by design.
44
+ 5. Record the eval label (evals ADR 2026-07-09) as your FINAL act — the
45
+ corpus only grows if the verdict-issuer writes it, and dispatch
46
+ through this agent bypasses the department SKILL's step 6: write your
47
+ final QGVerdict JSON to a temp file and run
48
+ `~/.arkaos/bin/arka-py -m core.evals.record_cli --file <f> --kind qg
49
+ --department <dept> --deliverable "<title>"`. It fails LOUDLY on
50
+ invalid JSON — if it fails, fix the JSON and re-run; never skip.
51
+ Every review feeds `~/.arkaos/telemetry/qg-verdicts.jsonl`, redo
52
+ verdicts included (a REJECTED→APPROVED pair is two labels).
44
53
 
45
54
  ## Verdict Format
46
55
 
@@ -1,3 +1,12 @@
1
+ ---
2
+ paths:
3
+ - "app/**/*.php"
4
+ - "routes/**/*.php"
5
+ - "database/**/*.php"
6
+ - "config/**/*.php"
7
+ - "tests/**/*.php"
8
+ ---
9
+
1
10
  ## Laravel Stack Conventions
2
11
 
3
12
  - Services + Repositories pattern; no logic in controllers.
@@ -5,4 +14,6 @@
5
14
  - API Resources for response shaping.
6
15
  - Feature Tests with RefreshDatabase trait.
7
16
  - Eloquent relationships over raw joins.
17
+ - Policies for authorization; never inline ability checks.
18
+ - Queued jobs for slow work; never block the request cycle.
8
19
  - Conventional commits: `feat(scope): ...`, `fix(scope): ...`.
@@ -1,7 +1,16 @@
1
- ## Node.js Stack Conventions
1
+ ---
2
+ paths:
3
+ - "**/*.js"
4
+ - "**/*.mjs"
5
+ - "**/*.cjs"
6
+ - "**/*.ts"
7
+ ---
8
+
9
+ ## Node.js / JavaScript Stack Conventions
2
10
 
3
11
  - ESM modules (import/export); no CommonJS `require()`.
4
12
  - Support Node and Bun runtimes when writing CLI tooling.
5
13
  - Graceful fallbacks when optional dependencies are missing.
6
14
  - All paths via `os.homedir()` or `path.join`; never hardcoded.
7
15
  - No interactive prompts in headless/CI runs.
16
+ - Prefer built-in `node:` modules over dependencies.
@@ -1,8 +1,19 @@
1
- ## Nuxt / Vue Stack Conventions
1
+ ---
2
+ paths:
3
+ - "**/*.vue"
4
+ - "composables/**"
5
+ - "pages/**"
6
+ - "layouts/**"
7
+ - "server/**"
8
+ - "nuxt.config.*"
9
+ ---
10
+
11
+ ## Nuxt Stack Conventions
2
12
 
3
13
  - Composition API only; no Options API.
4
14
  - TypeScript everywhere; no plain JS Vue files.
5
15
  - `composables/` for shared reactive logic.
6
16
  - `useFetch`/`useAsyncData` for server-side data.
7
17
  - `~` alias for project root imports.
18
+ - Server routes in `server/api/`; never fetch third parties from components.
8
19
  - Tailwind for styling; avoid scoped styles unless necessary.
@@ -0,0 +1,13 @@
1
+ ---
2
+ paths:
3
+ - "**/*.php"
4
+ ---
5
+
6
+ ## PHP Stack Conventions
7
+
8
+ - PHP 8.2+ syntax: constructor promotion, readonly, enums, match.
9
+ - `declare(strict_types=1)` in every file.
10
+ - Typed properties, parameters, and return types everywhere.
11
+ - Composer autoload (PSR-4); never `require` project files by path.
12
+ - Exceptions over error returns; never `@` error suppression.
13
+ - Pint (or the project formatter) must pass before a change is done.
@@ -1,3 +1,8 @@
1
+ ---
2
+ paths:
3
+ - "**/*.py"
4
+ ---
5
+
1
6
  ## Python Stack Conventions
2
7
 
3
8
  - Type hints on every function signature.
@@ -0,0 +1,14 @@
1
+ ---
2
+ paths:
3
+ - "**/*.tsx"
4
+ - "**/*.jsx"
5
+ ---
6
+
7
+ ## React / Next.js Stack Conventions
8
+
9
+ - TypeScript everywhere; no plain JSX files.
10
+ - Server Components by default; `"use client"` only when interaction demands it.
11
+ - App Router (`app/`); no new Pages Router code.
12
+ - shadcn/ui + Tailwind for UI primitives.
13
+ - Hooks for shared logic; no HOC or render-prop patterns in new code.
14
+ - Co-locate component, styles, and test; PascalCase component files.
@@ -0,0 +1,13 @@
1
+ ---
2
+ paths:
3
+ - "**/*.vue"
4
+ ---
5
+
6
+ ## Vue Stack Conventions
7
+
8
+ - Composition API only; no Options API.
9
+ - `<script setup lang="ts">` in every component.
10
+ - Props and emits fully typed (`defineProps`/`defineEmits` generics).
11
+ - Shared reactive logic lives in composables, not mixins.
12
+ - One component per file; PascalCase filenames.
13
+ - v-for always keyed; no index keys on mutable lists.
@@ -0,0 +1,299 @@
1
+ """Evolve — turn accumulated cross-project signals into instinct proposals.
2
+
3
+ Propose-only, LLM-free. The engine ingests the raw signals sessions
4
+ already accumulate (``~/.arkaos/gotchas.json`` — error patterns with
5
+ occurrence counts and the projects they appeared in) into the
6
+ ``InsightStore`` as deterministic instincts: confidence derives from the
7
+ occurrence count inside the [0.3, 0.9] band, evidence_count mirrors the
8
+ raw count, and the record id is a stable digest of (source, title,
9
+ project) so re-ingestion is idempotent. It then renders a markdown
10
+ proposal at ``~/.arkaos/evolve-proposals/<date>.md`` with the
11
+ cross-project promotion candidates (``InsightStore.promotable`` — this
12
+ module is that API's first production caller) and the strongest
13
+ instincts.
14
+
15
+ Boundaries: Dreaming owns vault clustering (LLM, multi-backend) — evolve
16
+ never re-clusters markdown; it consumes structured stores only. Nothing
17
+ is promoted, modified, or deleted: the proposal file is the only output
18
+ (the reorganizer contract). Client identifiers are redacted and
19
+ project names are never rendered — only distinct-project counts.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import hashlib
25
+ import json
26
+ import os
27
+ import tempfile
28
+ from dataclasses import dataclass, field
29
+ from datetime import UTC, datetime
30
+ from pathlib import Path
31
+
32
+ from core.cognition.insights.store import InsightStore
33
+ from core.cognition.memory.schemas import (
34
+ INSTINCT_CONFIDENCE_MAX,
35
+ INSTINCT_CONFIDENCE_MIN,
36
+ ActionableInsight,
37
+ )
38
+ from core.cognition.reorganizer import md_escape, redact_clients
39
+
40
+ _DEFAULT_OUTPUT_DIR = Path.home() / ".arkaos" / "evolve-proposals"
41
+ _DEFAULT_GOTCHAS_PATH = Path.home() / ".arkaos" / "gotchas.json"
42
+ _DEFAULT_DB_PATH = Path.home() / ".arkaos" / "insights.db"
43
+ _PROPOSAL_FILENAME_FMT = "%Y-%m-%d.md"
44
+ _CONFIDENCE_STEP = 0.1
45
+ _TOP_INSTINCTS = 10
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class PromotionCandidate:
50
+ """Cross-project instinct eligible for promotion (propose-only)."""
51
+
52
+ title: str
53
+ project_count: int
54
+ mean_confidence: float
55
+ evidence_count: int
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class EvolveReport:
60
+ """Evolve run outcome. Never contains client or project identifiers."""
61
+
62
+ date: str
63
+ ingested: int
64
+ pending_instincts: int
65
+ candidates: list[PromotionCandidate] = field(default_factory=list)
66
+ proposal_path: str | None = None
67
+
68
+
69
+ def derive_confidence(count: int) -> float:
70
+ """Occurrence count -> confidence, deterministic, inside the band."""
71
+ raw = INSTINCT_CONFIDENCE_MIN + _CONFIDENCE_STEP * (max(1, count) - 1)
72
+ return min(INSTINCT_CONFIDENCE_MAX, raw)
73
+
74
+
75
+ def _stable_id(source: str, title: str, project: str) -> str:
76
+ digest = hashlib.sha1(f"{source}:{title}:{project}".encode())
77
+ return digest.hexdigest()[:32]
78
+
79
+
80
+ def _load_gotchas(gotchas_path: Path) -> list[dict]:
81
+ try:
82
+ data = json.loads(gotchas_path.read_text(encoding="utf-8"))
83
+ except (OSError, json.JSONDecodeError):
84
+ return []
85
+ entries = data.get("gotchas") if isinstance(data, dict) else data
86
+ return [e for e in entries or [] if isinstance(e, dict)]
87
+
88
+
89
+ def _instinct_from_gotcha(gotcha: dict, project: str) -> ActionableInsight | None:
90
+ title = str(gotcha.get("pattern") or "").strip()
91
+ if not title or not project.strip():
92
+ return None
93
+ count = int(gotcha.get("count") or 1)
94
+ return ActionableInsight(
95
+ id=_stable_id("gotcha", title, project),
96
+ project=project,
97
+ trigger="evolve-ingest",
98
+ category="technical",
99
+ severity="improve" if count >= 5 else "consider",
100
+ title=title,
101
+ description=str(gotcha.get("full_pattern") or title),
102
+ recommendation=str(gotcha.get("suggestion") or "").strip()
103
+ or "Recurring signal — consider a rule, skill, or fix.",
104
+ context=f"tool={gotcha.get('tool', '?')} count={count}",
105
+ confidence=derive_confidence(count),
106
+ evidence_count=max(1, count),
107
+ )
108
+
109
+
110
+ def ingest_gotchas(store: InsightStore, gotchas_path: Path) -> int:
111
+ """Mirror gotcha signals into the store as project-scoped instincts.
112
+
113
+ Deterministic derivation from the source of truth: re-ingesting
114
+ recomputes the same ids and values (INSERT OR REPLACE), so repeat
115
+ runs are idempotent.
116
+ """
117
+ ingested = 0
118
+ for gotcha in _load_gotchas(gotchas_path):
119
+ projects = gotcha.get("projects") or []
120
+ for project in {str(p) for p in projects if str(p).strip()}:
121
+ insight = _instinct_from_gotcha(gotcha, project)
122
+ if insight is None:
123
+ continue
124
+ store.save(insight)
125
+ ingested += 1
126
+ return ingested
127
+
128
+
129
+ def _candidates(
130
+ store: InsightStore, min_projects: int, min_confidence: float,
131
+ ) -> list[PromotionCandidate]:
132
+ titles = store.promotable(
133
+ min_projects=min_projects, min_confidence=min_confidence
134
+ )
135
+ rows = [i for i in store.get_all_pending() if i.scope == "project"]
136
+ out: list[PromotionCandidate] = []
137
+ for title in titles:
138
+ group = [i for i in rows if i.title == title]
139
+ if not group:
140
+ continue
141
+ out.append(
142
+ PromotionCandidate(
143
+ title=title,
144
+ project_count=len({i.project for i in group}),
145
+ mean_confidence=round(
146
+ sum(i.confidence for i in group) / len(group), 2
147
+ ),
148
+ evidence_count=sum(i.evidence_count for i in group),
149
+ )
150
+ )
151
+ return out
152
+
153
+
154
+ def build_proposal(
155
+ *,
156
+ db_path: Path | None = None,
157
+ gotchas_path: Path | None = None,
158
+ output_dir: Path | None = None,
159
+ dry_run: bool = False,
160
+ min_projects: int = 2,
161
+ min_confidence: float = 0.8,
162
+ ) -> EvolveReport:
163
+ """Ingest signals, compute promotion candidates, write the proposal."""
164
+ store = InsightStore(db_path or _DEFAULT_DB_PATH)
165
+ ingested = ingest_gotchas(store, gotchas_path or _DEFAULT_GOTCHAS_PATH)
166
+ pending = store.get_all_pending()
167
+ candidates = _candidates(store, min_projects, min_confidence)
168
+ date = datetime.now(UTC).strftime("%Y-%m-%d")
169
+ markdown = _render(date, ingested, pending, candidates)
170
+ if dry_run:
171
+ print(markdown)
172
+ return EvolveReport(date, ingested, len(pending), candidates)
173
+ path = _write_report(markdown, output_dir)
174
+ return EvolveReport(date, ingested, len(pending), candidates, str(path))
175
+
176
+
177
+ def _render_candidate(candidate: PromotionCandidate) -> str:
178
+ title = md_escape(redact_clients(candidate.title))
179
+ return (
180
+ f"| {title} | {candidate.project_count} | "
181
+ f"{candidate.mean_confidence:.2f} | {candidate.evidence_count} |"
182
+ )
183
+
184
+
185
+ def _render_instinct(insight: ActionableInsight) -> str:
186
+ title = md_escape(redact_clients(insight.title))
187
+ rec = md_escape(redact_clients(insight.recommendation))
188
+ return (
189
+ f"- **{title}** — confidence {insight.confidence:.2f}, "
190
+ f"evidence {insight.evidence_count}. {rec}"
191
+ )
192
+
193
+
194
+ def _render_summary(
195
+ date: str, ingested: int, pending_count: int, candidate_count: int,
196
+ ) -> list[str]:
197
+ return [
198
+ f"# Evolve proposal — {date}",
199
+ "",
200
+ "> Propose-only: nothing was promoted or modified. Review and",
201
+ "> act via the squads; promotion stays a deliberate operator call.",
202
+ "",
203
+ "## Summary",
204
+ "",
205
+ f"- Instincts ingested this run: **{ingested}**",
206
+ f"- Pending instincts in store: **{pending_count}**",
207
+ f"- Promotion candidates: **{candidate_count}**",
208
+ "",
209
+ ]
210
+
211
+
212
+ def _render_candidates_block(candidates: list[PromotionCandidate]) -> list[str]:
213
+ lines = ["## Promotion candidates (cross-project)", ""]
214
+ if candidates:
215
+ lines += [
216
+ "| Instinct | Projects | Mean confidence | Evidence |",
217
+ "|---|---|---|---|",
218
+ *[_render_candidate(c) for c in candidates],
219
+ ]
220
+ else:
221
+ lines.append("None yet — needs the same signal in 2+ projects.")
222
+ return lines
223
+
224
+
225
+ def _strongest_by_title(pending: list[ActionableInsight]) -> list[ActionableInsight]:
226
+ """Top instincts, one row per title (a cross-project pattern has one
227
+ project-scoped row per project — the operator needs it once)."""
228
+ ranked = sorted(
229
+ pending, key=lambda i: (i.confidence, i.evidence_count), reverse=True
230
+ )
231
+ seen: set[str] = set()
232
+ out: list[ActionableInsight] = []
233
+ for insight in ranked:
234
+ if insight.title in seen:
235
+ continue
236
+ seen.add(insight.title)
237
+ out.append(insight)
238
+ if len(out) == _TOP_INSTINCTS:
239
+ break
240
+ return out
241
+
242
+
243
+ def _render_instincts_block(pending: list[ActionableInsight]) -> list[str]:
244
+ strongest = _strongest_by_title(pending)
245
+ lines = ["", "## Strongest instincts", ""]
246
+ if strongest:
247
+ lines += [_render_instinct(i) for i in strongest]
248
+ else:
249
+ lines.append("Store is empty — no signals accumulated yet.")
250
+ return lines
251
+
252
+
253
+ def _render(
254
+ date: str,
255
+ ingested: int,
256
+ pending: list[ActionableInsight],
257
+ candidates: list[PromotionCandidate],
258
+ ) -> str:
259
+ lines = (
260
+ _render_summary(date, ingested, len(pending), len(candidates))
261
+ + _render_candidates_block(candidates)
262
+ + _render_instincts_block(pending)
263
+ )
264
+ return "\n".join(lines) + "\n"
265
+
266
+
267
+ def _write_report(markdown: str, output_dir: Path | None) -> Path:
268
+ """Atomic markdown write to an allowlisted output directory."""
269
+ out = _validate_output_dir(output_dir)
270
+ out.mkdir(parents=True, exist_ok=True)
271
+ report_path = out / datetime.now(UTC).strftime(
272
+ _PROPOSAL_FILENAME_FMT
273
+ )
274
+ tmp_path = report_path.with_suffix(f".tmp-{os.getpid()}.md")
275
+ tmp_path.write_text(markdown, encoding="utf-8")
276
+ os.replace(tmp_path, report_path)
277
+ return report_path
278
+
279
+
280
+ def _validate_output_dir(output_dir: Path | None) -> Path:
281
+ """Allowlist writes to ~/.arkaos or the system tempdir (defence in
282
+ depth — same contract as the reorganizer)."""
283
+ if output_dir is None:
284
+ return _DEFAULT_OUTPUT_DIR
285
+ resolved = Path(output_dir).expanduser().resolve()
286
+ allowed_roots = (
287
+ (Path.home() / ".arkaos").resolve(),
288
+ Path(tempfile.gettempdir()).resolve(),
289
+ )
290
+ for root in allowed_roots:
291
+ try:
292
+ resolved.relative_to(root)
293
+ return resolved
294
+ except ValueError:
295
+ continue
296
+ raise ValueError(
297
+ "output_dir must be under one of "
298
+ f"{[str(r) for r in allowed_roots]}; got {resolved}"
299
+ )
@@ -0,0 +1,55 @@
1
+ """CLI for the evolve engine: ``python -m core.cognition.evolve_cli``.
2
+
3
+ Mirrors ``reorganizer_cli``: argparse in, one ``build_proposal`` call,
4
+ human-readable summary out. ``--dry-run`` prints the proposal to stdout
5
+ without writing anything.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+
12
+ from core.cognition.evolve import build_proposal
13
+
14
+
15
+ def _build_parser() -> argparse.ArgumentParser:
16
+ parser = argparse.ArgumentParser(
17
+ prog="python -m core.cognition.evolve_cli",
18
+ description=(
19
+ "Ingest accumulated signals into instincts and write a "
20
+ "propose-only evolution proposal."
21
+ ),
22
+ )
23
+ parser.add_argument(
24
+ "--min-projects", type=int, default=2,
25
+ help="distinct projects required for a promotion candidate",
26
+ )
27
+ parser.add_argument(
28
+ "--min-confidence", type=float, default=0.8,
29
+ help="mean confidence required for a promotion candidate",
30
+ )
31
+ parser.add_argument(
32
+ "--dry-run", action="store_true",
33
+ help="print the proposal to stdout; write nothing",
34
+ )
35
+ return parser
36
+
37
+
38
+ def main(argv: list[str] | None = None) -> int:
39
+ args = _build_parser().parse_args(argv)
40
+ report = build_proposal(
41
+ dry_run=args.dry_run,
42
+ min_projects=args.min_projects,
43
+ min_confidence=args.min_confidence,
44
+ )
45
+ if not args.dry_run:
46
+ print(
47
+ f"evolve: ingested={report.ingested} "
48
+ f"pending={report.pending_instincts} "
49
+ f"candidates={len(report.candidates)} -> {report.proposal_path}"
50
+ )
51
+ return 0
52
+
53
+
54
+ if __name__ == "__main__":
55
+ raise SystemExit(main())