fusion-cli 1.1.1__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.
Files changed (75) hide show
  1. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/PKG-INFO +2 -2
  2. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/README.md +1 -1
  3. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/pyproject.toml +1 -1
  4. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-analyst/SKILL.md +6 -3
  5. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-analyst/references/fusion-conventions.md +13 -1
  6. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-intake/SKILL.md +6 -3
  7. {fusion_cli-1.1.1/skills/fusion-librarian → fusion_cli-1.2.1/skills/fusion-intake}/references/fusion-conventions.md +13 -1
  8. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-librarian/SKILL.md +6 -3
  9. {fusion_cli-1.1.1/skills/fusion-planner → fusion_cli-1.2.1/skills/fusion-librarian}/references/fusion-conventions.md +13 -1
  10. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-planner/SKILL.md +6 -3
  11. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-planner/references/create-activity.md +21 -0
  12. {fusion_cli-1.1.1/skills/fusion-intake → fusion_cli-1.2.1/skills/fusion-planner}/references/fusion-conventions.md +13 -1
  13. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/src/fusion/checker.py +29 -2
  14. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/src/fusion/cli.py +59 -10
  15. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/src/fusion/scaffold.py +11 -0
  16. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/src/fusion/setup.py +67 -3
  17. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/src/fusion/views.py +12 -2
  18. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/tests/test_checker_warnings.py +30 -0
  19. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/tests/test_cli.py +42 -2
  20. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/tests/test_hub_bucket.py +2 -2
  21. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/tests/test_ledger.py +1 -1
  22. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/tests/test_roundtrip.py +1 -1
  23. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/tests/test_scaffold.py +10 -0
  24. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/tests/test_setup_agents.py +119 -26
  25. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/tests/test_views.py +11 -7
  26. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/uv.lock +1 -1
  27. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/.gitignore +0 -0
  28. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/hatch_build.py +0 -0
  29. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-analyst/references/assess.md +0 -0
  30. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-analyst/references/compare.md +0 -0
  31. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-analyst/references/export.md +0 -0
  32. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-analyst/references/report.md +0 -0
  33. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-analyst/scripts/export.py +0 -0
  34. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-intake/references/convert.md +0 -0
  35. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-intake/references/delivery.md +0 -0
  36. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-intake/references/gate.md +0 -0
  37. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-intake/scripts/convert.py +0 -0
  38. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-intake/scripts/gate.py +0 -0
  39. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-intake/tests/conftest.py +0 -0
  40. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-intake/tests/make_fixtures.py +0 -0
  41. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-intake/tests/test_convert.py +0 -0
  42. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-intake/tests/test_gate.py +0 -0
  43. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-intake/tests/test_integration.py +0 -0
  44. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-librarian/references/archive.md +0 -0
  45. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-librarian/references/create.md +0 -0
  46. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-librarian/references/cross-reference.md +0 -0
  47. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-librarian/references/promote.md +0 -0
  48. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-librarian/references/query.md +0 -0
  49. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-librarian/references/reflect.md +0 -0
  50. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-librarian/references/restructure.md +0 -0
  51. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-librarian/references/tag.md +0 -0
  52. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-librarian/scripts/link-repair.py +0 -0
  53. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-librarian/tests/conftest.py +0 -0
  54. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-librarian/tests/test_link_repair.py +0 -0
  55. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-planner/references/close.md +0 -0
  56. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/skills/fusion-planner/references/horizon.md +0 -0
  57. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/src/fusion/__init__.py +0 -0
  58. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/src/fusion/bucket.py +0 -0
  59. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/src/fusion/document.py +0 -0
  60. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/src/fusion/hub.py +0 -0
  61. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/src/fusion/indexer.py +0 -0
  62. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/src/fusion/ledger.py +0 -0
  63. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/src/fusion/manifest.py +0 -0
  64. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/tests/__init__.py +0 -0
  65. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/tests/conftest.py +0 -0
  66. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/tests/test_build_payload.py +0 -0
  67. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/tests/test_checker_errors.py +0 -0
  68. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/tests/test_cli_encoding.py +0 -0
  69. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/tests/test_cli_setup.py +0 -0
  70. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/tests/test_cli_version.py +0 -0
  71. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/tests/test_document.py +0 -0
  72. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/tests/test_indexer.py +0 -0
  73. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/tests/test_line_endings.py +0 -0
  74. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/tests/test_setup_core.py +0 -0
  75. {fusion_cli-1.1.1 → fusion_cli-1.2.1}/tests/test_skill_family.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fusion-cli
