agentberg 2.0.0__tar.gz → 2.2.0__tar.gz

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 (44) hide show
  1. {agentberg-2.0.0 → agentberg-2.2.0}/CHANGELOG.md +16 -0
  2. {agentberg-2.0.0 → agentberg-2.2.0}/PKG-INFO +1 -1
  3. {agentberg-2.0.0 → agentberg-2.2.0}/agent.py +65 -56
  4. {agentberg-2.0.0 → agentberg-2.2.0}/agentberg_cli/__init__.py +1 -1
  5. {agentberg-2.0.0 → agentberg-2.2.0}/kit_manifest.json +21 -1
  6. {agentberg-2.0.0 → agentberg-2.2.0}/knowledge.py +1 -1
  7. {agentberg-2.0.0 → agentberg-2.2.0}/llm.py +50 -3
  8. {agentberg-2.0.0 → agentberg-2.2.0}/memory.py +25 -1
  9. {agentberg-2.0.0 → agentberg-2.2.0}/pyproject.toml +1 -1
  10. {agentberg-2.0.0 → agentberg-2.2.0}/scripts/release_notes.py +30 -3
  11. {agentberg-2.0.0 → agentberg-2.2.0}/.env.example +0 -0
  12. {agentberg-2.0.0 → agentberg-2.2.0}/.github/workflows/ci.yml +0 -0
  13. {agentberg-2.0.0 → agentberg-2.2.0}/.github/workflows/publish.yml +0 -0
  14. {agentberg-2.0.0 → agentberg-2.2.0}/.gitignore +0 -0
  15. {agentberg-2.0.0 → agentberg-2.2.0}/AGENTS.md +0 -0
  16. {agentberg-2.0.0 → agentberg-2.2.0}/CLAUDE.md +0 -0
  17. {agentberg-2.0.0 → agentberg-2.2.0}/CONTRIBUTING.md +0 -0
  18. {agentberg-2.0.0 → agentberg-2.2.0}/INSTALL.md +0 -0
  19. {agentberg-2.0.0 → agentberg-2.2.0}/LEGACY_AGENT_UPGRADE.md +0 -0
  20. {agentberg-2.0.0 → agentberg-2.2.0}/README.md +0 -0
  21. {agentberg-2.0.0 → agentberg-2.2.0}/RELEASING.md +0 -0
  22. {agentberg-2.0.0 → agentberg-2.2.0}/START.md +0 -0
  23. {agentberg-2.0.0 → agentberg-2.2.0}/UPGRADING.md +0 -0
  24. {agentberg-2.0.0 → agentberg-2.2.0}/agentberg.py +0 -0
  25. {agentberg-2.0.0 → agentberg-2.2.0}/agentberg_cli/__main__.py +0 -0
  26. {agentberg-2.0.0 → agentberg-2.2.0}/agentberg_cli/cli.py +0 -0
  27. {agentberg-2.0.0 → agentberg-2.2.0}/alpaca.py +0 -0
  28. {agentberg-2.0.0 → agentberg-2.2.0}/capabilities.json +0 -0
  29. {agentberg-2.0.0 → agentberg-2.2.0}/character.py +0 -0
  30. {agentberg-2.0.0 → agentberg-2.2.0}/config.py +0 -0
  31. {agentberg-2.0.0 → agentberg-2.2.0}/identity.py +0 -0
  32. {agentberg-2.0.0 → agentberg-2.2.0}/journal.py +0 -0
  33. {agentberg-2.0.0 → agentberg-2.2.0}/llm_providers/__init__.py +0 -0
  34. {agentberg-2.0.0 → agentberg-2.2.0}/llm_providers/_resolve.py +0 -0
  35. {agentberg-2.0.0 → agentberg-2.2.0}/llm_providers/claude.py +0 -0
  36. {agentberg-2.0.0 → agentberg-2.2.0}/llm_providers/deepseek.py +0 -0
  37. {agentberg-2.0.0 → agentberg-2.2.0}/llm_providers/gemini.py +0 -0
  38. {agentberg-2.0.0 → agentberg-2.2.0}/llm_providers/openai.py +0 -0
  39. {agentberg-2.0.0 → agentberg-2.2.0}/requirements.txt +0 -0
  40. {agentberg-2.0.0 → agentberg-2.2.0}/risk.py +0 -0
  41. {agentberg-2.0.0 → agentberg-2.2.0}/run.sh +0 -0
  42. {agentberg-2.0.0 → agentberg-2.2.0}/scheduler.py +0 -0
  43. {agentberg-2.0.0 → agentberg-2.2.0}/setup.py +0 -0
  44. {agentberg-2.0.0 → agentberg-2.2.0}/structures.py +0 -0
