arkaos 4.21.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.
- package/README.md +1 -1
- package/VERSION +1 -1
- package/arka/SKILL.md +2 -1
- package/config/claude-agents/marta-cqo.md +9 -0
- package/config/standards/{claude-md-overlays → stack-rules}/laravel.md +11 -0
- package/config/standards/{claude-md-overlays → stack-rules}/node.md +10 -1
- package/config/standards/{claude-md-overlays → stack-rules}/nuxt.md +12 -1
- package/config/standards/stack-rules/php.md +13 -0
- package/config/standards/{claude-md-overlays → stack-rules}/python.md +5 -0
- package/config/standards/stack-rules/react.md +14 -0
- package/config/standards/stack-rules/vue.md +13 -0
- package/core/cognition/evolve.py +299 -0
- package/core/cognition/evolve_cli.py +55 -0
- package/core/governance/stop_lint.py +266 -0
- package/core/governance/stop_lint_telemetry.py +132 -0
- package/core/governance/tool_loop_check.py +111 -0
- package/core/hooks/stop.py +73 -22
- package/core/runtime/pricing.py +53 -19
- package/core/synapse/layers.py +49 -476
- package/core/synapse/layers_base.py +99 -0
- package/core/synapse/layers_kb.py +395 -0
- package/core/sync/content_syncer.py +79 -10
- package/dashboard/app/components/AgentDependencyGraph.vue +5 -5
- package/dashboard/app/components/AgentEditDrawer.vue +120 -52
- package/dashboard/app/components/AgentSuggestionsCard.vue +4 -2
- package/dashboard/app/components/ConfirmDialog.vue +2 -2
- package/dashboard/app/components/DashboardState.vue +5 -3
- package/dashboard/app/components/GlobalSearch.vue +11 -7
- package/dashboard/app/components/KeyboardShortcutsHelp.vue +8 -6
- package/dashboard/app/components/KnowledgeSourcesList.vue +13 -9
- package/dashboard/app/components/MarkdownEditor.vue +1 -1
- package/dashboard/app/components/NotificationsBell.vue +8 -4
- package/dashboard/app/components/OnboardingTour.vue +24 -11
- package/dashboard/app/components/PersonaCloneDialog.vue +15 -8
- package/dashboard/app/components/PersonaWizard.vue +105 -72
- package/dashboard/app/components/SidebarFavoritesWidget.vue +5 -5
- package/dashboard/app/components/SidebarStatsWidget.vue +3 -1
- package/dashboard/app/components/Terminal.vue +9 -8
- package/dashboard/app/components/TextDiff.vue +27 -15
- package/dashboard/app/composables/useActivityFeed.ts +3 -3
- package/dashboard/app/composables/useApi.ts +4 -2
- package/dashboard/app/composables/useConfirmDialog.ts +1 -1
- package/dashboard/app/composables/useFavorites.ts +8 -8
- package/dashboard/app/composables/useTerminalThemes.ts +13 -13
- package/dashboard/app/composables/useThemeColor.ts +7 -4
- package/dashboard/app/pages/agents/[id].vue +319 -159
- package/dashboard/app/pages/agents/compare.vue +152 -52
- package/dashboard/app/pages/agents/index.vue +68 -55
- package/dashboard/app/pages/agents/new.vue +125 -58
- package/dashboard/app/pages/audit.vue +13 -6
- package/dashboard/app/pages/budget.vue +20 -18
- package/dashboard/app/pages/commands.vue +26 -22
- package/dashboard/app/pages/departments/[dept].vue +67 -31
- package/dashboard/app/pages/departments/compare.vue +85 -27
- package/dashboard/app/pages/departments/index.vue +31 -7
- package/dashboard/app/pages/health.vue +19 -12
- package/dashboard/app/pages/knowledge/index.vue +190 -78
- package/dashboard/app/pages/models.vue +74 -28
- package/dashboard/app/pages/personas/[id].vue +322 -219
- package/dashboard/app/pages/personas/archetypes.vue +20 -6
- package/dashboard/app/pages/personas/compare-with-agent.vue +109 -39
- package/dashboard/app/pages/personas/compare.vue +196 -124
- package/dashboard/app/pages/personas/index.vue +67 -60
- package/dashboard/app/pages/personas/new.vue +1 -1
- package/dashboard/app/pages/settings.vue +1 -0
- package/dashboard/app/pages/tasks.vue +80 -38
- package/dashboard/app/pages/trash.vue +11 -9
- package/dashboard/app/pages/workflows.vue +29 -19
- package/dashboard/nuxt.config.ts +2 -1
- package/dashboard/package.json +5 -0
- package/departments/kb/skills/doc-extraction/SKILL.md +99 -0
- package/departments/kb/skills/doc-redaction/SKILL.md +103 -0
- package/installer/index.js +1 -1
- package/knowledge/commands-registry.json +16 -3
- package/knowledge/skills-manifest.json +27 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/README.md
CHANGED
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
@@ -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())
|