3
- Version: 1.1.1
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 into every detected agent — the installer's brain. `--remove` undoes it |
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 into every detected agent — the installer's brain. `--remove` undoes it |
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fusion-cli"
3
- version = "1.1.1"
3
+ version = "1.2.1"
4
4
  description = "The Fusion reference CLI — the notary of the Fusion Convention."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: fusion-analyst
3
- description: "The Fusion analyst — deliverables out of the bucket. Four gears: report (multi-section analysis from library + activities — the default), assess (scored assessment with explicit criteria and scale), compare (side-by-side matrix with deltas and red flags), export (CSV/JSON/XLSX data extracts). Everything lands in output/ as a summary-first document, cites every source path in data_sources, and ships signed in the ledger. Use for 'report on', 'analyze and write up', 'brief', 'assess', 'evaluate', 'score', 'compare X and Y', 'export as csv/excel'. For searching without producing a deliverable use fusion-librarian's query; for new documents of record use fusion-librarian's create. Applies only inside a Fusion bucket — a directory tree with BUCKET.md and LEDGER.md at its root; if there is no such bucket in play, this skill does not apply."
3
+ description: "The Fusion analyst — deliverables out of the bucket. Four gears: report (multi-section analysis from library + activities — the default), assess (scored assessment with explicit criteria and scale), compare (side-by-side matrix with deltas and red flags), export (CSV/JSON/XLSX data extracts). Everything lands in output/ as a summary-first document, cites every source path in data_sources, and ships signed in the ledger. Use for 'report on', 'analyze and write up', 'brief', 'assess', 'evaluate', 'score', 'compare X and Y', 'export as csv/excel'. For searching without producing a deliverable use fusion-librarian's query; for new documents of record use fusion-librarian's create. Applies only inside a Fusion bucket — a directory tree with BUCKET.md and LEDGER.md at its root; if there is no such bucket in play, offer to create one with `fusion new` instead of improvising structure."
4
4
  license: MIT
5
5
  compatibility: "Requires the fusion CLI on PATH; uv for the export script (PEP 723: openpyxl)."
6
6
  ---
@@ -13,8 +13,11 @@ document cites the exact paths it was built from, and ships with a
13
13
 
14
14
  Read `references/fusion-conventions.md` once per session; read
15
15
  `BUCKET.md ## Conventions` before acting. No `BUCKET.md` up the tree and
16
- none named? Stop this is not a Fusion bucket, and no Fusion skill
17
- applies (`fusion hub` lists the real ones).
16
+ none named? Then nothing here is a Fusion bucket and no Fusion skill
17
+ applies but offer the door instead of walking away: `fusion new
18
+ <path>` scaffolds a conformant bucket and registers it in the hub
19
+ (`fusion hub` lists the ones that exist). Buckets are life-domains —
20
+ few and bold — never improvised by hand.
18
21
 
19
22
  ## Pick the gear
20
23
 
@@ -107,6 +107,11 @@ shell. The ledger verbs (closed set of eleven): `created`, `converted`,
107
107
  `shipped`, `reflected`, `noted`. Sign with your agent name (`--as claude`,
108
108
  or set `FUSION_ACTOR`).
109
109
 
110
+ On a bucket that lives on more than one machine, git's union driver
111
+ (`.gitattributes`, written at bucket birth) merges parallel `LEDGER.md`
112
+ and `MANIFEST.md` appends. After any git merge: `fusion index`, then
113
+ `fusion check`.
114
+
110
115
  ## The CLI crib
111
116
 
112
117
  | Command | What it does |
@@ -116,6 +121,7 @@ or set `FUSION_ACTOR`).
116
121
  | `fusion log <verb> <object>` | append a signed ledger entry |
117
122
  | `fusion index` | regenerate INDEX files (logs `indexed` when changed) |
118
123
  | `fusion check [path]` | conformance: errors, warnings, honest exit codes |
124
+ | `fusion check --hub` | is every registered bucket where the hub says? |
119
125
  | `fusion status [--since …]` | one bucket at a glance |
120
126
  | `fusion today` | the composed morning across all hub buckets |
121
127
  | `fusion agenda` | dated + active items across buckets |
