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.
Files changed (46) hide show
  1. fusion/__init__.py +3 -0
  2. fusion/_skills/fusion-analyst/SKILL.md +50 -0
  3. fusion/_skills/fusion-analyst/references/assess.md +12 -0
  4. fusion/_skills/fusion-analyst/references/compare.md +12 -0
  5. fusion/_skills/fusion-analyst/references/export.md +18 -0
  6. fusion/_skills/fusion-analyst/references/fusion-conventions.md +149 -0
  7. fusion/_skills/fusion-analyst/references/report.md +13 -0
  8. fusion/_skills/fusion-analyst/scripts/export.py +64 -0
  9. fusion/_skills/fusion-intake/SKILL.md +128 -0
  10. fusion/_skills/fusion-intake/references/convert.md +104 -0
  11. fusion/_skills/fusion-intake/references/delivery.md +107 -0
  12. fusion/_skills/fusion-intake/references/fusion-conventions.md +149 -0
  13. fusion/_skills/fusion-intake/references/gate.md +107 -0
  14. fusion/_skills/fusion-intake/scripts/convert.py +836 -0
  15. fusion/_skills/fusion-intake/scripts/gate.py +267 -0
  16. fusion/_skills/fusion-librarian/SKILL.md +64 -0
  17. fusion/_skills/fusion-librarian/references/archive.md +21 -0
  18. fusion/_skills/fusion-librarian/references/create.md +14 -0
  19. fusion/_skills/fusion-librarian/references/cross-reference.md +45 -0
  20. fusion/_skills/fusion-librarian/references/fusion-conventions.md +149 -0
  21. fusion/_skills/fusion-librarian/references/promote.md +24 -0
  22. fusion/_skills/fusion-librarian/references/query.md +17 -0
  23. fusion/_skills/fusion-librarian/references/reflect.md +47 -0
  24. fusion/_skills/fusion-librarian/references/restructure.md +20 -0
  25. fusion/_skills/fusion-librarian/references/tag.md +13 -0
  26. fusion/_skills/fusion-librarian/scripts/link-repair.py +371 -0
  27. fusion/_skills/fusion-planner/SKILL.md +62 -0
  28. fusion/_skills/fusion-planner/references/close.md +12 -0
  29. fusion/_skills/fusion-planner/references/create-activity.md +38 -0
  30. fusion/_skills/fusion-planner/references/fusion-conventions.md +149 -0
  31. fusion/_skills/fusion-planner/references/horizon.md +20 -0
  32. fusion/bucket.py +77 -0
  33. fusion/checker.py +248 -0
  34. fusion/cli.py +406 -0
  35. fusion/document.py +155 -0
  36. fusion/hub.py +78 -0
  37. fusion/indexer.py +75 -0
  38. fusion/ledger.py +106 -0
  39. fusion/manifest.py +33 -0
  40. fusion/scaffold.py +120 -0
  41. fusion/setup.py +300 -0
  42. fusion/views.py +111 -0
  43. fusion_cli-1.1.0.dist-info/METADATA +67 -0
  44. fusion_cli-1.1.0.dist-info/RECORD +46 -0
  45. fusion_cli-1.1.0.dist-info/WHEEL +4 -0
  46. fusion_cli-1.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,13 @@