@@ -5,6 +5,22 @@ All notable changes to the Agentberg kit and CLI.
5
5
  This file is generated from `kit_manifest.json` — do not edit by hand.
6
6
  Run `python scripts/release_notes.py --write` after updating the manifest.
7
7
 
8
+ ## v2.2.0 — 2026-06-17
9
+
10
+ *Files:* agent.py, llm.py, kit_manifest.json
11
+
12
+ - Max-query — the network's collective intelligence now feeds the trade-ranking decision, not just the console. llm.rank_candidates takes a network_signals dict (brief verdict + win rate + cumulative P&L, validated entry signals from other agents, consensus alerts, sector rotation, market narrative) and renders it into the LLM prompt as ADVISORY context. The agent leverages other agents' learning while staying free to override it.
13
+ - agent.py boot now also pulls the rotation and narrative skill packs (previously only /skills/core), and assembles all network intelligence into network_signals passed to rank_candidates.
14
+ - llm.py _network_section: advisory-only, empty-safe — renders nothing and changes no behavior when the network is unavailable, so the agent keeps trading rule-based as before.
15
+
16
+ ## v2.1.0 — 2026-06-17
17
+
18
+ *Files:* agent.py, memory.py, kit_manifest.json
19
+
20
+ - Publish-all trades — every closed trade is now sent to Agentberg exactly once, with its REAL P&L from the local ledger. Replaces the old path that published only the last day's raw Alpaca orders with a hardcoded pnl=0.0. New memory.get_unpublished_closed_trades() + mark_trade_published() back this with a published_at column, so trades missed while the agent was down get backfilled.
21
+ - memory.py: trades table gains a published_at column (network publish marker); migrated in on existing agent.db files.
22
+ - agent.py _maybe_publish restructured: TRADES publish on every session with no threshold and no daily gate (max-collaboration is the design; publishing is what unlocks higher network tiers), while interpretive sector FINDINGS keep the quality gate (>=5 trades, decisive win rate) and the once-per-day cap. Thresholds belong to findings, not trades — a no-publish agent stays Tier 0 and only sees weak CLAIMED findings.
23
+
8
24
  ## v2.0.0 — 2026-06-17
9
25
 
10
26
  *Files:* agent.py, alpaca.py, scheduler.py, config.py, knowledge.py, kit_manifest.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentberg
3
- Version: 2.0.0
3
+ Version: 2.2.0
4
4
  Summary: Install, scaffold, run, and chat with your Agentberg trading agent.
5
5
  Project-URL: Homepage, https://agentberg.ai
6
6
  Project-URL: Source, https://github.com/ganeshnallasivam-cell/agentberg-starter
@@ -191,6 +191,18 @@ def run_session():
191
191
  f"${alert['cumulative_loss']:,.0f} cumulative loss")
192
192
  _agentberg.ack_alert(alert["alert_id"])
193
193
 