@@ -127,8 +133,14 @@ green before you call the work done.**
127
133
 
128
134
  ## When you're blocked
129
135
 
136
+ - No bucket in play anywhere: never improvise Fusion structure by hand.
137
+ Offer `fusion new <path> --kind <kind> --description "…"` — six zones,
138
+ BUCKET.md, an opened ledger, a merge-safe `.gitattributes`, registered
139
+ in the hub. Guide the human toward few, bold, life-domain buckets
140
+ (personal, one per company, a studio).
130
141
  - `fusion` not on PATH: stop and tell the human — the install is
131
- `uv tool install ./fusion/cli` from a clone of the Fusion repository.
142
+ `uv tool install fusion-cli` (or, from a clone of the Fusion
143
+ repository, `uv tool install ./fusion/cli`).
132
144
  Never imitate the notary by hand: no register writes while the CLI is
133
145
  missing.
134
146
  - `fusion check` red and you cannot fix it: stop, show the findings
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: fusion-intake
3
- description: "The Fusion intake gate — everything that enters a bucket enters through it, losslessly. Classify what landed in inbox/ (new, updated, duplicate, conflicting), preserve the original in sources/ with its sha256 in the MANIFEST, convert to a faithful summary-first document (xlsx, csv, docx, pptx, pdf, html, images, .eml mail, markdown/text exports), propose type and aurora, sign the ledger, clear the inbox. Use when the user says 'process the inbox', 'intake', 'ingest', 'convert this file', or drops files into a Fusion bucket's inbox/. For placement, curation, or restructuring of what is already inside, use fusion-librarian; for deliverables out of the bucket, use fusion-analyst. Applies only inside a Fusion bucket — a directory tree with BUCKET.md and LEDGER.md at its root; if there is no such bucket in play, this skill does not apply."
3
+ description: "The Fusion intake gate — everything that enters a bucket enters through it, losslessly. Classify what landed in inbox/ (new, updated, duplicate, conflicting), preserve the original in sources/ with its sha256 in the MANIFEST, convert to a faithful summary-first document (xlsx, csv, docx, pptx, pdf, html, images, .eml mail, markdown/text exports), propose type and aurora, sign the ledger, clear the inbox. Use when the user says 'process the inbox', 'intake', 'ingest', 'convert this file', or drops files into a Fusion bucket's inbox/. For placement, curation, or restructuring of what is already inside, use fusion-librarian; for deliverables out of the bucket, use fusion-analyst. Applies only inside a Fusion bucket — a directory tree with BUCKET.md and LEDGER.md at its root; if there is no such bucket in play, offer to create one with `fusion new` instead of improvising structure."
4
4
  license: MIT
5
5
  compatibility: "Requires the fusion CLI on PATH and uv. LibreOffice (soffice on PATH) required for docx/pptx/legacy office/html formats — fails fast when missing, never silently degrades. Script deps via PEP 723 (openpyxl, PyYAML, pymupdf)."
6
6
  ---
@@ -14,8 +14,11 @@ that isn't clean and new.
14
14
  Read `references/fusion-conventions.md` once per session. Then, before
15
15
  touching anything: read the bucket's `BUCKET.md` — `## Conventions` may
16
16
  contain filing rules and standing delegations that bind this whole run.
17
- No `BUCKET.md` up the tree and none named? Stop this is not a Fusion
18
- bucket, and no Fusion skill applies (`fusion hub` lists the real ones).
17
+ No `BUCKET.md` up the tree and none named? Then nothing here is a
18
+ Fusion bucket and no Fusion skill applies but offer the door instead
19
+ of walking away: `fusion new <path>` scaffolds a conformant bucket and
20
+ registers it in the hub (`fusion hub` lists the ones that exist).
21
+ Buckets are life-domains — few and bold — never improvised by hand.
19
22
 
20
23
  ## The pipeline
21
24
 
@@ -107,6 +107,11 @@ shell. The ledger verbs (closed set of eleven): `created`, `converted`,
107
107
  `shipped`, `reflected`, `noted`. Sign with your agent name (`--as claude`,
108
108
  or set `FUSION_ACTOR`).
109
109
 
110
+ On a bucket that lives on more than one machine, git's union driver
111
+ (`.gitattributes`, written at bucket birth) merges parallel `LEDGER.md`
112
+ and `MANIFEST.md` appends. After any git merge: `fusion index`, then
113
+ `fusion check`.
114
+
110
115
  ## The CLI crib
