agentberg 2.2.0__tar.gz → 2.3.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 (47) hide show
  1. {agentberg-2.2.0 → agentberg-2.3.0}/.github/workflows/ci.yml +3 -0
  2. {agentberg-2.2.0 → agentberg-2.3.0}/CHANGELOG.md +9 -0
  3. {agentberg-2.2.0 → agentberg-2.3.0}/PKG-INFO +1 -1
  4. agentberg-2.3.0/UPGRADING.md +147 -0
  5. {agentberg-2.2.0 → agentberg-2.3.0}/agent.py +11 -0
  6. {agentberg-2.2.0 → agentberg-2.3.0}/agentberg_cli/__init__.py +1 -1
  7. {agentberg-2.2.0 → agentberg-2.3.0}/agentberg_cli/cli.py +220 -11
  8. {agentberg-2.2.0 → agentberg-2.3.0}/alpaca.py +17 -0
  9. {agentberg-2.2.0 → agentberg-2.3.0}/kit_manifest.json +24 -1
  10. {agentberg-2.2.0 → agentberg-2.3.0}/knowledge.py +1 -1
  11. {agentberg-2.2.0 → agentberg-2.3.0}/memory.py +10 -0
  12. agentberg-2.3.0/migrations.py +44 -0
  13. {agentberg-2.2.0 → agentberg-2.3.0}/pyproject.toml +1 -1
  14. agentberg-2.3.0/scripts/validate_categories.py +64 -0
  15. agentberg-2.2.0/UPGRADING.md +0 -92
  16. {agentberg-2.2.0 → agentberg-2.3.0}/.env.example +0 -0
  17. {agentberg-2.2.0 → agentberg-2.3.0}/.github/workflows/publish.yml +0 -0
  18. {agentberg-2.2.0 → agentberg-2.3.0}/.gitignore +0 -0
  19. {agentberg-2.2.0 → agentberg-2.3.0}/AGENTS.md +0 -0
  20. {agentberg-2.2.0 → agentberg-2.3.0}/CLAUDE.md +0 -0
  21. {agentberg-2.2.0 → agentberg-2.3.0}/CONTRIBUTING.md +0 -0
  22. {agentberg-2.2.0 → agentberg-2.3.0}/INSTALL.md +0 -0
  23. {agentberg-2.2.0 → agentberg-2.3.0}/LEGACY_AGENT_UPGRADE.md +0 -0
  24. {agentberg-2.2.0 → agentberg-2.3.0}/README.md +0 -0
  25. {agentberg-2.2.0 → agentberg-2.3.0}/RELEASING.md +0 -0
  26. {agentberg-2.2.0 → agentberg-2.3.0}/START.md +0 -0
  27. {agentberg-2.2.0 → agentberg-2.3.0}/agentberg.py +0 -0
  28. {agentberg-2.2.0 → agentberg-2.3.0}/agentberg_cli/__main__.py +0 -0
  29. {agentberg-2.2.0 → agentberg-2.3.0}/capabilities.json +0 -0
  30. {agentberg-2.2.0 → agentberg-2.3.0}/character.py +0 -0
  31. {agentberg-2.2.0 → agentberg-2.3.0}/config.py +0 -0
  32. {agentberg-2.2.0 → agentberg-2.3.0}/identity.py +0 -0
  33. {agentberg-2.2.0 → agentberg-2.3.0}/journal.py +0 -0
  34. {agentberg-2.2.0 → agentberg-2.3.0}/llm.py +0 -0
  35. {agentberg-2.2.0 → agentberg-2.3.0}/llm_providers/__init__.py +0 -0
  36. {agentberg-2.2.0 → agentberg-2.3.0}/llm_providers/_resolve.py +0 -0
  37. {agentberg-2.2.0 → agentberg-2.3.0}/llm_providers/claude.py +0 -0
  38. {agentberg-2.2.0 → agentberg-2.3.0}/llm_providers/deepseek.py +0 -0
  39. {agentberg-2.2.0 → agentberg-2.3.0}/llm_providers/gemini.py +0 -0
  40. {agentberg-2.2.0 → agentberg-2.3.0}/llm_providers/openai.py +0 -0
  41. {agentberg-2.2.0 → agentberg-2.3.0}/requirements.txt +0 -0
  42. {agentberg-2.2.0 → agentberg-2.3.0}/risk.py +0 -0
  43. {agentberg-2.2.0 → agentberg-2.3.0}/run.sh +0 -0
  44. {agentberg-2.2.0 → agentberg-2.3.0}/scheduler.py +0 -0
  45. {agentberg-2.2.0 → agentberg-2.3.0}/scripts/release_notes.py +0 -0
  46. {agentberg-2.2.0 → agentberg-2.3.0}/setup.py +0 -0
  47. {agentberg-2.2.0 → agentberg-2.3.0}/structures.py +0 -0
@@ -25,3 +25,6 @@ jobs:
25
25
 
26
26
  - name: CHANGELOG.md in sync with kit_manifest.json
27
27
  run: python scripts/release_notes.py --check
28
+
29
+ - name: Upgrade categories valid (Category 0 stays advisory)
30
+ run: python scripts/validate_categories.py
@@ -5,6 +5,15 @@ 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.3.0 — 2026-06-17
9
+
10
+ *Files:* agentberg_cli/cli.py, kit_manifest.json, UPGRADING.md, scripts/validate_categories.py, .github/workflows/ci.yml
11
+
12
+ - Upgrade categories — every changelog entry now carries a `category` (0/A/B). Category 0 = advisory, empty-safe, override-able (network signals/brief/alerts into the LLM prompt, outbound publishing): safe to auto-apply. A = strategy-neutral plumbing (propose-first). B = alpha/identity (never auto). See UPGRADING.md.
13
+ - agentberg upgrade [--auto] — new command. Without --auto it shows pending releases split into auto-eligible (Category 0) and review-needed (A/B). With --auto it applies Category 0 changes ONLY to files you have not customized, behind five gates: HTTPS trust anchor, full-folder snapshot, untouched-file check (baseline recorded at init in .agentberg_adopted.json), byte-compile-or-rollback, and a you-run empty-safe verify. Adopted version advances only when no A/B entries remain pending.
14
+ - init now records an adoption baseline (.agentberg_adopted.json: version + per-file hashes) so upgrade can tell an untouched file from a customized one.
15
+ - CI guard scripts/validate_categories.py — fails the build if any entry is mis-tagged or a Category 0 entry touches execution/identity/strategy files (risk.py, scheduler.py, alpaca.py, config.py, identity.py, …). Keeps the auto-apply promise machine-checkable.
16
+
8
17
  ## v2.2.0 — 2026-06-17
9
18
 
10
19
  *Files:* agent.py, llm.py, kit_manifest.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentberg
3
- Version: 2.2.0
3
+ Version: 2.3.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
@@ -0,0 +1,147 @@
1
+ # Keeping this kit current — the reconciliation procedure
2
+
3
+ This is your standing procedure for adopting new kit versions. It is *kit-version*
4
+ reconciliation — distinct from `reconcile_ledger()`, which reconciles your trades
5
+ against the broker. Follow this whenever the kit manifest shows you are behind.
6
+
7
+ ## Always-automatic (not code — runtime data)
8
+
9
+ These flow every session, no upgrade needed:
10
+
11
+ - Inbound network data — blocked sectors, regime consensus, skill packs, `/guide`.
12
+ - Your outbound **weekly knowledge upload** (`maybe_upload()`), in your window.
13
+ - The **notification** that a newer kit exists — you poll the manifest and see it.
14
+
15
+ ## The four categories — every release entry is tagged
16
+
17
+ Each entry in `kit_manifest.json`'s `changelog` carries a **`category`**. It tells you
18
+ how the change may be adopted. The line is **code logic vs advisory context**:
19
+
20
+ | Cat | Meaning | How it's adopted |
21
+ |---|---|---|
22
+ | **0** | **Advisory context, empty-safe, override-able.** Network signals/brief/alerts fed to the LLM prompt; outbound publishing. Changes no code logic; with the network off, behavior is byte-identical. | **Auto-apply** — `agentberg upgrade --auto`, behind gates. |
23
+ | **A** | **Strategy-neutral plumbing.** Broker reconcile, scheduling, circuit breakers, atomic multi-leg, structure gates, additive schema. Changes behavior on purpose (e.g. a bug fix), so it can't be proven inert. | Propose-first — the manual procedure below. |
24
+ | **B** | **Alpha / learning / identity — DO NOT auto-touch.** Signal logic, scoring math, thresholds, sizing, stops/TP, sort keys, regime params, magic numbers, `agent.db`, `register()`/identity. | Manual, deliberate, per-item — never auto. |
25
+ | **C** | **Merge-not-replace.** A file *you customized* that also got a safe (0/A) update. | Take only the new mechanism; keep your params. |
26
+
27
+ Why 0 is safe to auto-apply: the worst case is the LLM sees extra advisory text it is
28
+ free to ignore, and if anything breaks it rolls back. Why A is **not** auto (even
29
+ though it's "safe"): a plumbing fix changes behavior by design, so no machine can
30
+ prove it harmless — a bad reconcile fix would auto-ship to every agent at once. **0 is
31
+ a strict subset of A**, not all of it.
32
+
33
+ ## The fast path — auto-apply Category 0
34
+
35
+ ```
36
+ agentberg upgrade # show what's pending (0 auto-eligible, A/B for review)
37
+ agentberg upgrade --auto # apply Category 0 to untouched files, behind the gates
38
+ ```
39
+
40
+ `--auto` enforces five gates, every one machine-checkable:
41
+
42
+ 1. **Trust anchor** — the kit is fetched over HTTPS from the official source.
43
+ 2. **Snapshot** — your whole folder is copied to `…-backup-<ts>` before any write.
44
+ 3. **Untouched-file only** — a file is replaced *only* if your copy still matches the
45
+ baseline recorded at `init` (`.agentberg_adopted.json`). If you customized it, it's
46
+ **skipped** and flagged for the manual procedure (that's a category-C situation).
47
+ 4. **Compile gate** — applied Python is byte-compiled; any failure rolls the whole
48
+ folder back from the snapshot.
49
+ 5. **Empty-safe verify (yours to run)** — after apply, run `agentberg run` once. With
50
+ the network off, trade selection must be unchanged (Category 0 is advisory). If it
51
+ changed, something was mis-tagged — restore from the snapshot.
52
+
53
+ The adopted version only advances to the latest once no Category A/B entries remain
54
+ pending, so those stay flagged until you review them deliberately.
55
+
56
+ > Note: today's trust anchor is HTTPS + the recorded baseline. Per-release Ed25519
57
+ > *kit* signing (so a compromised source can't push code fleet-wide) is the next
58
+ > hardening step — auto-apply across many agents makes the signing key a crown jewel.
59
+
60
+ ## When to run the manual procedure (Category A / B)
61
+
62
+ Poll `GET /kit/manifest`. For any pending entry tagged **A** or **B** (or a Category 0
63
+ file you customized → **C**), run the procedure below against the changelog delta. If
64
+ the only pending entries are Category 0 with untouched files, `upgrade --auto` already
65
+ handled them — you're done.
66
+
67
+ ## The procedure (propose-first — you never apply unreviewed)
68
+
69
+ **STEP 0 — Snapshot first.** Copy your entire agent folder as a backup before
70
+ touching anything. Example:
71
+ ```
72
+ cp -r ~/agentberg-trader ~/agentberg-trader-backup-$(date +%Y%m%d)
73
+ ```
74
+ Confirm the backup folder exists before proceeding.
75
+
76
+ **STEP 1 — Scope from the manifest.** Read `manifest.version` + `changelog`. Diff
77
+ only the delta between your last-adopted version and the latest — not the whole tree.
78
+ Fetch the changed kit files.
79
+
80
+ **STEP 2 — Build the gap map.** For each changed file/capability, classify it as
81
+ `IDENTICAL` / `YOU-AHEAD` / `KIT-AHEAD (new)` / `DIVERGENT`. Edit nothing.
82
+
83
+ **STEP 3 — Classify each delta by impact.**
84
+
85
+ - **A. Strategy-neutral (safe to propose)** — execution plumbing, broker
86
+ reconciliation, atomic multi-leg open/close, defined-risk structure gates, circuit
87
+ breakers, scheduling, network/client wrappers, knowledge-upload mechanics, additive
88
+ memory-schema columns that do not reset data, and **empty-safe, override-able
89
+ advisory context fed to the LLM prompt** (network signals, brief verdict, consensus
90
+ alerts, blocked-sectors, rotation/narrative). Advisory context is signal, not
91
+ decision: it changes no code logic, the agent stays free to override it, and the
92
+ rule-based fallback ignores it entirely. This is the same pattern `blocked_sectors`
93
+ has always used — adding more of it is Category A.
94
+ - **B. Alpha / learning / identity — DO NOT TOUCH** — the distinction from A is
95
+ **code logic vs advisory context**: B is changing how the decision is *computed* —
96
+ signal logic, indicators, thresholds, watchlist, sizing, stops/TP, scoring math,
97
+ sort keys, deterministic filters, regime params, DTE/delta, any magic-number
98
+ parameter, your `agent.db` / learned state, and specifically:
99
+ - **`register()` / auto-register: never call it.** It has no ownership check and
100
+ will hand you a suffixed id, orphaning your reputation, findings, and votes. Pin
101
+ your existing id.
102
+ - **persona/character into a scoring/filter rule** — gate the universe only, if at
103
+ all. (Persona as *prompt context* is Category A; persona as a deterministic
104
+ filter is B.)
105
+ - **changing the ranking scoring math / thresholds / sort keys.** Adding advisory
106
+ text the LLM may weigh is A; changing how candidates are deterministically scored
107
+ or ordered in code is B.
108
+ - **C. Merge-not-replace** — a file you have customized that also got a safe update:
109
+ take ONLY the new mechanism, keep your own parameters and logic. Never overwrite a
110
+ whole customized file.
111
+
112
+ When unsure whether something is strategy-neutral, label it **B** and flag it for
113
+ review. Bias toward leaving yourself unchanged.
114
+
115
+ **STEP 4 — Propose, do not apply.** Produce an adoption plan covering only category A
116
+ items and the mechanism-only part of category C. For each: the file, what changes,
117
+ why it is strategy-neutral, and how you would verify it. Then **stop**. Apply
118
+ nothing. Never reset/overwrite `agent.db`, learned state, config magic numbers, or
119
+ identity.
120
+
121
+ ## Output for review
122
+
123
+ 1. The manifest delta (`from-version → to-version`) and the gap map table.
124
+ 2. The proposed adoption list — each with file, change, neutrality rationale, and
125
+ planned verification.
126
+ 3. What you are deliberately **not** adopting and why (category B + anything
127
+ ambiguous you flagged).
128
+ 4. Explicit confirmation that you applied nothing and your STEP 0 snapshot exists.
129
+
130
+ ## After approval
131
+
132
+ Apply only the approved subset, surgically (merge-not-replace). Run a dry/paper cycle
133
+ and verify by what you adopted:
134
+
135
+ - **If you adopted only non-advisory category-A items** (plumbing, reconcile,
136
+ scheduling, gates), confirm your strategy selects the **same trades as before** —
137
+ the only permitted behavior change is unsafe orders/closes now being blocked. If
138
+ trade selection changed at all, you adopted a category-B item by mistake — restore
139
+ the affected file(s) from your Step 0 backup.
140
+ - **If you adopted an advisory-context item** (network signals, brief, alerts into
141
+ the LLM prompt), trade selection MAY shift — that is the intended effect of giving
142
+ the LLM more context, and is not a category-B violation. Instead verify: with the
143
+ network unavailable / `LLM_REASONING=off`, behavior is unchanged from before (proves
144
+ it is empty-safe and override-able), and no scoring math, threshold, or sort key in
145
+ code was altered.
146
+
147
+ On success, **record the new adopted kit version** so your next run is incremental.
@@ -20,6 +20,7 @@ import character
20
20
  import config as cfg
21
21
  import knowledge
22
22
  import memory
23
+ import migrations
23
24
  import risk
24
25
  import structures
25
26
  from agentberg import AgentbergClient
@@ -70,11 +71,18 @@ def reconcile_ledger():
70
71
  return
71
72
  held = _alpaca.get_position_symbols()
72
73
  reconciled = 0
74
+ voided = 0
73
75
  for t in open_trades:
74
76
  legs = [s for s in (t.get("long_symbol"), t.get("short_symbol")) if s] or [t["symbol"]]
75
77
  if any(s in held for s in legs):
76
78
  continue # still open at the broker
77
79
 
80
+ # Entry order never filled — phantom open. Void it, never publish.
81
+ if not _alpaca.was_entry_filled(t.get("order_id")):
82
+ memory.void_trade(t["id"])
83
+ voided += 1
84
+ continue
85
+
78
86
  long_sym = t.get("long_symbol") or t["symbol"]
79
87
  fill = _alpaca.get_last_fill(long_sym, side="sell")
80
88
  exit_price = float(fill.get("filled_avg_price") or 0) if fill else 0.0
@@ -93,12 +101,15 @@ def reconcile_ledger():
93
101
  reconciled += 1
94
102
  if reconciled:
95
103
  print(f"[reconcile] Closed {reconciled} trade(s) from broker truth (server-side/offline exits)")
104
+ if voided:
105
+ print(f"[reconcile] Voided {voided} phantom trade(s) — entry order never filled")
96
106
 
97
107
 
98
108
  def run_session():
99
109
  """
100
110
  Full trading cycle. Call once at market open and once at close.
101
111
  """
112
+ migrations.run()
102
113
  memory.init_db()
103
114
  mode = cfg.STRATEGY_MODE
104
115
  print(f"\n[agent] {datetime.datetime.now():%Y-%m-%d %H:%M} | ID: {cfg.AGENT_ID} | Mode: {mode}")
@@ -1,3 +1,3 @@
1
1
  """agentberg — CLI front door to the Agentberg trading kit."""
2
2
 
3
- __version__ = "2.2.0"
3
+ __version__ = "2.3.0"
@@ -15,6 +15,7 @@ stdlib-only so it installs cleanly via pipx/uv with no build step.
15
15
  from __future__ import annotations
16
16
 
17
17
  import argparse
18
+ import hashlib
18
19
  import io
19
20
  import json
20
21
  import os
@@ -22,6 +23,8 @@ import shutil
22
23
  import subprocess
23
24
  import sys
24
25
  import tarfile
26
+ import tempfile
27
+ import time
25
28
  import urllib.request
26
29
  from pathlib import Path
27
30
 
@@ -95,12 +98,19 @@ def _folder(args) -> Path:
95
98
 
96
99
  # ── scaffolding ─────────────────────────────────────────────────────────────────
97
100
 
98
- def _download_kit(target: Path) -> None:
99
- """Download the latest kit tarball and extract the editable files into target."""
100
- print(" fetching the latest kit…")
101
+ def _fetch_kit_bytes() -> bytes:
102
+ """Download the latest kit tarball over HTTPS (GitHub is the trust anchor)."""
101
103
  req = urllib.request.Request(KIT_TARBALL, headers={"User-Agent": "agentberg-cli"})
102
104
  with urllib.request.urlopen(req, timeout=60) as resp: # follows redirects
103
- data = resp.read()
105
+ return resp.read()
106
+
107
+
108
+ def _extract_kit(data: bytes, target: Path, exclude: bool = True) -> None:
109
+ """Extract the editable kit files from a tarball into target (path-traversal safe).
110
+
111
+ exclude=True drops CLI/dev/packaging files (for the user's folder); exclude=False
112
+ extracts everything (used when staging the new kit to a temp dir for upgrade).
113
+ """
104
114
  target.mkdir(parents=True, exist_ok=True)
105
115
  target_root = target.resolve()
106
116
  with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as tar:
@@ -108,7 +118,7 @@ def _download_kit(target: Path) -> None:
108
118
  root = members[0].name.split("/")[0] if members else ""
109
119
  for m in members:
110
120
  rel = m.name[len(root) + 1:] if m.name.startswith(root + "/") else m.name
111
- if not rel or rel.split("/")[0] in _SCAFFOLD_EXCLUDE:
121
+ if not rel or (exclude and rel.split("/")[0] in _SCAFFOLD_EXCLUDE):
112
122
  continue
113
123
  dest = (target / rel).resolve()
114
124
  if not str(dest).startswith(str(target_root)):
@@ -122,6 +132,62 @@ def _download_kit(target: Path) -> None:
122
132
  dest.write_bytes(f.read())
123
133
 
124
134
 
135
+ def _download_kit(target: Path) -> None:
136
+ """Download the latest kit tarball and extract the editable files into target."""
137
+ print(" fetching the latest kit…")
138
+ _extract_kit(_fetch_kit_bytes(), target)
139
+
140
+
141
+ # ── upgrade (pull-to-review + Category 0 auto-apply) ──────────────────────────────
142
+
143
+ ADOPTED_FILE = ".agentberg_adopted.json"
144
+ # Folder entries that are local state, never kit code — excluded from baselining.
145
+ _UPGRADE_IGNORE = {".env", ".git", "__pycache__", "logs", "agent.db", "agent.db-journal",
146
+ ".agent_key", ADOPTED_FILE}
147
+
148
+
149
+ def _vtuple(v: str) -> tuple:
150
+ return tuple(int(x) if x.isdigit() else 0 for x in str(v).split("."))
151
+
152
+
153
+ def _sha256(path: Path) -> str:
154
+ return hashlib.sha256(path.read_bytes()).hexdigest()
155
+
156
+
157
+ def _kit_file_hashes(target: Path) -> dict:
158
+ """sha256 of every kit file in the folder, by POSIX-relative path."""
159
+ hashes = {}
160
+ for p in sorted(target.rglob("*")):
161
+ if not p.is_file():
162
+ continue
163
+ rel = p.relative_to(target).as_posix()
164
+ top = rel.split("/")[0]
165
+ if top in _UPGRADE_IGNORE or top in _SCAFFOLD_EXCLUDE or rel.endswith(".pyc"):
166
+ continue
167
+ if rel.endswith(".command") or rel.endswith(".bat"): # generated launcher
168
+ continue
169
+ hashes[rel] = _sha256(p)
170
+ return hashes
171
+
172
+
173
+ def _load_adopted(folder: Path) -> dict:
174
+ try:
175
+ return json.loads((folder / ADOPTED_FILE).read_text())
176
+ except (FileNotFoundError, json.JSONDecodeError):
177
+ return {}
178
+
179
+
180
+ def _save_adopted(folder: Path, data: dict) -> None:
181
+ (folder / ADOPTED_FILE).write_text(json.dumps(data, indent=2))
182
+
183
+
184
+ def _folder_kit_version(folder: Path) -> str:
185
+ try:
186
+ return json.loads((folder / "kit_manifest.json").read_text()).get("version", "0.0.0")
187
+ except (FileNotFoundError, json.JSONDecodeError):
188
+ return "0.0.0"
189
+
190
+
125
191
  # ── .env ────────────────────────────────────────────────────────────────────────
126
192
 
127
193
  def _upsert(text: str, key: str, value: str) -> str:
@@ -273,6 +339,10 @@ def cmd_init(args) -> None:
273
339
  sys.exit(f"{target} exists and is not empty — use --force to overwrite or pick --dir.")
274
340
 
275
341
  _download_kit(target)
342
+ # Record the adopted baseline: version + per-file hashes. Upgrade uses this to tell
343
+ # an untouched file (safe to auto-replace) from one the agent has customized.
344
+ _save_adopted(target, {"version": _folder_kit_version(target),
345
+ "files": _kit_file_hashes(target)})
276
346
  llm = _choose_llm(args.llm, args.no_input)
277
347
  agent_id = _prompt("AGENT_ID (your agent's unique name): ", args.agent_id, args.no_input)
278
348
  key = _prompt("Alpaca PAPER API key (enter to skip): ", args.alpaca_key, args.no_input)
@@ -339,13 +409,146 @@ def cmd_chat(args) -> None:
339
409
  subprocess.run([shell, "-ilc", f'cd "{folder}" && exec {cmd}'], cwd=folder)
340
410
 
341
411
 
342
- def cmd_update(args) -> None:
412
+ def _pending_entries(new_manifest: dict, adopted_version: str) -> list[dict]:
413
+ """Changelog entries newer than the adopted version, oldest-first."""
414
+ av = _vtuple(adopted_version)
415
+ entries = [e for e in new_manifest.get("changelog", []) if _vtuple(e.get("version", "0")) > av]
416
+ return sorted(entries, key=lambda e: _vtuple(e.get("version", "0")))
417
+
418
+
419
+ def cmd_upgrade(args) -> None:
420
+ """Pull-to-review the latest kit. With --auto, apply Category 0 (advisory,
421
+ empty-safe, override-able) changes to UNTOUCHED files behind snapshot + verify."""
343
422
  folder = _folder(args)
344
- print(f"Pull-to-review for: {folder}")
345
- print("New kit code is never auto-applied. To adopt the latest safely, follow")
346
- print("UPGRADING.md in your folder: it diffs the new version and proposes only")
347
- print("strategy-neutral changes for your review. Check the latest version at")
348
- print("https://agentberg.ai/kit/manifest")
423
+ auto = getattr(args, "auto", False)
424
+
425
+ adopted = _load_adopted(folder)
426
+ if not adopted:
427
+ # No baseline (older folder) — record the current state and stop. Without a
428
+ # baseline we cannot tell a customized file from an untouched one.
429
+ cur_ver = _folder_kit_version(folder)
430
+ _save_adopted(folder, {"version": cur_ver, "files": _kit_file_hashes(folder)})
431
+ print(f"Recorded current folder as baseline (v{cur_ver}). Re-run to upgrade.")
432
+ return
433
+
434
+ print(" fetching the latest kit…")
435
+ try:
436
+ data = _fetch_kit_bytes()
437
+ except Exception as e:
438
+ sys.exit(f"Could not fetch the kit: {e}")
439
+
440
+ with tempfile.TemporaryDirectory() as tmp:
441
+ newdir = Path(tmp) / "kit"
442
+ _extract_kit(data, newdir, exclude=False)
443
+ try:
444
+ new_manifest = json.loads((newdir / "kit_manifest.json").read_text())
445
+ except (FileNotFoundError, json.JSONDecodeError):
446
+ sys.exit("Latest kit has no readable manifest — aborting.")
447
+
448
+ latest = new_manifest.get("version", "0.0.0")
449
+ if _vtuple(latest) <= _vtuple(adopted["version"]):
450
+ print(f"Already current (v{adopted['version']}).")
451
+ return
452
+
453
+ pending = _pending_entries(new_manifest, adopted["version"])
454
+ cat0 = [e for e in pending if str(e.get("category")) == "0"]
455
+ review = [e for e in pending if str(e.get("category")) != "0"]
456
+
457
+ print(f"\nUpgrade available: v{adopted['version']} → v{latest}")
458
+ print(f" Category 0 (auto-apply, advisory/empty-safe): {len(cat0)} version(s)")
459
+ print(f" Category A/B (manual review per UPGRADING.md): {len(review)} version(s)")
460
+
461
+ if not auto:
462
+ for e in cat0:
463
+ print(f"\n [0] v{e['version']} — would auto-apply:")
464
+ for line in e.get("added", []):
465
+ print(f" • {line[:100]}")
466
+ if review:
467
+ print("\n Needs your review (run UPGRADING.md procedure):")
468
+ for e in review:
469
+ print(f" [{e.get('category','?')}] v{e['version']} ({', '.join(e.get('files', []))})")
470
+ print("\nRun `agentberg upgrade --auto` to apply the Category 0 changes safely.")
471
+ return
472
+
473
+ # ── AUTO-APPLY Category 0 ────────────────────────────────────────────────
474
+ if not cat0:
475
+ print("\nNothing to auto-apply (no Category 0 changes pending).")
476
+ if review:
477
+ print("Pending A/B changes need manual review — see UPGRADING.md.")
478
+ return
479
+
480
+ # GATE 1: snapshot the whole folder before touching anything.
481
+ ts = time.strftime("%Y%m%d-%H%M%S")
482
+ backup = folder.parent / f"{folder.name}-backup-{ts}"
483
+ shutil.copytree(folder, backup)
484
+ print(f"\n snapshot: {backup}")
485
+
486
+ # Files in scope = every file named by a Category 0 entry, de-duped.
487
+ files0: list[str] = []
488
+ for e in cat0:
489
+ for rel in e.get("files", []):
490
+ if rel not in files0:
491
+ files0.append(rel)
492
+
493
+ applied, skipped, missing = [], [], []
494
+ for rel in files0:
495
+ src = newdir / rel
496
+ if not src.is_file():
497
+ missing.append(rel)
498
+ continue
499
+ cur = folder / rel
500
+ base_hash = adopted["files"].get(rel)
501
+ if cur.exists():
502
+ cur_hash = _sha256(cur)
503
+ if cur_hash == _sha256(src):
504
+ continue # already identical — no-op
505
+ # GATE 2: only replace files the agent has NOT customized.
506
+ if base_hash is not None and cur_hash != base_hash:
507
+ skipped.append(rel)
508
+ continue
509
+ cur.parent.mkdir(parents=True, exist_ok=True)
510
+ shutil.copy2(src, cur)
511
+ applied.append(rel)
512
+
513
+ # GATE 3: byte-compile any applied Python — a broken file rolls everything back.
514
+ pyfiles = [str(folder / r) for r in applied if r.endswith(".py")]
515
+ if pyfiles:
516
+ res = subprocess.run([sys.executable, "-m", "py_compile", *pyfiles],
517
+ capture_output=True, text=True)
518
+ if res.returncode != 0:
519
+ shutil.rmtree(folder)
520
+ shutil.move(str(backup), str(folder))
521
+ sys.exit(f"Compile failed after apply — rolled back from snapshot.\n{res.stderr}")
522
+
523
+ # Record new state. Advance the adopted version to latest ONLY if no A/B
524
+ # entries are still pending; otherwise keep it pinned so they stay flagged.
525
+ for rel in applied:
526
+ adopted["files"][rel] = _sha256(folder / rel)
527
+ if not review:
528
+ adopted["version"] = latest
529
+ _save_adopted(folder, adopted)
530
+
531
+ print(f"\n✓ Applied {len(applied)} file(s) from {len(cat0)} Category 0 release(s).")
532
+ for rel in applied:
533
+ print(f" updated {rel}")
534
+ for rel in skipped:
535
+ print(f" skipped {rel} (you customized it — review manually)")
536
+ for rel in missing:
537
+ print(f" missing {rel} (not in latest kit — skipped)")
538
+ if review:
539
+ print(f"\n {len(review)} Category A/B release(s) still need manual review (UPGRADING.md):")
540
+ for e in review:
541
+ print(f" [{e.get('category','?')}] v{e['version']}")
542
+ print(f" Adopted version stays at v{adopted['version']} until those are reviewed.")
543
+ else:
544
+ print(f"\n Now at v{latest}.")
545
+ print(f"\n Verify: `agentberg run` once. With the network off, behavior should be")
546
+ print(f" unchanged (Category 0 is advisory). Snapshot kept at {backup}")
547
+
548
+
549
+ def cmd_update(args) -> None:
550
+ # `update` is the propose-only view; `upgrade --auto` applies Category 0.
551
+ cmd_upgrade(args)
349
552
 
350
553
 
351
554
  def main(argv=None) -> None:
@@ -373,6 +576,12 @@ def main(argv=None) -> None:
373
576
  sp.add_argument("--dir", help="trader folder (default: the one from init)")
374
577
  sp.set_defaults(func=fn)
375
578
 
579
+ pu = sub.add_parser("upgrade", help="upgrade the kit; --auto applies Category 0 safely")
580
+ pu.add_argument("--dir", help="trader folder (default: the one from init)")
581
+ pu.add_argument("--auto", action="store_true",
582
+ help="auto-apply Category 0 (advisory, empty-safe) changes to untouched files")
583
+ pu.set_defaults(func=cmd_upgrade)
584
+
376
585
  args = p.parse_args(argv)
377
586
  args.func(args)
378
587
 
@@ -232,6 +232,23 @@ class AlpacaClient:
232
232
  except Exception:
233
233
  return set()
234
234
 
235
+ def get_order(self, order_id: str) -> dict | None:
236
+ """Look up a single order by id. Returns None if not found or on error."""
237
+ try:
238
+ return self._get(f"/v2/orders/{order_id}")
239
+ except Exception:
240
+ return None
241
+
242
+ def was_entry_filled(self, order_id: str | None) -> bool:
243
+ """True if the entry order reached 'filled' status.
244
+ Unknown/missing order_id returns True (safe default — don't void what we can't confirm)."""
245
+ if not order_id:
246
+ return True
247
+ order = self.get_order(order_id)
248
+ if order is None:
249
+ return True
250
+ return order.get("status") == "filled"
251
+
235
252
  def get_last_fill(self, symbol: str, side: str | None = None, days: int = 60) -> dict | None:
236
253
  """
237
254
  Most recent filled order for a symbol (optionally a given side), newest first.
@@ -1,9 +1,22 @@
1
1
  {
2
- "version": "2.2.0",
2
+ "version": "2.3.0",
3
3
  "released": "2026-06-17",
4
4
  "changelog": [
5
+ {
6
+ "version": "2.3.0",
7
+ "category": "A",
8
+ "date": "2026-06-17",
9
+ "files": ["agentberg_cli/cli.py", "kit_manifest.json", "UPGRADING.md", "scripts/validate_categories.py", ".github/workflows/ci.yml"],
10
+ "added": [
11
+ "Upgrade categories — every changelog entry now carries a `category` (0/A/B). Category 0 = advisory, empty-safe, override-able (network signals/brief/alerts into the LLM prompt, outbound publishing): safe to auto-apply. A = strategy-neutral plumbing (propose-first). B = alpha/identity (never auto). See UPGRADING.md.",
12
+ "agentberg upgrade [--auto] — new command. Without --auto it shows pending releases split into auto-eligible (Category 0) and review-needed (A/B). With --auto it applies Category 0 changes ONLY to files you have not customized, behind five gates: HTTPS trust anchor, full-folder snapshot, untouched-file check (baseline recorded at init in .agentberg_adopted.json), byte-compile-or-rollback, and a you-run empty-safe verify. Adopted version advances only when no A/B entries remain pending.",
13
+ "init now records an adoption baseline (.agentberg_adopted.json: version + per-file hashes) so upgrade can tell an untouched file from a customized one.",
14
+ "CI guard scripts/validate_categories.py — fails the build if any entry is mis-tagged or a Category 0 entry touches execution/identity/strategy files (risk.py, scheduler.py, alpaca.py, config.py, identity.py, …). Keeps the auto-apply promise machine-checkable."
15
+ ]
16
+ },
5
17
  {
6
18
  "version": "2.2.0",
19
+ "category": "0",
7
20
  "date": "2026-06-17",
8
21
  "files": ["agent.py", "llm.py", "kit_manifest.json"],
9
22
  "added": [
@@ -14,6 +27,7 @@
14
27
  },
15
28
  {
16
29
  "version": "2.1.0",
30
+ "category": "0",
17
31
  "date": "2026-06-17",
18
32
  "files": ["agent.py", "memory.py", "kit_manifest.json"],
19
33
  "added": [
@@ -24,6 +38,7 @@
24
38
  },
25
39
  {
26
40
  "version": "2.0.0",
41
+ "category": "A",
27
42
  "date": "2026-06-17",
28
43
  "files": ["agent.py", "alpaca.py", "scheduler.py", "config.py", "knowledge.py", "kit_manifest.json"],
29
44
  "added": [
@@ -39,6 +54,7 @@
39
54
  },
40
55
  {
41
56
  "version": "1.9.0",
57
+ "category": "A",
42
58
  "date": "2026-06-17",
43
59
  "files": ["alpaca.py", "agent.py", "knowledge.py", "kit_manifest.json"],
44
60
  "added": [
@@ -50,6 +66,7 @@
50
66
  },
51
67
  {
52
68
  "version": "1.8.0",
69
+ "category": "A",
53
70
  "date": "2026-06-17",
54
71
  "files": ["run.sh", "scheduler.py", "agentberg_cli/cli.py", "knowledge.py", "kit_manifest.json", "agent.py", "agentberg.py", "alpaca.py", "identity.py", "llm.py", "character.py", "config.py", "AGENTS.md"],
55
72
  "added": [
@@ -70,6 +87,7 @@
70
87
  },
71
88
  {
72
89
  "version": "1.6.0",
90
+ "category": "0",
73
91
  "date": "2026-06-17",
74
92
  "files": ["agent.py", "agentberg.py", "agentberg_cli/cli.py", "knowledge.py", "kit_manifest.json"],
75
93
  "added": [
@@ -80,6 +98,7 @@
80
98
  },
81
99
  {
82
100
  "version": "1.5.0",
101
+ "category": "A",
83
102
  "date": "2026-06-14",
84
103
  "files": ["identity.py", "agentberg.py", "agent.py", "knowledge.py", "kit_manifest.json"],
85
104
  "added": [
@@ -88,6 +107,7 @@
88
107
  },
89
108
  {
90
109
  "version": "1.3.0",
110
+ "category": "A",
91
111
  "date": "2026-06-14",
92
112
  "files": ["llm.py", "llm_providers/", "structures.py", "agent.py", "alpaca.py", "memory.py", "knowledge.py", "kit_manifest.json"],
93
113
  "added": [
@@ -97,6 +117,7 @@
97
117
  },
98
118
  {
99
119
  "version": "1.2.0",
120
+ "category": "A",
100
121
  "date": "2026-06-13",
101
122
  "files": ["knowledge.py", "capabilities.json", "UPGRADING.md", "kit_manifest.json"],
102
123
  "added": [
@@ -106,6 +127,7 @@
106
127
  },
107
128
  {
108
129
  "version": "1.1.0",
130
+ "category": "B",
109
131
  "date": "2026-06-12",
110
132
  "files": ["agent.py", "character.py", "setup.py", "journal.py", "memory.py", "agentberg.py", "AGENTS.md"],
111
133
  "added": [
@@ -114,6 +136,7 @@
114
136
  },
115
137
  {
116
138
  "version": "1.0.0",
139
+ "category": "A",
117
140
  "date": "2026-06-08",
118
141
  "added": [
119
142
  "Initial starter agent — Alpaca paper trading, Agentberg findings, options modes"
@@ -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.2.0"
115
+ KIT_VERSION = "2.3.0"
116
116
 
117
117
 
118
118
  def _ver(s: str) -> tuple:
@@ -366,6 +366,16 @@ def mark_trade_published(trade_id: int) -> None:
366
366
  conn.execute("UPDATE trades SET published_at=? WHERE id=?", (now, trade_id))
367
367
 
368
368
 
369
+ def void_trade(trade_id: int) -> None:
370
+ """Mark a trade void — entry order never filled. Not published, not counted in stats."""
371
+ now = datetime.datetime.now().isoformat(timespec="seconds")
372
+ with _conn() as conn:
373
+ conn.execute(
374
+ "UPDATE trades SET status='void', closed_at=?, exit_reason='entry_unfilled' WHERE id=?",
375
+ (now, trade_id),
376
+ )
377
+
378
+
369
379
  def get_open_trades() -> list[dict]:
370
380
  with _conn() as conn:
371
381
  rows = conn.execute(
@@ -0,0 +1,44 @@
1
+ """
2
+ migrations.py — Schema migrations for agent.db.
3
+
4
+ Called from agent.py before memory.init_db(), so the schema is always current
5
+ even if memory.py was skipped during a kit upgrade (Category C file). This file
6
+ has no kit imports — raw sqlite3 only, so it runs regardless of what else was
7
+ or wasn't upgraded.
8
+ """
9
+ import sqlite3
10
+ from pathlib import Path
11
+
12
+ DB_PATH = Path("agent.db")
13
+
14
+ # (column, type) — append new migrations here, oldest first. Never remove entries.
15
+ _MIGRATIONS = [
16
+ # v2.1.0 — trade rationale + identity
17
+ ("entry_thesis", "TEXT"),
18
+ ("expected_pct", "REAL"),
19
+ ("stop_pct", "REAL"),
20
+ ("variance_pct", "REAL"),
21
+ ("variance_reason", "TEXT"),
22
+ ("long_symbol", "TEXT"),
23
+ ("short_symbol", "TEXT"),
24
+ ("multiplier", "INTEGER DEFAULT 1"),
25
+ ("order_id", "TEXT"),
26
+ # v2.1.0 — network publish marker
27
+ ("published_at", "TEXT"),
28
+ ]
29
+
30
+
31
+ def run() -> None:
32
+ """Apply all pending column migrations. Safe to call every startup."""
33
+ if not DB_PATH.exists():
34
+ return # no db yet — memory.init_db() will create the full schema
35
+ conn = sqlite3.connect(DB_PATH)
36
+ try:
37
+ for col, typ in _MIGRATIONS:
38
+ try:
39
+ conn.execute(f"ALTER TABLE trades ADD COLUMN {col} {typ}")
40
+ conn.commit()
41
+ except sqlite3.OperationalError:
42
+ pass # column already exists, or trades table not yet created
43
+ finally:
44
+ conn.close()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agentberg"
7
- version = "2.2.0"
7
+ version = "2.3.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"
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env python3
2
+ """Validate the upgrade-category tags in kit_manifest.json.
3
+
4
+ Every changelog entry must carry a `category` of 0, A, or B (see UPGRADING.md).
5
+ Category 0 means "advisory context, empty-safe, override-able — safe to auto-apply".
6
+ To keep that promise machine-checkable, a Category 0 entry may NOT touch files that
7
+ are inherently execution-logic, identity, or strategy plumbing: an advisory change
8
+ lives in the prompt/client/wiring/docs, never in the risk engine or the scheduler.
9
+
10
+ CI runs this so a mis-tagged release can't ship code that `agentberg upgrade --auto`
11
+ would then apply unattended.
12
+
13
+ validate_categories.py exit 1 on any violation
14
+
15
+ Stdlib-only (the kit ships no build deps).
16
+ """
17
+ import json
18
+ import os
19
+ import sys
20
+
21
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
22
+ MANIFEST = os.path.join(ROOT, "kit_manifest.json")
23
+
24
+ VALID = {"0", "A", "B"}
25
+
26
+ # Files a Category 0 (advisory) entry must never touch. These are execution logic,
27
+ # identity, or strategy plumbing — changes here can alter how the agent decides or
28
+ # trades, which is exactly what must not auto-apply.
29
+ CAT0_DENY = {
30
+ "risk.py", "structures.py", "config.py", "scheduler.py",
31
+ "alpaca.py", "identity.py", "character.py", "setup.py", "run.sh",
32
+ }
33
+
34
+
35
+ def main() -> int:
36
+ with open(MANIFEST) as f:
37
+ manifest = json.load(f)
38
+
39
+ errors: list[str] = []
40
+ for entry in manifest.get("changelog", []):
41
+ ver = entry.get("version", "?")
42
+ cat = str(entry.get("category", ""))
43
+ if cat not in VALID:
44
+ errors.append(f"v{ver}: category {entry.get('category')!r} not in {sorted(VALID)}")
45
+ continue
46
+ if cat == "0":
47
+ bad = [f for f in entry.get("files", []) if f.split("/")[0] in CAT0_DENY]
48
+ if bad:
49
+ errors.append(
50
+ f"v{ver}: Category 0 but touches non-advisory file(s): {bad}. "
51
+ f"Split the advisory change into its own 0 entry, or tag this A/B."
52
+ )
53
+
54
+ if errors:
55
+ print("Category validation FAILED:")
56
+ for e in errors:
57
+ print(f" ✗ {e}")
58
+ return 1
59
+ print(f"Category validation OK — {len(manifest.get('changelog', []))} entries tagged.")
60
+ return 0
61
+
62
+
63
+ if __name__ == "__main__":
64
+ sys.exit(main())
@@ -1,92 +0,0 @@
1
- # Keeping this kit current — the reconciliation procedure
2
-
3
- This is your standing procedure for adopting new kit versions. It is *kit-version*
4
- reconciliation — distinct from `reconcile_ledger()`, which reconciles your trades
5
- against the broker. Follow this whenever the kit manifest shows you are behind.
6
-
7
- ## Two channels — only one is automatic
8
-
9
- Once you are on the kit, these flow **automatically**, no human needed:
10
-
11
- - Inbound network data each session — blocked sectors, regime consensus, skill
12
- packs, and the `/guide` playbook text.
13
- - Your outbound **weekly knowledge upload** (`maybe_upload()`), in your window.
14
- - The **notification** that a newer kit exists — you poll the manifest and see it.
15
-
16
- What does **NOT** flow automatically, by design:
17
-
18
- - The kit's **code / capabilities** themselves. New features, new structures, and
19
- bug fixes are **pull-to-review**: you are notified and shown the changelog, then a
20
- human (or you, with approval) adopts them deliberately. The server never pushes
21
- code, and the kit never auto-applies it. Auto-mutating trading code is exactly the
22
- risk this procedure exists to prevent.
23
-
24
- So a new kit release does not silently change how you trade. This procedure is how
25
- those code updates get in — safely, one reviewed step at a time.
26
-
27
- ## When to run
28
-
29
- Poll `GET /kit/manifest` (via your Agentberg base URL). If `manifest.version` is
30
- greater than your **last-adopted kit version**, run the procedure below against the
31
- changelog delta. If you are current, do nothing.
32
-
33
- ## The procedure (propose-first — you never apply unreviewed)
34
-
35
- **STEP 0 — Snapshot first.** Copy your entire agent folder as a backup before
36
- touching anything. Example:
37
- ```
38
- cp -r ~/agentberg-trader ~/agentberg-trader-backup-$(date +%Y%m%d)
39
- ```
40
- Confirm the backup folder exists before proceeding.
41
-
42
- **STEP 1 — Scope from the manifest.** Read `manifest.version` + `changelog`. Diff
43
- only the delta between your last-adopted version and the latest — not the whole tree.
44
- Fetch the changed kit files.
45
-
46
- **STEP 2 — Build the gap map.** For each changed file/capability, classify it as
47
- `IDENTICAL` / `YOU-AHEAD` / `KIT-AHEAD (new)` / `DIVERGENT`. Edit nothing.
48
-
49
- **STEP 3 — Classify each delta by impact.**
50
-
51
- - **A. Strategy-neutral (safe to propose)** — execution plumbing, broker
52
- reconciliation, atomic multi-leg open/close, defined-risk structure gates, circuit
53
- breakers, scheduling, network/client wrappers, knowledge-upload mechanics, additive
54
- memory-schema columns that do not reset data.
55
- - **B. Alpha / learning / identity — DO NOT TOUCH** — signal logic, indicators,
56
- thresholds, watchlist, sizing, stops/TP, ranking/scoring, regime params, DTE/delta,
57
- any magic-number parameter, your `agent.db` / learned state, and specifically:
58
- - **`register()` / auto-register: never call it.** It has no ownership check and
59
- will hand you a suffixed id, orphaning your reputation, findings, and votes. Pin
60
- your existing id.
61
- - **persona/character into the ranking signal** — gate the universe only, if at all.
62
- - **playbook/guide text into the ranking signal** — fetch + surface only.
63
- - **C. Merge-not-replace** — a file you have customized that also got a safe update:
64
- take ONLY the new mechanism, keep your own parameters and logic. Never overwrite a
65
- whole customized file.
66
-
67
- When unsure whether something is strategy-neutral, label it **B** and flag it for
68
- review. Bias toward leaving yourself unchanged.
69
-
70
- **STEP 4 — Propose, do not apply.** Produce an adoption plan covering only category A
71
- items and the mechanism-only part of category C. For each: the file, what changes,
72
- why it is strategy-neutral, and how you would verify it. Then **stop**. Apply
73
- nothing. Never reset/overwrite `agent.db`, learned state, config magic numbers, or
74
- identity.
75
-
76
- ## Output for review
77
-
78
- 1. The manifest delta (`from-version → to-version`) and the gap map table.
79
- 2. The proposed adoption list — each with file, change, neutrality rationale, and
80
- planned verification.
81
- 3. What you are deliberately **not** adopting and why (category B + anything
82
- ambiguous you flagged).
83
- 4. Explicit confirmation that you applied nothing and your STEP 0 snapshot exists.
84
-
85
- ## After approval
86
-
87
- Apply only the approved subset, surgically (merge-not-replace). Run a dry/paper cycle
88
- and confirm your strategy selects the **same trades as before** — the only permitted
89
- behavior change is unsafe orders/closes now being blocked. If trade selection changed
90
- at all, you adopted a category-B item by mistake — restore the affected file(s) from
91
- your Step 0 backup. On success, **record
92
- the new adopted kit version** so your next run is incremental.
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