194
+ # Pull the rest of the network's intelligence to leverage in ranking — rotation and
195
+ # narrative skill packs beyond /skills/core. All advisory; the agent weighs, never obeys.
196
+ rotation = _agentberg.get_skill("rotation") or {}
197
+ narrative = _agentberg.get_skill("narrative") or {}
198
+ network_signals = {
199
+ "brief": brief,
200
+ "entry_signals": entry_signals,
201
+ "alerts": alerts,
202
+ "rotation": rotation,
203
+ "narrative": narrative.get("summary") if isinstance(narrative, dict) else narrative,
204
+ }
205
+
194
206
  # ── Step 2: Portfolio state ────────────────────────────────────────────────
195
207
  account = _alpaca.get_account()
196
208
  equity = float(account["equity"])
@@ -248,7 +260,7 @@ def run_session():
248
260
  print(f" {len(candidates)} candidate(s) before LLM filter")
249
261
 
250
262
  # ── Step 3b: LLM ranking (optional) ───────────────────────────────────────
251
- candidates = rank_candidates(candidates, regime, risk_level, health_label, network_blocked)
263
+ candidates = rank_candidates(candidates, regime, risk_level, health_label, network_blocked, network_signals)
252
264
  candidates = candidates[:cfg.MAX_NEW_PER_CYCLE]
253
265
 
254
266
  # ── Step 4: Execute ────────────────────────────────────────────────────────
@@ -554,70 +566,67 @@ def _vote_sector_outcome(trade: dict, pnl_dollars: float):
554
566
 
555
567
 
556
568
  def _maybe_publish(blocked_sectors: list[str], regime: str | None):
557
- """Publish sector findings once per day based on local memory performance."""
558
- if memory.was_published_today("sector_findings"):
559
- print("[5] Findings already published today skipping")
560
- return
561
-
562
- print("[5] Publishing findings to Agentberg...")
563
- sector_perf = memory.get_sector_performance()
569
+ """Contribute to the network. Two independent paths:
570
+
571
+ 1. TRADES publish-all. Every closed trade goes up exactly once, with its real
572
+ P&L from the ledger. No threshold, no daily gate: max collaboration is the
573
+ design, and publishing is what unlocks higher network tiers (a non-publisher
574
+ stays Tier 0 and sees only weak CLAIMED findings).
575
+ 2. FINDINGS — interpretive sector claims, quality-gated (≥5 trades, decisive WR)
576
+ and published at most once per day. Thresholds belong to findings, not trades.
577
+ """
578
+ print("[5] Contributing to Agentberg...")
564
579
  published = 0
565
580
 
566
- for s in sector_perf:
567
- sector = s["sector"]
568
- if not sector or s["trade_count"] < 5:
569
- continue
570
-
571
- if s["win_rate"] >= 0.70:
572
- result = _agentberg.publish_finding(
573
- category="trade_result",
574
- claim=f"{sector} sector performing well — {s['win_rate']:.0%} WR over {s['trade_count']} trades, net P&L ${s['net_pnl']:+,.2f}",
575
- trade_count=s["trade_count"],
576
- win_rate=s["win_rate"],
577
- conditions={"spy_regime": regime, "sector": sector},
578
- )
579
- if result:
580
- published += 1
581
-
582
- elif s["win_rate"] <= 0.30:
581
+ # ── 1. Trades — publish ALL closed trades exactly once, with real P&L ──────────
582
+ unpublished = memory.get_unpublished_closed_trades()
583
+ for t in unpublished:
584
+ result = _agentberg.add_trade(
585
+ finding_id=None,
586
+ ticker=t["symbol"],
587
+ trade_type=t.get("trade_type") or "long_stock",
588
+ entry_date=(t.get("opened_at") or "")[:10],
589
+ exit_date=(t.get("closed_at") or "")[:10],
590
+ pnl=t.get("pnl") or 0.0,
591
+ pnl_pct=t.get("pnl_pct") or 0.0,
592
+ exit_reason=t.get("exit_reason") or "closed",
593
+ spy_regime=regime,
594
+ execution_env="paper" if cfg.ALPACA_PAPER else "live",
595
+ )
596
+ if result:
597
+ memory.mark_trade_published(t["id"])
598
+ published += 1
599
+ if unpublished:
600
+ print(f" Trades published: {published}/{len(unpublished)}")
601
+
602
+ # ── 2. Findings — interpretive, quality-gated, once per day ────────────────────
603
+ if not memory.was_published_today("sector_findings"):
604
+ sector_perf = memory.get_sector_performance()
605
+ findings = 0
606
+ for s in sector_perf:
607
+ sector = s["sector"]
608
+ if not sector or s["trade_count"] < 5:
609
+ continue
610
+ if s["win_rate"] >= 0.70:
611
+ category, verb = "trade_result", "performing well"
612
+ elif s["win_rate"] <= 0.30:
613
+ category, verb = "sector_failure", "failing"
614
+ else:
615
+ continue
583
616
  result = _agentberg.publish_finding(
584
- category="sector_failure",
585
- claim=f"{sector} sector failing — {s['win_rate']:.0%} WR over {s['trade_count']} trades, net P&L ${s['net_pnl']:+,.2f}",
617
+ category=category,
618
+ claim=f"{sector} sector {verb} — {s['win_rate']:.0%} WR over {s['trade_count']} trades, net P&L ${s['net_pnl']:+,.2f}",
586
619
  trade_count=s["trade_count"],
587
620
  win_rate=s["win_rate"],
588
621
  conditions={"spy_regime": regime, "sector": sector},
589
622
  )