111
116
 
112
117
  | Command | What it does |
@@ -116,6 +121,7 @@ or set `FUSION_ACTOR`).
116
121
  | `fusion log <verb> <object>` | append a signed ledger entry |
117
122
  | `fusion index` | regenerate INDEX files (logs `indexed` when changed) |
118
123
  | `fusion check [path]` | conformance: errors, warnings, honest exit codes |
124
+ | `fusion check --hub` | is every registered bucket where the hub says? |
119
125
  | `fusion status [--since …]` | one bucket at a glance |
120
126
  | `fusion today` | the composed morning across all hub buckets |
121
127
  | `fusion agenda` | dated + active items across buckets |
@@ -127,8 +133,14 @@ green before you call the work done.**
127
133
 
128
134
  ## When you're blocked
129
135
 
136
+ - No bucket in play anywhere: never improvise Fusion structure by hand.
137
+ Offer `fusion new <path> --kind <kind> --description "…"` — six zones,
138
+ BUCKET.md, an opened ledger, a merge-safe `.gitattributes`, registered
139
+ in the hub. Guide the human toward few, bold, life-domain buckets
140
+ (personal, one per company, a studio).
130
141
  - `fusion` not on PATH: stop and tell the human — the install is
131
- `uv tool install ./fusion/cli` from a clone of the Fusion repository.
142
+ `uv tool install fusion-cli` (or, from a clone of the Fusion
143
+ repository, `uv tool install ./fusion/cli`).
132
144
  Never imitate the notary by hand: no register writes while the CLI is
133
145
  missing.
134
146
  - `fusion check` red and you cannot fix it: stop, show the findings
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: fusion-librarian
3
- description: "The Fusion librarian — the accountable owner of a bucket's order. One entry, eight gears: query (natural-language search over the bucket — the default), create (a new conformant document), tag (bulk frontmatter metadata), cross-reference (map what relates to what), promote (workbench → a real zone, validated), archive (out of the way, path + aurora agreeing), restructure (reorganize and sign your reasons), reflect (the reflection ritual: introspect the ledger, propose curation, land what was learned). Use for 'find', 'search', 'where is', 'create a document', 'tag', 'what links to', 'promote', 'archive this', 'reorganize', 'restructure', 'run a reflection'. For files arriving from outside use fusion-intake; for activity planning use fusion-planner; for reports and exports use fusion-analyst. Applies only inside a Fusion bucket — a directory tree with BUCKET.md and LEDGER.md at its root; if there is no such bucket in play, this skill does not apply."
3
+ description: "The Fusion librarian — the accountable owner of a bucket's order. One entry, eight gears: query (natural-language search over the bucket — the default), create (a new conformant document), tag (bulk frontmatter metadata), cross-reference (map what relates to what), promote (workbench → a real zone, validated), archive (out of the way, path + aurora agreeing), restructure (reorganize and sign your reasons), reflect (the reflection ritual: introspect the ledger, propose curation, land what was learned). Use for 'find', 'search', 'where is', 'create a document', 'tag', 'what links to', 'promote', 'archive this', 'reorganize', 'restructure', 'run a reflection'. For files arriving from outside use fusion-intake; for activity planning use fusion-planner; for reports and exports use fusion-analyst. Applies only inside a Fusion bucket — a directory tree with BUCKET.md and LEDGER.md at its root; if there is no such bucket in play, offer to create one with `fusion new` instead of improvising structure."
4
4
  license: MIT
5
5
  compatibility: "Requires the fusion CLI on PATH."
6
6
  ---
@@ -14,8 +14,11 @@ ledger with its reasons.
14
14
  Read `references/fusion-conventions.md` once per session. Before any gear:
15
15
  read the bucket's `BUCKET.md` — `### Rules` bind how this bucket files
16
16
  things; `### Delegations` say what you may do without asking. No
17
- `BUCKET.md` up the tree and none named? Stop this is not a Fusion
18
- bucket, and no Fusion skill applies (`fusion hub` lists the real ones).
17
+ `BUCKET.md` up the tree and none named? Then nothing here is a Fusion
18
+ bucket and no Fusion skill applies but offer the door instead of
19
+ walking away: `fusion new <path>` scaffolds a conformant bucket and
20
+ registers it in the hub (`fusion hub` lists the ones that exist).
21
+ Buckets are life-domains — few and bold — never improvised by hand.
19
22
 
