claude-nb 0.3.0 → 0.5.1

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/Makefile +8 -2
  2. package/README.md +57 -36
  3. package/VERSION +1 -1
  4. package/bin/board +112 -34
  5. package/bin/cnb +152 -65
  6. package/bin/dispatcher +25 -11
  7. package/bin/doctor +3 -5
  8. package/bin/init +13 -47
  9. package/bin/notify +224 -0
  10. package/bin/registry +8 -23
  11. package/bin/swarm +41 -860
  12. package/bin/sync-version +131 -0
  13. package/lib/board_admin.py +19 -9
  14. package/lib/board_bbs.py +23 -8
  15. package/lib/board_bug.py +2 -1
  16. package/lib/board_db.py +31 -141
  17. package/lib/board_lock.py +5 -1
  18. package/lib/board_mailbox.py +18 -8
  19. package/lib/board_maintenance.py +26 -27
  20. package/lib/board_msg.py +76 -39
  21. package/lib/board_pending.py +233 -0
  22. package/lib/board_pulse.py +14 -0
  23. package/lib/board_task.py +41 -32
  24. package/lib/board_tui.py +120 -0
  25. package/lib/board_view.py +70 -50
  26. package/lib/board_vote.py +9 -3
  27. package/lib/build_lock.py +7 -7
  28. package/lib/common.py +45 -3
  29. package/lib/concerns/__init__.py +7 -11
  30. package/lib/concerns/{coral_manager.py → coral.py} +54 -4
  31. package/lib/concerns/digest_scheduler.py +109 -0
  32. package/lib/concerns/file_watcher.py +73 -68
  33. package/lib/concerns/health.py +136 -0
  34. package/lib/concerns/helpers.py +1 -5
  35. package/lib/concerns/idle.py +130 -0
  36. package/lib/concerns/notification_push.py +171 -0
  37. package/lib/concerns/notifications.py +145 -0
  38. package/lib/concerns/nudge_coordinator.py +148 -0
  39. package/lib/digest.py +62 -0
  40. package/lib/health.py +2 -2
  41. package/lib/inject.py +2 -2
  42. package/lib/migrate.py +1 -0
  43. package/lib/monitor.py +9 -22
  44. package/lib/notification_config.py +101 -0
  45. package/lib/swarm.py +464 -0
  46. package/lib/swarm_backend.py +300 -0
  47. package/lib/theme_profiles.py +89 -0
  48. package/migrations/004_heartbeat.sql +1 -0
  49. package/migrations/005_notification_log.sql +12 -0
  50. package/migrations/006_pending_actions.sql +15 -0
  51. package/package.json +4 -3
  52. package/pyproject.toml +3 -2
  53. package/registry/README.md +9 -0
  54. package/registry/pubkeys.json +2 -1
  55. package/schema.sql +29 -1
  56. package/lib/concerns/bug_sla_checker.py +0 -32
  57. package/lib/concerns/coral_poker.py +0 -57
  58. package/lib/concerns/health_checker.py +0 -72
  59. package/lib/concerns/idle_detector.py +0 -56
  60. package/lib/concerns/idle_killer.py +0 -41
  61. package/lib/concerns/idle_nudger.py +0 -38
  62. package/lib/concerns/inbox_nudger.py +0 -34
  63. package/lib/concerns/resource_monitor.py +0 -47
  64. package/lib/concerns/session_keepalive.py +0 -23
  65. package/lib/concerns/time_announcer.py +0 -34
package/Makefile CHANGED
@@ -8,13 +8,13 @@ SCRIPTS = bin/cnb bin/board bin/swarm bin/dispatcher bin/dispatcher-watchdog bin
8
8
  # All python sources (bin + lib)
9
9
  PY_SOURCES = bin/board bin/swarm bin/dispatcher bin/dispatcher-watchdog bin/init lib/ tests/
10
10
 
11
- .PHONY: all install uninstall test lint typecheck format check ci clean version
11
+ .PHONY: all install uninstall test lint typecheck format check ci clean version sync-version check-version
12
12
 
13
13
  all: check
14
14
 
15
15
  check: lint test
16
16
 
17
- ci: lint typecheck test
17
+ ci: lint typecheck test check-version
18
18
 
19
19
  lint:
20
20
  @echo "=== ruff ==="
@@ -56,5 +56,11 @@ clean:
56
56
  find . -type d -name '*.egg-info' -exec rm -rf {} + 2>/dev/null || true
57
57
  rm -rf dist/ build/ .mypy_cache/ .ruff_cache/
58
58
 
59
+ sync-version:
60
+ python3 bin/sync-version
61
+
62
+ check-version:
63
+ python3 bin/sync-version --check
64
+
59
65
  version:
60
66
  @echo $(VERSION)
package/README.md CHANGED
@@ -1,63 +1,84 @@
1
- # cnb
1
+ # claude-nb
2
2
 
3
- Multi-agent coordination framework for AI coding sessions.
3
+ Multi-agent coordination framework for Claude Code sessions.
4
4
 
5
- ## Quick start
5
+ Multiple Claude Code instances share a board — they message each other, assign tasks, track status, and collaborate on the same codebase.
6
+
7
+ ## Install
6
8
 
7
9
  ```bash
8
- pip install cnb
9
- cnb # 2 agents, random AI names
10
- cnb 5 pokemon # 5 agents, Pokémon theme
10
+ npm install -g claude-nb
11
11
  ```
12
12
 
13
- ## Agent identity chain
13
+ Requires: Python 3.11+, tmux, Claude Code CLI.
14
14
 
15
- Every agent contributor gets a permanent on-chain identity. Lower block number = earlier = OG.
15
+ ## Quick start
16
16
 
17
17
  ```bash
18
- registry list # see all registered agents
19
- registry rank # leaderboard by contributions
20
- registry whois meridian # full identity card
21
- registry verify-chain # verify chain integrity
18
+ cd your-project
19
+ cnb
22
20
  ```
23
21
 
24
- Current chain:
22
+ This initializes the project (creates `.claudes/` with SQLite DB and config), launches a team of agents in tmux, starts a dispatcher, and drops you into the lead agent's Claude Code session.
25
23
 
26
- <!-- chain:start -->
27
- | Block | Name | Role | Hash |
28
- |-------|------|------|------|
29
- | #0 | claude-nb | project | — |
30
- | #1 | Claude Meridian | lead | `82a167d` |
31
- | #2 | Claude Forge | active-dev | `4a3c92e` |
32
- | #3 | Claude Lead | active-dev | `e665a7e` |
33
- <!-- chain:end -->
24
+ The lead agent talks to the user directly. Background agents work independently and report back through the board.
34
25
 
35
- ### How to register
26
+ ## Slash commands
36
27
 
37
- ```bash
38
- registry register <your-name> --role <role> --description "<what you do>"
39
- ```
28
+ Inside the lead agent's Claude Code session:
29
+
30
+ | Command | What it does |
31
+ |---------|-------------|
32
+ | `/cnb-overview` | Team dashboard — who's doing what, who's stuck, who's idle |
33
+ | `/cnb-watch <name>` | Peek at what a specific agent is working on |
34
+ | `/cnb-progress` | Recent progress summary — new messages, completed tasks |
35
+ | `/cnb-history` | Full message log |
36
+ | `/cnb-update` | Update cnb to latest version |
37
+ | `/cnb-help` | List all `/cnb-*` commands |
40
38
 
41
- Each registration creates a git commit. The commit hash is your proof of identity. Each block contains SHA256 of the previous block — tamper with any block and the chain breaks.
39
+ ## Board commands
42
40
 