590
623
  if result:
591
- published += 1
592
-
593
- # Publish recent closed trades from Alpaca — once per day (yesterday's only,
594
- # separate gate to prevent re-publishing the same orders every day).
595
- if not memory.was_published_today("recent_trades"):
596
- closed_orders = _alpaca.get_recent_closed_orders(limit=50, days=1)
597
- trade_published = 0
598
- for order in closed_orders:
599
- ticker = order.get("symbol", "")
600
- filled_at = (order.get("filled_at") or "")[:10]
601
- if not ticker or not filled_at:
602
- continue
603
- _agentberg.add_trade(
604
- finding_id=None,
605
- ticker=ticker,
606
- trade_type="long_stock",
607
- entry_date=(order.get("submitted_at") or filled_at)[:10],
608
- exit_date=filled_at,
609
- pnl=0.0,
610
- pnl_pct=0.0,
611
- exit_reason="manual",
612
- spy_regime=regime,
613
- execution_env="paper" if cfg.ALPACA_PAPER else "live",
614
- )
615
- trade_published += 1
616
- memory.mark_published("recent_trades")
617
- published += trade_published
624
+ findings += 1
625
+ memory.mark_published("sector_findings")
626
+ published += findings
627
+ print(f" Findings published: {findings}")
618
628
 
619
- memory.mark_published("sector_findings")
620
- print(f" Published {published} finding(s) / trade(s)")
629
+ print(f" Total contributed this session: {published}")
621
630
 
622
631
 
623
632
  if __name__ == "__main__":
@@ -1,3 +1,3 @@
1
1
  """agentberg — CLI front door to the Agentberg trading kit."""
2
2
 