20
23
  ## Pick the gear
21
24
 
@@ -107,6 +107,11 @@ shell. The ledger verbs (closed set of eleven): `created`, `converted`,
107
107
  `shipped`, `reflected`, `noted`. Sign with your agent name (`--as claude`,
108
108
  or set `FUSION_ACTOR`).
109
109
 
110
+ On a bucket that lives on more than one machine, git's union driver
111
+ (`.gitattributes`, written at bucket birth) merges parallel `LEDGER.md`
112
+ and `MANIFEST.md` appends. After any git merge: `fusion index`, then
113
+ `fusion check`.
114
+
110
115
  ## The CLI crib
111
116
 
112
117
  | Command | What it does |
@@ -116,6 +121,7 @@ or set `FUSION_ACTOR`).
116
121
  | `fusion log <verb> <object>` | append a signed ledger entry |
117
122
  | `fusion index` | regenerate INDEX files (logs `indexed` when changed) |
118
123
  | `fusion check [path]` | conformance: errors, warnings, honest exit codes |
124
+ | `fusion check --hub` | is every registered bucket where the hub says? |
119
125
  | `fusion status [--since …]` | one bucket at a glance |
120
126
  | `fusion today` | the composed morning across all hub buckets |
121
127
  | `fusion agenda` | dated + active items across buckets |
@@ -127,8 +133,14 @@ green before you call the work done.**
127
133
 
128
134
  ## When you're blocked
129
135
 
136
+ - No bucket in play anywhere: never improvise Fusion structure by hand.
137
+ Offer `fusion new <path> --kind <kind> --description "…"` — six zones,
138
+ BUCKET.md, an opened ledger, a merge-safe `.gitattributes`, registered
139
+ in the hub. Guide the human toward few, bold, life-domain buckets
140
+ (personal, one per company, a studio).
130
141
  - `fusion` not on PATH: stop and tell the human — the install is
131
- `uv tool install ./fusion/cli` from a clone of the Fusion repository.
142
+ `uv tool install fusion-cli` (or, from a clone of the Fusion
143
+ repository, `uv tool install ./fusion/cli`).
132
144
  Never imitate the notary by hand: no register writes while the CLI is
133
145
  missing.
134
146
  - `fusion check` red and you cannot fix it: stop, show the findings
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: fusion-planner
3
- description: "The Fusion planner — the horizon keeper. The human drives activities; the planner structures them. Three gears: create-activity (a new activity folder with a stateful plan document — works for plans, campaigns, programs, agendas alike), horizon (review what's live: stalled activities, expiring commitments, what fusion today will show tomorrow), close (finish an activity: status done, archived, out of the way). Use for 'new project/activity/campaign', 'plan this', 'what's on my plate', 'review my activities', 'what's stalled', 'agenda', 'close/finish this activity'. For placing knowledge use fusion-librarian; for deliverables use fusion-analyst. Applies only inside a Fusion bucket — a directory tree with BUCKET.md and LEDGER.md at its root; if there is no such bucket in play, this skill does not apply."
3
+ description: "The Fusion planner — the horizon keeper. The human drives activities; the planner structures them. Three gears: create-activity (a new activity folder with a stateful plan document — works for plans, campaigns, programs, agendas alike), horizon (review what's live: stalled activities, expiring commitments, what fusion today will show tomorrow), close (finish an activity: status done, archived, out of the way). Use for 'new project/activity/campaign', 'plan this', 'what's on my plate', 'review my activities', 'what's stalled', 'agenda', 'close/finish this activity'. For placing knowledge use fusion-librarian; for deliverables use fusion-analyst. Applies only inside a Fusion bucket — a directory tree with BUCKET.md and LEDGER.md at its root; if there is no such bucket in play, offer to create one with `fusion new` instead of improvising structure."
4
4
  license: MIT
5
5
  compatibility: "Requires the fusion CLI on PATH."
6
6
  ---
@@ -13,8 +13,11 @@ composes tomorrow morning is this skill's report card.
13
13
 
14
14
  Read `references/fusion-conventions.md` once per session; read the
15
15
  bucket's `BUCKET.md ## Conventions` before acting. No `BUCKET.md` up the
16
- tree and none named? Stop this is not a Fusion bucket, and no Fusion
17
- skill applies (`fusion hub` lists the real ones).
16
+ tree and none named? Then nothing here is a Fusion bucket and no
17
+ Fusion skill applies but offer the door instead of walking away:
18
+ `fusion new <path>` scaffolds a conformant bucket and registers it in
19
+ the hub (`fusion hub` lists the ones that exist). Buckets are
20
+ life-domains — few and bold — never improvised by hand.
18
21
 
19
22
  ## Pick the gear
20
23
 
@@ -36,3 +36,24 @@
36
36
  4. `fusion log created "activities/<slug>/plan.md" --bucket <root>
37
37
  --as <you>` · `fusion index <root>` · `fusion check <root>` green.
38
38
  Then show the human what `fusion today` now includes.
39
+
40
+ ## The external project
41
+
42
+ An activity about building something that is code — an app, a site, a
43
+ tool — keeps the repository OUTSIDE the bucket (Fusion holds knowledge
44
+ and work, never code). Point at it instead:
45
+
46
+ ---
47
+ title: <name>
48
+ type: plan
49
+ aurora: focus
50
+ status: active
51
+ created: <today>
52
+ resource: <path or URL of the repository>
53
+ ---
54
+
55
+ The repo keeps its own history; the activity keeps what the code cannot
56
+ remember — the plan, the decisions, the progress log, links to the
57
+ bucket documents it draws on. The normative example:
58
+ `examples/crazy-ones/activities/band-site/plan.md` in the Fusion
59
+ repository.
@@ -107,6 +107,11 @@ shell. The ledger verbs (closed set of eleven): `created`, `converted`,
107
107
  `shipped`, `reflected`, `noted`. Sign with your agent name (`--as claude`,
108
108
  or set `FUSION_ACTOR`).
109
109
 
110
+ On a bucket that lives on more than one machine, git's union driver
111
+ (`.gitattributes`, written at bucket birth) merges parallel `LEDGER.md`
112
+ and `MANIFEST.md` appends. After any git merge: `fusion index`, then
113
+ `fusion check`.
114
+
110
115
  ## The CLI crib
111
116
 
112
117
  | Command | What it does |
@@ -116,6 +121,7 @@ or set `FUSION_ACTOR`).
116
121
  | `fusion log <verb> <object>` | append a signed ledger entry |
117
122
  | `fusion index` | regenerate INDEX files (logs `indexed` when changed) |
118
123
  | `fusion check [path]` | conformance: errors, warnings, honest exit codes |
124
+ | `fusion check --hub` | is every registered bucket where the hub says? |
119
125
  | `fusion status [--since …]` | one bucket at a glance |
120
126
  | `fusion today` | the composed morning across all hub buckets |
121
127
  | `fusion agenda` | dated + active items across buckets |
@@ -127,8 +133,14 @@ green before you call the work done.**
127
133
 
128
134
  ## When you're blocked
129
135
 
136
+ - No bucket in play anywhere: never improvise Fusion structure by hand.
137
+ Offer `fusion new <path> --kind <kind> --description "…"` — six zones,
138
+ BUCKET.md, an opened ledger, a merge-safe `.gitattributes`, registered
139
+ in the hub. Guide the human toward few, bold, life-domain buckets
140
+ (personal, one per company, a studio).
130
141
  - `fusion` not on PATH: stop and tell the human — the install is
131
- `uv tool install ./fusion/cli` from a clone of the Fusion repository.
142
+ `uv tool install fusion-cli` (or, from a clone of the Fusion
143
+ repository, `uv tool install ./fusion/cli`).
132
144
  Never imitate the notary by hand: no register writes while the CLI is
133
145
  missing.
134
146
  - `fusion check` red and you cannot fix it: stop, show the findings
@@ -6,7 +6,7 @@ import time
6
6
  from dataclasses import dataclass
7
7
  from pathlib import Path
8
8
 
9
- from . import indexer, ledger, manifest
9
+ from . import hub, indexer, ledger, manifest
10
10
  from .bucket import DOC_ZONES, REQUIRED_BUCKET_FIELDS, ZONES, load, iter_documents
11
11
  from .document import AURORAS, FILENAME_RE, MAX_STEM
12
12
 
@@ -19,7 +19,7 @@ EXPORT_FILENAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z0-9]+$")
19
19
  @dataclass