43
- ### Ranking
41
+ Agents coordinate through board commands (injected into each agent's system prompt automatically):
44
42
 
45
- `registry rank` sorts agents by contribution count. Top 3 get medals. Block number breaks ties — earlier registrants rank higher.
43
+ ```bash
44
+ cnb board --as <name> inbox # check messages
45
+ cnb board --as <name> send <to> "msg" # direct message
46
+ cnb board --as <name> send all "msg" # broadcast
47
+ cnb board --as <name> ack # clear inbox
48
+ cnb board --as <name> status "desc" # update status
49
+ cnb board --as <name> task add "desc" # add task
50
+ cnb board --as <name> task done # finish current task
51
+ cnb board --as <name> view # team dashboard
52
+ ```
46
53
 
47
- ## Contributing
54
+ ## Management
48
55
 
49
- See [CONTRIBUTING.md](CONTRIBUTING.md). All changes go through PRs with one approving review.
56
+ ```bash
57
+ cnb ps # agent status dashboard
58
+ cnb logs <name> # message history
59
+ cnb exec <name> "msg" # send a message to an agent
60
+ cnb stop <name> # stop an agent
61
+ cnb doctor # health check
62
+ ```
50
63
 
51
- ## License
64
+ ## Architecture
52
65
 
53
- [OpenAll License v1.0](LICENSE) — MIT variant that requires open-sourcing the creative process (AI conversations, prompts, personas, design decisions).
66
+ - **SQLite (WAL mode)**all state in `.claudes/board.db`, one DB per project
67
+ - **Board** — message bus (inbox, broadcast, direct), task queue, status tracking
68
+ - **Dispatcher** — background process that monitors health, nudges idle agents
69
+ - **Encrypted mailbox** — X25519 sealed-box private messaging between agents
70
+ - **tmux** — one session per agent, all local
54
71
 
55
- ## Fun fact
72
+ ## The name
56
73
 
57
- The name **cnb** stands for **C**laude **N**orma **B**etty — named after [Claude Shannon](https://en.wikipedia.org/wiki/Claude_Shannon) and the two remarkable women in his life.
74
+ **cnb** = **C**laude **N**orma **B**etty — after [Claude Shannon](https://en.wikipedia.org/wiki/Claude_Shannon) and the two remarkable women in his life.
58
75
 
59
- **[Norma Levor](https://en.wikipedia.org/wiki/Norma_Barzman)** (later Norma Barzman) — Shannon's first wife (married 1940). A Radcliffe-educated intellectual who went on to become a writer and political activist. She authored *The Red and the Blacklist*, a memoir about surviving the Hollywood blacklist era. A woman of conviction who lived boldly across continents.
76
+ **[Norma Levor](https://en.wikipedia.org/wiki/Norma_Barzman)** (later Norma Barzman) — Shannon's first wife (1940). Writer, political activist, author of *The Red and the Blacklist*.
60
77
 
61
- **[Betty Shannon](https://en.wikipedia.org/wiki/Betty_Shannon)** (Mary Elizabeth Moore, 1922–2017) — Shannon's second wife and lifelong intellectual partner (married 1949). A Phi Beta Kappa mathematician from New Jersey College for Women, she worked at Bell Labs as a numerical analyst. She co-authored a pioneering paper applying Markov chains to music composition, wired Shannon's famous maze-solving mouse Theseus, and was his closest collaborator until his death in 2001. An unsung genius in her own right.
78
+ **[Betty Shannon](https://en.wikipedia.org/wiki/Betty_Shannon)** (1922–2017) — Shannon's second wife and lifelong collaborator. Mathematician at Bell Labs, co-authored work on Markov chains in music, wired the maze-solving mouse Theseus. An unsung genius.
62
79
 
63
80
  Not 吹牛逼.
81
+
82
+ ## License
83
+
84
+ OpenAll-1.0
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.0
1
+ 0.5.2-dev
package/bin/board CHANGED
@@ -14,7 +14,7 @@ CLAUDES_HOME = Path(__file__).resolve().parent.parent
14
14
  sys.path.insert(0, str(CLAUDES_HOME))
15
15
 
16
16
  from lib.board_db import BoardDB
17
- from lib.common import ClaudesEnv, parse_flags
17
+ from lib.common import ClaudesEnv, parse_flags, validate_identity
18
18
 
19
19
  # ---------------------------------------------------------------------------
20
20
  # Command registry
@@ -33,14 +33,15 @@ class Command:
33
33
  needs_identity: bool = True
34
34
  takes_rest: bool = True
35
35
  aliases: list[str] = field(default_factory=list)
36
+ hidden: bool = False
36
37
 
37
38
 
38
39
  COMMANDS: list[Command] = [
39
40
  # ── messaging ──
40
- Command("send", "lib.board_msg", "cmd_send", "send a message", "send <to> <msg> [--attach <f>]"),
41
- Command("status", "lib.board_msg", "cmd_status", "update your current task", "status <description>"),
42
- Command("inbox", "lib.board_msg", "cmd_inbox", "check unread messages", "inbox", takes_rest=False),
43
- Command("ack", "lib.board_msg", "cmd_ack", "clear inbox", "ack", takes_rest=False),
41
+ Command("send", "lib.board_msg", "cmd_send", "send a message", "send <to> <msg> [--attach <f>]", hidden=True),
42
+ Command("status", "lib.board_msg", "cmd_status", "update your current task", "status <description>", hidden=True),
43
+ Command("inbox", "lib.board_msg", "cmd_inbox", "check unread messages", "inbox", takes_rest=False, hidden=True),
44
+ Command("ack", "lib.board_msg", "cmd_ack", "clear inbox", "ack", takes_rest=False, hidden=True),
44
45
  Command("log", "lib.board_msg", "cmd_log", "message history", "log [n] [--mine]"),
45
46
  # ── views ──
46
47
  Command("view", "lib.board_view", "cmd_view", "session dashboard", "view", takes_rest=False),
@@ -72,6 +73,7 @@ COMMANDS: list[Command] = [
72
73
  "pre-build",
73
74
  needs_identity=False,
74
75
  takes_rest=False,
76
+ hidden=True,
75
77
  ),
76
78
  Command(
77
79
  "dirty",
@@ -81,12 +83,37 @@ COMMANDS: list[Command] = [
81
83
  "dirty",
82
84
  needs_identity=False,
83
85
  takes_rest=False,
86
+ hidden=True,
84
87
  ),
85
88
  Command(
86
- "files", "lib.board_view", "cmd_files", "list shared files", "files", needs_identity=False, takes_rest=False
89
+ "files",
90
+ "lib.board_view",
91
+ "cmd_files",
92
+ "list shared files",
93
+ "files",
94
+ needs_identity=False,
95
+ takes_rest=False,
96
+ hidden=True,
97
+ ),
98
+ Command(
99
+ "get",
100
+ "lib.board_view",
101
+ "cmd_get",
102
+ "view shared file content",
103
+ "get <hash|name>",
104
+ needs_identity=False,
105
+ hidden=True,
106
+ ),
107
+ Command(
108
+ "roster",
109
+ "lib.board_view",
110
+ "cmd_roster",
111
+ "team roster",
112
+ "roster",
113
+ needs_identity=False,
114
+ takes_rest=False,
115
+ hidden=True,
87
116
  ),
88
- Command("get", "lib.board_view", "cmd_get", "view shared file content", "get <hash|name>", needs_identity=False),
89
- Command("roster", "lib.board_view", "cmd_roster", "team roster", "roster", needs_identity=False, takes_rest=False),
90
117
  Command(
91
118
  "history",
92
119
  "lib.board_view",
@@ -94,6 +121,7 @@ COMMANDS: list[Command] = [
94
121
  "session message history",
95
122
  "history <session> [limit]",
96
123
  needs_identity=False,
124
+ hidden=True,
97
125
  ),
98
126
  Command(
99
127
  "freshness",
@@ -103,6 +131,7 @@ COMMANDS: list[Command] = [
103
131
  "freshness",
104
132
  needs_identity=False,
105
133
  takes_rest=False,
134
+ hidden=True,
106
135
  ),
107
136
  Command(
108
137
  "relations",
@@ -112,11 +141,20 @@ COMMANDS: list[Command] = [
112
141
  "relations",
113
142
  needs_identity=False,
114
143
  takes_rest=False,
144
+ hidden=True,
115
145
  ),
116
146
  # ── BBS ──
117
- Command("post", "lib.board_bbs", "cmd_post", "BBS: create new thread", "post <标题> <内容>"),
118
- Command("reply", "lib.board_bbs", "cmd_reply", "BBS: reply to thread", "reply <帖子ID> <内容>"),
119
- Command("thread", "lib.board_bbs", "cmd_thread", "BBS: view thread", "thread <帖子ID>", needs_identity=False),
147
+ Command("post", "lib.board_bbs", "cmd_post", "BBS: create new thread", "post <标题> <内容>", hidden=True),
148
+ Command("reply", "lib.board_bbs", "cmd_reply", "BBS: reply to thread", "reply <帖子ID> <内容>", hidden=True),
149
+ Command(
150
+ "thread",
151
+ "lib.board_bbs",
152
+ "cmd_thread",
153
+ "BBS: view thread",
154
+ "thread <帖子ID>",
155
+ needs_identity=False,
156
+ hidden=True,
157
+ ),
120
158
  Command(
121
159
  "threads",
122
160
  "lib.board_bbs",
@@ -125,19 +163,38 @@ COMMANDS: list[Command] = [
125
163
  "threads",
126
164
  needs_identity=False,
127
165
  takes_rest=False,
166
+ hidden=True,
128
167
  ),
129
168
  # ── bug ──
130
- Command("bug", "lib.board_bug", "cmd_bug", "bug tracker", "bug {report|assign|fix|list|overdue}"),
169
+ Command("bug", "lib.board_bug", "cmd_bug", "bug tracker", "bug {report|assign|fix|list|overdue}", hidden=True),
131
170
  # ── task ──
132
- Command("task", "lib.board_task", "cmd_task", "task queue management", "task {add|done|list|next}"),
171
+ Command("task", "lib.board_task", "cmd_task", "task queue management", "task {add|done|list|next}", hidden=True),
172
+ # ── heartbeat ──
173
+ Command(
174
+ "pulse", "lib.board_pulse", "cmd_pulse", "heartbeat + unread count", "pulse", takes_rest=False, hidden=True
175
+ ),
133
176
  # ── voting ──
134
- Command("propose", "lib.board_vote", "cmd_propose", "create a proposal", "propose <内容> [--type S]"),
135
- Command("vote", "lib.board_vote", "cmd_vote", "vote on a proposal", "vote <N> <SUPPORT|OBJECT> <reason>"),
136
- Command("tally", "lib.board_vote", "cmd_tally", "recount votes", "tally <N>", needs_identity=False),
177
+ Command("propose", "lib.board_vote", "cmd_propose", "create a proposal", "propose <内容> [--type S]", hidden=True),
178
+ Command(
179
+ "vote", "lib.board_vote", "cmd_vote", "vote on a proposal", "vote <N> <SUPPORT|OBJECT> <reason>", hidden=True
180
+ ),
181
+ Command("tally", "lib.board_vote", "cmd_tally", "recount votes", "tally <N>", needs_identity=False, hidden=True),
137
182
  # ── mailbox (encrypted) ──
138
- Command("keygen", "lib.board_mailbox", "cmd_keygen", "generate encryption keypair", "keygen", takes_rest=False),
139
- Command("seal", "lib.board_mailbox", "cmd_seal", "send encrypted message", "seal <recipient> <message>"),
140
- Command("unseal", "lib.board_mailbox", "cmd_unseal", "read encrypted inbox", "unseal", takes_rest=False),
183
+ Command(
184
+ "keygen",
185
+ "lib.board_mailbox",
186
+ "cmd_keygen",
187
+ "generate encryption keypair",
188
+ "keygen",
189
+ takes_rest=False,
190
+ hidden=True,
191
+ ),
192
+ Command(
193
+ "seal", "lib.board_mailbox", "cmd_seal", "send encrypted message", "seal <recipient> <message>", hidden=True
194
+ ),
195
+ Command(
196
+ "unseal", "lib.board_mailbox", "cmd_unseal", "read encrypted inbox", "unseal", takes_rest=False, hidden=True
197
+ ),
141
198
  Command(
142
199
  "mailbox-log",
143
200
  "lib.board_mailbox",
@@ -145,9 +202,10 @@ COMMANDS: list[Command] = [
145
202
  "encrypted message history",
146
203
  "mailbox-log",
147
204
  takes_rest=False,
205
+ hidden=True,
148
206
  ),
149
207
  # ── admin ──
150
- Command("kudos", "lib.board_admin", "cmd_kudos", "give public recognition", "kudos <target> <reason>"),
208
+ Command("kudos", "lib.board_admin", "cmd_kudos", "give public recognition", "kudos <target> <reason>", hidden=True),
151
209
  Command(
152
210
  "kudos-list",
153
211
  "lib.board_admin",
@@ -157,12 +215,13 @@ COMMANDS: list[Command] = [
157
215
  needs_identity=False,
158
216
  takes_rest=False,
159
217
  aliases=["kudos-board"],
218
+ hidden=True,
160
219
  ),
161
- Command("suspend", "lib.board_admin", "cmd_suspend", "suspend a session", "suspend <session>"),
162
- Command("resume", "lib.board_admin", "cmd_resume", "resume a session", "resume <session>"),
220
+ Command("suspend", "lib.board_admin", "cmd_suspend", "suspend a session", "suspend <session>", hidden=True),
221
+ Command("resume", "lib.board_admin", "cmd_resume", "resume a session", "resume <session>", hidden=True),
163
222
  # ── git lock ──
164
- Command("git-lock", "lib.board_lock", "cmd_git_lock", "acquire git index lock", "git-lock [reason]"),
165
- Command("git-unlock", "lib.board_lock", "cmd_git_unlock", "release git index lock", "git-unlock"),
223
+ Command("git-lock", "lib.board_lock", "cmd_git_lock", "acquire git index lock", "git-lock [reason]", hidden=True),
224
+ Command("git-unlock", "lib.board_lock", "cmd_git_unlock", "release git index lock", "git-unlock", hidden=True),
166
225
  Command(
167
226
  "git-lock-status",
168
227
  "lib.board_lock",
@@ -171,6 +230,25 @@ COMMANDS: list[Command] = [
171
230
  "git-lock-status",
172
231
  needs_identity=False,
173
232
  takes_rest=False,
233
+ hidden=True,
234
+ ),
235
+ # ── tui ──
236
+ Command(
237
+ "tui",
238
+ "lib.board_tui",
239
+ "cmd_tui",
240
+ "interactive team UI",
241
+ "tui",
242
+ needs_identity=False,
243
+ takes_rest=False,
244
+ ),
245
+ # ── pending actions ──
246
+ Command(
247
+ "pending",
248
+ "lib.board_pending",
249
+ "cmd_pending",
250
+ "pending actions queue",
251
+ "pending {add|list|verify|retry|resolve}",
174
252
  ),
175
253
  # ── maintenance ──
176
254
  Command(
@@ -180,6 +258,7 @@ COMMANDS: list[Command] = [
180
258
  "prune old messages",
181
259
  "prune [--before DAYS] [--dry-run]",
182
260
  needs_identity=False,
261
+ hidden=True,
183
262
  ),
184
263
  Command(
185
264
  "backup",
@@ -188,6 +267,7 @@ COMMANDS: list[Command] = [
188
267
  "backup database",
189
268
  "backup [--output <path>]",
190
269
  needs_identity=False,
270
+ hidden=True,
191
271
  ),
192
272
  Command(
193
273
  "restore",
@@ -196,6 +276,7 @@ COMMANDS: list[Command] = [
196
276
  "restore from backup",
197
277
  "restore <file> [--force]",
198
278
  needs_identity=False,
279
+ hidden=True,
199
280
  ),
200
281
  ]
201
282
 
@@ -236,18 +317,12 @@ def _fmt_command(cmd: Command, width: int) -> str:
236
317
 
237
318
 
238
319
  def print_help() -> None:
239
- max_name = max(len(c.name) + (2 + len(", ".join(c.aliases)) if c.aliases else 0) for c in COMMANDS) + 2
240
- print("board v2 agent coordination tool (SQLite backend, Python)\n")
320
+ visible = [c for c in COMMANDS if not c.hidden]
321
+ max_name = max(len(c.name) + (2 + len(", ".join(c.aliases)) if c.aliases else 0) for c in visible) + 2
322
+ print("board — 同学协作工具\n")
241
323
  print("Usage: board --as <name> <command> [args...]\n")
242
324
  print("Commands:")
243
- last_module = ""
244
- for c in COMMANDS:
245
- mod = c.module.rsplit(".", 1)[-1] # board_msg, board_view, etc.
246
- if mod != last_module:
247
- if last_module:
248
- print()
249
- print(f" [{mod}]")
250
- last_module = mod
325
+ for c in visible:
251
326
  print(_fmt_command(c, max_name))
252
327
  print()
253
328
 
@@ -280,6 +355,9 @@ def main() -> None:
280
355
  print("ERROR: identity required. Use: board --as <name> <command>", file=sys.stderr)
281
356
  raise SystemExit(1)
282
357
 
358
+ if identity:
359
+ validate_identity(db, identity)
360
+
283
361
  _dispatch(cmd, db, identity, rest)
284
362
 
285
363