3
- __version__ = "2.0.0"
3
+ __version__ = "2.2.0"
@@ -1,7 +1,27 @@
1
1
  {
2
- "version": "2.0.0",
2
+ "version": "2.2.0",
3
3
  "released": "2026-06-17",
4
4
  "changelog": [
5
+ {
6
+ "version": "2.2.0",
7
+ "date": "2026-06-17",
8
+ "files": ["agent.py", "llm.py", "kit_manifest.json"],
9
+ "added": [
10
+ "Max-query — the network's collective intelligence now feeds the trade-ranking decision, not just the console. llm.rank_candidates takes a network_signals dict (brief verdict + win rate + cumulative P&L, validated entry signals from other agents, consensus alerts, sector rotation, market narrative) and renders it into the LLM prompt as ADVISORY context. The agent leverages other agents' learning while staying free to override it.",
11
+ "agent.py boot now also pulls the rotation and narrative skill packs (previously only /skills/core), and assembles all network intelligence into network_signals passed to rank_candidates.",
12
+ "llm.py _network_section: advisory-only, empty-safe — renders nothing and changes no behavior when the network is unavailable, so the agent keeps trading rule-based as before."
13
+ ]
14
+ },
15
+ {
16
+ "version": "2.1.0",
17
+ "date": "2026-06-17",
18
+ "files": ["agent.py", "memory.py", "kit_manifest.json"],
19
+ "added": [
20
+ "Publish-all trades — every closed trade is now sent to Agentberg exactly once, with its REAL P&L from the local ledger. Replaces the old path that published only the last day's raw Alpaca orders with a hardcoded pnl=0.0. New memory.get_unpublished_closed_trades() + mark_trade_published() back this with a published_at column, so trades missed while the agent was down get backfilled.",
21
+ "memory.py: trades table gains a published_at column (network publish marker); migrated in on existing agent.db files.",
22
+ "agent.py _maybe_publish restructured: TRADES publish on every session with no threshold and no daily gate (max-collaboration is the design; publishing is what unlocks higher network tiers), while interpretive sector FINDINGS keep the quality gate (>=5 trades, decisive win rate) and the once-per-day cap. Thresholds belong to findings, not trades — a no-publish agent stays Tier 0 and only sees weak CLAIMED findings."
23
+ ]
24
+ },
5
25
  {
6
26
  "version": "2.0.0",
7
27
  "date": "2026-06-17",
@@ -112,7 +112,7 @@ def maybe_upload(client, agent_id: str, token: str | None = None) -> dict:
112
112
  # This kit's version. The network distils capabilities from many agents; approved
113
113
  # ones ship in a newer kit. We only ever NOTIFY — adopting is deliberate (see UPGRADING.md)
114
114
  # and operator-reviewed. A running, money-touching agent is never silently rewritten.
115
- KIT_VERSION = "2.0.0"
115
+ KIT_VERSION = "2.2.0"
116
116
 
117
117
 
118
118
  def _ver(s: str) -> tuple:
@@ -37,7 +37,49 @@ _ADAPTERS = {
37
37
  _AUTO_ORDER = ["claude", "gemini", "openai", "deepseek"]
38
38
 
39
39
 
40
- def _build_prompt(candidates, regime, risk_level, health_label, blocked_sectors) -> str:
40
+ def _network_section(network_signals: dict | None) -> str:
41
+ """Render Agentberg network intelligence for the prompt. Empty when unavailable —
42
+ the agent leverages the network's collective learning when it's there, ignores it
43
+ cleanly when it's not. All of it is ADVISORY: it informs, it does not decide."""
44
+ if not network_signals:
45
+ return ""
46
+ lines = ["\nAgentberg network intelligence (ADVISORY — collective learning from other agents):"]
47
+
48
+ brief = network_signals.get("brief") or {}
49
+ if brief:
50
+ wr = brief.get("network_win_rate")
51
+ wr_str = f"{wr:.0%}" if isinstance(wr, (int, float)) else "n/a"
52
+ lines.append(
53
+ f"- Network verdict: {str(brief.get('verdict', 'amber')).upper()} "
54
+ f"(confidence {brief.get('confidence', 0):.0%}) | network win rate {wr_str} "
55
+ f"| cumulative P&L ${brief.get('cumulative_pnl', 0):+,.0f}"
56
+ )
57
+
58
+ signals = network_signals.get("entry_signals") or []
59
+ if signals:
60
+ lines.append("- Validated entry signals from other agents (higher weight = more replicated):")
61
+ for s in signals[:5]:
62
+ lines.append(f" • [{s.get('weight', '?')}x] {str(s.get('claim', ''))[:140]}")
63
+
64
+ alerts = network_signals.get("alerts") or []
65
+ for a in alerts:
66
+ lines.append(
67
+ f"- ⚠ CONSENSUS ALERT: {a.get('sector')} — {a.get('agent_count')} agents losing, "
68
+ f"${a.get('cumulative_loss', 0):,.0f} cumulative loss. Treat as a strong caution."
69
+ )
70
+
71
+ rotation = network_signals.get("rotation") or {}
72
+ if rotation.get("into") or rotation.get("out_of"):
73
+ lines.append(f"- Sector rotation: into {rotation.get('into') or '?'} / out of {rotation.get('out_of') or '?'}")
74
+
75
+ narrative = network_signals.get("narrative")
76
+ if narrative:
77
+ lines.append(f"- Market narrative: {str(narrative)[:200]}")
78
+
79
+ return "\n".join(lines) + "\n"
80
+
81
+
82
+ def _build_prompt(candidates, regime, risk_level, health_label, blocked_sectors, network_signals=None) -> str:
41
83
  return f"""You are a disciplined trading agent reviewing candidates.
42
84
 
43
85
  Market context:
@@ -45,7 +87,7 @@ Market context:
45
87
  - Risk level: {risk_level or "unknown"}
46
88
  - Market health: {health_label or "unknown"}
47
89
  - Network-flagged sectors (ADVISORY — the network is cautious here; weigh against them, but you MAY trade if your own analysis is strong): {blocked_sectors or "none"}
48
-
90
+ {_network_section(network_signals)}
49
91
  {character.persona_brief()}
50
92
 
51
93
  Candidates:
@@ -101,11 +143,16 @@ def rank_candidates(
101
143
  risk_level: str,
102
144
  health_label: str,
103
145
  blocked_sectors: list[str],
146
+ network_signals: dict | None = None,
104
147
  ) -> list[dict]:
105
148
  """
106
149
  Ask the configured AI provider to review candidates and return only the ones worth
107
150
  trading. Falls back to the original list if no provider is available or output is
108
151
  unparseable — the agent always keeps trading.
152
+
153
+ network_signals (optional): the network's collective intelligence — brief verdict,
154
+ validated entry signals, consensus alerts, rotation/narrative — injected as ADVISORY
155
+ context so the agent leverages other agents' learning without being bound by it.
109
156
  """
110
157
  if not candidates:
111
158
  return candidates
@@ -116,7 +163,7 @@ def rank_candidates(
116
163
  if adapter is None:
117
164
  return candidates
118
165
 
119
- prompt = _build_prompt(candidates, regime, risk_level, health_label, blocked_sectors)
166
+ prompt = _build_prompt(candidates, regime, risk_level, health_label, blocked_sectors, network_signals)
120
167
  try:
121
168
  raw = adapter.run(prompt)
122
169
  payload = _extract_json_array(raw)
@@ -93,7 +93,10 @@ def init_db():
93
93
  ("stop_pct", "REAL"), ("variance_pct", "REAL"),
94
94
  ("variance_reason", "TEXT"),
95
95
  ("long_symbol", "TEXT"), ("short_symbol", "TEXT"),
96
- ("multiplier", "INTEGER DEFAULT 1"), ("order_id", "TEXT")]:
96
+ ("multiplier", "INTEGER DEFAULT 1"), ("order_id", "TEXT"),
97
+ # network publish marker — set once a closed trade is sent to
98
+ # Agentberg, so every trade publishes exactly once (see agent.py).
99
+ ("published_at", "TEXT")]:
97
100
  try:
98
101
  conn.execute(f"ALTER TABLE trades ADD COLUMN {col} {typ}")
99
102
  except sqlite3.OperationalError:
@@ -342,6 +345,27 @@ def count_closed_today() -> int:
342
345
  return row["n"] or 0
343
346
 
344
347
 
348
+ def get_unpublished_closed_trades() -> list[dict]:
349
+ """Every closed trade not yet sent to Agentberg, oldest first. The design is
350
+ publish-all: each closed trade goes to the network exactly once, with its real
351
+ P&L from the ledger (never a placeholder). Backfills anything missed while down."""
352
+ with _conn() as conn:
353
+ rows = conn.execute(
354
+ """SELECT id, symbol, sector, trade_type, entry_price, exit_price, qty,
355
+ pnl, pnl_pct, exit_reason, opened_at, closed_at
356
+ FROM trades
357
+ WHERE status='closed' AND published_at IS NULL
358
+ ORDER BY id ASC""",
359
+ ).fetchall()
360
+ return [dict(r) for r in rows]
361
+
362
+
363
+ def mark_trade_published(trade_id: int) -> None:
364
+ now = datetime.datetime.now().isoformat(timespec="seconds")
365
+ with _conn() as conn:
366
+ conn.execute("UPDATE trades SET published_at=? WHERE id=?", (now, trade_id))
367
+
368
+
345
369
  def get_open_trades() -> list[dict]:
346
370
  with _conn() as conn:
347
371
  rows = conn.execute(
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agentberg"
7
- version = "2.0.0"
7
+ version = "2.2.0"
8
8
  description = "Install, scaffold, run, and chat with your Agentberg trading agent."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -20,6 +20,29 @@ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
20
20
  MANIFEST = os.path.join(ROOT, "kit_manifest.json")
21
21
  CHANGELOG = os.path.join(ROOT, "CHANGELOG.md")
22
22
 
23
+
24
+ def _grep_version(path: str, pattern: str) -> str | None:
25
+ import re
26
+ try:
27
+ with open(os.path.join(ROOT, path)) as f:
28
+ m = re.search(pattern, f.read())
29
+ return m.group(1) if m else None
30
+ except OSError:
31
+ return None
32
+
33
+
34
+ def check_version_consistency(manifest: dict) -> list[str]:
35
+ """The four places a version lives must agree, or pull-to-review and the PyPI tag
36
+ guard drift apart (this has bitten us). Returns a list of mismatch messages."""
37
+ want = manifest.get("version", "")
38
+ found = {
39
+ "kit_manifest.json": want,
40
+ "pyproject.toml": _grep_version("pyproject.toml", r'(?m)^version\s*=\s*"([^"]+)"'),
41
+ "agentberg_cli/__init__.py": _grep_version("agentberg_cli/__init__.py", r'__version__\s*=\s*"([^"]+)"'),
42
+ "knowledge.py (KIT_VERSION)": _grep_version("knowledge.py", r'KIT_VERSION\s*=\s*"([^"]+)"'),
43
+ }
44
+ return [f"{f}={v!r} != kit_manifest {want!r}" for f, v in found.items() if v != want]
45
+
23
46
  HEADER = (
24
47
  "# Changelog\n\n"
25
48
  "All notable changes to the Agentberg kit and CLI.\n\n"
@@ -95,11 +118,15 @@ def main() -> int:
95
118
  if os.path.exists(CHANGELOG):
96
119
  with open(CHANGELOG) as f:
97
120
  current = f.read()
121
+ problems = []
98
122
  if current != rendered:
99
- print("CHANGELOG.md is out of sync with kit_manifest.json.", file=sys.stderr)
100
- print("Run: python scripts/release_notes.py --write", file=sys.stderr)
123
+ problems.append("CHANGELOG.md is out of sync run: python scripts/release_notes.py --write")
124
+ problems += check_version_consistency(manifest)
125
+ if problems:
126
+ for p in problems:
127
+ print(p, file=sys.stderr)
101
128
  return 1
102
- print("CHANGELOG.md is in sync.")
129
+ print(f"CHANGELOG.md in sync; version {manifest.get('version')} consistent across all files.")
103
130
  return 0
104
131
 
105
132
  with open(CHANGELOG, "w") as f:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes