system-diff 0.4.2__tar.gz → 0.4.3__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.
- {system_diff-0.4.2 → system_diff-0.4.3}/PKG-INFO +5 -3
- {system_diff-0.4.2 → system_diff-0.4.3}/README.md +4 -2
- {system_diff-0.4.2 → system_diff-0.4.3}/docs/diagrams/system.json +20 -11
- {system_diff-0.4.2 → system_diff-0.4.3}/docs/diagrams/system.svg +3 -1
- {system_diff-0.4.2 → system_diff-0.4.3}/mods/system-diff/.claude-plugin/plugin.json +1 -1
- {system_diff-0.4.2 → system_diff-0.4.3}/mods/system-diff/hooks/register.tsx +1 -1
- {system_diff-0.4.2 → system_diff-0.4.3}/pyproject.toml +1 -1
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/__init__.py +1 -1
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/agent.py +20 -14
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/agents.py +20 -3
- {system_diff-0.4.2 → system_diff-0.4.3}/tests/test_design_and_pane.py +33 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/tests/test_terminal_and_learning.py +18 -6
- {system_diff-0.4.2 → system_diff-0.4.3}/.claude-plugin/marketplace.json +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/.gitattributes +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/.github/workflows/ci.yml +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/.github/workflows/release.yml +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/.github/workflows/system-diff.yml +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/.gitignore +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/LICENSE +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/mods/system-diff/hooks/hooks.json +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/mods/system-diff/hooks/page.tsx +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/mods/system-diff/hooks/pane.test.tsx +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/mods/system-diff/types/index.d.ts +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/__main__.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/activity.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/architecture.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/canvas.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/cli.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/companion.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/compare.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/config.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/design.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/diff.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/extract.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/graph.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/inside.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/learn.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/mcp_server.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/narrate.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/pane.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/panel.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/pictures.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/png.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/profile.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/readme.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/sources.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/statusline.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/textmap.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/topics.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/tui.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/src/system_diff/watch.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/system-diff.toml +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/tests/conftest.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/tests/test_capture.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/tests/test_checkpoint_watch_mcp.py +0 -0
- {system_diff-0.4.2 → system_diff-0.4.3}/tests/test_system_diff.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: system-diff
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Draws your system from its code (parts, entry points, data, outside systems) and what each change adds, changes or removes. No dependencies; SVG pictures for PRs and READMEs.
|
|
5
5
|
Project-URL: Homepage, https://github.com/simplifiedmachine/system-diff
|
|
6
6
|
Project-URL: Documentation, https://github.com/simplifiedmachine/system-diff/wiki
|
|
@@ -132,8 +132,8 @@ Claude does most work alone unless asked otherwise, so system-diff asks it to pl
|
|
|
132
132
|
- **The bus:** an async publish/subscribe channel whose topics are the blocks. Each agent follows the blocks its own
|
|
133
133
|
depend on. Every change an agent makes to a block's structure is published on that block's topic as it happens
|
|
134
134
|
("API no longer answers DELETE /orders/{id}"), with why it matters. Agents publish their own notes with `publish`
|
|
135
|
-
(to a block, to "all", or to "agent:<name>"). Each agent's unread messages reach it with its next tool result
|
|
136
|
-
nobody waits on anybody.
|
|
135
|
+
(to a block, to "all", or to "agent:<name>"). Each agent's unread messages reach it with its next tool result
|
|
136
|
+
of any kind (a read, an edit, a shell command, a search), once: nobody waits on anybody.
|
|
137
137
|
- **Claims:** `claim` gives an agent its blocks. An edit to someone else's block is published to its owner and shown
|
|
138
138
|
under Needs your attention.
|
|
139
139
|
- **One agent, one area:** when an agent (the main conversation included) edits blocks in two or more areas in one
|
|
@@ -146,6 +146,8 @@ The MCP tools: `plan_agents`, `claim`, `publish`, `subscribe`, `inbox`, `team`.
|
|
|
146
146
|
|
|
147
147
|
- **After an edit that changes the system's structure**, one line under it: "System diff: API now answers GET
|
|
148
148
|
/health · a new way in: it needs the same checks as the others". Edits that change no structure stay quiet.
|
|
149
|
+
- **Messages from other agents** on the bus arrive with an agent's next tool result of any kind, not only after
|
|
150
|
+
an edit.
|
|
149
151
|
- **At the end of each turn**, one line on what the turn did to the system, or which parts it edited when the
|
|
150
152
|
structure stayed the same; nothing for a turn that only talked.
|
|
151
153
|
- **Ask Claude** "what is the API part?" or "what does this change do?": the MCP server gives it `explain` (the facts
|
|
@@ -100,8 +100,8 @@ Claude does most work alone unless asked otherwise, so system-diff asks it to pl
|
|
|
100
100
|
- **The bus:** an async publish/subscribe channel whose topics are the blocks. Each agent follows the blocks its own
|
|
101
101
|
depend on. Every change an agent makes to a block's structure is published on that block's topic as it happens
|
|
102
102
|
("API no longer answers DELETE /orders/{id}"), with why it matters. Agents publish their own notes with `publish`
|
|
103
|
-
(to a block, to "all", or to "agent:<name>"). Each agent's unread messages reach it with its next tool result
|
|
104
|
-
nobody waits on anybody.
|
|
103
|
+
(to a block, to "all", or to "agent:<name>"). Each agent's unread messages reach it with its next tool result
|
|
104
|
+
of any kind (a read, an edit, a shell command, a search), once: nobody waits on anybody.
|
|
105
105
|
- **Claims:** `claim` gives an agent its blocks. An edit to someone else's block is published to its owner and shown
|
|
106
106
|
under Needs your attention.
|
|
107
107
|
- **One agent, one area:** when an agent (the main conversation included) edits blocks in two or more areas in one
|
|
@@ -114,6 +114,8 @@ The MCP tools: `plan_agents`, `claim`, `publish`, `subscribe`, `inbox`, `team`.
|
|
|
114
114
|
|
|
115
115
|
- **After an edit that changes the system's structure**, one line under it: "System diff: API now answers GET
|
|
116
116
|
/health · a new way in: it needs the same checks as the others". Edits that change no structure stay quiet.
|
|
117
|
+
- **Messages from other agents** on the bus arrive with an agent's next tool result of any kind, not only after
|
|
118
|
+
an edit.
|
|
117
119
|
- **At the end of each turn**, one line on what the turn did to the system, or which parts it edited when the
|
|
118
120
|
structure stayed the same; nothing for a turn that only talked.
|
|
119
121
|
- **Ask Claude** "what is the API part?" or "what does this change do?": the MCP server gives it `explain` (the facts
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"container": "",
|
|
10
10
|
"declared": true,
|
|
11
11
|
"files": 3,
|
|
12
|
-
"lines":
|
|
12
|
+
"lines": 314,
|
|
13
13
|
"hub": false,
|
|
14
14
|
"quiet": false
|
|
15
15
|
},
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"container": "",
|
|
34
34
|
"declared": true,
|
|
35
35
|
"files": 1,
|
|
36
|
-
"lines":
|
|
36
|
+
"lines": 462,
|
|
37
37
|
"hub": false,
|
|
38
38
|
"quiet": false
|
|
39
39
|
},
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"container": "",
|
|
46
46
|
"declared": true,
|
|
47
47
|
"files": 1,
|
|
48
|
-
"lines":
|
|
48
|
+
"lines": 368,
|
|
49
49
|
"hub": false,
|
|
50
50
|
"quiet": false
|
|
51
51
|
},
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"container": "",
|
|
70
70
|
"declared": true,
|
|
71
71
|
"files": 1,
|
|
72
|
-
"lines":
|
|
72
|
+
"lines": 531,
|
|
73
73
|
"hub": false,
|
|
74
74
|
"quiet": false
|
|
75
75
|
},
|
|
@@ -201,7 +201,7 @@
|
|
|
201
201
|
"container": "",
|
|
202
202
|
"declared": true,
|
|
203
203
|
"files": 2,
|
|
204
|
-
"lines":
|
|
204
|
+
"lines": 745,
|
|
205
205
|
"hub": false,
|
|
206
206
|
"quiet": false
|
|
207
207
|
},
|
|
@@ -317,8 +317,10 @@
|
|
|
317
317
|
"agent>learn": 1,
|
|
318
318
|
"agent>sources": 1,
|
|
319
319
|
"agents>activity": 1,
|
|
320
|
-
"agents>architecture":
|
|
320
|
+
"agents>architecture": 2,
|
|
321
|
+
"agents>config": 1,
|
|
321
322
|
"agents>design": 1,
|
|
323
|
+
"agents>extract": 1,
|
|
322
324
|
"agents>sources": 1,
|
|
323
325
|
"architecture>design": 2,
|
|
324
326
|
"architecture>diff": 1,
|
|
@@ -433,7 +435,8 @@
|
|
|
433
435
|
],
|
|
434
436
|
"agent>agents": [
|
|
435
437
|
"ORCHESTRATE",
|
|
436
|
-
"after_edit"
|
|
438
|
+
"after_edit",
|
|
439
|
+
"deliver"
|
|
437
440
|
],
|
|
438
441
|
"agent>companion": [
|
|
439
442
|
"add_vscode_task",
|
|
@@ -466,22 +469,28 @@
|
|
|
466
469
|
],
|
|
467
470
|
"agents>activity": [
|
|
468
471
|
"EDIT",
|
|
469
|
-
"_replace",
|
|
470
472
|
"PROMPT",
|
|
471
|
-
"
|
|
473
|
+
"_replace",
|
|
472
474
|
"is_system",
|
|
473
|
-
"part_of"
|
|
475
|
+
"part_of",
|
|
476
|
+
"READ"
|
|
474
477
|
],
|
|
475
478
|
"agents>architecture": [
|
|
476
479
|
"groups_of"
|
|
477
480
|
],
|
|
481
|
+
"agents>config": [
|
|
482
|
+
"lenient"
|
|
483
|
+
],
|
|
478
484
|
"agents>design": [
|
|
479
485
|
"depends",
|
|
486
|
+
"kinds",
|
|
480
487
|
"and_list",
|
|
481
488
|
"fan_in",
|
|
482
|
-
"kinds",
|
|
483
489
|
"summarize"
|
|
484
490
|
],
|
|
491
|
+
"agents>extract": [
|
|
492
|
+
"Code"
|
|
493
|
+
],
|
|
485
494
|
"agents>sources": [
|
|
486
495
|
"git_dir"
|
|
487
496
|
],
|
|
@@ -64,9 +64,11 @@ text { font-family: 'Segoe UI', FreeSans, 'Helvetica Neue', Arial, sans-serif; }
|
|
|
64
64
|
<path class="be k-call s-same" d="M214,172 C243,172 243,188 272,188" marker-end="url(#ar-call-same)"><title>agent → extract</title></path>
|
|
65
65
|
<path class="be k-call s-same" d="M214,172 C243,172 243,788 272,788" marker-end="url(#ar-call-same)"><title>agent → learn</title></path>
|
|
66
66
|
<path class="be k-call s-same" d="M214,172 C246,172 246,99 278,99 L478,99 C507,99 507,52 536,52" marker-end="url(#ar-call-same)"><title>agent → sources</title></path>
|
|
67
|
-
<path class="be k-call s-same" d="M478,488 C507,488 507,112 536,112" marker-end="url(#ar-call-same)"><title>agents → activity</title></path>
|
|
68
67
|
<path class="be k-call s-same" d="M478,488 C514,488 514,428 484,428" marker-end="url(#ar-call-same)"><title>agents → architecture</title></path>
|
|
68
|
+
<path class="be k-call s-same" d="M478,488 C507,488 507,112 536,112" marker-end="url(#ar-call-same)"><title>agents → activity</title></path>
|
|
69
|
+
<path class="be k-call s-same" d="M478,488 C544,488 544,128 484,128" marker-end="url(#ar-call-same)"><title>agents → config</title></path>
|
|
69
70
|
<path class="be k-call s-same" d="M478,488 C514,488 514,548 484,548" marker-end="url(#ar-call-same)"><title>agents → design</title></path>
|
|
71
|
+
<path class="be k-call s-same" d="M478,488 C544,488 544,188 484,188" marker-end="url(#ar-call-same)"><title>agents → extract</title></path>
|
|
70
72
|
<path class="be k-call s-same" d="M478,488 C507,488 507,52 536,52" marker-end="url(#ar-call-same)"><title>agents → sources</title></path>
|
|
71
73
|
<path class="be k-call s-same" d="M478,428 C524,428 524,548 484,548" marker-end="url(#ar-call-same)"><title>architecture → design</title></path>
|
|
72
74
|
<path class="be k-call s-same" d="M478,428 C524,428 524,308 484,308" marker-end="url(#ar-call-same)"><title>architecture → graph</title></path>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "system-diff",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "A pane beside the chat: what Claude is working on in your system, zoomed in, what just happened and why it matters, and a topic to learn (system-diff)",
|
|
5
5
|
"types": "./types/index.d.ts",
|
|
6
6
|
"author": {
|
|
@@ -14,7 +14,7 @@ import type { Act, Page, Row } from '../types'
|
|
|
14
14
|
// green what is new, done or to learn, white what to read, gray the background, red only what was taken away.
|
|
15
15
|
|
|
16
16
|
const PANE = 'system-diff'
|
|
17
|
-
const VERSION = '0.4.
|
|
17
|
+
const VERSION = '0.4.3' // the plugin's own, as .claude-plugin/plugin.json says (a test keeps them one)
|
|
18
18
|
const WIDEST = 999 // as wide as Claude Code docks a pane: 45% of the terminal
|
|
19
19
|
const COMMANDS = ['system-diff', 'system-diff-pane'] as const
|
|
20
20
|
const VIEWS = [['now', 'Now', 'n'], ['map', 'Map', 'm'], ['agents', 'Agents', 'a'], ['changes', 'Changes', 'c'],
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "system-diff"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.3"
|
|
8
8
|
description = "Draws your system from its code (parts, entry points, data, outside systems) and what each change adds, changes or removes. No dependencies; SVG pictures for PRs and READMEs."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -137,30 +137,36 @@ def on_prompt(root: Path, payload: Dict[str, Any]) -> Optional[Dict[str, str]]:
|
|
|
137
137
|
|
|
138
138
|
|
|
139
139
|
def on_tool(root: Path, payload: Dict[str, Any]) -> Optional[Dict[str, str]]:
|
|
140
|
-
"""
|
|
141
|
-
|
|
140
|
+
"""After every tool: hand the agent what other agents published since its last tool. For a read or an edit of
|
|
141
|
+
a file in a part, also note the file and keep a brief of its part for the status line; after an edit to code,
|
|
142
|
+
look at what the turn has done to the system so far, and say what this edit added to it (one hook at a time:
|
|
143
|
+
they can overlap)."""
|
|
142
144
|
tool = payload.get("tool_name", "")
|
|
145
|
+
agent_id = payload.get("agent_id") or ""
|
|
146
|
+
|
|
147
|
+
def unread() -> Dict[str, Any]:
|
|
148
|
+
return {"message": None, "context": agents.deliver(root, agent_id)}
|
|
149
|
+
|
|
143
150
|
if tool not in EDIT_TOOLS + READ_TOOLS:
|
|
144
|
-
return
|
|
151
|
+
return unread()
|
|
145
152
|
given = payload.get("tool_input") or {}
|
|
146
153
|
path = activity.relative(root, given.get("file_path") or given.get("notebook_path") or "")
|
|
147
154
|
if not path:
|
|
148
|
-
return
|
|
155
|
+
return unread()
|
|
149
156
|
edit = tool in EDIT_TOOLS
|
|
150
|
-
agent_id = payload.get("agent_id") or ""
|
|
151
157
|
activity.record(root, {"event": activity.EDIT if edit else activity.READ, "path": path, "tool": tool,
|
|
152
158
|
"session": payload.get("session_id", ""), "agent": agent_id})
|
|
153
159
|
cfg = config.lenient(root)
|
|
154
160
|
part = activity.part_of(path, Code(cfg))
|
|
155
161
|
if part is None:
|
|
156
|
-
return
|
|
162
|
+
return unread()
|
|
157
163
|
if not activity.load_brief(root, part, max_age=600):
|
|
158
164
|
activity.save_brief(root, learn.brief(root, cfg, part))
|
|
159
165
|
if not (edit and turn_sha(root)):
|
|
160
|
-
return
|
|
166
|
+
return unread()
|
|
161
167
|
with activity.only_one(root, wait=10) as mine:
|
|
162
168
|
if not mine:
|
|
163
|
-
return
|
|
169
|
+
return unread()
|
|
164
170
|
last = activity.read_state(root).get("turn") or {}
|
|
165
171
|
before = last.get("told") or [s["text"] for s in last.get("story") or []]
|
|
166
172
|
since, turn = compare_worktree(root, ["checkpoint", "turn"])
|
|
@@ -246,12 +252,12 @@ HOOKS = ( # event, matcher, how it runs, what it does
|
|
|
246
252
|
"WezTerm, kitty, Windows Terminal)"),
|
|
247
253
|
("UserPromptSubmit", None, {"timeout": 10},
|
|
248
254
|
"marks where each turn starts, and asks Claude to plan the work for a team of agents"),
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
#
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
"
|
|
255
|
+
# One entry for every tool, and not in the background: only a hook Claude Code waits for can show you a line in
|
|
256
|
+
# the conversation or hand the agent its messages. A timeout (seconds) so a stuck git never holds up the agent;
|
|
257
|
+
# Claude Code's own default is 600.
|
|
258
|
+
("PostToolUse", None, {"timeout": 15},
|
|
259
|
+
"notes the files Claude reads and, after an edit that changes the system's structure, one line on what it "
|
|
260
|
+
"changed; and hands each agent what other agents published"),
|
|
255
261
|
("Stop", None, {"timeout": 30}, "after each turn, one line on what the turn changed in the system"),
|
|
256
262
|
)
|
|
257
263
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
CLAIMS, so agents keep to their own blocks; and a BUS, an async publish/subscribe channel whose topics are the system's
|
|
3
3
|
blocks: an agent subscribes to the blocks its own depend on, every structural change to a block is published on that
|
|
4
4
|
block's topic (by the hooks, as it happens), agents publish their own notes too, and each agent's unread messages
|
|
5
|
-
reach it with its next tool
|
|
5
|
+
reach it with its next tool result of any kind, without waiting on anyone.
|
|
6
6
|
|
|
7
7
|
Who is who: Claude Code names each subagent by an id (`agent_id` in its hook events; the main conversation has none,
|
|
8
8
|
"main" here). A plan names agents by area ("billing"); an id is tied to a name when it first edits a block that name
|
|
@@ -65,6 +65,10 @@ def tie(root: Path, agent_id: str, name: str) -> None:
|
|
|
65
65
|
data = _load(root)
|
|
66
66
|
if data["ids"].get(agent_id) != name:
|
|
67
67
|
data["ids"][agent_id] = name
|
|
68
|
+
short = data["agents"].get("agent-" + agent_id[:6]) # what it read before it had a name stays read
|
|
69
|
+
if short:
|
|
70
|
+
a = _agent(data, name)
|
|
71
|
+
a["read"] = max(a["read"], short["read"])
|
|
68
72
|
_save(root, data)
|
|
69
73
|
|
|
70
74
|
|
|
@@ -163,6 +167,20 @@ def as_context(msgs: List[Dict[str, Any]], most: int = 8) -> str:
|
|
|
163
167
|
|
|
164
168
|
# ── what the hooks do for each agent ─────────────────────────────────────────
|
|
165
169
|
|
|
170
|
+
def _for_context(msgs: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
171
|
+
"""The messages worth putting in an agent's context: spread warnings reach the agent they are about directly; on
|
|
172
|
+
the bus they are a record for the inbox tool."""
|
|
173
|
+
return [m for m in msgs if not (m.get("from") == SYSTEM and m.get("kind") == "warning")]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def deliver(root: Path, agent_id: str) -> str:
|
|
177
|
+
"""The unread messages for the agent with this Claude Code id ("" or "main": the main conversation), as context
|
|
178
|
+
text, marked read; "" without writing anything when there is no bus yet."""
|
|
179
|
+
if not (git_dir(root) / BUS).exists():
|
|
180
|
+
return ""
|
|
181
|
+
return as_context(_for_context(inbox(root, name_of(root, agent_id))))
|
|
182
|
+
|
|
183
|
+
|
|
166
184
|
def after_edit(root: Path, agent_id: str, part: Optional[str], notes: List[Dict[str, Any]], bp: dict) -> Dict[str, str]:
|
|
167
185
|
"""After an agent's edit: tie it to a plan name if it edited a claimed block nobody else is tied to, subscribe it
|
|
168
186
|
to its block and the blocks that block depends on, publish what the edit changed in the structure on the topics
|
|
@@ -186,8 +204,7 @@ def after_edit(root: Path, agent_id: str, part: Optional[str], notes: List[Dict[
|
|
|
186
204
|
publish(root, f"agent:{claimed}", f"{name} edited your block {part}", sender=name, kind="warning")
|
|
187
205
|
from . import config
|
|
188
206
|
wide = spread(root, name, agent_id, bp, config.lenient(root))
|
|
189
|
-
|
|
190
|
-
unread = [m for m in inbox(root, name) if not (m.get("from") == SYSTEM and m.get("kind") == "warning")]
|
|
207
|
+
unread = _for_context(inbox(root, name))
|
|
191
208
|
context = "\n".join(t for t in (as_context(unread), wide) if t)
|
|
192
209
|
warn = "; ".join(t for t in (warn, wide) if t)
|
|
193
210
|
return {"context": context, "warn": warn, "name": name}
|
|
@@ -270,6 +270,31 @@ def test_the_agents_tab_shows_each_agent_and_warns_when_two_edit_one_block(repo,
|
|
|
270
270
|
assert "● API @api @st" in "\n".join(text) # who works where, on the diagram
|
|
271
271
|
|
|
272
272
|
|
|
273
|
+
def test_messages_reach_an_agent_with_its_next_tool_result_of_any_kind_once(repo):
|
|
274
|
+
from system_diff import agent, agents
|
|
275
|
+
from system_diff.sources import git_dir
|
|
276
|
+
|
|
277
|
+
def tool(name, **given):
|
|
278
|
+
said: list = []
|
|
279
|
+
agent.run_hook(json.dumps({"cwd": str(repo), "hook_event_name": "PostToolUse", "tool_name": name,
|
|
280
|
+
"agent_id": "a1", "tool_input": given}), emit=said.append)
|
|
281
|
+
return [json.loads(s) for s in said]
|
|
282
|
+
|
|
283
|
+
bus = git_dir(repo) / agents.BUS
|
|
284
|
+
assert not bus.exists() and tool("Bash", command="ls") == [] # no bus: nothing said, nothing written
|
|
285
|
+
assert not bus.exists()
|
|
286
|
+
agents.tie(repo, "a1", "api")
|
|
287
|
+
agents.publish(repo, "agent:api", "please keep GET /api/orders", sender="web")
|
|
288
|
+
out = tool("Bash", command="ls") # not a file tool: it still gets it
|
|
289
|
+
assert "web on agent:api: please keep GET /api/orders" in out[0]["hookSpecificOutput"]["additionalContext"]
|
|
290
|
+
assert out[0]["hookSpecificOutput"]["hookEventName"] == "PostToolUse"
|
|
291
|
+
assert tool("Bash", command="ls") == [] # once only
|
|
292
|
+
agents.publish(repo, "agent:api", "store moved to app/db", sender="store")
|
|
293
|
+
out = tool("Read", file_path=str(repo / "app/core/rules.py"))
|
|
294
|
+
assert "store on agent:api: store moved to app/db" in out[0]["hookSpecificOutput"]["additionalContext"]
|
|
295
|
+
assert tool("Read", file_path=str(repo / "app/core/rules.py")) == []
|
|
296
|
+
|
|
297
|
+
|
|
273
298
|
def test_one_agent_editing_two_areas_in_a_turn_is_told_once_to_plan_a_team(repo, files):
|
|
274
299
|
from system_diff import agent
|
|
275
300
|
|
|
@@ -316,3 +341,11 @@ def test_a_narrow_pane_gets_compact_diagrams_unless_asked_otherwise(repo, capsys
|
|
|
316
341
|
def _run(capsys, repo, *args):
|
|
317
342
|
assert main(["--repo", str(repo), "panel", "--protocol", "2", *args]) == 0
|
|
318
343
|
return capsys.readouterr().out
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def test_what_an_agent_read_before_it_had_a_name_stays_read(repo):
|
|
347
|
+
from system_diff import agents
|
|
348
|
+
agents.publish(repo, "all", "hello", sender="main")
|
|
349
|
+
assert len(agents.inbox(repo, "agent-a1b2c3")) == 1
|
|
350
|
+
agents.tie(repo, "a1b2c3d4", "api")
|
|
351
|
+
assert agents.inbox(repo, "api") == []
|
|
@@ -332,10 +332,8 @@ def test_setup_wires_three_hooks_and_a_status_line_and_leaves_someone_elses_alon
|
|
|
332
332
|
agent.setup_claude(repo, "local", out=said.append)
|
|
333
333
|
path = repo / ".claude" / "settings.local.json"
|
|
334
334
|
data = json.loads(path.read_text())
|
|
335
|
-
assert data["hooks"]["PostToolUse"] == [ #
|
|
336
|
-
{"
|
|
337
|
-
{"matcher": "Edit|Write|MultiEdit|NotebookEdit", "hooks": [
|
|
338
|
-
{"type": "command", "command": "system-diff hook", "timeout": 15}]}]
|
|
335
|
+
assert data["hooks"]["PostToolUse"] == [ # every tool, waited for: it can speak and hand over messages
|
|
336
|
+
{"hooks": [{"type": "command", "command": "system-diff hook", "timeout": 15}]}]
|
|
339
337
|
assert data["hooks"]["UserPromptSubmit"][0]["hooks"][0]["timeout"] == 10 # waited for: it tells Claude
|
|
340
338
|
assert data["statusLine"] == {"type": "command", "command": "system-diff statusline"}
|
|
341
339
|
agent.setup_claude(repo, "local", undo=True, out=said.append)
|
|
@@ -347,11 +345,25 @@ def test_setup_wires_three_hooks_and_a_status_line_and_leaves_someone_elses_alon
|
|
|
347
345
|
said.clear()
|
|
348
346
|
agent.setup_claude(repo, "local", out=said.append)
|
|
349
347
|
post = json.loads(path.read_text())["hooks"]["PostToolUse"]
|
|
350
|
-
assert post
|
|
351
|
-
|
|
348
|
+
assert post == [{"hooks": [{"command": "mine"}]},
|
|
349
|
+
{"hooks": [{"type": "command", "command": "system-diff hook", "timeout": 15}]}]
|
|
352
350
|
assert any(s.startswith("updated the PostToolUse hook") for s in said)
|
|
353
351
|
agent.setup_claude(repo, "local", undo=True, out=said.append)
|
|
354
352
|
|
|
353
|
+
two = {"hooks": {"PostToolUse": [ # the two entries the setup before this one wrote
|
|
354
|
+
{"matcher": "Read", "hooks": [{"type": "command", "command": "system-diff hook", "async": True}]},
|
|
355
|
+
{"matcher": "Edit|Write|MultiEdit|NotebookEdit", "hooks": [
|
|
356
|
+
{"type": "command", "command": "system-diff hook", "timeout": 15}]}]}}
|
|
357
|
+
path.write_text(json.dumps(two))
|
|
358
|
+
said.clear()
|
|
359
|
+
agent.setup_claude(repo, "local", out=said.append)
|
|
360
|
+
assert json.loads(path.read_text())["hooks"]["PostToolUse"] == [
|
|
361
|
+
{"hooks": [{"type": "command", "command": "system-diff hook", "timeout": 15}]}] # one, not three
|
|
362
|
+
assert sum(s.startswith("updated the PostToolUse hook") for s in said) == 1
|
|
363
|
+
agent.setup_claude(repo, "local", out=said.append)
|
|
364
|
+
assert len(json.loads(path.read_text())["hooks"]["PostToolUse"]) == 1 # and it stays one
|
|
365
|
+
agent.setup_claude(repo, "local", undo=True, out=said.append)
|
|
366
|
+
|
|
355
367
|
user = home / ".claude" / "settings.json"
|
|
356
368
|
user.parent.mkdir(parents=True)
|
|
357
369
|
user.write_text(json.dumps({"statusLine": {"type": "command", "command": "my-line"}}))
|
|
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
|
|
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
|