arkaos 4.17.0 → 4.18.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **The Operating System for AI Agent Teams.**
4
4
 
5
- 86 agents. 17 departments. 275 skills. Enterprise frameworks. Multi-runtime. One install.
5
+ 86 agents. 17 departments. 279 skills. Enterprise frameworks. Multi-runtime. One install.
6
6
 
7
7
  ```bash
8
8
  npx arkaos install
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.17.0
1
+ 4.18.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. 275 skills. Multi-runtime. Dashboard. Knowledge RAG.
28
+ > 86 agents. 17 departments. 279 skills. Multi-runtime. Dashboard. Knowledge RAG.
29
29
 
30
30
  ## ⛔ Evidence flow — 4 gates (NON-NEGOTIABLE)
31
31
 
@@ -0,0 +1,126 @@
1
+ # Agent provenance classification — the control that makes omission impossible.
2
+ #
3
+ # Mirror of config/skills-provenance.yaml for the agent tree. The parser
4
+ # (core/agents/provenance.py) closes every way of writing a BROKEN
5
+ # provenance block, but it cannot see an agent that never writes one:
6
+ # absence reads as first-party, which is exactly what a forgotten ECC
7
+ # agent port looks like.
8
+ #
9
+ # EVERY agent YAML must be CLASSIFIED — listed in `first_party` below,
10
+ # listed in `derived` below, or declaring a `provenance:` block in its
11
+ # own YAML. An agent in none of the three fails CI by name.
12
+ # See tests/python/test_agent_provenance.py.
13
+ #
14
+ # `first_party` is the baseline: the agents that existed on the cut date,
15
+ # snapshotted once so they need no edits. CLOSED set — do not add to it.
16
+ # A new agent declares its origin in its own YAML; a ported one lands in
17
+ # `derived` with its licence written down, in a diff, in front of a
18
+ # reviewer.
19
+ #
20
+ # What the baseline is and is not:
21
+ # ASSERTED: these are ArkaOS originals.
22
+ # PROVEN: none can be ECC-derived — every one was created before
23
+ # 2026-07-14, the day we first read that repository (git log
24
+ # --diff-filter=A; verified once).
25
+ # The property the control rests on is that the set is CLOSED:
26
+ # anything new must classify itself.
27
+ #
28
+ # Keys are repo-relative agent YAML paths.
29
+ #
30
+ # derived format:
31
+ # departments/<dept>/agents/<slug>.yaml:
32
+ # origin: <lowercase-slug> # e.g. ecc-derived, community
33
+ # source: https://...
34
+ # license: MIT
35
+
36
+ baseline_date: 2026-07-14
37
+
38
+ derived: {}
39
+
40
+ first_party:
41
+ - departments/brand/agents/brand-director.yaml
42
+ - departments/brand/agents/brand-strategist.yaml
43
+ - departments/brand/agents/design-ops/design-ops-lead.yaml
44
+ - departments/brand/agents/design-ops/extraction-script-writer.yaml
45
+ - departments/brand/agents/design-ops/shadcn-padronizer.yaml
46
+ - departments/brand/agents/design-ops/wcag-auditor.yaml
47
+ - departments/brand/agents/ux-designer.yaml
48
+ - departments/brand/agents/ux-researcher.yaml
49
+ - departments/brand/agents/ux-strategist.yaml
50
+ - departments/brand/agents/visual-designer.yaml
51
+ - departments/community/agents/community-manager.yaml
52
+ - departments/community/agents/community-strategist.yaml
53
+ - departments/community/agents/engagement-designer.yaml
54
+ - departments/content/agents/content-strategist.yaml
55
+ - departments/content/agents/production/content-researcher.yaml
56
+ - departments/content/agents/production/info-compiler.yaml
57
+ - departments/content/agents/production/trends-analyst.yaml
58
+ - departments/content/agents/production/video-producer.yaml
59
+ - departments/content/agents/repurpose-distributor.yaml
60
+ - departments/content/agents/scriptwriter.yaml
61
+ - departments/content/agents/viral-engineer.yaml
62
+ - departments/dev/agents/ai-engineering/ai-engineering-lead.yaml
63
+ - departments/dev/agents/architect.yaml
64
+ - departments/dev/agents/backend-core/laravel-eng.yaml
65
+ - departments/dev/agents/backend-core/node-ts-eng.yaml
66
+ - departments/dev/agents/backend-core/python-eng.yaml
67
+ - departments/dev/agents/backend-dev.yaml
68
+ - departments/dev/agents/cto.yaml
69
+ - departments/dev/agents/data-platform/etl-eng.yaml
70
+ - departments/dev/agents/dba.yaml
71
+ - departments/dev/agents/devops-eng.yaml
72
+ - departments/dev/agents/frontend-dev.yaml
73
+ - departments/dev/agents/qa-eng.yaml
74
+ - departments/dev/agents/research-assistant.yaml
75
+ - departments/dev/agents/security-eng.yaml
76
+ - departments/dev/agents/tech-lead.yaml
77
+ - departments/ecom/agents/commerce-engineer.yaml
78
+ - departments/ecom/agents/cro-specialist.yaml
79
+ - departments/ecom/agents/ecom-director.yaml
80
+ - departments/ecom/agents/retention-manager.yaml
81
+ - departments/finance/agents/cfo.yaml
82
+ - departments/finance/agents/financial-analyst.yaml
83
+ - departments/finance/agents/investment-strategist.yaml
84
+ - departments/kb/agents/data-collector.yaml
85
+ - departments/kb/agents/knowledge-curator.yaml
86
+ - departments/kb/agents/knowledge-director.yaml
87
+ - departments/kb/agents/research-analyst.yaml
88
+ - departments/landing/agents/affiliate-manager.yaml
89
+ - departments/landing/agents/conversion-strategist.yaml
90
+ - departments/landing/agents/cro-specialist.yaml
91
+ - departments/landing/agents/sales-copywriter.yaml
92
+ - departments/leadership/agents/culture-coach.yaml
93
+ - departments/leadership/agents/hr-specialist.yaml
94
+ - departments/leadership/agents/leadership-director.yaml
95
+ - departments/marketing/agents/content-marketer.yaml
96
+ - departments/marketing/agents/marketing-director.yaml
97
+ - departments/marketing/agents/paid-specialist.yaml
98
+ - departments/marketing/agents/seo-specialist.yaml
99
+ - departments/ops/agents/automation-engineer.yaml
100
+ - departments/ops/agents/doc-writer.yaml
101
+ - departments/ops/agents/ops-lead.yaml
102
+ - departments/org/agents/chief-of-staff.yaml
103
+ - departments/org/agents/coo.yaml
104
+ - departments/org/agents/okr-steward.yaml
105
+ - departments/org/agents/org-designer.yaml
106
+ - departments/org/agents/people-ops.yaml
107
+ - departments/pm/agents/pm-director.yaml
108
+ - departments/pm/agents/product-owner.yaml
109
+ - departments/pm/agents/scrum-master.yaml
110
+ - departments/pm/agents/strategic-pm.yaml
111
+ - departments/quality/agents/copy-director.yaml
112
+ - departments/quality/agents/cqo.yaml
113
+ - departments/quality/agents/tech-director.yaml
114
+ - departments/saas/agents/cs-manager.yaml
115
+ - departments/saas/agents/growth-engineer.yaml
116
+ - departments/saas/agents/metrics-analyst.yaml
117
+ - departments/saas/agents/revops-lead.yaml
118
+ - departments/saas/agents/saas-strategist.yaml
119
+ - departments/sales/agents/sales-closer.yaml
120
+ - departments/sales/agents/sales-director.yaml
121
+ - departments/sales/agents/sales-ops.yaml
122
+ - departments/sales/agents/sdr.yaml
123
+ - departments/strategy/agents/business-model-designer.yaml
124
+ - departments/strategy/agents/decision-quality.yaml
125
+ - departments/strategy/agents/market-analyst.yaml
126
+ - departments/strategy/agents/strategy-director.yaml
@@ -58,7 +58,26 @@
58
58
 
59
59
  baseline_date: 2026-07-14
60
60
 
61
- derived: {}
61
+ derived:
62
+ # ECC review lenses (Track C1) — specialised review skills ArkaOS
63
+ # lacked, ported from the MIT-licensed everything-claude-code repo and
64
+ # rewritten in ArkaOS voice with ArkaOS personas.
65
+ departments/dev/skills/silent-failure-hunter:
66
+ origin: ecc-derived
67
+ source: https://github.com/affaan-m/ecc
68
+ license: MIT
69
+ departments/dev/skills/type-design-analyzer:
70
+ origin: ecc-derived
71
+ source: https://github.com/affaan-m/ecc
72
+ license: MIT
73
+ departments/dev/skills/pr-test-analyzer:
74
+ origin: ecc-derived
75
+ source: https://github.com/affaan-m/ecc
76
+ license: MIT
77
+ departments/dev/skills/click-path-audit:
78
+ origin: ecc-derived
79
+ source: https://github.com/affaan-m/ecc
80
+ license: MIT
62
81
 
63
82
  first_party:
64
83
  - arka
@@ -0,0 +1,117 @@
1
+ """Agent provenance — supply-chain lineage for every agent YAML.
2
+
3
+ The skill tree got provenance in v4.17.0; agents did not, and the ECC
4
+ teardown puts ~15 derived agents on the roadmap. An ECC agent ported
5
+ today would land with exactly the hole skill provenance closes: no
6
+ origin, no licence trail, indistinguishable from an ArkaOS original.
7
+ This is the agent-side mirror, and it must land before the first agent
8
+ port.
9
+
10
+ An agent YAML may carry a top-level ``provenance`` block:
11
+
12
+ id: silent-failure-hunter
13
+ name: ...
14
+ provenance:
15
+ origin: ecc-derived
16
+ source: https://github.com/affaan-m/ecc
17
+ license: MIT
18
+
19
+ Absent means first-party. The validation rules are the shared
20
+ ``core.provenance`` core (https source, licence required for derived,
21
+ no trail on ``arkaos``). This module only reads the YAML.
22
+
23
+ As with skills, no parser can see the omission vector — a port that
24
+ never writes the block reads as first-party. ``config/agents-provenance.yaml``
25
+ classifies every agent as baseline, derived, or self-declaring, and
26
+ ``tests/python/test_agent_provenance.py`` fails CI on any agent that is
27
+ none of the three.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from pathlib import Path
33
+
34
+ import yaml
35
+
36
+ from core.provenance import (
37
+ FIRST_PARTY,
38
+ Provenance,
39
+ ProvenanceError,
40
+ provenance_from_block,
41
+ )
42
+
43
+ __all__ = [
44
+ "FIRST_PARTY",
45
+ "Provenance",
46
+ "ProvenanceError",
47
+ "agent_provenance",
48
+ "declares_provenance",
49
+ "provenance_from_yaml",
50
+ "provenance_issues_from_yaml",
51
+ ]
52
+
53
+
54
+ def _provenance_block(data: dict) -> dict | None:
55
+ """The ``provenance`` mapping from a loaded agent YAML.
56
+
57
+ Present-but-not-a-mapping is an error, never a silent first-party
58
+ fallback — the same fail-closed rule the skill parser uses.
59
+ """
60
+ if "provenance" not in data:
61
+ return None
62
+ block = data["provenance"]
63
+ if not isinstance(block, dict):
64
+ raise ProvenanceError(
65
+ "provenance must be a mapping of origin/source/license"
66
+ )
67
+ return block
68
+
69
+
70
+ def provenance_from_yaml(data: dict) -> Provenance:
71
+ """Parse the provenance of a loaded agent YAML mapping.
72
+
73
+ Raises ``ProvenanceError``/``ValidationError`` when the block is
74
+ present but incoherent.
75
+ """
76
+ if not isinstance(data, dict):
77
+ raise ProvenanceError("agent YAML is not a mapping")
78
+ return provenance_from_block(_provenance_block(data))
79
+
80
+
81
+ def declares_provenance(data: dict) -> bool:
82
+ """Did the author WRITE a provenance block? (vs the first-party
83
+ default). The classification control needs this distinction, which
84
+ ``provenance_from_yaml`` collapses."""
85
+ if not isinstance(data, dict):
86
+ return False
87
+ try:
88
+ return _provenance_block(data) is not None
89
+ except ProvenanceError:
90
+ return True # broken but attempted
91
+
92
+
93
+ def _load_yaml(path: Path) -> dict:
94
+ try:
95
+ data = yaml.safe_load(path.read_text(encoding="utf-8"))
96
+ except (OSError, yaml.YAMLError) as exc:
97
+ raise ProvenanceError(f"{path}: cannot read agent YAML — {exc}") from exc
98
+ return data if isinstance(data, dict) else {}
99
+
100
+
101
+ def agent_provenance(path: str | Path) -> Provenance:
102
+ """Provenance of an agent YAML file. Raises on an incoherent block."""
103
+ return provenance_from_yaml(_load_yaml(Path(path)))
104
+
105
+
106
+ def provenance_issues_from_yaml(data: dict) -> list[str]:
107
+ """Non-raising validation of a loaded agent YAML. Empty = clean."""
108
+ from pydantic import ValidationError
109
+ try:
110
+ provenance_from_yaml(data)
111
+ except ProvenanceError as exc:
112
+ return [str(exc)]
113
+ except ValidationError as exc:
114
+ return [
115
+ err["msg"].removeprefix("Value error, ") for err in exc.errors()
116
+ ]
117
+ return []
@@ -11,6 +11,77 @@ from datetime import datetime
11
11
  from pathlib import Path
12
12
 
13
13
  from core.agents.loader import load_agent
14
+ from core.agents.provenance import Provenance, agent_provenance
15
+
16
+
17
+ def _provenance_entry(prov: Provenance) -> dict:
18
+ """The full origin/source/licence trail for the registry — not just
19
+ origin, so the shipped file can answer 'what licence is this under?'."""
20
+ entry = {"origin": prov.origin}
21
+ if not prov.is_first_party:
22
+ entry["source"] = prov.source
23
+ entry["license"] = prov.license
24
+ return entry
25
+
26
+
27
+ def _dna_dict(dna) -> dict:
28
+ """The four-framework behavioral DNA, flattened for the registry."""
29
+ return {
30
+ "disc": {
31
+ "primary": dna.disc.primary.value,
32
+ "secondary": dna.disc.secondary.value,
33
+ "label": dna.disc.label,
34
+ },
35
+ "enneagram": {
36
+ "type": dna.enneagram.type.value,
37
+ "wing": dna.enneagram.wing,
38
+ "label": dna.enneagram.label,
39
+ },
40
+ "big_five": {
41
+ "O": dna.big_five.openness,
42
+ "C": dna.big_five.conscientiousness,
43
+ "E": dna.big_five.extraversion,
44
+ "A": dna.big_five.agreeableness,
45
+ "N": dna.big_five.neuroticism,
46
+ },
47
+ "mbti": dna.mbti.type.value,
48
+ }
49
+
50
+
51
+ def _agent_entry(agent, prov: Provenance, yaml_file: Path, root: Path) -> dict:
52
+ """One registry entry for a loaded agent + its provenance."""
53
+ return {
54
+ "id": agent.id,
55
+ "name": agent.name,
56
+ "role": agent.role,
57
+ "department": agent.department,
58
+ "tier": agent.tier,
59
+ "model": agent.get_model(),
60
+ "provenance": _provenance_entry(prov),
61
+ "parent_squad": agent.parent_squad,
62
+ "sub_squad_role": agent.sub_squad_role,
63
+ **_dna_dict(agent.behavioral_dna),
64
+ "authority": {
65
+ k: v for k, v in agent.authority.model_dump().items()
66
+ if v and v != [] and k not in ("delegates_to", "escalates_to")
67
+ },
68
+ "expertise_domains": agent.expertise.domains,
69
+ "frameworks": agent.expertise.frameworks,
70
+ "knowledge_sources": agent.expertise.knowledge_sources,
71
+ "file": str(yaml_file.relative_to(root)),
72
+ "memory_path": agent.memory_path,
73
+ }
74
+
75
+
76
+ def _summaries(agents: list[dict]) -> dict:
77
+ """Tier, department, and DISC-primary counts for the registry meta."""
78
+ tiers, depts, disc = {}, {}, {}
79
+ for a in agents:
80
+ tiers[a["tier"]] = tiers.get(a["tier"], 0) + 1
81
+ depts[a["department"]] = depts.get(a["department"], 0) + 1
82
+ primary = a["disc"]["primary"]
83
+ disc[primary] = disc.get(primary, 0) + 1
84
+ return {"tiers": tiers, "departments": depts, "disc_distribution": disc}
14
85
 
15
86
 
16
87
  def generate_registry(departments_dir: str | Path, output_path: str | Path) -> dict:
@@ -34,72 +105,19 @@ def generate_registry(departments_dir: str | Path, output_path: str | Path) -> d
34
105
  for yaml_file in sorted(departments_dir.glob("*/agents/**/*.yaml")):
35
106
  try:
36
107
  agent = load_agent(yaml_file)
37
- entry = {
38
- "id": agent.id,
39
- "name": agent.name,
40
- "role": agent.role,
41
- "department": agent.department,
42
- "tier": agent.tier,
43
- "model": agent.get_model(),
44
- "parent_squad": agent.parent_squad,
45
- "sub_squad_role": agent.sub_squad_role,
46
- "disc": {
47
- "primary": agent.behavioral_dna.disc.primary.value,
48
- "secondary": agent.behavioral_dna.disc.secondary.value,
49
- "label": agent.behavioral_dna.disc.label,
50
- },
51
- "enneagram": {
52
- "type": agent.behavioral_dna.enneagram.type.value,
53
- "wing": agent.behavioral_dna.enneagram.wing,
54
- "label": agent.behavioral_dna.enneagram.label,
55
- },
56
- "big_five": {
57
- "O": agent.behavioral_dna.big_five.openness,
58
- "C": agent.behavioral_dna.big_five.conscientiousness,
59
- "E": agent.behavioral_dna.big_five.extraversion,
60
- "A": agent.behavioral_dna.big_five.agreeableness,
61
- "N": agent.behavioral_dna.big_five.neuroticism,
62
- },
63
- "mbti": agent.behavioral_dna.mbti.type.value,
64
- "authority": {
65
- k: v for k, v in agent.authority.model_dump().items()
66
- if v and v != [] and k not in ("delegates_to", "escalates_to")
67
- },
68
- "expertise_domains": agent.expertise.domains,
69
- "frameworks": agent.expertise.frameworks,
70
- "knowledge_sources": agent.expertise.knowledge_sources,
71
- "file": str(yaml_file.relative_to(departments_dir.parent)),
72
- "memory_path": agent.memory_path,
73
- }
74
- agents.append(entry)
108
+ prov = agent_provenance(yaml_file)
109
+ agents.append(_agent_entry(
110
+ agent, prov, yaml_file, departments_dir.parent))
75
111
  except Exception as e:
76
112
  errors.append(f"{yaml_file.name}: {e}")
77
113
 
78
- # Build tier summary
79
- tier_counts = {}
80
- for a in agents:
81
- tier_counts[a["tier"]] = tier_counts.get(a["tier"], 0) + 1
82
-
83
- # Build department summary
84
- dept_counts = {}
85
- for a in agents:
86
- dept_counts[a["department"]] = dept_counts.get(a["department"], 0) + 1
87
-
88
- # Build DISC distribution
89
- disc_counts = {}
90
- for a in agents:
91
- p = a["disc"]["primary"]
92
- disc_counts[p] = disc_counts.get(p, 0) + 1
93
-
94
114
  registry = {
95
115
  "_meta": {
96
116
  "version": "2.0.0",
97
117
  "generated": datetime.now().isoformat(),
98
118
  "total_agents": len(agents),
99
119
  "generator": "core/agents/registry_gen.py",
100
- "tiers": tier_counts,
101
- "departments": dept_counts,
102
- "disc_distribution": disc_counts,
120
+ **_summaries(agents),
103
121
  },
104
122
  "agents": agents,
105
123
  }
@@ -5,10 +5,14 @@ project filtering, presentation lifecycle, and dismissal analytics.
5
5
  """
6
6
 
7
7
  import sqlite3
8
- from datetime import datetime, timezone
8
+ from datetime import UTC, datetime
9
9
  from pathlib import Path
10
10
 
11
- from core.cognition.memory.schemas import ActionableInsight
11
+ from core.cognition.memory.schemas import (
12
+ INSTINCT_CONFIDENCE_MAX,
13
+ INSTINCT_CONFIDENCE_MIN,
14
+ ActionableInsight,
15
+ )
12
16
 
13
17
 
14
18
  class InsightStore:
@@ -26,6 +30,27 @@ class InsightStore:
26
30
  conn.execute("PRAGMA journal_mode=WAL")
27
31
  return conn
28
32
 
33
+ # Instinct columns added after the original 12-column schema. An
34
+ # operator's existing DB has the old shape, so these are ALTER-added
35
+ # with defaults rather than baked into CREATE TABLE — a fresh install
36
+ # gets them from CREATE, an upgrade gets them from _migrate.
37
+ #
38
+ # Each entry carries the FULL, literal ALTER statement — never an
39
+ # f-string built from parts. SQLite cannot parameterise a DDL
40
+ # identifier, so the only safe construction is a constant statement,
41
+ # and a constant statement is also the only one a scanner can trust:
42
+ # there is no interpolation site to become an injection the day these
43
+ # names turn dynamic.
44
+ _INSTINCT_COLUMNS = (
45
+ ("confidence",
46
+ "ALTER TABLE insights ADD COLUMN confidence REAL NOT NULL DEFAULT 0.5"),
47
+ ("scope",
48
+ "ALTER TABLE insights ADD COLUMN scope TEXT NOT NULL DEFAULT 'project'"),
49
+ ("evidence_count",
50
+ "ALTER TABLE insights ADD COLUMN evidence_count "
51
+ "INTEGER NOT NULL DEFAULT 1"),
52
+ )
53
+
29
54
  def _init_db(self) -> None:
30
55
  with self._conn() as conn:
31
56
  conn.execute("""
@@ -41,9 +66,13 @@ class InsightStore:
41
66
  recommendation TEXT NOT NULL,
42
67
  context TEXT NOT NULL,
43
68
  status TEXT NOT NULL DEFAULT 'pending',
44
- presented_at TEXT
69
+ presented_at TEXT,
70
+ confidence REAL NOT NULL DEFAULT 0.5,
71
+ scope TEXT NOT NULL DEFAULT 'project',
72
+ evidence_count INTEGER NOT NULL DEFAULT 1
45
73
  )
46
74
  """)
75
+ self._migrate(conn)
47
76
  conn.execute(
48
77
  "CREATE INDEX IF NOT EXISTS idx_insights_project ON insights (project)"
49
78
  )
@@ -51,6 +80,16 @@ class InsightStore:
51
80
  "CREATE INDEX IF NOT EXISTS idx_insights_status ON insights (status)"
52
81
  )
53
82
 
83
+ def _migrate(self, conn: sqlite3.Connection) -> None:
84
+ """Add instinct columns to a pre-existing table. Idempotent."""
85
+ existing = {
86
+ row["name"]
87
+ for row in conn.execute("PRAGMA table_info(insights)").fetchall()
88
+ }
89
+ for name, alter_stmt in self._INSTINCT_COLUMNS:
90
+ if name not in existing:
91
+ conn.execute(alter_stmt)
92
+
54
93
  def _row_to_insight(self, row: sqlite3.Row) -> ActionableInsight:
55
94
  data = dict(row)
56
95
  # Map trigger_source back to trigger (SQL keyword workaround)
@@ -69,8 +108,8 @@ class InsightStore:
69
108
  INSERT OR REPLACE INTO insights
70
109
  (id, project, trigger_source, date_generated, category,
71
110
  severity, title, description, recommendation, context,
72
- status, presented_at)
73
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
111
+ status, presented_at, confidence, scope, evidence_count)
112
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
74
113
  """,
75
114
  (
76
115
  insight.id,
@@ -85,6 +124,9 @@ class InsightStore:
85
124
  insight.context,
86
125
  insight.status,
87
126
  insight.presented_at.isoformat() if insight.presented_at else None,
127
+ insight.confidence,
128
+ insight.scope,
129
+ insight.evidence_count,
88
130
  ),
89
131
  )
90
132
 
@@ -130,11 +172,12 @@ class InsightStore:
130
172
  """Mark insights as presented and record the timestamp."""
131
173
  if not ids:
132
174
  return
133
- now = datetime.now(timezone.utc).isoformat()
175
+ now = datetime.now(UTC).isoformat()
134
176
  placeholders = ",".join("?" * len(ids))
135
177
  with self._conn() as conn:
136
178
  conn.execute(
137
- f"UPDATE insights SET status = 'presented', presented_at = ? WHERE id IN ({placeholders})",
179
+ "UPDATE insights SET status = 'presented', presented_at = ? "
180
+ f"WHERE id IN ({placeholders})",
138
181
  [now, *ids],
139
182
  )
140
183
 
@@ -151,5 +194,69 @@ class InsightStore:
151
194
  ).fetchall()
152
195
  return {r["category"]: r["cnt"] for r in rows}
153
196
 
197
+ def reinforce(self, insight_id: str, step: float = 0.1) -> None:
198
+ """Corroboration: raise confidence (clamped) and count evidence.
199
+
200
+ The clamp lives in ``ActionableInsight``; the SQL bounds mirror it
201
+ so a direct UPDATE cannot push a value past the band either.
202
+ """
203
+ with self._conn() as conn:
204
+ conn.execute(
205
+ """
206
+ UPDATE insights
207
+ SET confidence = MIN(?, confidence + ?),
208
+ evidence_count = evidence_count + 1
209
+ WHERE id = ?
210
+ """,
211
+ (INSTINCT_CONFIDENCE_MAX, step, insight_id),
212
+ )
213
+
214
+ def weaken(self, insight_id: str, step: float = 0.2) -> None:
215
+ """Contradiction: lower confidence (clamped to the band floor).
216
+
217
+ A correction cuts harder than a corroboration lifts — the default
218
+ step is larger — because one contradiction outweighs several
219
+ passive confirmations.
220
+ """
221
+ with self._conn() as conn:
222
+ conn.execute(
223
+ "UPDATE insights SET confidence = MAX(?, confidence - ?) WHERE id = ?",
224
+ (INSTINCT_CONFIDENCE_MIN, step, insight_id),
225
+ )
226
+
227
+ def promotable(
228
+ self, min_projects: int = 2, min_confidence: float = 0.8
229
+ ) -> list[str]:
230
+ """Titles seen as project-scoped across >= min_projects distinct
231
+ projects with mean confidence >= min_confidence.
232
+
233
+ The signal B7 (``/arka promote``) consumes: a pattern that has
234
+ proven itself in more than one project has earned global scope.
235
+ Grouped by title because that is the instinct's stable identity
236
+ across projects (the id is per-occurrence).
237
+ """
238
+ with self._conn() as conn:
239
+ rows = conn.execute(
240
+ """
241
+ SELECT title FROM insights
242
+ WHERE scope = 'project'
243
+ GROUP BY title
244
+ HAVING COUNT(DISTINCT project) >= ?
245
+ AND AVG(confidence) >= ?
246
+ """,
247
+ (min_projects, min_confidence),
248
+ ).fetchall()
249
+ return [r["title"] for r in rows]
250
+
251
+ def promote_to_global(self, title: str) -> int:
252
+ """Flip every project-scoped insight with this title to global.
253
+ Returns the number of rows promoted."""
254
+ with self._conn() as conn:
255
+ cur = conn.execute(
256
+ "UPDATE insights SET scope = 'global' WHERE title = ? AND scope = 'project'",
257
+ (title,),
258
+ )
259
+ return cur.rowcount
260
+
154
261
  def close(self) -> None:
155
262
  """No-op — connections are opened per-operation."""