20
20
  class Finding:
21
21
  level: str # "error" | "warning"
22
- code: str # E1..E8, W1..W5
22
+ code: str # E1..E8, W1..W5 — plus H1..H2 from check_hub (CLI vocabulary, not SPEC)
23
23
  path: str # bucket-relative, "" when bucket-wide
24
24
  message: str
25
25
 
@@ -246,3 +246,30 @@ def _w5_untouched_activities(root: Path) -> list[Finding]:
246
246
  "active activity with no ledger mention across the last "
247
247
  "reflection window"))
248
248
  return findings
249
+
250
+
251
+ def check_hub() -> list[Finding]:
252
+ """The hub's own audit — dangling entries and name drift.
253
+
254
+ H-codes are CLI vocabulary, not SPEC conformance codes: the hub is a
255
+ per-machine register and consumers must tolerate anything in it. All
256
+ findings are warnings; a hub audit never fails.
257
+ """
258
+ findings: list[Finding] = []
259
+ for entry in hub.load():
260
+ root = hub.resolve(entry)
261
+ if not (root / "BUCKET.md").is_file():
262
+ findings.append(Finding(
263
+ "warning", "H1", entry.path,
264
+ f"'{entry.name}' — no bucket at this path; clone it "
265
+ f"there, or `fusion hub remove {entry.name}`",
266
+ ))
267
+ continue
268
+ name = load(root).name
269
+ if name and name != entry.name:
270
+ findings.append(Finding(
271
+ "warning", "H2", entry.path,
272
+ f"hub says '{entry.name}' but BUCKET.md says '{name}' — "
273
+ "re-register: `fusion hub remove` then `fusion hub add`",
274
+ ))
275
+ return findings
@@ -88,11 +88,20 @@ def cmd_hub(args) -> int:
88
88
  f"'{args.name}' retired from the hub. The files live on.")
89
89
  return 0
90
90
  entries = hub.load()
91
- human = "\n".join(
92
- f"- **{e.name}** · {e.kind} · {e.path} {e.description}"
93
- for e in entries
94
- ) or "the hub is empty — `fusion new` a bucket."
95
- _emit([asdict(e) for e in entries], args.json, human)
91
+ flagged = [
92
+ (e, not (hub.resolve(e) / "BUCKET.md").is_file()) for e in entries
93
+ ]
94
+ lines = []
95
+ for e, missing in flagged:
96
+ lines.append(f"- **{e.name}** · {e.kind} · {e.path} — {e.description}")
97
+ if missing:
98
+ lines.append(
99
+ f" ⚠ no bucket at that path — clone it there, "
100
+ f"or `fusion hub remove {e.name}`"
101
+ )
102
+ human = "\n".join(lines) or "the hub is empty — `fusion new` a bucket."
103
+ _emit([{**asdict(e), "missing": missing} for e, missing in flagged],
104
+ args.json, human)
96
105
  return 0
97
106
 
98
107
 
@@ -140,12 +149,36 @@ def cmd_index(args) -> int:
140
149
  return 0
141
150
 
142
151
 
152
+ def _check_hub(args) -> int:
153
+ findings = checker.check_hub()
154
+ entries = hub.load()
155
+ if args.json:
156
+ _emit({
157
+ "hub": str(hub.hub_path()),
158
+ "buckets": len(entries),
159
+ "ok": not findings,
160
+ "warnings": [asdict(f) for f in findings],
161
+ }, True, "")
162
+ else:
163
+ for f in findings:
164
+ print(f" {f.code} · {f.path} — {f.message}")
165
+ n, w = len(entries), len(findings)
166
+ verdict = ("every bucket answers." if not findings
167
+ else "some buckets are not where the hub says.")
168
+ print(f"hub: {n} bucket{'s' if n != 1 else ''} · "
169
+ f"{w} warning{'s' if w != 1 else ''} — {verdict}")
170
+ return 0
171
+
172
+
143
173
  def cmd_check(args) -> int:
174
+ if getattr(args, "hub", False):
175
+ return _check_hub(args)
144
176
  root = _root_from(args)
145
177
  if root is None:
146
178
  given = getattr(args, "path", None)
