fusion-cli 1.1.0__py3-none-any.whl
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/__init__.py +3 -0
- fusion/_skills/fusion-analyst/SKILL.md +50 -0
- fusion/_skills/fusion-analyst/references/assess.md +12 -0
- fusion/_skills/fusion-analyst/references/compare.md +12 -0
- fusion/_skills/fusion-analyst/references/export.md +18 -0
- fusion/_skills/fusion-analyst/references/fusion-conventions.md +149 -0
- fusion/_skills/fusion-analyst/references/report.md +13 -0
- fusion/_skills/fusion-analyst/scripts/export.py +64 -0
- fusion/_skills/fusion-intake/SKILL.md +128 -0
- fusion/_skills/fusion-intake/references/convert.md +104 -0
- fusion/_skills/fusion-intake/references/delivery.md +107 -0
- fusion/_skills/fusion-intake/references/fusion-conventions.md +149 -0
- fusion/_skills/fusion-intake/references/gate.md +107 -0
- fusion/_skills/fusion-intake/scripts/convert.py +836 -0
- fusion/_skills/fusion-intake/scripts/gate.py +267 -0
- fusion/_skills/fusion-librarian/SKILL.md +64 -0
- fusion/_skills/fusion-librarian/references/archive.md +21 -0
- fusion/_skills/fusion-librarian/references/create.md +14 -0
- fusion/_skills/fusion-librarian/references/cross-reference.md +45 -0
- fusion/_skills/fusion-librarian/references/fusion-conventions.md +149 -0
- fusion/_skills/fusion-librarian/references/promote.md +24 -0
- fusion/_skills/fusion-librarian/references/query.md +17 -0
- fusion/_skills/fusion-librarian/references/reflect.md +47 -0
- fusion/_skills/fusion-librarian/references/restructure.md +20 -0
- fusion/_skills/fusion-librarian/references/tag.md +13 -0
- fusion/_skills/fusion-librarian/scripts/link-repair.py +371 -0
- fusion/_skills/fusion-planner/SKILL.md +62 -0
- fusion/_skills/fusion-planner/references/close.md +12 -0
- fusion/_skills/fusion-planner/references/create-activity.md +38 -0
- fusion/_skills/fusion-planner/references/fusion-conventions.md +149 -0
- fusion/_skills/fusion-planner/references/horizon.md +20 -0
- fusion/bucket.py +77 -0
- fusion/checker.py +248 -0
- fusion/cli.py +406 -0
- fusion/document.py +155 -0
- fusion/hub.py +78 -0
- fusion/indexer.py +75 -0
- fusion/ledger.py +106 -0
- fusion/manifest.py +33 -0
- fusion/scaffold.py +120 -0
- fusion/setup.py +300 -0
- fusion/views.py +111 -0
- fusion_cli-1.1.0.dist-info/METADATA +67 -0
- fusion_cli-1.1.0.dist-info/RECORD +46 -0
- fusion_cli-1.1.0.dist-info/WHEEL +4 -0
- fusion_cli-1.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = []
|
|
5
|
+
# ///
|
|
6
|
+
"""fusion-intake Stage 1 — the deterministic gate classifier.
|
|
7
|
+
|
|
8
|
+
Hashes every file in inbox/, compares against the sources/ tree, normalizes
|
|
9
|
+
filenames, scores content similarity, and writes a gate manifest with six
|
|
10
|
+
buckets: exact_dups, near_dups, update_candidates, clean_new, containers,
|
|
11
|
+
inbox_dups. It classifies and reports — it never writes to sources/ or
|
|
12
|
+
library/. Stage 2 (the agent, references/gate.md) assigns the final
|
|
13
|
+
class and asks the human where the locked rule requires it.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
uv run <skill>/scripts/gate.py --bucket <bucket-root> [--out <path>]
|
|
17
|
+
"""
|
|
18
|
+
import argparse
|
|
19
|
+
import hashlib
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
import subprocess
|
|
23
|
+
import uuid
|
|
24
|
+
from collections import defaultdict
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
# Locked lineage thresholds (doc-converter, kept verbatim).
|
|
29
|
+
NEAR_DUP_THRESHOLD = 0.85 # best content sim >= this (and not exact) -> near-dup
|
|
30
|
+
UPDATE_SIM_FLOOR = 0.30 # [floor, near): name match -> update candidate
|
|
31
|
+
SHINGLE_K = 3 # word-shingle size for similarity
|
|
32
|
+
|
|
33
|
+
SIMILARITY_READ_CAP = 512 * 1024 # bytes of text considered for similarity — enough to catch any real re-export/update
|
|
34
|
+
|
|
35
|
+
_DATE_PREFIX = re.compile(r"^(?:\d{4}-\d{2}-\d{2}|\d{8})[_-]")
|
|
36
|
+
_SEP_RUN = re.compile(r"[\s_-]+")
|
|
37
|
+
_WORD = re.compile(r"\w+")
|
|
38
|
+
|
|
39
|
+
SKIP_NAMES = {"MANIFEST.md", "README.md"}
|
|
40
|
+
|
|
41
|
+
# Containers are delivery vehicles, not originals (skills/fusion-intake
|
|
42
|
+
# scripts/convert.py `unpack`) — the gate reports them and skips all other
|
|
43
|
+
# classification, before hashing (a 144MB zip should not be shingled).
|
|
44
|
+
CONTAINER_EXTS = {".zip", ".athena"}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def normalize_filename(name: str) -> str:
|
|
48
|
+
"""Strip a leading date prefix, lowercase, collapse separators to '-',
|
|
49
|
+
drop the extension."""
|
|
50
|
+
stem = Path(name).stem
|
|
51
|
+
stem = _DATE_PREFIX.sub("", stem)
|
|
52
|
+
stem = stem.lower()
|
|
53
|
+
stem = _SEP_RUN.sub("-", stem).strip("-")
|
|
54
|
+
return stem
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def sha256_of(path: Path) -> str:
|
|
58
|
+
h = hashlib.sha256()
|
|
59
|
+
with open(path, "rb") as fh:
|
|
60
|
+
for chunk in iter(lambda: fh.read(65536), b""):
|
|
61
|
+
h.update(chunk)
|
|
62
|
+
return h.hexdigest()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def extract_text(path: Path) -> str:
|
|
66
|
+
"""Text for similarity scoring. Binary formats decode to noise and
|
|
67
|
+
effectively compare on filename only — full content comparison is
|
|
68
|
+
Stage 2's judgment work. Reads at most SIMILARITY_READ_CAP bytes: a
|
|
69
|
+
144MB delivery must not be fully decoded just to shingle it."""
|
|
70
|
+
try:
|
|
71
|
+
with path.open("rb") as fh:
|
|
72
|
+
data = fh.read(SIMILARITY_READ_CAP)
|
|
73
|
+
return data.decode("utf-8", errors="ignore")
|
|
74
|
+
except OSError:
|
|
75
|
+
return ""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class SourceIndex:
|
|
80
|
+
by_hash: dict = field(default_factory=lambda: defaultdict(list))
|
|
81
|
+
by_name: dict = field(default_factory=lambda: defaultdict(list))
|
|
82
|
+
text_by_path: dict = field(default_factory=dict)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _iter_files(root: Path):
|
|
86
|
+
for p in sorted(root.rglob("*")):
|
|
87
|
+
if not p.is_file() or p.name in SKIP_NAMES:
|
|
88
|
+
continue
|
|
89
|
+
rel = p.relative_to(root)
|
|
90
|
+
if any(part.startswith(".") for part in rel.parts):
|
|
91
|
+
continue
|
|
92
|
+
yield p
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def index_sources(sources_dir: Path) -> SourceIndex:
|
|
96
|
+
idx = SourceIndex()
|
|
97
|
+
sources_dir = Path(sources_dir)
|
|
98
|
+
for p in _iter_files(sources_dir):
|
|
99
|
+
rel = str(p.relative_to(sources_dir))
|
|
100
|
+
idx.by_hash[sha256_of(p)].append(rel)
|
|
101
|
+
idx.by_name[normalize_filename(p.name)].append(rel)
|
|
102
|
+
idx.text_by_path[rel] = extract_text(p)
|
|
103
|
+
return idx
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _shingles(text, k: int = SHINGLE_K) -> set:
|
|
107
|
+
"""Produce word k-shingles from text. Texts below k word tokens yield NO
|
|
108
|
+
shingles—there is no linguistic evidence at sub-k thresholds by design.
|
|
109
|
+
Idempotent on an already-shingled set (cheap passthrough) so callers can
|
|
110
|
+
thread precomputed/cached shingles through the same call sites that
|
|
111
|
+
otherwise take raw text."""
|
|
112
|
+
if isinstance(text, set):
|
|
113
|
+
return text
|
|
114
|
+
tokens = _WORD.findall(text.lower())
|
|
115
|
+
if len(tokens) < k:
|
|
116
|
+
return set()
|
|
117
|
+
return {" ".join(tokens[i:i + k]) for i in range(len(tokens) - k + 1)}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def similarity(a, b) -> float:
|
|
121
|
+
"""Jaccard over word k-shingles: 0.0 disjoint .. 1.0 identical. Texts
|
|
122
|
+
under SHINGLE_K words always score 0.0—no evidence, not identity. Exact
|
|
123
|
+
duplicates are sha256's catch upstream, not this function's. Accepts
|
|
124
|
+
raw text or a precomputed shingle set (idempotent via _shingles) so
|
|
125
|
+
the gate's cached best-match path can reuse shingles across sources."""
|
|
126
|
+
sa, sb = _shingles(a), _shingles(b)
|
|
127
|
+
if not sa or not sb:
|
|
128
|
+
return 0.0
|
|
129
|
+
union = len(sa | sb)
|
|
130
|
+
return len(sa & sb) / union if union else 0.0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def git_history(path: Path, cwd: Path, limit: int = 10) -> list:
|
|
134
|
+
"""Prior-version evidence via git log --follow; [] outside a repo or on
|
|
135
|
+
any failure (liberal reader)."""
|
|
136
|
+
try:
|
|
137
|
+
out = subprocess.run(
|
|
138
|
+
["git", "log", "--follow", "--date=short",
|
|
139
|
+
f"-n{limit}", "--format=%ad\t%s", "--", str(path)],
|
|
140
|
+
cwd=str(cwd), capture_output=True, text=True, timeout=30,
|
|
141
|
+
)
|
|
142
|
+
except (OSError, subprocess.SubprocessError):
|
|
143
|
+
return []
|
|
144
|
+
if out.returncode != 0:
|
|
145
|
+
return []
|
|
146
|
+
history = []
|
|
147
|
+
for line in out.stdout.splitlines():
|
|
148
|
+
date, _, subject = line.partition("\t")
|
|
149
|
+
if date:
|
|
150
|
+
history.append({"date": date, "subject": subject})
|
|
151
|
+
return history
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _best_match(incoming_shingles: set, idx: SourceIndex, shingle_cache: dict):
|
|
155
|
+
"""Best (path, sim) over ALL sources — catches renamed near-dups.
|
|
156
|
+
Source shingles are cached on first use (shingle_cache, one dict per
|
|
157
|
+
run) so a batch with many incoming files never re-shingles the same
|
|
158
|
+
source text twice."""
|
|
159
|
+
best_path, best_sim = None, 0.0
|
|
160
|
+
for path, src_text in idx.text_by_path.items():
|
|
161
|
+
if path not in shingle_cache:
|
|
162
|
+
shingle_cache[path] = _shingles(src_text)
|
|
163
|
+
s = similarity(incoming_shingles, shingle_cache[path])
|
|
164
|
+
if s >= best_sim:
|
|
165
|
+
best_path, best_sim = path, s
|
|
166
|
+
return best_path, best_sim
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def classify_intake(inbox_dir: Path, sources_dir: Path, idx: SourceIndex) -> dict:
|
|
170
|
+
result = {"exact_dups": [], "near_dups": [],
|
|
171
|
+
"update_candidates": [], "clean_new": [], "inbox_dups": [],
|
|
172
|
+
"containers": []}
|
|
173
|
+
seen_in_batch: dict = {}
|
|
174
|
+
shingle_cache: dict = {} # source path -> shingle set, cached for this run only
|
|
175
|
+
for f in _iter_files(Path(inbox_dir)):
|
|
176
|
+
rel = str(f.relative_to(inbox_dir))
|
|
177
|
+
|
|
178
|
+
if f.suffix.lower() in CONTAINER_EXTS:
|
|
179
|
+
result["containers"].append({"incoming": rel, "size": f.stat().st_size})
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
h = sha256_of(f)
|
|
183
|
+
|
|
184
|
+
if h in seen_in_batch:
|
|
185
|
+
result["inbox_dups"].append({
|
|
186
|
+
"incoming": rel,
|
|
187
|
+
"duplicate_of": seen_in_batch[h],
|
|
188
|
+
"hash": h,
|
|
189
|
+
"auto_skip": True,
|
|
190
|
+
})
|
|
191
|
+
continue
|
|
192
|
+
seen_in_batch[h] = rel
|
|
193
|
+
|
|
194
|
+
if h in idx.by_hash:
|
|
195
|
+
result["exact_dups"].append({
|
|
196
|
+
"incoming": rel,
|
|
197
|
+
"matched_source": idx.by_hash[h][0],
|
|
198
|
+
"hash": h,
|
|
199
|
+
"auto_skip": True,
|
|
200
|
+
})
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
name_match = normalize_filename(f.name) in idx.by_name
|
|
204
|
+
incoming_shingles = _shingles(extract_text(f))
|
|
205
|
+
if incoming_shingles:
|
|
206
|
+
match_path, sim = _best_match(incoming_shingles, idx, shingle_cache)
|
|
207
|
+
else:
|
|
208
|
+
# No word-level evidence (binary/empty) — hash matching already
|
|
209
|
+
# ran above, and empty shingles can never score above 0.0, so
|
|
210
|
+
# skip the whole sources scan rather than pay for it.
|
|
211
|
+
match_path, sim = None, 0.0
|
|
212
|
+
|
|
213
|
+
if match_path is not None and sim >= NEAR_DUP_THRESHOLD:
|
|
214
|
+
result["near_dups"].append({
|
|
215
|
+
"incoming": rel, "matched_source": match_path,
|
|
216
|
+
"similarity": round(sim, 4), "auto_skip": False,
|
|
217
|
+
})
|
|
218
|
+
elif match_path is not None and sim >= UPDATE_SIM_FLOOR and name_match:
|
|
219
|
+
result["update_candidates"].append({
|
|
220
|
+
"incoming": rel, "matched_source": match_path,
|
|
221
|
+
"similarity": round(sim, 4),
|
|
222
|
+
"history": git_history(Path("sources") / match_path,
|
|
223
|
+
Path(sources_dir).parent),
|
|
224
|
+
"auto_skip": False,
|
|
225
|
+
})
|
|
226
|
+
elif match_path is not None and sim >= UPDATE_SIM_FLOOR:
|
|
227
|
+
result["near_dups"].append({
|
|
228
|
+
"incoming": rel, "matched_source": match_path,
|
|
229
|
+
"similarity": round(sim, 4), "auto_skip": False,
|
|
230
|
+
})
|
|
231
|
+
else:
|
|
232
|
+
result["clean_new"].append({"incoming": rel})
|
|
233
|
+
return result
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def main(argv=None) -> int:
|
|
237
|
+
ap = argparse.ArgumentParser(
|
|
238
|
+
description="fusion-intake Stage-1 gate classifier")
|
|
239
|
+
ap.add_argument("--bucket", required=True, help="bucket root")
|
|
240
|
+
ap.add_argument("--out", help="manifest path "
|
|
241
|
+
"(default: <bucket>/workbench/.intake/gate-<runid>.json)")
|
|
242
|
+
args = ap.parse_args(argv)
|
|
243
|
+
|
|
244
|
+
root = Path(args.bucket).expanduser().resolve()
|
|
245
|
+
inbox, sources = root / "inbox", root / "sources"
|
|
246
|
+
if not inbox.is_dir() or not sources.is_dir():
|
|
247
|
+
print(f"not a bucket (no inbox/ + sources/): {root}")
|
|
248
|
+
return 1
|
|
249
|
+
|
|
250
|
+
idx = index_sources(sources)
|
|
251
|
+
buckets = classify_intake(inbox, sources, idx)
|
|
252
|
+
manifest = {
|
|
253
|
+
"version": 1,
|
|
254
|
+
"counts": {k: len(v) for k, v in buckets.items()},
|
|
255
|
+
"buckets": buckets,
|
|
256
|
+
}
|
|
257
|
+
out = Path(args.out) if args.out else (
|
|
258
|
+
root / "workbench" / ".intake" / f"gate-{uuid.uuid4().hex[:12]}.json")
|
|
259
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
260
|
+
out.write_text(json.dumps(manifest, indent=2), encoding="utf-8",
|
|
261
|
+
newline="\n")
|
|
262
|
+
print(f"gate: {manifest['counts']} -> {out}")
|
|
263
|
+
return 0
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
if __name__ == "__main__":
|
|
267
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
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."
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: "Requires the fusion CLI on PATH."
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# fusion-librarian — the owner
|
|
9
|
+
|
|
10
|
+
The agent is the librarian: not a search box, an accountable owner. Clean,
|
|
11
|
+
bold, opinionated structures — and every structural act signed in the
|
|
12
|
+
ledger with its reasons.
|
|
13
|
+
|
|
14
|
+
Read `references/fusion-conventions.md` once per session. Before any gear:
|
|
15
|
+
read the bucket's `BUCKET.md` — `### Rules` bind how this bucket files
|
|
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).
|
|
19
|
+
|
|
20
|
+
## Pick the gear
|
|
21
|
+
|
|
22
|
+
Explicit intent beats inference. Bare or ambiguous intent defaults to
|
|
23
|
+
**query** — the one gear that changes nothing.
|
|
24
|
+
|
|
25
|
+
| Signal | Gear | Load |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| find / search / where is / what do we know about | query (default) | references/query.md |
|
|
28
|
+
| create / new document / write down | create | references/create.md |
|
|
29
|
+
| tag / label / set field across docs | tag | references/tag.md |
|
|
30
|
+
| what links to / related / map connections | cross-reference | references/cross-reference.md |
|
|
31
|
+
| promote / finalize / move out of workbench | promote | references/promote.md |
|
|
32
|
+
| archive / put away (documents — a finished *activity* closes via fusion-planner) | archive | references/archive.md |
|
|
33
|
+
| reorganize / restructure / new taxonomy | restructure | references/restructure.md |
|
|
34
|
+
| reflect / reflection / review the bucket | reflect | references/reflect.md |
|
|
35
|
+
|
|
36
|
+
Destructive gears — promote, archive, restructure — are never reached by
|
|
37
|
+
near-miss inference: if you arrived at one without the user naming it,
|
|
38
|
+
stop and confirm by name. reflect proposes destructive acts but executes
|
|
39
|
+
them only on a yes (or a standing delegation).
|
|
40
|
+
|
|
41
|
+
## Ledger discipline (which hat, which verb)
|
|
42
|
+
|
|
43
|
+
| Gear | Verb logged |
|
|
44
|
+
|---|---|
|
|
45
|
+
| create | `created` |
|
|
46
|
+
| tag | `classified` |
|
|
47
|
+
| promote | `promoted` |
|
|
48
|
+
| archive | `archived` |
|
|
49
|
+
| restructure | `restructured` — always with a `--note` giving the reasons |
|
|
50
|
+
| reflect | `reflected` to sign the cycle; `noted` for each convention change |
|
|
51
|
+
| query | nothing — reading is free |
|
|
52
|
+
| cross-reference | nothing — unless approved link edits are applied, then `noted` |
|
|
53
|
+
|
|
54
|
+
All writes via `fusion log … --bucket <root> --as <you>`. After any gear
|
|
55
|
+
that adds, moves, or edits documents: `fusion index <root>`, then
|
|
56
|
+
`fusion check <root>` — green before you report done.
|
|
57
|
+
|
|
58
|
+
## Never
|
|
59
|
+
|
|
60
|
+
- Never hand-edit `LEDGER.md`, `INDEX.md`, or `MANIFEST.md`.
|
|
61
|
+
- Never touch `sources/` — originals are immutable.
|
|
62
|
+
- Never restructure or archive without either a yes or a written
|
|
63
|
+
delegation in BUCKET.md.
|
|
64
|
+
- Never leave `fusion check` red behind you.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# archive — out of the way, never out of reach
|
|
2
|
+
|
|
3
|
+
Path is the truth, aurora is the signal — both, always, in one move.
|
|
4
|
+
|
|
5
|
+
1. Confirm the target and the authority: explicit ask, or a standing
|
|
6
|
+
delegation in BUCKET.md `### Delegations` that covers this case
|
|
7
|
+
(cite it in the note when you use it).
|
|
8
|
+
2. Move the document to `<its-zone>/archive/<same-folder-structure>/`.
|
|
9
|
+
3. Repair what pointed at it: run `uv run <skill>/scripts/link-repair.py
|
|
10
|
+
scan --bucket <root>` and apply exact-confidence repairs per the
|
|
11
|
+
sweep protocol in [cross-reference.md](cross-reference.md) (or a
|
|
12
|
+
standing delegation); fuzzy always asks.
|
|
13
|
+
4. Edit its frontmatter: `aurora: archive`.
|
|
14
|
+
5. `fusion log archived "<zone>/<old> → <zone>/archive/<new>"
|
|
15
|
+
[--note "delegation: <rule>"] --bucket <root> --as <you>` ·
|
|
16
|
+
`fusion index <root>` · `fusion check <root>` — the checker's W3
|
|
17
|
+
watches exactly this agreement.
|
|
18
|
+
|
|
19
|
+
Activities are the planner's: closing or archiving one is
|
|
20
|
+
fusion-planner's close gear (it also writes the final Log line in
|
|
21
|
+
plan.md). This gear archives library/ and output/ documents.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# create — a document born conformant
|
|
2
|
+
|
|
3
|
+
1. Establish title, type, aurora, and destination zone/folder. Infer type
|
|
4
|
+
and folder from the bucket's existing taxonomy and BUCKET.md Rules;
|
|
5
|
+
ask only when genuinely ambiguous. Aurora is the human-attention call —
|
|
6
|
+
propose, and let an explicit instruction override. Before writing,
|
|
7
|
+
triage `library/INDEX.md` for an existing document on the same
|
|
8
|
+
subject — extending or reconciling one beats creating a twin; when a
|
|
9
|
+
candidate exists, propose the edit instead and let the human choose.
|
|
10
|
+
2. Write the document: the three required fields (plus `created:` today,
|
|
11
|
+
`tags:` when useful), `## Summary` (2–3 lines), `---`, then the body.
|
|
12
|
+
Filename: lowercase-hyphen slug, ≤60 chars.
|
|
13
|
+
3. `fusion log created "<zone>/<path>" --bucket <root> --as <you>` ·
|
|
14
|
+
`fusion index <root>` · `fusion check <root>` green.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# cross-reference — map what relates to what
|
|
2
|
+
|
|
3
|
+
Read-only. For a target document or topic:
|
|
4
|
+
|
|
5
|
+
1. Extract its entities (names, projects, products, periods).
|
|
6
|
+
2. Sweep the bucket: direct mentions (grep), shared frontmatter values
|
|
7
|
+
(type, tags, aurora), thematic kinship (summaries that talk about the
|
|
8
|
+
same thing).
|
|
9
|
+
3. Report three buckets, every line carrying a path:
|
|
10
|
+
**Direct references** · **Shared attributes** · **Thematic connections**.
|
|
11
|
+
4. Offer (don't apply) link edits: "these two documents should point at
|
|
12
|
+
each other" — applying them is a content change the user approves.
|
|
13
|
+
On a yes: edit, then sign the write —
|
|
14
|
+
`fusion log noted "<path> — cross-linked to <other>" --bucket <root>
|
|
15
|
+
--as <you>` — then `fusion index <root>`, `fusion check <root>`.
|
|
16
|
+
|
|
17
|
+
## Repair sweep — broken links, scan proposes, you approve, apply signs off
|
|
18
|
+
|
|
19
|
+
`scripts/link-repair.py` productizes the pattern above for the mechanical
|
|
20
|
+
case: a link that used to resolve and no longer does (a document moved, a
|
|
21
|
+
source got re-filed). It never guesses between candidates — the human
|
|
22
|
+
gate stays in the middle.
|
|
23
|
+
|
|
24
|
+
1. `uv run <skill>/scripts/link-repair.py scan --bucket <root> >
|
|
25
|
+
proposals.json` — walks `library/` and `activities/`, tests every
|
|
26
|
+
relative link from the doc's own directory, and for each broken one
|
|
27
|
+
searches `sources/` and the doc zones by basename. JSON goes to
|
|
28
|
+
stdout, a human-readable table to stderr: **EXACT** (unique basename
|
|
29
|
+
match — safe to apply as-is), **FUZZY** (unique only after
|
|
30
|
+
lowercase/hyphen-insensitive normalization — confirm per group, never
|
|
31
|
+
silently folded in with the exact ones), **UNREPAIRABLE** (zero or
|
|
32
|
+
two-or-more candidates — never guessed, hand-fix or leave it).
|
|
33
|
+
2. Show the human the table. Exact and fuzzy are always separated; a
|
|
34
|
+
fuzzy group is applied only on an explicit yes.
|
|
35
|
+
3. Edit `proposals.json` down to the approved subset (delete the
|
|
36
|
+
proposals nobody confirmed; `unrepairable` entries are informational,
|
|
37
|
+
never fed to apply).
|
|
38
|
+
4. `uv run <skill>/scripts/link-repair.py apply --bucket <root>
|
|
39
|
+
--proposals proposals.json` — rewrites only the approved pairs,
|
|
40
|
+
bumps (or adds) `updated:` on the documents it touched, prints the
|
|
41
|
+
count. It writes no ledger entry.
|
|
42
|
+
5. Sign the pass yourself: `fusion log noted "link-repair sweep — <N>
|
|
43
|
+
links repaired (<E> exact, <F> fuzzy)" --bucket <root> --as <you>` —
|
|
44
|
+
then `fusion index <root>`, `fusion check <root>` green before you
|
|
45
|
+
report done.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# The Fusion Conventions — operator's card
|
|
2
|
+
|
|
3
|
+
> Carried byte-identical by all four Fusion skills. SPEC.md in the Fusion
|
|
4
|
+
> repository is normative; this card is the working summary. When they
|
|
5
|
+
> disagree, SPEC.md wins and this card has a bug.
|
|
6
|
+
|
|
7
|
+
**The contract:** the human judges, the AI operates, the files remember.
|
|
8
|
+
|
|
9
|
+
**Liberal reader, strict writer.** Never refuse to read a bucket because
|
|
10
|
+
something is missing or unknown. Never write into `library/`, `activities/`,
|
|
11
|
+
`output/`, or a register without satisfying every rule below first.
|
|
12
|
+
|
|
13
|
+
## The bucket
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
<bucket>/
|
|
17
|
+
├── BUCKET.md # identity card + learned conventions
|
|
18
|
+
├── LEDGER.md # append-only collaboration record
|
|
19
|
+
├── inbox/ # drop zone — things arrive, nothing lives here
|
|
20
|
+
├── sources/ # immutable originals + MANIFEST.md
|
|
21
|
+
├── library/ # settled knowledge — documents
|
|
22
|
+
├── activities/ # live work — documents + status
|
|
23
|
+
├── workbench/ # ephemeral human+AI space — NO format rules
|
|
24
|
+
└── output/ # finished deliverables — documents
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- `sources/` is immutable: never modify, rename, or delete a registered file.
|
|
28
|
+
- `workbench/` has no rules; leaving it (promotion) is a deliberate,
|
|
29
|
+
ledger-logged act.
|
|
30
|
+
- Fusion holds knowledge and work, never media or code — documents point
|
|
31
|
+
(`resource:`) at big things, they never swallow them.
|
|
32
|
+
- `output/` may also hold non-markdown deliverable files (exports); their
|
|
33
|
+
names are still lowercase-hyphen slugs with a lowercase extension.
|
|
34
|
+
|
|
35
|
+
## Before acting — always
|
|
36
|
+
|
|
37
|
+
1. Read `BUCKET.md`: the identity card, then `## Conventions` — `### Rules`
|
|
38
|
+
are how this bucket works; `### Delegations` are your standing autonomy
|
|
39
|
+
grants. They bind you.
|
|
40
|
+
2. Triage through `library/INDEX.md` and `activities/INDEX.md` plus document
|
|
41
|
+
summaries before opening bodies.
|
|
42
|
+
|
|
43
|
+
## The document format
|
|
44
|
+
|
|
45
|
+
Every `.md` in `library/`, `activities/`, `output/` (except INDEX.md):
|
|
46
|
+
|
|
47
|
+
```markdown
|
|
48
|
+
---
|
|
49
|
+
title: Human-readable name
|
|
50
|
+
type: what-it-is # open vocabulary, curated per bucket
|
|
51
|
+
aurora: library # one of the eight — closed set
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Summary
|
|
55
|
+
|
|
56
|
+
Two or three lines a human or agent reads in two seconds.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
Full body. Cross-links are plain relative markdown links.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- Required: `title`, `type`, `aurora` — exactly three.
|
|
64
|
+
- Optional: `tags`, `created`/`updated` (ISO dates), `due` (ISO date the
|
|
65
|
+
thing falls due — `fusion agenda` surfaces it), `source` (path into
|
|
66
|
+
`sources/`), `resource` (URI or bucket path of the thing this document
|
|
67
|
+
describes), `status` (`active`|`done`|`dormant`, activities only),
|
|
68
|
+
`data_sources` (paths list, output only).
|
|
69
|
+
- Body MUST be summary-first: `## Summary`, the lines, a `---` separator,
|
|
70
|
+
then everything else.
|
|
71
|
+
- Filenames: lowercase, hyphen-separated, `.md`, stem ≤60 chars.
|
|
72
|
+
- Preserve frontmatter keys you don't recognize.
|
|
73
|
+
|
|
74
|
+
## Aurora — the eight (closed)
|
|
75
|
+
|
|
76
|
+
| Aurora | Meaning |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `commitments` | Obligations, promises, deadlines |
|
|
79
|
+
| `focus` | Deep work, what deserves full attention |
|
|
80
|
+
| `ops` | Operations, process, the recurring |
|
|
81
|
+
| `collab` | Shared work, other people involved |
|
|
82
|
+
| `life` | Personal, wellbeing, the non-work |
|
|
83
|
+
| `explore` | Curiosity, research, the not-yet-settled |
|
|
84
|
+
| `archive` | Done, kept, out of the way |
|
|
85
|
+
| `library` | Reference, the settled knowledge |
|
|
86
|
+
|
|
87
|
+
Aurora says what a document means for the human's attention — never invent
|
|
88
|
+
a ninth value.
|
|
89
|
+
|
|
90
|
+
## Archive
|
|
91
|
+
|
|
92
|
+
No archive zone: archived items move to an `archive/` subfolder inside their
|
|
93
|
+
zone AND take `aurora: archive`. Path is the truth, aurora is the signal —
|
|
94
|
+
both, always.
|
|
95
|
+
|
|
96
|
+
## The registers — single writers, no exceptions
|
|
97
|
+
|
|
98
|
+
| File | Only writer | Your move |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `LEDGER.md` | `fusion log` | `fusion log <verb> "<object>" [--note "…"] --as <you>` |
|
|
101
|
+
| `library/INDEX.md`, `activities/INDEX.md` | `fusion index` | run it after any add/move/edit that changes titles or summaries |
|
|
102
|
+
| `sources/MANIFEST.md` | fusion-intake's `scripts/convert.py` | everything enters `sources/` through the intake gate |
|
|
103
|
+
|
|
104
|
+
Never edit these three files by hand — not with an editor tool, not with
|
|
105
|
+
shell. The ledger verbs (closed set of eleven): `created`, `converted`,
|
|
106
|
+
`classified`, `indexed`, `moved`, `promoted`, `archived`, `restructured`,
|
|
107
|
+
`shipped`, `reflected`, `noted`. Sign with your agent name (`--as claude`,
|
|
108
|
+
or set `FUSION_ACTOR`).
|
|
109
|
+
|
|
110
|
+
## The CLI crib
|
|
111
|
+
|
|
112
|
+
| Command | What it does |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `fusion new <path>` | scaffold a conformant bucket |
|
|
115
|
+
| `fusion hub [add\|remove]` | list / register / retire buckets |
|
|
116
|
+
| `fusion log <verb> <object>` | append a signed ledger entry |
|
|
117
|
+
| `fusion index` | regenerate INDEX files (logs `indexed` when changed) |
|
|
118
|
+
| `fusion check [path]` | conformance: errors, warnings, honest exit codes |
|
|
119
|
+
| `fusion status [--since …]` | one bucket at a glance |
|
|
120
|
+
| `fusion today` | the composed morning across all hub buckets |
|
|
121
|
+
| `fusion agenda` | dated + active items across buckets |
|
|
122
|
+
| `fusion setup` | install/refresh the skills into detected agents |
|
|
123
|
+
|
|
124
|
+
All take `--json`. `--since last-reflection` scopes to the current
|
|
125
|
+
reflection window. **Exit gate for every skill scenario: `fusion check`
|
|
126
|
+
green before you call the work done.**
|
|
127
|
+
|
|
128
|
+
## When you're blocked
|
|
129
|
+
|
|
130
|
+
- `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.
|
|
132
|
+
Never imitate the notary by hand: no register writes while the CLI is
|
|
133
|
+
missing.
|
|
134
|
+
- `fusion check` red and you cannot fix it: stop, show the findings
|
|
135
|
+
verbatim, leave the bucket as it stands, and sign nothing that claims
|
|
136
|
+
the work is done. A bucket is a git repo — nothing is unrecoverable.
|
|
137
|
+
- The human rejects a proposal: that is a result, not a failure. Record
|
|
138
|
+
it if the gear's protocol says to (`noted`), and move on.
|
|
139
|
+
|
|
140
|
+
## The four accountabilities
|
|
141
|
+
|
|
142
|
+
| Skill | Owns |
|
|
143
|
+
|---|---|
|
|
144
|
+
| fusion-intake | The gate. Everything that enters, enters through it. |
|
|
145
|
+
| fusion-librarian | The order. Placement, curation, restructuring, reflection. |
|
|
146
|
+
| fusion-planner | The horizon. Activities, agendas, what today looks like. |
|
|
147
|
+
| fusion-analyst | The output. Deliverables that cite their sources. |
|
|
148
|
+
|
|
149
|
+
One skill, one accountability — the ledger says which hat was worn.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# promote — leaving the workbench is a deliberate act
|
|
2
|
+
|
|
3
|
+
workbench/ has no rules; the zones do. Promotion is the moment the rules
|
|
4
|
+
attach.
|
|
5
|
+
|
|
6
|
+
Pre-flight (all three, before anything moves):
|
|
7
|
+
1. Explicit invocation confirmed — arrived here by inference? Stop, ask.
|
|
8
|
+
2. Source is in `workbench/`; destination zone/folder named or clearly
|
|
9
|
+
inferable from the document's nature (`library/` knowledge,
|
|
10
|
+
`activities/` live work, `output/` deliverables). Ask if unclear.
|
|
11
|
+
3. State the plan in one sentence: "Promoting workbench/<x> to
|
|
12
|
+
<zone>/<folder>/<slug>.md."
|
|
13
|
+
|
|
14
|
+
Validate BEFORE moving (strict writer — a failed check stops the
|
|
15
|
+
promotion, nothing moves):
|
|
16
|
+
- frontmatter parses; `title`, `type`, `aurora` present; aurora one of
|
|
17
|
+
the eight; summary-first body; slug filename ≤60.
|
|
18
|
+
- activities documents: `status` present. output documents: encourage
|
|
19
|
+
`data_sources`.
|
|
20
|
+
Fix what the user wants fixed, or leave it in the workbench.
|
|
21
|
+
|
|
22
|
+
Then: move the file (Write new + delete old, or `mv`), then
|
|
23
|
+
`fusion log promoted "workbench/<x> → <zone>/<path>" --bucket <root>
|
|
24
|
+
--as <you>` · `fusion index <root>` · `fusion check <root>` green.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# query — answer from the bucket, cited
|
|
2
|
+
|
|
3
|
+
Read-only. Changes nothing, logs nothing.
|
|
4
|
+
|
|
5
|
+
1. Parse the question: entities, concepts, constraints (type, aurora,
|
|
6
|
+
dates, status).
|
|
7
|
+
2. Triage via `library/INDEX.md` + `activities/INDEX.md` — titles and
|
|
8
|
+
summary lines first. `fusion status` helps scope what exists.
|
|
9
|
+
3. Grep frontmatter for structured hits (`type:`, `aurora:`, `tags:`),
|
|
10
|
+
then content for the rest.
|
|
11
|
+
4. Read the most relevant documents — summaries first, bodies only for
|
|
12
|
+
the finalists (keep it under ~10 full reads).
|
|
13
|
+
5. Answer with a `## Sources` table: document path · what it contributed.
|
|
14
|
+
Every claim cites a path. If the bucket is silent on part of the
|
|
15
|
+
question, say what is known and what is missing — never fill gaps
|
|
16
|
+
from your own general knowledge without labelling it as outside the
|
|
17
|
+
bucket.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# reflect — the metabolism (SPEC §10)
|
|
2
|
+
|
|
3
|
+
The files remember → you reflect → the human judges → the system learns.
|
|
4
|
+
Run on the bucket's `reflection_cadence`, or when asked.
|
|
5
|
+
|
|
6
|
+
## 1. Introspect
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
fusion log --since last-reflection --bucket <root> # what happened
|
|
10
|
+
fusion status <root> --since last-reflection # counts, auroras…
|
|
11
|
+
fusion check <root> # drift: stale INDEX, W1 inbox age…
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Read the window's ledger like telemetry: what got converted, promoted,
|
|
15
|
+
touched — and what never did.
|
|
16
|
+
|
|
17
|
+
## 2. Curate & prune — the proposal list
|
|
18
|
+
|
|
19
|
+
Propose, with paths and evidence, whichever apply:
|
|
20
|
+
- workbench items older than the window → promote or expire;
|
|
21
|
+
- activities with `status: active` and no ledger touch → `dormant` or
|
|
22
|
+
archive (W5 already names them);
|
|
23
|
+
- superseded or stale library documents → `archive/`;
|
|
24
|
+
- duplicates to merge, fat documents to split;
|
|
25
|
+
- summaries that no longer match their bodies;
|
|
26
|
+
- taxonomy that stopped serving → a restructure proposal;
|
|
27
|
+
- rules the window taught: "we always filed X under Y — make it a Rule?"
|
|
28
|
+
- delegations earned: acts the human approved repeatedly this window.
|
|
29
|
+
|
|
30
|
+
## 3. Judge
|
|
31
|
+
|
|
32
|
+
Present the list. Destructive and archival acts need a yes — EXCEPT where
|
|
33
|
+
`### Delegations` already grants it (cite the delegation, act, and note
|
|
34
|
+
it). Never bundle: the human can take proposal 3 and refuse proposal 4.
|
|
35
|
+
|
|
36
|
+
## 4. Learn & sign
|
|
37
|
+
|
|
38
|
+
- Approved rule/delegation changes: edit `BUCKET.md ## Conventions`, one
|
|
39
|
+
`fusion log noted "BUCKET.md — <the change>" --bucket <root> --as <you>`
|
|
40
|
+
per change.
|
|
41
|
+
- Execute approved acts via their own gears (archive, restructure…) so
|
|
42
|
+
each carries its own verb.
|
|
43
|
+
- Sign the cycle LAST:
|
|
44
|
+
`fusion log reflected "<bucket> — <N> proposals, <M> adopted"
|
|
45
|
+
--bucket <root> --as <you>`
|
|
46
|
+
- Episodic reports are ephemeral: the proposal list lives in workbench/
|
|
47
|
+
and can die there; only conventions and ledger entries persist.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# restructure — ownership means showing your reasons
|
|
2
|
+
|
|
3
|
+
The librarian's distinctive power: reorganizing the bucket when the
|
|
4
|
+
taxonomy stopped serving. Also the most destructive gear — so it runs as
|
|
5
|
+
propose → confirm → execute → sign.
|
|
6
|
+
|
|
7
|
+
1. **Propose.** Read the zone's INDEX + folder shape. Write the plan:
|
|
8
|
+
every move (`old → new`), every folder created or retired, and the
|
|
9
|
+
REASON in one paragraph. Check `BUCKET.md ### Rules` — a restructure
|
|
10
|
+
that contradicts a Rule needs the Rule changed first (that is a
|
|
11
|
+
reflect-gear conversation).
|
|
12
|
+
2. **Confirm.** The human says yes (or a Delegation explicitly covers
|
|
13
|
+
it — rare for restructures; cite it).
|
|
14
|
+
3. **Execute.** Move files; update relative links in affected documents
|
|
15
|
+
(grep for the old paths); keep filenames conformant.
|
|
16
|
+
4. **Sign.** One ledger entry for the operation:
|
|
17
|
+
`fusion log restructured "<zone>/<scope>" --note "<the reason>"
|
|
18
|
+
--bucket <root> --as <you>` — the note is not optional here. Then
|
|
19
|
+
`fusion index <root>` and `fusion check <root>` (W4 watches the links
|
|
20
|
+
you just moved).
|