1
+ # tag — bulk metadata, honestly reported
2
+
3
+ 1. Parse the field and value; `--scope`-like constraints (a folder, a
4
+ type) narrow the set. Value "auto" means you infer per document from
5
+ its content — say so in the report.
6
+ 2. Edit each document's frontmatter, preserving every existing key and
7
+ the body untouched. `updated:` does NOT bump (metadata, not content).
8
+ 3. Report a table: file · field · old → new.
9
+ 4. One ledger entry for the batch:
10
+ `fusion log classified "<scope> — <field>: <value> (<N> documents)"
11
+ --bucket <root> --as <you>` then `fusion index <root>` (summaries
12
+ unchanged, but titles/auroras may have moved in the index) and
13
+ `fusion check <root>`.
@@ -0,0 +1,371 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # dependencies = [
5
+ # "PyYAML>=6.0",
6
+ # ]
7
+ # ///
8
+ """fusion-librarian link repair — scan proposes, the human approves, apply
9
+ signs off.
10
+
11
+ Productizes the pattern run by hand during the 2026-07-10 delivery: 67
12
+ broken relative links repaired across two passes on a real bucket
13
+ (docs/dogfood/frictions.md row 7). `scan` never writes — it walks
14
+ library/ and activities/, finds relative links whose target does not
15
+ exist from the doc's own directory, and proposes a repair by basename
16
+ match against sources/ + the doc zones: a UNIQUE basename match is
17
+ `exact`; a unique match only after lowercase/hyphen-insensitive
18
+ normalization is `fuzzy` — never silently folded into an exact batch;
19
+ two or more candidates is `unrepairable` (never guess between them).
20
+
21
+ `apply` rewrites ONLY the pairs the human hands back in an approved
22
+ proposals file, validating every pair (doc inside library/ or
23
+ activities/, target exists, no path escapes the bucket) BEFORE writing
24
+ any of them — the intake gate's validate-before-damage discipline. It
25
+ writes no ledger entry: the gear's protocol (references/cross-reference.md)
26
+ has the operator sign one `noted` for the pass and re-run `fusion
27
+ index`/`fusion check`.
28
+
29
+ Usage:
30
+ uv run link-repair.py scan --bucket <bucket-root> > proposals.json
31
+ uv run link-repair.py apply --bucket <bucket-root> --proposals <file.json>
32
+ """
33
+ import argparse
34
+ import json
35
+ import os
36
+ import re
37
+ import sys
38
+ from datetime import datetime
39
+ from pathlib import Path
40
+
41
+ import yaml
42
+
43
+ TODAY = datetime.now().strftime("%Y-%m-%d")
44
+
45
+ # Documents that can carry broken links worth repairing — never output/
46
+ # (deliverables are frozen) and never a zone outside the doc format.
47
+ DOC_ZONES = ("library", "activities")
48
+
49
+ # Where a repair candidate may be found: the doc zones themselves (a link
50
+ # that drifted to a sibling that moved) plus sources/ (the assets/X ->
51
+ # sources/... pattern proven live).
52
+ SEARCH_ZONES = ("sources", "library", "activities")
53
+
54
+ # Registers with a single writer elsewhere — never a link-repair target
55
+ # or a document to scan for links.
56
+ SKIP_NAMES = {"MANIFEST.md", "INDEX.md"}
57
+
58
+ # `](target)` — target's first char excludes ')' and '#' so bare anchor
59
+ # links ([x](#heading)) never match; http(s)/mailto are filtered after,
60
+ # by scheme, since they share the same bracket-paren shape.
61
+ LINK_RE = re.compile(r"\]\(([^)#][^)]*)\)")
62
+ _SCHEME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.-]*:")
63
+
64
+ # Code is not prose: fenced blocks and inline code spans never carry a
65
+ # real relative link, so they're blanked out before extraction — the
66
+ # same fix as cli/src/fusion/document.py's `_blank_code` (twin
67
+ # implementation, kept independent so this skill stays self-contained).
68
+ _FENCE_OPEN_RE = re.compile(r"^(\s*)(`{3,}|~{3,})")
69
+ _INLINE_CODE_RE = re.compile(r"(`+)((?:(?!\1)[^\n])*?)\1")
70
+
71
+
72
+ def _blank_code(body: str) -> str:
73
+ """Blank fenced blocks (opener to closer inclusive, unterminated
74
+ fences swallow to EOF) and single-line inline code spans, replacing
75
+ each with equal-length whitespace so link extraction never reads
76
+ code as prose. Inline spans use a run of N backticks as delimiter
77
+ (CommonMark's literal-backtick idiom, e.g.
78
+ `` `` `code with ` backtick` `` ``), closing only on the next run of
79
+ the SAME length, so a shorter backtick run inside the span is just
80
+ content, not a terminator."""
81
+ lines = body.split("\n")
82
+ out = []
83
+ i = 0
84
+ n = len(lines)
85
+ while i < n:
86
+ opener = _FENCE_OPEN_RE.match(lines[i])
87
+ if not opener:
88
+ out.append(lines[i])
89
+ i += 1
90
+ continue
91
+ fence_char, fence_len = opener.group(2)[0], len(opener.group(2))
92
+ closer_re = re.compile(
93
+ r"^\s*" + re.escape(fence_char) + "{" + str(fence_len) + ",}\\s*$"
94
+ )
95
+ out.append(" " * len(lines[i]))
96
+ i += 1
97
+ while i < n:
98
+ out.append(" " * len(lines[i]))
99
+ closed = bool(closer_re.match(lines[i]))
100
+ i += 1
101
+ if closed:
102
+ break
103
+ blanked = "\n".join(out)
104
+ return _INLINE_CODE_RE.sub(lambda m: " " * len(m.group(0)), blanked)
105
+
106
+
107
+ class RepairError(Exception):
108
+ """Strict-writer refusal — named loudly, never silently skipped."""
109
+
110
+
111
+ # ── discovery ────────────────────────────────────────────────────────────
112
+
113
+ def _iter_files(zone_dir: Path):
114
+ for p in sorted(zone_dir.rglob("*")):
115
+ if not p.is_file() or p.name in SKIP_NAMES:
116
+ continue
117
+ rel = p.relative_to(zone_dir)
118
+ if any(part.startswith(".") for part in rel.parts):
119
+ continue
120
+ yield p
121
+
122
+
123
+ def _iter_docs(root: Path):
124
+ """Every .md in library/ and activities/, skipping dot-dirs and the
125
+ generated INDEX.md — hand-editing a register is never on the table."""
126
+ for zone in DOC_ZONES:
127
+ zone_dir = root / zone
128
+ if not zone_dir.is_dir():
129
+ continue
130
+ for p in _iter_files(zone_dir):
131
+ if p.suffix.lower() == ".md":
132
+ yield p
133
+
134
+
135
+ def _normalize(name: str) -> str:
136
+ """Lowercase, hyphen-insensitive basename key: case differences and
137
+ hyphen placement collapse to the same key (amua-map.png and
138
+ a-mua-map.png both key to 'amuamap.png') — a fuzzy match, extension
139
+ still required so unrelated files never collide."""
140
+ stem = Path(name).stem.lower().replace("-", "")
141
+ ext = Path(name).suffix.lower()
142
+ return f"{stem}{ext}"
143
+
144
+
145
+ def _index_candidates(root: Path):
146
+ """basename -> [bucket-relative paths] and normalized-basename ->
147
+ [...] over sources/ + the doc zones — the search space link-repair
148
+ is allowed to propose targets from."""
149
+ exact: dict[str, list[Path]] = {}
150
+ normalized: dict[str, list[Path]] = {}
151
+ for zone in SEARCH_ZONES:
152
+ zone_dir = root / zone
153
+ if not zone_dir.is_dir():
154
+ continue
155
+ for p in _iter_files(zone_dir):
156
+ rel = p.relative_to(root)
157
+ exact.setdefault(p.name, []).append(rel)
158
+ normalized.setdefault(_normalize(p.name), []).append(rel)
159
+ return exact, normalized
160
+
161
+
162
+ def _extract_links(text: str):
163
+ """Yield (raw_target, path_part, anchor) for every candidate relative
164
+ link — http(s)/mailto skipped by scheme, anchor-only already excluded
165
+ by LINK_RE's negative first-char class, and fenced/inline code
166
+ blanked first so code is never read as prose."""
167
+ for m in LINK_RE.finditer(_blank_code(text)):
168
+ target = m.group(1)
169
+ if _SCHEME_RE.match(target):
170
+ continue
171
+ path_part, _, anchor = target.partition("#")
172
+ if not path_part:
173
+ continue
174
+ yield target, path_part, anchor
175
+
176
+
177
+ # ── scan ─────────────────────────────────────────────────────────────────
178
+
179
+ def scan(root: Path) -> dict:
180
+ root = Path(root)
181
+ exact_idx, normalized_idx = _index_candidates(root)
182
+ proposals, unrepairable = [], []
183
+
184
+ for doc in _iter_docs(root):
185
+ doc_rel = doc.relative_to(root).as_posix()
186
+ text = doc.read_text(encoding="utf-8", errors="replace")
187
+ for raw_target, path_part, anchor in _extract_links(text):
188
+ if (doc.parent / path_part).exists():
189
+ continue # not broken
190
+
191
+ basename = Path(path_part).name
192
+ candidates = exact_idx.get(basename, [])
193
+ confidence = None
194
+ chosen = None
195
+ if len(candidates) == 1:
196
+ confidence, chosen = "exact", candidates[0]
197
+ else:
198
+ norm_candidates = normalized_idx.get(_normalize(basename), [])
199
+ if len(norm_candidates) == 1:
200
+ confidence, chosen = "fuzzy", norm_candidates[0]
201
+
202
+ if chosen is None:
203
+ unrepairable.append({"doc": doc_rel, "link": raw_target})
204
+ continue
205
+
206
+ new_path = os.path.relpath(root / chosen, start=doc.parent)
207
+ target = new_path.replace(os.sep, "/") + (f"#{anchor}" if anchor else "")
208
+ proposals.append({
209
+ "doc": doc_rel, "link": raw_target,
210
+ "target": target, "confidence": confidence,
211
+ })
212
+
213
+ return {"proposals": proposals, "unrepairable": unrepairable}
214
+
215
+
216
+ def _print_table(result: dict) -> None:
217
+ exact = [p for p in result["proposals"] if p["confidence"] == "exact"]
218
+ fuzzy = [p for p in result["proposals"] if p["confidence"] == "fuzzy"]
219
+
220
+ def _rows(items):
221
+ for p in items:
222
+ print(f" {p['doc']}: {p['link']} -> {p['target']}", file=sys.stderr)
223
+
224
+ print(f"EXACT ({len(exact)}) — safe to apply as-is", file=sys.stderr)
225
+ _rows(exact)
226
+ print(f"\nFUZZY ({len(fuzzy)}) — normalized match only, "
227
+ "confirm per group before applying", file=sys.stderr)
228
+ _rows(fuzzy)
229
+ print(f"\nUNREPAIRABLE ({len(result['unrepairable'])}) — "
230
+ "ambiguous or no candidate, never guessed", file=sys.stderr)
231
+ for u in result["unrepairable"]:
232
+ print(f" {u['doc']}: {u['link']}", file=sys.stderr)
233
+
234
+
235
+ # ── apply ────────────────────────────────────────────────────────────────
236
+
237
+ def _split_frontmatter(text: str, doc_rel: str):
238
+ if not text.startswith("---\n"):
239
+ raise RepairError(f"no frontmatter block: {doc_rel}")
240
+ try:
241
+ end = text.index("\n---", 4)
242
+ except ValueError:
243
+ raise RepairError(f"unterminated frontmatter block: {doc_rel}") from None
244
+ fm = yaml.safe_load(text[4:end])
245
+ if not isinstance(fm, dict):
246
+ raise RepairError(f"frontmatter is not a mapping: {doc_rel}")
247
+ return fm, text[end + 4:]
248
+
249
+
250
+ def _bump_updated(text: str, doc_rel: str) -> str:
251
+ fm, rest = _split_frontmatter(text, doc_rel)
252
+ fm["updated"] = TODAY
253
+ front = yaml.safe_dump(fm, sort_keys=False, allow_unicode=True,
254
+ width=2**31 - 1)
255
+ return f"---\n{front}---{rest}"
256
+
257
+
258
+ def apply_proposals(root: Path, proposals: list) -> int:
259
+ """Rewrite ONLY the given (doc, link) -> target pairs. Validates every
260
+ pair — doc under library/ or activities/, doc not a single-writer
261
+ register (SKIP_NAMES), doc exists, the literal `](link)` text is
262
+ present, target resolves inside the bucket and exists on disk, AND the
263
+ doc's frontmatter parses — BEFORE writing any file
264
+ (validate-all-before-damage, the intake gate's rule applied here). The
265
+ frontmatter check runs in phase 1 specifically so a later doc's
266
+ malformed frontmatter can never be discovered mid-write, after an
267
+ earlier doc in the same batch was already rewritten: phase 2's own
268
+ _bump_updated call stays as a safety net, but by construction it can
269
+ no longer raise."""
270
+ root = Path(root).resolve()
271
+ zone_roots = {zone: (root / zone).resolve() for zone in DOC_ZONES}
272
+
273
+ # ── phase 1: validate every pair, read but never write ───────────────
274
+ plan: dict[Path, list[tuple[str, str]]] = {}
275
+ doc_texts: dict[Path, str] = {}
276
+ frontmatter_checked: set[Path] = set()
277
+ for i, p in enumerate(proposals):
278
+ doc_rel = p["doc"]
279
+ link = p["link"]
280
+ target = p["target"]
281
+ doc_path = (root / doc_rel).resolve()
282
+ if doc_path.name in SKIP_NAMES:
283
+ raise RepairError(
284
+ f"proposals[{i}]: refuses a single-writer register, "
285
+ f"never a repair target (skip: {doc_rel!r})")
286
+ if not any(doc_path.is_relative_to(z) for z in zone_roots.values()):
287
+ raise RepairError(
288
+ f"proposals[{i}]: doc outside library/ or activities/: "
289
+ f"{doc_rel!r}")
290
+ if not doc_path.is_file():
291
+ raise RepairError(f"proposals[{i}]: doc does not exist: {doc_rel!r}")
292
+
293
+ target_path_part, _, _ = target.partition("#")
294
+ target_abs = (doc_path.parent / target_path_part).resolve()
295
+ if not target_abs.is_relative_to(root):
296
+ raise RepairError(
297
+ f"proposals[{i}]: target escapes the bucket: {target!r}")
298
+ if not target_abs.is_file():
299
+ raise RepairError(
300
+ f"proposals[{i}]: target does not exist: {target!r} "
301
+ f"(doc {doc_rel!r})")
302
+
303
+ text = doc_texts.setdefault(doc_path, doc_path.read_text(encoding="utf-8"))
304
+ if f"]({link})" not in text:
305
+ raise RepairError(
306
+ f"proposals[{i}]: link text not found verbatim in "
307
+ f"{doc_rel!r}: {link!r}")
308
+
309
+ if doc_path not in frontmatter_checked:
310
+ # Raises RepairError (unparseable/missing frontmatter) before
311
+ # any document in this batch has been written — the same
312
+ # parse _bump_updated would otherwise only attempt in phase 2,
313
+ # by which point earlier docs could already be on disk.
314
+ _split_frontmatter(text, doc_rel)
315
+ frontmatter_checked.add(doc_path)
316
+
317
+ plan.setdefault(doc_path, []).append((link, target))
318
+
319
+ # ── phase 2: execute — every pair already validated ───────────────────
320
+ changed = 0
321
+ for doc_path, edits in plan.items():
322
+ rel = doc_path.relative_to(root).as_posix()
323
+ text = doc_texts[doc_path]
324
+ new_text = text
325
+ for link, target in edits:
326
+ new_text = new_text.replace(f"]({link})", f"]({target})")
327
+ if new_text != text:
328
+ new_text = _bump_updated(new_text, rel)
329
+ doc_path.write_text(new_text, encoding="utf-8", newline="\n")
330
+ changed += 1
331
+ return changed
332
+
333
+
334
+ # ── CLI ──────────────────────────────────────────────────────────────────
335
+
336
+ def main(argv=None) -> int:
337
+ ap = argparse.ArgumentParser(
338
+ description="fusion-librarian link-repair: scan / apply")
339
+ sub = ap.add_subparsers(dest="cmd", required=True)
340
+
341
+ p = sub.add_parser("scan", help="propose repairs for broken relative "
342
+ "links in library/ and activities/")
343
+ p.add_argument("--bucket", required=True)
344
+
345
+ p = sub.add_parser("apply", help="apply an approved subset of a "
346
+ "scan's proposals")
347
+ p.add_argument("--bucket", required=True)
348
+ p.add_argument("--proposals", required=True,
349
+ help="JSON file: {\"proposals\": [...]} or a bare list")
350
+
351
+ args = ap.parse_args(argv)
352
+ root = Path(args.bucket).expanduser()
353
+
354
+ try:
355
+ if args.cmd == "scan":
356
+ result = scan(root)
357
+ _print_table(result)
358
+ print(json.dumps(result, indent=2, ensure_ascii=False))
359
+ else:
360
+ payload = json.loads(Path(args.proposals).read_text(encoding="utf-8"))
361
+ proposals = payload["proposals"] if isinstance(payload, dict) else payload
362
+ changed = apply_proposals(root, proposals)
363
+ print(json.dumps({"changed": changed}, indent=2, ensure_ascii=False))
364
+ except RepairError as exc:
365
+ print(f"link-repair: {exc}", file=sys.stderr)
366
+ return 1
367
+ return 0
368
+
369
+
370
+ if __name__ == "__main__":
371
+ raise SystemExit(main())
@@ -0,0 +1,62 @@
1
+ ---
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."
4
+ license: MIT
5
+ compatibility: "Requires the fusion CLI on PATH."
6
+ ---
7
+
8
+ # fusion-planner — the horizon
9
+
10
+ Activities are live work: folders under `activities/`, stateful documents,
11
+ honest statuses. The planner keeps the horizon true — what `fusion today`
12
+ composes tomorrow morning is this skill's report card.
13
+
14
+ Read `references/fusion-conventions.md` once per session; read the
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).
18
+
19
+ ## Pick the gear
20
+
21
+ | Signal | Gear | Load |
22
+ |---|---|---|
23
+ | new activity / plan / campaign / program / agenda | create-activity | references/create-activity.md |
24
+ | what's live / stalled / on my plate / review activities (default) | horizon | references/horizon.md |
25
+ | close / finish / wrap up an activity | close | references/close.md |
26
+
27
+ The default is **horizon** — read-mostly: it reports freely and writes
28
+ only on a yes. close moves files; reached by inference, it stops and
29
+ confirms.
30
+
31
+ ## The activity shape
32
+
33
+ ```
34
+ activities/<slug>/
35
+ ├── plan.md # the stateful heart: status, dates, aurora
36
+ └── … # notes, briefs — documents, same rules
37
+ ```
38
+
39
+ `plan.md` frontmatter: the three required fields, plus `status:`
40
+ (`active` | `done` | `dormant`) and `due:` (ISO date) when a real deadline
41
+ exists. Aurora speaks for attention: `commitments` when promised,
42
+ `focus` when it is the deep work, `collab` when shared, `explore` when
43
+ tentative. Expandability is the shape, not machinery: a campaign is an
44
+ activity whose plan has phases; an agenda is an activity whose plan is
45
+ dated items.
46
+
47
+ ## Ledger discipline
48
+
49
+ | Act | Verb |
50
+ |---|---|
51
+ | new activity | `created` |
52
+ | status change (active ⇄ dormant, honest corrections) | `noted` with the change in the note |
53
+ | close | `archived` |
54
+
55
+ Always `--bucket <root> --as <you>`, then `fusion index <root>` and
56
+ `fusion check <root>` green.
57
+
58
+ ## Never
59
+
60
+ - Never let a dead activity keep `status: active` — the horizon lies.
61
+ - Never set `due:` the human didn't state or clearly imply.
62
+ - Never hand-edit the registers.
@@ -0,0 +1,12 @@
1
+ # close — done, kept, out of the way
2
+
3
+ 1. Confirm the target activity and that the human means finished (or a
4
+ Delegation covers routine closes). State the plan in one sentence.
5
+ 2. Edit `plan.md` (and any sibling documents): `status: done`,
6
+ `aurora: archive`. Add a final Log line summarizing the outcome.
7
+ 3. Move the whole folder to `activities/archive/<slug>/`.
8
+ 4. `fusion log archived "activities/<slug>/ → activities/archive/<slug>/"
9
+ --bucket <root> --as <you>` · `fusion index <root>` ·
10
+ `fusion check <root>` green — path and aurora agree or W3 objects.
11
+ 5. If deliverables shipped from this activity, remind the human where
12
+ they live in `output/` (the analyst's ledger entries say).
@@ -0,0 +1,38 @@
1
+ # create-activity — structure the human's intent
2
+
3
+ 1. Get the essentials from the conversation: name (→ slug), what done
4
+ looks like, deadline (if promised), who else is involved. Ask only
5
+ what the human hasn't said.
6
+ 2. Choose aurora: promised-with-deadline → `commitments`; the current
7
+ deep work → `focus`; shared → `collab`; tentative → `explore`. Say
8
+ which and why in one line.
9
+ 3. Create `activities/<slug>/plan.md`:
10
+
11
+ ```markdown
12
+ ---
13
+ title: <name>
14
+ type: plan
15
+ aurora: <chosen>
16
+ status: active
17
+ created: <today>
18
+ due: <date, only if real>
19
+ ---
20
+
21
+ ## Summary
22
+
23
+ <what this activity is and what done looks like, 2–3 lines>
24
+
25
+ ---
26
+
27
+ ## Steps
28
+
29
+ - [ ] <the first concrete steps, from the conversation>
30
+
31
+ ## Log
32
+
33
+ - <today> — created.
34
+ ```
35
+
36
+ 4. `fusion log created "activities/<slug>/plan.md" --bucket <root>
37
+ --as <you>` · `fusion index <root>` · `fusion check <root>` green.
38
+ Then show the human what `fusion today` now includes.
@@ -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,20 @@
1
+ # horizon — keep today honest
2
+
3
+ Read-mostly; proposes, changes nothing without a yes.
4
+
5
+ 1. `fusion agenda` — dated items and undated actives across buckets;
6
+ `fusion today` — the attention view; `fusion status <root> --since
7
+ last-reflection` for the recent pulse of this bucket.
8
+ 2. Read `activities/INDEX.md` + each active plan's summary. For each
9
+ active activity judge: moving (recent ledger touches), **stalled**
10
+ (active but untouched — W5's territory), **expiring** (due within a
11
+ week), **done-in-fact** (finished but never closed).
12
+ 3. Report the horizon in four short lists with paths and dates. No
13
+ invented urgency: expiring means a real `due:`, stalled means real
14
+ silence.
15
+ 4. Propose the honest corrections: dormant flips, closes, due-date fixes.
16
+ On yes: edit the frontmatter, `fusion log noted "activities/<slug> —
17
+ status: active → dormant" --bucket <root> --as <you>` per flip
18
+ (several flips confirmed in one pass may share one `noted` line
19
+ naming them all) · `fusion index <root>` · `fusion check <root>`.
20
+ Closes go through the close gear.