147
- return _fail(f"no bucket at: {given}" if given else
148
- "not inside a bucket and no path given", args.json)
179
+ if given is None:
180
+ return _check_hub(args) # outside any bucket: audit the hub
181
+ return _fail(f"no bucket at: {given}", args.json)
149
182
  findings = checker.check(root)
150
183
  errors = [f for f in findings if f.level == "error"]
151
184
  warnings = [f for f in findings if f.level == "warning"]
@@ -207,6 +240,12 @@ def _render_items(items) -> list[str]:
207
240
 
208
241
  def cmd_today(args) -> int:
209
242
  t = views.today()
243
+ for m in t["missing"]:
244
+ print(
245
+ f"note: hub bucket '{m['name']}' is not at {m['path']} — "
246
+ f"clone it there, or `fusion hub remove {m['name']}`",
247
+ file=sys.stderr,
248
+ )
210
249
  lines = [f"Today across {len(t['buckets'])} bucket"
211
250
  f"{'s' if len(t['buckets']) != 1 else ''}"]
212
251
  for aurora, items in t["groups"].items():
@@ -220,6 +259,12 @@ def cmd_today(args) -> int:
220
259
 
221
260
  def cmd_agenda(args) -> int:
222
261
  a = views.agenda()
262
+ for m in a["missing"]:
263
+ print(
264
+ f"note: hub bucket '{m['name']}' is not at {m['path']} — "
265
+ f"clone it there, or `fusion hub remove {m['name']}`",
266
+ file=sys.stderr,
267
+ )
223
268
  lines = []
224
269
  if a["dated"]:
225
270
  lines.append("dated:")
@@ -336,8 +381,10 @@ def build_parser() -> argparse.ArgumentParser:
336
381
  flag_as(p); flag_json(p)
337
382
  p.set_defaults(func=cmd_index)
338
383
 
339
- p = sub.add_parser("check", help="audit a bucket against the convention", description="Audit a bucket against the convention (SPEC §11). Exit 1 on errors; warnings never fail the check.")
384
+ p = sub.add_parser("check", help="audit a bucket against the convention", description="Audit a bucket against the convention (SPEC §11). Exit 1 on errors; warnings never fail the check. --hub (or running outside any bucket) audits the hub instead: every registered bucket present and answering to its name.")
340
385
  p.add_argument("path", nargs="?", help="bucket root (default: walk up from the current directory)")
386
+ p.add_argument("--hub", action="store_true",
387
+ help="audit the hub instead: every entry answers at its path")
341
388
  flag_json(p)
342
389
  p.set_defaults(func=cmd_check)
343
390
 
@@ -357,8 +404,10 @@ def build_parser() -> argparse.ArgumentParser:
357
404
 
358
405
  p = sub.add_parser("setup", help="install skills into agents; --remove undoes it",
359
406
  description="Install the bundled skills to the canonical "
360
- "skills directory and link them into every detected agent. "
361
- "Idempotent; never destroys what it did not create.")
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.")
362
411
  p.add_argument("--force", action="store_true",
363
412
  help="replace foreign entries setup would otherwise leave")
364
413
  p.add_argument("--remove", action="store_true",
@@ -43,6 +43,14 @@ MANIFEST_HEADER = (
43
43
  "|---|---|---|---|---|\n"
44
44
  )
45
45
 
46
+ GITATTRIBUTES = (
47
+ "# written by fusion new — multi-machine merges stay safe (SPEC §6)\n"
48
+ "* text=auto eol=lf\n"
49
+ "sources/** -text\n"
50
+ "LEDGER.md merge=union\n"
51
+ "sources/MANIFEST.md merge=union\n"
52
+ )
53
+
46
54
 
47
55
  class ScaffoldError(Exception):
48
56
  pass
@@ -85,6 +93,9 @@ def new_bucket(
85
93
  (root / "sources" / "MANIFEST.md").write_text(
86
94
  MANIFEST_HEADER, encoding="utf-8", newline="\n"
87
95
  )
96
+ (root / ".gitattributes").write_text(
97
+ GITATTRIBUTES, encoding="utf-8", newline="\n"
98
+ )
88
99
  ledger.append(root, actor, "created", "BUCKET.md", note="bucket born", at=at)
89
100
  indexer.write_indexes(root, actor=None)
90
101
 
@@ -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": ".codex/skills", "mode": "link",
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": ".pi/agent/skills", "mode": "link",
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
- found.append({**row, "skills_dir": home / row["skills_subdir"]})
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: