fusion-cli 1.2.0__tar.gz → 1.2.1__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.
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/PKG-INFO +2 -2
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/README.md +1 -1
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/pyproject.toml +1 -1
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/src/fusion/cli.py +4 -2
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/src/fusion/setup.py +67 -3
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/tests/test_setup_agents.py +119 -26
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/uv.lock +1 -1
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/.gitignore +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/hatch_build.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-analyst/SKILL.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-analyst/references/assess.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-analyst/references/compare.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-analyst/references/export.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-analyst/references/fusion-conventions.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-analyst/references/report.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-analyst/scripts/export.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-intake/SKILL.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-intake/references/convert.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-intake/references/delivery.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-intake/references/fusion-conventions.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-intake/references/gate.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-intake/scripts/convert.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-intake/scripts/gate.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-intake/tests/conftest.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-intake/tests/make_fixtures.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-intake/tests/test_convert.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-intake/tests/test_gate.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-intake/tests/test_integration.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-librarian/SKILL.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-librarian/references/archive.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-librarian/references/create.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-librarian/references/cross-reference.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-librarian/references/fusion-conventions.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-librarian/references/promote.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-librarian/references/query.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-librarian/references/reflect.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-librarian/references/restructure.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-librarian/references/tag.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-librarian/scripts/link-repair.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-librarian/tests/conftest.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-librarian/tests/test_link_repair.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-planner/SKILL.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-planner/references/close.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-planner/references/create-activity.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-planner/references/fusion-conventions.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-planner/references/horizon.md +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/src/fusion/__init__.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/src/fusion/bucket.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/src/fusion/checker.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/src/fusion/document.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/src/fusion/hub.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/src/fusion/indexer.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/src/fusion/ledger.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/src/fusion/manifest.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/src/fusion/scaffold.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/src/fusion/views.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/tests/__init__.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/tests/conftest.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/tests/test_build_payload.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/tests/test_checker_errors.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/tests/test_checker_warnings.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/tests/test_cli.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/tests/test_cli_encoding.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/tests/test_cli_setup.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/tests/test_cli_version.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/tests/test_document.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/tests/test_hub_bucket.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/tests/test_indexer.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/tests/test_ledger.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/tests/test_line_endings.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/tests/test_roundtrip.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/tests/test_scaffold.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/tests/test_setup_core.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/tests/test_skill_family.py +0 -0
- {fusion_cli-1.2.0 → fusion_cli-1.2.1}/tests/test_views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fusion-cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: The Fusion reference CLI — the notary of the Fusion Convention.
|
|
5
5
|
Author: Bluewaves Boutique
|
|
6
6
|
License-Expression: MIT
|
|
@@ -50,7 +50,7 @@ fusion --version
|
|
|
50
50
|
| `fusion status [path]` | One bucket at a glance |
|
|
51
51
|
| `fusion today` | The composed day, across every bucket in the hub |
|
|
52
52
|
| `fusion agenda` | The wider horizon — dated and active, across the hub |
|
|
53
|
-
| `fusion setup` | Install the skills
|
|
53
|
+
| `fusion setup` | Install the skills to `~/.agents/skills` and serve every detected agent (links only where needed) — the installer's brain. `--remove` undoes it |
|
|
54
54
|
|
|
55
55
|
Every command takes `--json` (agents parse, never scrape). `log`, `new`,
|
|
56
56
|
and `index` take `--as <actor>`; the pen defaults to `FUSION_ACTOR`, then
|
|
@@ -40,7 +40,7 @@ fusion --version
|
|
|
40
40
|
| `fusion status [path]` | One bucket at a glance |
|
|
41
41
|
| `fusion today` | The composed day, across every bucket in the hub |
|
|
42
42
|
| `fusion agenda` | The wider horizon — dated and active, across the hub |
|
|
43
|
-
| `fusion setup` | Install the skills
|
|
43
|
+
| `fusion setup` | Install the skills to `~/.agents/skills` and serve every detected agent (links only where needed) — the installer's brain. `--remove` undoes it |
|
|
44
44
|
|
|
45
45
|
Every command takes `--json` (agents parse, never scrape). `log`, `new`,
|
|
46
46
|
and `index` take `--as <actor>`; the pen defaults to `FUSION_ACTOR`, then
|
|
@@ -404,8 +404,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
404
404
|
|
|
405
405
|
p = sub.add_parser("setup", help="install skills into agents; --remove undoes it",
|
|
406
406
|
description="Install the bundled skills to the canonical "
|
|
407
|
-
"skills directory and
|
|
408
|
-
"
|
|
407
|
+
"skills directory (~/.agents/skills) and make them available "
|
|
408
|
+
"to every detected agent — most read that directory "
|
|
409
|
+
"natively; agents that do not (Claude Code) get symlinks. "
|
|
410
|
+
"--remove undoes exactly what setup can prove it created.")
|
|
409
411
|
p.add_argument("--force", action="store_true",
|
|
410
412
|
help="replace foreign entries setup would otherwise leave")
|
|
411
413
|
p.add_argument("--remove", action="store_true",
|
|
@@ -99,15 +99,20 @@ def install_canonical(payload: Path, skills_dir: Path,
|
|
|
99
99
|
# ~/.agents/skills dir, so each fusion-* skill gets a symlink (or copy)
|
|
100
100
|
# in its own skills dir. mode "standard": the agent reads the standard
|
|
101
101
|
# dir — creating links there too would load every skill twice.
|
|
102
|
+
# legacy_subdir: a dir an earlier fusion-cli release linked into before
|
|
103
|
+
# the agent's native standard-dir support was verified; setup sweeps
|
|
104
|
+
# OUR leftovers out of it (attribution-checked, like everything else).
|
|
102
105
|
AGENTS = [
|
|
103
106
|
{"name": "Claude Code", "marker": ".claude",
|
|
104
107
|
"skills_subdir": ".claude/skills", "mode": "link",
|
|
105
108
|
"docs_url": "https://code.claude.com/docs/en/skills"},
|
|
106
109
|
{"name": "Codex", "marker": ".codex",
|
|
107
|
-
"skills_subdir": ".
|
|
110
|
+
"skills_subdir": ".agents/skills", "mode": "standard",
|
|
111
|
+
"legacy_subdir": ".codex/skills",
|
|
108
112
|
"docs_url": "https://developers.openai.com/codex/skills"},
|
|
109
113
|
{"name": "Pi", "marker": ".pi",
|
|
110
|
-
"skills_subdir": ".
|
|
114
|
+
"skills_subdir": ".agents/skills", "mode": "standard",
|
|
115
|
+
"legacy_subdir": ".pi/agent/skills",
|
|
111
116
|
"docs_url": "https://pi.dev/docs/latest/skills"},
|
|
112
117
|
{"name": "Cursor", "marker": ".cursor",
|
|
113
118
|
"skills_subdir": ".agents/skills", "mode": "standard",
|
|
@@ -128,7 +133,10 @@ def detect_agents(home: Path) -> list[dict]:
|
|
|
128
133
|
found = []
|
|
129
134
|
for row in AGENTS:
|
|
130
135
|
if (home / row["marker"]).is_dir():
|
|
131
|
-
|
|
136
|
+
entry = {**row, "skills_dir": home / row["skills_subdir"]}
|
|
137
|
+
if "legacy_subdir" in row:
|
|
138
|
+
entry["legacy_dir"] = home / row["legacy_subdir"]
|
|
139
|
+
found.append(entry)
|
|
132
140
|
return found
|
|
133
141
|
|
|
134
142
|
|
|
@@ -139,11 +147,51 @@ def _points_into(link: Path, canonical: Path) -> bool:
|
|
|
139
147
|
return False
|
|
140
148
|
|
|
141
149
|
|
|
150
|
+
def _sweep_legacy(agent: dict, canonical: Path) -> list[dict]:
|
|
151
|
+
"""Remove OUR leftovers from a dir an earlier release linked into.
|
|
152
|
+
|
|
153
|
+
fusion-cli <= 1.2.0 symlinked (or copy-fell-back) into Pi's and
|
|
154
|
+
Codex's own dirs; both agents read the canonical dir natively, so
|
|
155
|
+
those entries double-loaded every skill. Attribution-checked:
|
|
156
|
+
symlinks into the canonical and sentinel-marked copies are ours;
|
|
157
|
+
anything else is left with a report row. If the legacy dir is
|
|
158
|
+
itself aliased at the canonical, there is nothing to sweep.
|
|
159
|
+
"""
|
|
160
|
+
legacy = agent.get("legacy_dir")
|
|
161
|
+
if legacy is None or not legacy.is_dir():
|
|
162
|
+
return []
|
|
163
|
+
if _points_into(legacy, canonical):
|
|
164
|
+
return [] # the legacy dir IS the canonical through an alias —
|
|
165
|
+
# sweeping it would reach canonical entries
|
|
166
|
+
results = []
|
|
167
|
+
for entry in sorted(legacy.glob("fusion-*")):
|
|
168
|
+
row = {"agent": agent["name"], "skill": entry.name}
|
|
169
|
+
if entry.is_symlink() and _points_into(entry, canonical):
|
|
170
|
+
entry.unlink()
|
|
171
|
+
results.append({**row, "action": "unlinked",
|
|
172
|
+
"detail": f"{entry} — this agent reads the "
|
|
173
|
+
f"standard dir; the extra link "
|
|
174
|
+
f"double-loaded every skill"})
|
|
175
|
+
elif (entry.is_dir() and not entry.is_symlink()
|
|
176
|
+
and (entry / ".fusion-setup").is_file()):
|
|
177
|
+
shutil.rmtree(entry)
|
|
178
|
+
results.append({**row, "action": "unlinked",
|
|
179
|
+
"detail": f"{entry} — copy from an earlier "
|
|
180
|
+
f"fusion-cli; this agent reads the "
|
|
181
|
+
f"standard dir"})
|
|
182
|
+
else:
|
|
183
|
+
results.append({**row, "action": "left",
|
|
184
|
+
"detail": f"{entry} is not attributable to "
|
|
185
|
+
f"setup — left in place"})
|
|
186
|
+
return results
|
|
187
|
+
|
|
188
|
+
|
|
142
189
|
def fan_out(canonical: Path, agents: list[dict], force: bool) -> list[dict]:
|
|
143
190
|
results = []
|
|
144
191
|
skills = sorted(canonical.glob("fusion-*"))
|
|
145
192
|
for agent in agents:
|
|
146
193
|
if agent["mode"] == "standard":
|
|
194
|
+
results.extend(_sweep_legacy(agent, canonical))
|
|
147
195
|
if canonical.resolve() != agent["skills_dir"].resolve():
|
|
148
196
|
detail = (f"reads ~/.agents/skills — but your skills are "
|
|
149
197
|
f"at {canonical}, which this agent does not read")
|
|
@@ -152,6 +200,13 @@ def fan_out(canonical: Path, agents: list[dict], force: bool) -> list[dict]:
|
|
|
152
200
|
results.append({"agent": agent["name"], "skill": "*",
|
|
153
201
|
"action": "standard", "detail": detail})
|
|
154
202
|
continue
|
|
203
|
+
if _points_into(agent["skills_dir"], canonical):
|
|
204
|
+
results.append({
|
|
205
|
+
"agent": agent["name"], "skill": "*", "action": "served",
|
|
206
|
+
"detail": f"{agent['skills_dir']} resolves into "
|
|
207
|
+
f"{canonical} — the canonical install already "
|
|
208
|
+
f"serves this agent; nothing to link"})
|
|
209
|
+
continue
|
|
155
210
|
agent["skills_dir"].mkdir(parents=True, exist_ok=True)
|
|
156
211
|
for skill in skills:
|
|
157
212
|
target = agent["skills_dir"] / skill.name
|
|
@@ -170,6 +225,11 @@ def fan_out(canonical: Path, agents: list[dict], force: bool) -> list[dict]:
|
|
|
170
225
|
target.unlink()
|
|
171
226
|
replaced = True
|
|
172
227
|
elif target.is_dir():
|
|
228
|
+
if target.resolve() == skill.resolve():
|
|
229
|
+
results.append({**row, "action": "served",
|
|
230
|
+
"detail": f"{target} already resolves "
|
|
231
|
+
f"to the canonical skill"})
|
|
232
|
+
continue
|
|
173
233
|
if tree_digest(target) == tree_digest(skill):
|
|
174
234
|
pass # our current copy, refresh below keeps it current
|
|
175
235
|
elif (target / ".fusion-setup").is_file():
|
|
@@ -211,7 +271,11 @@ def remove_all(canonical: Path, home: Path) -> list[dict]:
|
|
|
211
271
|
results = []
|
|
212
272
|
for agent in detect_agents(home):
|
|
213
273
|
if agent["mode"] == "standard":
|
|
274
|
+
results.extend(_sweep_legacy(agent, canonical))
|
|
214
275
|
continue
|
|
276
|
+
if _points_into(agent["skills_dir"], canonical):
|
|
277
|
+
continue # entries here ARE the canonical skills; the
|
|
278
|
+
# canonical phase below removes them exactly once
|
|
215
279
|
entries = (sorted(agent["skills_dir"].glob("fusion-*"))
|
|
216
280
|
if agent["skills_dir"].is_dir() else [])
|
|
217
281
|
for entry in entries:
|
|
@@ -26,7 +26,12 @@ def test_registry_shape():
|
|
|
26
26
|
"opencode", "Goose"} == names
|
|
27
27
|
modes = {a["name"]: a["mode"] for a in setup.AGENTS}
|
|
28
28
|
assert modes["Claude Code"] == "link"
|
|
29
|
+
assert modes["Pi"] == "standard"
|
|
30
|
+
assert modes["Codex"] == "standard"
|
|
29
31
|
assert modes["Cursor"] == "standard"
|
|
32
|
+
legacy = {a["name"]: a.get("legacy_subdir") for a in setup.AGENTS}
|
|
33
|
+
assert legacy["Pi"] == ".pi/agent/skills"
|
|
34
|
+
assert legacy["Codex"] == ".codex/skills"
|
|
30
35
|
|
|
31
36
|
|
|
32
37
|
def test_detect_agents_only_present(home):
|
|
@@ -97,15 +102,15 @@ def test_fan_out_leaves_plain_file_unless_forced(home, canonical):
|
|
|
97
102
|
|
|
98
103
|
|
|
99
104
|
def test_fan_out_copies_when_symlink_unavailable(home, canonical, monkeypatch):
|
|
100
|
-
(home / ".
|
|
105
|
+
(home / ".claude").mkdir()
|
|
101
106
|
|
|
102
107
|
def no_symlink(*a, **k):
|
|
103
108
|
raise OSError("symlinks disabled")
|
|
104
109
|
monkeypatch.setattr(setup.os, "symlink", no_symlink)
|
|
105
110
|
results = setup.fan_out(canonical, setup.detect_agents(home), force=False)
|
|
106
|
-
|
|
107
|
-
assert {r["action"] for r in
|
|
108
|
-
copied = home / ".
|
|
111
|
+
claude = [r for r in results if r["agent"] == "Claude Code"]
|
|
112
|
+
assert {r["action"] for r in claude} == {"copied"}
|
|
113
|
+
copied = home / ".claude" / "skills" / "fusion-intake"
|
|
109
114
|
assert copied.is_dir() and not copied.is_symlink()
|
|
110
115
|
# the copy carries a provenance sentinel — that's how we know it's ours
|
|
111
116
|
sentinel = copied / ".fusion-setup"
|
|
@@ -117,8 +122,8 @@ def test_fan_out_copies_when_symlink_unavailable(home, canonical, monkeypatch):
|
|
|
117
122
|
# itself is an extra file — so the sentinel is what proves provenance)
|
|
118
123
|
(copied / "SKILL.md").write_text("stale")
|
|
119
124
|
again = setup.fan_out(canonical, setup.detect_agents(home), force=False)
|
|
120
|
-
|
|
121
|
-
assert
|
|
125
|
+
claude_intake = [r for r in again if r["skill"] == "fusion-intake"][0]
|
|
126
|
+
assert claude_intake["action"] == "copied"
|
|
122
127
|
assert (copied / ".fusion-setup").is_file()
|
|
123
128
|
|
|
124
129
|
|
|
@@ -136,34 +141,122 @@ def test_remove_all_is_attribution_checked(home, canonical):
|
|
|
136
141
|
|
|
137
142
|
def test_remove_all_removes_sentinel_copies_and_leaves_foreign(
|
|
138
143
|
home, canonical, monkeypatch):
|
|
139
|
-
(home / ".
|
|
144
|
+
(home / ".claude").mkdir()
|
|
145
|
+
# a foreign dir sharing a canonical skill's name — same shape, no
|
|
146
|
+
# sentinel, different content — must survive removal untouched
|
|
147
|
+
foreign = home / ".claude" / "skills" / "fusion-planner"
|
|
148
|
+
foreign.mkdir(parents=True)
|
|
149
|
+
(foreign / "SKILL.md").write_text("---\nname: my-own-tool\n---\n")
|
|
150
|
+
(foreign / "data.txt").write_text("do not touch\n")
|
|
140
151
|
|
|
141
152
|
def no_symlink(*a, **k):
|
|
142
153
|
raise OSError("symlinks disabled")
|
|
143
154
|
monkeypatch.setattr(setup.os, "symlink", no_symlink)
|
|
144
155
|
setup.fan_out(canonical, setup.detect_agents(home), force=False)
|
|
145
|
-
ours = home / ".
|
|
146
|
-
assert (ours / ".fusion-setup").is_file()
|
|
156
|
+
ours = home / ".claude" / "skills" / "fusion-intake"
|
|
157
|
+
assert (ours / ".fusion-setup").is_file() # our sentinel-marked copy
|
|
147
158
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
159
|
+
results = setup.remove_all(canonical, home)
|
|
160
|
+
|
|
161
|
+
assert not ours.exists() # our copy: removed
|
|
162
|
+
intake = [r for r in results if r["agent"] == "Claude Code"
|
|
163
|
+
and r["skill"] == "fusion-intake"][0]
|
|
164
|
+
assert intake["action"] == "removed"
|
|
165
|
+
assert foreign.is_dir() # foreign: left in place
|
|
166
|
+
assert (foreign / "data.txt").read_text() == "do not touch\n"
|
|
167
|
+
planner = [r for r in results if r["agent"] == "Claude Code"
|
|
168
|
+
and r["skill"] == "fusion-planner"][0]
|
|
169
|
+
assert planner["action"] == "left"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_fan_out_survives_skills_dir_symlinked_to_canonical(home, canonical):
|
|
173
|
+
# the topology that destroyed skills in v1.1.0-v1.2.0: the agent's
|
|
174
|
+
# whole skills dir is a symlink AT the canonical directory
|
|
151
175
|
(home / ".claude").mkdir()
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
176
|
+
(home / ".claude" / "skills").symlink_to(
|
|
177
|
+
canonical, target_is_directory=True)
|
|
178
|
+
results = setup.fan_out(canonical, setup.detect_agents(home),
|
|
179
|
+
force=False)
|
|
180
|
+
claude = [r for r in results if r["agent"] == "Claude Code"]
|
|
181
|
+
assert len(claude) == 1 and claude[0]["action"] == "served"
|
|
182
|
+
# the canonical skills are alive, real, and readable
|
|
183
|
+
assert (canonical / "fusion-intake" / "SKILL.md").is_file()
|
|
184
|
+
assert not (canonical / "fusion-intake").is_symlink()
|
|
185
|
+
|
|
156
186
|
|
|
187
|
+
def test_remove_all_survives_skills_dir_symlinked_to_canonical(
|
|
188
|
+
home, canonical):
|
|
189
|
+
(home / ".claude").mkdir()
|
|
190
|
+
(home / ".claude" / "skills").symlink_to(
|
|
191
|
+
canonical, target_is_directory=True)
|
|
157
192
|
results = setup.remove_all(canonical, home)
|
|
193
|
+
# the guard means the agent sweep never touches the alias — every
|
|
194
|
+
# removal row must come from the canonical phase
|
|
195
|
+
assert not any(r["agent"] == "Claude Code" for r in results)
|
|
196
|
+
# the canonical phase removed the skills exactly once; the user's
|
|
197
|
+
# own dir-level symlink is not ours and stays
|
|
198
|
+
assert not (canonical / "fusion-intake").exists()
|
|
199
|
+
assert (home / ".claude" / "skills").is_symlink()
|
|
158
200
|
|
|
159
|
-
assert not ours.exists() # our copy: removed
|
|
160
|
-
pi_result = [r for r in results
|
|
161
|
-
if r["agent"] == "Pi" and r["skill"] == "fusion-intake"][0]
|
|
162
|
-
assert pi_result["action"] == "removed"
|
|
163
201
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
202
|
+
def test_fan_out_standard_sweeps_our_legacy_links(home, canonical):
|
|
203
|
+
(home / ".pi").mkdir()
|
|
204
|
+
legacy = home / ".pi" / "agent" / "skills"
|
|
205
|
+
legacy.mkdir(parents=True)
|
|
206
|
+
(legacy / "fusion-intake").symlink_to(
|
|
207
|
+
canonical / "fusion-intake", target_is_directory=True)
|
|
208
|
+
stale_copy = legacy / "fusion-planner" # 1.2.0 copy-fallback relic
|
|
209
|
+
stale_copy.mkdir()
|
|
210
|
+
(stale_copy / "SKILL.md").write_text("old payload")
|
|
211
|
+
(stale_copy / ".fusion-setup").write_text("1.0\nabc\n")
|
|
212
|
+
mine = legacy / "fusion-mine" # the user's own, not ours
|
|
213
|
+
mine.mkdir()
|
|
214
|
+
results = setup.fan_out(canonical, setup.detect_agents(home),
|
|
215
|
+
force=False)
|
|
216
|
+
pi = {r["skill"]: r for r in results if r["agent"] == "Pi"}
|
|
217
|
+
assert pi["fusion-intake"]["action"] == "unlinked"
|
|
218
|
+
assert not (legacy / "fusion-intake").exists()
|
|
219
|
+
assert pi["fusion-planner"]["action"] == "unlinked"
|
|
220
|
+
assert not stale_copy.exists()
|
|
221
|
+
assert pi["fusion-mine"]["action"] == "left"
|
|
222
|
+
assert mine.is_dir()
|
|
223
|
+
assert pi["*"]["action"] == "standard" # and Pi reads the std dir
|
|
224
|
+
# no new links were created in the legacy dir
|
|
225
|
+
assert not (legacy / "fusion-analyst").exists()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_remove_all_sweeps_legacy_dirs(home, canonical):
|
|
229
|
+
(home / ".codex").mkdir()
|
|
230
|
+
legacy = home / ".codex" / "skills"
|
|
231
|
+
legacy.mkdir(parents=True)
|
|
232
|
+
(legacy / "fusion-intake").symlink_to(
|
|
233
|
+
canonical / "fusion-intake", target_is_directory=True)
|
|
234
|
+
results = setup.remove_all(canonical, home)
|
|
235
|
+
assert not (legacy / "fusion-intake").exists()
|
|
236
|
+
assert any(r["agent"] == "Codex" and r["action"] == "unlinked"
|
|
237
|
+
for r in results)
|
|
238
|
+
assert not (canonical / "fusion-intake").exists()
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_sweep_legacy_skips_dir_aliased_at_canonical(home, canonical):
|
|
242
|
+
# topology B from the v1.2.1 final review: legacy dir symlinked AT
|
|
243
|
+
# the canonical, plus a canonical entry that is itself a symlink
|
|
244
|
+
# resolving into the canonical — the sweep must not reach through
|
|
245
|
+
# the alias and unlink it
|
|
246
|
+
(home / ".pi").mkdir()
|
|
247
|
+
(home / ".pi" / "agent").mkdir()
|
|
248
|
+
(home / ".pi" / "agent" / "skills").symlink_to(
|
|
249
|
+
canonical, target_is_directory=True)
|
|
250
|
+
store = canonical / "_store" / "fusion-extra"
|
|
251
|
+
store.mkdir(parents=True)
|
|
252
|
+
(store / "SKILL.md").write_text("---\nname: fusion-extra\n---\n")
|
|
253
|
+
user_link = canonical / "fusion-extra"
|
|
254
|
+
user_link.symlink_to(store, target_is_directory=True)
|
|
255
|
+
results = setup.fan_out(canonical, setup.detect_agents(home),
|
|
256
|
+
force=False)
|
|
257
|
+
assert user_link.is_symlink() # the user's link survives
|
|
258
|
+
pi = [r for r in results if r["agent"] == "Pi"]
|
|
259
|
+
assert [r["action"] for r in pi] == ["standard"] # sweep stayed out
|
|
260
|
+
# remove is equally alias-safe
|
|
261
|
+
setup.remove_all(canonical, home)
|
|
262
|
+
assert user_link.is_symlink()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-analyst/references/fusion-conventions.md
RENAMED
|
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
|
{fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-librarian/references/fusion-conventions.md
RENAMED
|
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
|
{fusion_cli-1.2.0 → fusion_cli-1.2.1}/skills/fusion-planner/references/fusion-conventions.md
RENAMED
|
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
|