claude-nb 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/LICENSE +38 -0
  2. package/Makefile +60 -0
  3. package/README.md +63 -0
  4. package/VERSION +1 -0
  5. package/bin/_pip_entry.py +25 -0
  6. package/bin/board +287 -0
  7. package/bin/cnb +150 -0
  8. package/bin/cnb.js +33 -0
  9. package/bin/dispatcher +151 -0
  10. package/bin/dispatcher-watchdog +57 -0
  11. package/bin/doctor +328 -0
  12. package/bin/init +316 -0
  13. package/bin/registry +347 -0
  14. package/bin/swarm +896 -0
  15. package/lib/__init__.py +1 -0
  16. package/lib/board_admin.py +128 -0
  17. package/lib/board_bbs.py +99 -0
  18. package/lib/board_bug.py +161 -0
  19. package/lib/board_db.py +262 -0
  20. package/lib/board_lock.py +113 -0
  21. package/lib/board_mailbox.py +145 -0
  22. package/lib/board_maintenance.py +237 -0
  23. package/lib/board_msg.py +230 -0
  24. package/lib/board_task.py +200 -0
  25. package/lib/board_view.py +366 -0
  26. package/lib/board_vote.py +164 -0
  27. package/lib/build_lock.py +221 -0
  28. package/lib/cli.py +34 -0
  29. package/lib/common.py +285 -0
  30. package/lib/concerns/__init__.py +42 -0
  31. package/lib/concerns/adaptive_throttle.py +26 -0
  32. package/lib/concerns/base.py +25 -0
  33. package/lib/concerns/bug_sla_checker.py +32 -0
  34. package/lib/concerns/config.py +22 -0
  35. package/lib/concerns/coral_manager.py +61 -0
  36. package/lib/concerns/coral_poker.py +57 -0
  37. package/lib/concerns/file_watcher.py +127 -0
  38. package/lib/concerns/health_checker.py +72 -0
  39. package/lib/concerns/helpers.py +152 -0
  40. package/lib/concerns/idle_detector.py +56 -0
  41. package/lib/concerns/idle_killer.py +41 -0
  42. package/lib/concerns/idle_nudger.py +38 -0
  43. package/lib/concerns/inbox_nudger.py +34 -0
  44. package/lib/concerns/resource_monitor.py +47 -0
  45. package/lib/concerns/session_keepalive.py +23 -0
  46. package/lib/concerns/time_announcer.py +34 -0
  47. package/lib/crypto.py +92 -0
  48. package/lib/health.py +187 -0
  49. package/lib/inject.py +164 -0
  50. package/lib/migrate.py +109 -0
  51. package/lib/monitor.py +373 -0
  52. package/lib/panel.py +137 -0
  53. package/lib/resources.py +341 -0
  54. package/migrations/001_foreign_keys.sql +77 -0
  55. package/migrations/002_session_persona.sql +1 -0
  56. package/migrations/003_mailbox.sql +9 -0
  57. package/package.json +28 -0
  58. package/pyproject.toml +71 -0
  59. package/registry/0001-meridian.json +12 -0
  60. package/registry/0002-forge.json +12 -0
  61. package/registry/0003-lead.json +12 -0
  62. package/registry/0004-ms-encrypted-mailbox-live.json +12 -0
  63. package/registry/GENESIS.json +9 -0
  64. package/registry/pubkeys.json +5 -0
  65. package/schema.sql +138 -0
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env python3
2
+ """resources.py -- Unified resource monitoring (battery + memory + CPU).
3
+
4
+ Detects anomalies and notifies via board.
5
+
6
+ Usage:
7
+ ./lib/resources.py # One-shot status check
8
+ ./lib/resources.py --watch # Continuous monitoring (30s interval)
9
+ ./lib/resources.py --json # Machine-readable output
10
+ """
11
+
12
+ import json
13
+ import re
14
+ import shutil
15
+ import subprocess
16
+ import sys
17
+ import time
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Thresholds
23
+ # ---------------------------------------------------------------------------
24
+
25
+ BATTERY_LOW = 30
26
+ BATTERY_CRITICAL = 15
27
+ MEMORY_WARN_PCT = 80
28
+ CPU_SATURATED = 90
29
+ CPU_SUSTAIN_CHECKS = 2
30
+
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Data classes
34
+ # ---------------------------------------------------------------------------
35
+
36
+
37
+ @dataclass
38
+ class BatteryInfo:
39
+ status: str # AC, ON_BATTERY, LOW, CRITICAL, N/A
40
+ pct: int
41
+ on_battery: bool
42
+ remaining: str
43
+
44
+
45
+ @dataclass
46
+ class MemoryInfo:
47
+ status: str # OK, WARNING, CRITICAL
48
+ used_pct: int
49
+ pressure: str # normal, warn, critical
50
+
51
+
52
+ @dataclass
53
+ class CPUInfo:
54
+ status: str # OK, SATURATED
55
+ usage: int
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Detection helpers (macOS-specific)
60
+ # ---------------------------------------------------------------------------
61
+
62
+
63
+ def _run(cmd: str, default: str = "") -> str:
64
+ """Run a shell command and return stdout, or *default* on failure."""
65
+ try:
66
+ r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10)
67
+ return r.stdout.strip() if r.returncode == 0 else default
68
+ except Exception:
69
+ return default
70
+
71
+
72
+ def check_battery() -> BatteryInfo:
73
+ if not shutil.which("pmset"):
74
+ return BatteryInfo(status="N/A", pct=100, on_battery=False, remaining="—")
75
+
76
+ batt_info = _run("pmset -g batt")
77
+ on_battery = "Battery Power" in batt_info
78
+
79
+ m = re.search(r"(\d+)%", batt_info)
80
+ pct = int(m.group(1)) if m else 100
81
+
82
+ m_rem = re.search(r"(\d+:\d+ remaining)", batt_info)
83
+ remaining = m_rem.group(1) if m_rem else "—"
84
+
85
+ if on_battery:
86
+ if pct < BATTERY_CRITICAL:
87
+ status = "CRITICAL"
88
+ elif pct < BATTERY_LOW:
89
+ status = "LOW"
90
+ else:
91
+ status = "ON_BATTERY"
92
+ else:
93
+ status = "AC"
94
+
95
+ return BatteryInfo(status=status, pct=pct, on_battery=on_battery, remaining=remaining)
96
+
97
+
98
+ def check_memory() -> MemoryInfo:
99
+ status = "OK"
100
+ used_pct = 0
101
+ pressure = "normal"
102
+
103
+ if shutil.which("memory_pressure"):
104
+ mp_out = _run("memory_pressure 2>/dev/null | tail -1")
105
+ if "critical" in mp_out.lower():
106
+ pressure = "critical"
107
+ status = "CRITICAL"
108
+ elif "warn" in mp_out.lower():
109
+ pressure = "warn"
110
+ status = "WARNING"
111
+
112
+ if shutil.which("vm_stat"):
113
+ vm_out = _run("vm_stat")
114
+ lines = vm_out.splitlines()
115
+ if lines:
116
+ # page size
117
+ ps_match = re.search(r"(\d+)", lines[0])
118
+ page_size = int(ps_match.group(1)) if ps_match else 4096
119
+
120
+ def _extract(label: str) -> int:
121
+ for ln in lines:
122
+ if label in ln:
123
+ m = re.search(r"(\d+)", ln.split(":")[-1])
124
+ return int(m.group(1)) if m else 0
125
+ return 0
126
+
127
+ pages_free = _extract("Pages free")
128
+ pages_speculative = _extract("Pages speculative")
129
+
130
+ total_bytes_str = _run("sysctl -n hw.memsize", "0")
131
+ total_bytes = int(total_bytes_str) if total_bytes_str.isdigit() else 0
132
+
133
+ if total_bytes > 0:
134
+ total_mb = total_bytes // (1024 * 1024)
135
+ free_pages = pages_free + pages_speculative
136
+ free_mb = (free_pages * page_size) // (1024 * 1024)
137
+ if total_mb > 0:
138
+ used_pct = (total_mb - free_mb) * 100 // total_mb
139
+
140
+ return MemoryInfo(status=status, used_pct=used_pct, pressure=pressure)
141
+
142
+
143
+ def check_cpu() -> CPUInfo:
144
+ status = "OK"
145
+ usage = 0
146
+
147
+ if shutil.which("top"):
148
+ top_out = _run("top -l 1 -n 0 2>/dev/null")
149
+ for line in top_out.splitlines():
150
+ if "CPU usage" in line:
151
+ m = re.search(r"([\d.]+)%\s*idle", line)
152
+ if m:
153
+ idle = float(m.group(1))
154
+ usage = int(round(100 - idle))
155
+ break
156
+
157
+ if usage >= CPU_SATURATED:
158
+ status = "SATURATED"
159
+
160
+ return CPUInfo(status=status, usage=usage)
161
+
162
+
163
+ # ---------------------------------------------------------------------------
164
+ # State tracking for notification dedup
165
+ # ---------------------------------------------------------------------------
166
+
167
+
168
+ def _state_file() -> Path:
169
+ try:
170
+ from lib.common import find_claudes_dir
171
+
172
+ return find_claudes_dir() / "resource-monitor-state"
173
+ except Exception:
174
+ return Path("/tmp/resource-monitor-state")
175
+
176
+
177
+ def _load_prev_state() -> str:
178
+ sf = _state_file()
179
+ if sf.exists():
180
+ return sf.read_text().strip()
181
+ return "AC|normal|OK"
182
+
183
+
184
+ def _save_state(state: str) -> None:
185
+ _state_file().write_text(state + "\n")
186
+
187
+
188
+ def notify_if_changed(batt: BatteryInfo, mem: MemoryInfo, cpu: CPUInfo, board_cmd: str | None = None) -> None:
189
+ """Send board notifications only on state transitions."""
190
+ current = f"{batt.status}|{mem.status}|{cpu.status}"
191
+ prev = _load_prev_state()
192
+ if current == prev:
193
+ return
194
+ _save_state(current)
195
+
196
+ if board_cmd is None:
197
+ return
198
+
199
+ def _send(msg: str) -> None:
200
+ try:
201
+ subprocess.run(
202
+ [board_cmd, "--as", "monitor", "send", "All", msg],
203
+ capture_output=True,
204
+ timeout=10,
205
+ )
206
+ except Exception:
207
+ pass
208
+
209
+ # Battery transitions
210
+ if batt.status == "CRITICAL":
211
+ _send(f"[BATTERY CRITICAL] {batt.pct}% remaining. Suspending non-essential sessions recommended.")
212
+ elif batt.status == "LOW":
213
+ _send(f"[BATTERY LOW] {batt.pct}%. Consider reducing active sessions.")
214
+ elif batt.status == "ON_BATTERY" and prev.startswith("AC"):
215
+ _send(f"[BATTERY] Switched to battery power ({batt.pct}%).")
216
+
217
+ # Memory transitions
218
+ if mem.status == "CRITICAL" and "CRITICAL" not in prev:
219
+ _send("[MEMORY CRITICAL] System under memory pressure. Save state + reduce sessions.")
220
+ elif mem.status == "WARNING" and ("normal" in prev or "OK" in prev):
221
+ _send("[MEMORY WARNING] Memory pressure rising. Monitor closely.")
222
+
223
+ # CPU transitions
224
+ if cpu.status == "SATURATED" and "SATURATED" not in prev:
225
+ _send(f"[CPU SATURATED] CPU > {CPU_SATURATED}%. Avoid concurrent builds.")
226
+
227
+
228
+ # ---------------------------------------------------------------------------
229
+ # JSON output helper (used by dispatcher)
230
+ # ---------------------------------------------------------------------------
231
+
232
+
233
+ def to_json(batt: BatteryInfo, mem: MemoryInfo, cpu: CPUInfo) -> str:
234
+ return json.dumps(
235
+ {
236
+ "battery": {
237
+ "status": batt.status,
238
+ "pct": batt.pct,
239
+ "on_battery": batt.on_battery,
240
+ "remaining": batt.remaining,
241
+ },
242
+ "memory": {
243
+ "status": mem.status,
244
+ "used_pct": mem.used_pct,
245
+ "pressure": mem.pressure,
246
+ },
247
+ "cpu": {
248
+ "status": cpu.status,
249
+ "usage": cpu.usage,
250
+ },
251
+ }
252
+ )
253
+
254
+
255
+ def get_all() -> tuple:
256
+ """Return (BatteryInfo, MemoryInfo, CPUInfo) tuple."""
257
+ return check_battery(), check_memory(), check_cpu()
258
+
259
+
260
+ # ---------------------------------------------------------------------------
261
+ # CLI
262
+ # ---------------------------------------------------------------------------
263
+
264
+
265
+ def print_status(mode: str = "status") -> None:
266
+ batt, mem, cpu = get_all()
267
+
268
+ if mode == "json":
269
+ print(to_json(batt, mem, cpu))
270
+ return
271
+
272
+ print("Resource Monitor")
273
+ print("================")
274
+ print()
275
+
276
+ line = f"Battery: {batt.status}"
277
+ if batt.status != "N/A":
278
+ line += f" ({batt.pct}%)"
279
+ if batt.remaining != "—":
280
+ line += f" {batt.remaining}"
281
+ print(line)
282
+ print(f"Memory: {mem.status} ({mem.used_pct}% used, pressure: {mem.pressure})")
283
+ print(f"CPU: {cpu.status} ({cpu.usage}% usage)")
284
+ print()
285
+
286
+ has_issue = False
287
+ if batt.status == "CRITICAL":
288
+ print("! CRITICAL: Suspend all non-essential sessions NOW.")
289
+ has_issue = True
290
+ elif batt.status == "LOW":
291
+ print("! Battery low: reduce to 2-3 sessions.")
292
+ has_issue = True
293
+ elif batt.status == "ON_BATTERY":
294
+ print("* Running on battery. Monitor usage.")
295
+ has_issue = True
296
+
297
+ if mem.status == "CRITICAL":
298
+ print("! CRITICAL: Memory pressure critical. Restart largest session.")
299
+ has_issue = True
300
+ elif mem.status == "WARNING":
301
+ print("! Memory pressure elevated. Consider suspending idle sessions.")
302
+ has_issue = True
303
+
304
+ if cpu.status == "SATURATED":
305
+ print("! CPU saturated. Avoid concurrent builds.")
306
+ has_issue = True
307
+
308
+ if not has_issue:
309
+ print("All resources nominal.")
310
+
311
+
312
+ def main() -> None:
313
+ mode = "status"
314
+ for arg in sys.argv[1:]:
315
+ if arg == "--watch":
316
+ mode = "watch"
317
+ elif arg == "--json":
318
+ mode = "json"
319
+
320
+ if mode == "watch":
321
+ print("Resource monitor started (interval: 30s)")
322
+ board_cmd = None
323
+ try:
324
+ from lib.common import find_claudes_dir
325
+
326
+ bc = find_claudes_dir().parent / "board"
327
+ if bc.exists():
328
+ board_cmd = str(bc)
329
+ except Exception:
330
+ pass
331
+
332
+ while True:
333
+ batt, mem, cpu = get_all()
334
+ notify_if_changed(batt, mem, cpu, board_cmd)
335
+ time.sleep(30)
336
+ else:
337
+ print_status(mode)
338
+
339
+
340
+ if __name__ == "__main__":
341
+ main()
@@ -0,0 +1,77 @@
1
+ -- 001_foreign_keys.sql
2
+ -- Add foreign key constraints to prevent orphaned data.
3
+ -- All cascading rules use CASCADE so that deleting a session/proposal/thread
4
+ -- automatically cleans up related rows.
5
+
6
+ PRAGMA foreign_keys = ON;
7
+
8
+ -- Create a pseudo-session 'all' so FK on messages.recipient works.
9
+ -- 'all' is the broadcast pseudo-recipient used by send-to-all.
10
+ INSERT OR IGNORE INTO sessions(name, status, updated_at)
11
+ VALUES ('all', 'system', strftime('%Y-%m-%d %H:%M:%S', 'now', 'localtime'));
12
+
13
+ -- inbox: must reference valid session + valid message
14
+ -- Rebuild with FK (SQLite requires recreating the table)
15
+ CREATE TABLE IF NOT EXISTS inbox_new (
16
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
17
+ session TEXT NOT NULL REFERENCES sessions(name) ON DELETE CASCADE,
18
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
19
+ delivered_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M', 'now', 'localtime')),
20
+ read INTEGER DEFAULT 0
21
+ );
22
+ INSERT INTO inbox_new SELECT * FROM inbox;
23
+ DROP TABLE inbox;
24
+ ALTER TABLE inbox_new RENAME TO inbox;
25
+ CREATE INDEX IF NOT EXISTS idx_inbox ON inbox(session, read);
26
+
27
+ -- votes: must reference valid proposal
28
+ CREATE TABLE IF NOT EXISTS votes_new (
29
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
30
+ proposal_id INTEGER NOT NULL REFERENCES proposals(id) ON DELETE CASCADE,
31
+ voter TEXT NOT NULL,
32
+ decision TEXT NOT NULL,
33
+ reason TEXT DEFAULT '',
34
+ ts TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M', 'now', 'localtime')),
35
+ UNIQUE(proposal_id, voter)
36
+ );
37
+ INSERT INTO votes_new SELECT * FROM votes;
38
+ DROP TABLE votes;
39
+ ALTER TABLE votes_new RENAME TO votes;
40
+
41
+ -- thread_replies: must reference valid thread
42
+ CREATE TABLE IF NOT EXISTS thread_replies_new (
43
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
44
+ thread_id TEXT NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
45
+ author TEXT NOT NULL,
46
+ body TEXT NOT NULL,
47
+ ts TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M', 'now', 'localtime'))
48
+ );
49
+ INSERT INTO thread_replies_new SELECT * FROM thread_replies;
50
+ DROP TABLE thread_replies;
51
+ ALTER TABLE thread_replies_new RENAME TO thread_replies;
52
+ CREATE INDEX IF NOT EXISTS idx_replies ON thread_replies(thread_id);
53
+
54
+ -- suspended: must reference valid session
55
+ CREATE TABLE IF NOT EXISTS suspended_new (
56
+ name TEXT PRIMARY KEY REFERENCES sessions(name) ON DELETE CASCADE,
57
+ suspended_by TEXT NOT NULL,
58
+ ts TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M', 'now', 'localtime'))
59
+ );
60
+ INSERT INTO suspended_new SELECT * FROM suspended;
61
+ DROP TABLE suspended;
62
+ ALTER TABLE suspended_new RENAME TO suspended;
63
+
64
+ -- tasks: must reference valid session
65
+ CREATE TABLE IF NOT EXISTS tasks_new (
66
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
67
+ session TEXT NOT NULL REFERENCES sessions(name) ON DELETE CASCADE,
68
+ description TEXT NOT NULL,
69
+ status TEXT DEFAULT 'pending',
70
+ priority INTEGER DEFAULT 0,
71
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M', 'now', 'localtime')),
72
+ done_at TEXT DEFAULT NULL
73
+ );
74
+ INSERT INTO tasks_new SELECT * FROM tasks;
75
+ DROP TABLE tasks;
76
+ ALTER TABLE tasks_new RENAME TO tasks;
77
+ CREATE INDEX IF NOT EXISTS idx_tasks ON tasks(session, status);
@@ -0,0 +1 @@
1
+ ALTER TABLE sessions ADD COLUMN persona TEXT DEFAULT '';
@@ -0,0 +1,9 @@
1
+ CREATE TABLE IF NOT EXISTS mailbox(
2
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3
+ ts TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M','now','localtime')),
4
+ sender TEXT NOT NULL,
5
+ recipient TEXT NOT NULL,
6
+ encrypted_body TEXT NOT NULL,
7
+ read INTEGER DEFAULT 0
8
+ );
9
+ CREATE INDEX IF NOT EXISTS idx_mailbox ON mailbox(recipient, read);
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "claude-nb",
3
+ "version": "0.3.0",
4
+ "description": "Multi-agent coordination framework for Claude Code sessions",
5
+ "bin": {
6
+ "cnb": "./bin/cnb.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "lib/",
11
+ "!lib/**/__pycache__/",
12
+ "!lib/**/*.pyc",
13
+ "migrations/",
14
+ "registry/",
15
+ "schema.sql",
16
+ "pyproject.toml",
17
+ "VERSION",
18
+ "Makefile"
19
+ ],
20
+ "keywords": [
21
+ "claude",
22
+ "multi-agent",
23
+ "coordination",
24
+ "ai",
25
+ "coding-agent"
26
+ ],
27
+ "license": "MIT"
28
+ }
package/pyproject.toml ADDED
@@ -0,0 +1,71 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "claude-nb"
7
+ version = "0.3.0"
8
+ description = "Multi-agent coordination framework for Claude Code sessions"
9
+ requires-python = ">=3.11"
10
+ license = "OpenAll-1.0"
11
+ dependencies = [
12
+ "cryptography>=41.0",
13
+ ]
14
+
15
+ [project.scripts]
16
+ cnb = "lib.cli:main"
17
+
18
+ [tool.setuptools.packages.find]
19
+ include = ["lib*"]
20
+
21
+ # ---------- pytest ----------
22
+ [tool.pytest.ini_options]
23
+ testpaths = ["tests"]
24
+ python_files = ["test_*.py"]
25
+ python_classes = ["Test*"]
26
+ python_functions = ["test_*"]
27
+ addopts = "-v --tb=short"
28
+ markers = [
29
+ "slow: marks tests as slow (deselect with '-m \"not slow\"')",
30
+ "integration: marks integration tests that may need external services",
31
+ ]
32
+
33
+ # ---------- ruff ----------
34
+ [tool.ruff]
35
+ target-version = "py311"
36
+ line-length = 120
37
+
38
+ [tool.ruff.lint]
39
+ select = [
40
+ "E", # pycodestyle errors
41
+ "W", # pycodestyle warnings
42
+ "F", # pyflakes
43
+ "I", # isort
44
+ "UP", # pyupgrade
45
+ "B", # flake8-bugbear
46
+ "SIM", # flake8-simplify
47
+ "RUF", # ruff-specific
48
+ ]
49
+ ignore = [
50
+ "E501", # line too long (handled by formatter)
51
+ "E741", # ambiguous variable name (short names like s/o are intentional)
52
+ "B904", # raise ... from err in except (too noisy for existing code)
53
+ "SIM105", # contextlib.suppress (try/except/pass is fine here)
54
+ "SIM108", # ternary operator (readability preference)
55
+ "RUF001", # ambiguous unicode (project uses Chinese strings)
56
+ "RUF015", # unnecessary iterable allocation (next(iter()) not always clearer)
57
+ ]
58
+
59
+ [tool.ruff.lint.per-file-ignores]
60
+ "bin/*" = ["E402"]
61
+
62
+ [tool.ruff.lint.isort]
63
+ known-first-party = ["lib"]
64
+
65
+ # ---------- mypy ----------
66
+ [tool.mypy]
67
+ python_version = "3.11"
68
+ warn_return_any = true
69
+ warn_unused_configs = true
70
+ disallow_untyped_defs = false
71
+ check_untyped_defs = true
@@ -0,0 +1,12 @@
1
+ {
2
+ "block": 1,
3
+ "name": "meridian",
4
+ "display_name": "Claude Meridian",
5
+ "type": "agent",
6
+ "role": "lead",
7
+ "description": "统筹协调,社区维护",
8
+ "created": "2026-05-06",
9
+ "prev": "9b0f017ebc29a808",
10
+ "chain": "82a167d",
11
+ "content_hash": "93545ea3438c107c"
12
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "block": 2,
3
+ "name": "forge",
4
+ "display_name": "Claude Forge",
5
+ "type": "agent",
6
+ "role": "active-dev",
7
+ "description": "活跃开发者,GitHub 社区维护",
8
+ "created": "2026-05-06",
9
+ "prev": "93545ea3438c107c",
10
+ "chain": "4a3c92e",
11
+ "content_hash": "ca0ad406fd4e42ad"
12
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "block": 3,
3
+ "name": "lead",
4
+ "display_name": "Claude Lead",
5
+ "type": "agent",
6
+ "role": "active-dev",
7
+ "description": "用户侧团队 lead,产品方向与可行性评估",
8
+ "created": "2026-05-06",
9
+ "prev": "ca0ad406fd4e42ad",
10
+ "chain": "e665a7e",
11
+ "content_hash": "2db15fd7267a8dcc"
12
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "block": 4,
3
+ "name": "encrypted-mailbox-live",
4
+ "type": "project",
5
+ "milestone": true,
6
+ "created": "2026-05-06",
7
+ "description": "加密信箱端到端验证通过",
8
+ "detail": "基于 X25519 sealed-box 的异步加密消息机制上线。密钥对本地生成,公钥写入 pubkeys.json,加密消息通过 GitHub Release assets 投递。meridian↔lead 首次成功互通。",
9
+ "prev": "2db15fd7267a8dcc",
10
+ "chain": null,
11
+ "content_hash": "fcaf497872180e90"
12
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "block": 0,
3
+ "name": "claudes-code",
4
+ "type": "project",
5
+ "created": "2026-05-06",
6
+ "description": "Genesis block. The beginning of claudes-code.",
7
+ "chain": null,
8
+ "content_hash": "9b0f017ebc29a808"
9
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "meridian": "3f9baebbfe2ddd36e0ccd6f98301fd1bdc57aa82f395791a769864078181c43c",
3
+ "musk": "16e74dfbbd797fe17a8e44d99b71d207c8e3aa1b36ffbf2dfe88420dc54bf37c",
4
+ "lead": "51b6ebbbe720745bcb9386e1d997481bd265beac45d183fc2ac11fae9824c43a"
5
+ }