@tophtab/homer 0.1.0

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.
@@ -0,0 +1,424 @@
1
+ #!/usr/bin/env python3
2
+ """Mechanical state helper for Homer projects."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import hashlib
8
+ import json
9
+ import re
10
+ import shutil
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+
16
+ ROOT = Path(__file__).resolve().parents[2]
17
+ STATE_PATH = ROOT / ".homer" / "state" / "chapters.json"
18
+ AUTHOR_DIRS = [ROOT / "设定", ROOT / "大纲", ROOT / "正文"]
19
+ HOMER_DIRS = [
20
+ ROOT / ".homer" / "spec",
21
+ ROOT / ".homer" / "state",
22
+ ROOT / ".homer" / "knowledge" / "author-lore",
23
+ ROOT / ".homer" / "knowledge" / "public-lore",
24
+ ROOT / ".homer" / "knowledge" / "tracking",
25
+ ROOT / ".homer" / "cache" / "context-packs",
26
+ ROOT / ".homer" / "scripts",
27
+ ROOT / ".homer" / "adapters",
28
+ ]
29
+ VALID_STATUSES = {"planned", "draft", "accepted"}
30
+ VALID_KNOWLEDGE_STATUSES = {"none", "current", "stale"}
31
+
32
+
33
+ def rel(path: Path) -> str:
34
+ return path.resolve().relative_to(ROOT).as_posix()
35
+
36
+
37
+ def sha256_file(path: Path) -> str:
38
+ h = hashlib.sha256()
39
+ with path.open("rb") as fh:
40
+ for chunk in iter(lambda: fh.read(1024 * 1024), b""):
41
+ h.update(chunk)
42
+ return f"sha256:{h.hexdigest()}"
43
+
44
+
45
+ def ensure_dirs() -> None:
46
+ for path in AUTHOR_DIRS + HOMER_DIRS:
47
+ path.mkdir(parents=True, exist_ok=True)
48
+
49
+
50
+ def empty_state() -> dict[str, Any]:
51
+ return {"schema_version": 1, "chapters": []}
52
+
53
+
54
+ def load_state() -> dict[str, Any]:
55
+ if not STATE_PATH.is_file():
56
+ return empty_state()
57
+ try:
58
+ data = json.loads(STATE_PATH.read_text(encoding="utf-8"))
59
+ except json.JSONDecodeError as exc:
60
+ raise SystemExit(f"Invalid JSON: {STATE_PATH}: {exc}") from exc
61
+ if not isinstance(data, dict):
62
+ raise SystemExit(f"Invalid state object: {STATE_PATH}")
63
+ chapters = data.setdefault("chapters", [])
64
+ if not isinstance(chapters, list):
65
+ raise SystemExit(f"Invalid chapters array: {STATE_PATH}")
66
+ data.setdefault("schema_version", 1)
67
+ return data
68
+
69
+
70
+ def save_state(data: dict[str, Any]) -> None:
71
+ STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
72
+ STATE_PATH.write_text(
73
+ json.dumps(data, ensure_ascii=False, indent=2) + "\n",
74
+ encoding="utf-8",
75
+ )
76
+
77
+
78
+ def chapter_files() -> list[Path]:
79
+ body = ROOT / "正文"
80
+ if not body.is_dir():
81
+ return []
82
+ files = [
83
+ p
84
+ for p in body.rglob("*")
85
+ if p.is_file() and p.suffix.lower() in {".md", ".txt"}
86
+ ]
87
+ return sorted(
88
+ files,
89
+ key=lambda p: (
90
+ infer_number(p) is None,
91
+ infer_number(p) or 10**9,
92
+ rel(p),
93
+ ),
94
+ )
95
+
96
+
97
+ def infer_number(path: Path) -> int | None:
98
+ name = path.stem
99
+ patterns = [
100
+ r"第\s*0*(\d+)\s*[章节章回]",
101
+ r"chapter[-_\s]*0*(\d+)",
102
+ r"ch[-_\s]*0*(\d+)",
103
+ r"^0*(\d+)[-_\s]",
104
+ r"^0*(\d+)$",
105
+ ]
106
+ for pattern in patterns:
107
+ match = re.search(pattern, name, re.IGNORECASE)
108
+ if match:
109
+ return int(match.group(1))
110
+ return None
111
+
112
+
113
+ def infer_title(path: Path, number: int | None) -> str:
114
+ title = path.stem.strip()
115
+ if number is not None:
116
+ title = re.sub(rf"^第\s*0*{number}\s*[章节章回][_ -]*", "", title)
117
+ title = re.sub(rf"^(chapter|ch)[-_\s]*0*{number}[_ -]*", "", title, flags=re.I)
118
+ title = re.sub(rf"^0*{number}[_ -]*", "", title)
119
+ return title.strip("_- ") or path.stem
120
+
121
+
122
+ def normalize_chapter(entry: dict[str, Any]) -> dict[str, Any]:
123
+ status = entry.get("status", "draft")
124
+ if status not in VALID_STATUSES:
125
+ status = "draft"
126
+ knowledge_status = entry.get("knowledge_status")
127
+ if knowledge_status not in VALID_KNOWLEDGE_STATUSES:
128
+ knowledge_status = "current" if status == "accepted" else "none"
129
+ return {
130
+ "number": entry.get("number"),
131
+ "title": entry.get("title") or "",
132
+ "file": entry.get("file") or "",
133
+ "status": status,
134
+ "content_hash": entry.get("content_hash") or "",
135
+ "knowledge_status": knowledge_status,
136
+ }
137
+
138
+
139
+ def next_available_number(existing: set[int]) -> int:
140
+ number = 1
141
+ while number in existing:
142
+ number += 1
143
+ existing.add(number)
144
+ return number
145
+
146
+
147
+ def scan_chapters(default_status: str = "draft", keep_existing: bool = True) -> dict[str, Any]:
148
+ if default_status not in VALID_STATUSES:
149
+ raise SystemExit(f"Invalid status: {default_status}")
150
+
151
+ state = load_state()
152
+ existing_by_file = {
153
+ item.get("file"): normalize_chapter(item)
154
+ for item in state.get("chapters", [])
155
+ if isinstance(item, dict) and item.get("file")
156
+ }
157
+ used_numbers: set[int] = set()
158
+ scanned: list[dict[str, Any]] = []
159
+
160
+ for path in chapter_files():
161
+ file_rel = rel(path)
162
+ old = existing_by_file.get(file_rel) if keep_existing else None
163
+ old_number = old.get("number") if old else None
164
+ if isinstance(old_number, int) and old_number not in used_numbers:
165
+ number = old_number
166
+ used_numbers.add(number)
167
+ else:
168
+ number = infer_number(path)
169
+ if number is None or number in used_numbers:
170
+ number = next_available_number(used_numbers)
171
+ else:
172
+ used_numbers.add(number)
173
+
174
+ status = old["status"] if old else default_status
175
+ knowledge_status = old["knowledge_status"] if old else (
176
+ "current" if status == "accepted" else "none"
177
+ )
178
+ scanned.append(
179
+ {
180
+ "number": number,
181
+ "title": old["title"] if old and old["title"] else infer_title(path, number),
182
+ "file": file_rel,
183
+ "status": status,
184
+ "content_hash": sha256_file(path),
185
+ "knowledge_status": knowledge_status,
186
+ }
187
+ )
188
+
189
+ state["chapters"] = sorted(scanned, key=lambda c: (c["number"], c["file"]))
190
+ return state
191
+
192
+
193
+ def find_chapter(state: dict[str, Any], target: str) -> dict[str, Any]:
194
+ normalized = target.strip()
195
+ if not normalized:
196
+ raise SystemExit("Missing chapter target")
197
+
198
+ for item in state.get("chapters", []):
199
+ if not isinstance(item, dict):
200
+ continue
201
+ chapter = normalize_chapter(item)
202
+ number = chapter.get("number")
203
+ file_name = chapter.get("file", "")
204
+ title = chapter.get("title", "")
205
+ if str(number) == normalized or file_name == normalized or Path(file_name).name == normalized or title == normalized:
206
+ return item
207
+ raise SystemExit(f"Chapter not found: {target}")
208
+
209
+
210
+ def cmd_init(args: argparse.Namespace) -> int:
211
+ ensure_dirs()
212
+ if args.scan:
213
+ state = scan_chapters(default_status=args.status, keep_existing=True)
214
+ else:
215
+ state = load_state()
216
+ save_state(state)
217
+ print_status(state)
218
+ return 0
219
+
220
+
221
+ def cmd_scan(args: argparse.Namespace) -> int:
222
+ ensure_dirs()
223
+ state = scan_chapters(default_status=args.status, keep_existing=not args.reset_status)
224
+ save_state(state)
225
+ print_status(state)
226
+ return 0
227
+
228
+
229
+ def cmd_status(_: argparse.Namespace) -> int:
230
+ state = load_state()
231
+ print_status(state)
232
+ return 0
233
+
234
+
235
+ def cmd_check(_: argparse.Namespace) -> int:
236
+ state = load_state()
237
+ changed = False
238
+ for item in state.get("chapters", []):
239
+ if not isinstance(item, dict):
240
+ continue
241
+ chapter = normalize_chapter(item)
242
+ path = ROOT / chapter["file"]
243
+ if not path.is_file():
244
+ continue
245
+ current_hash = sha256_file(path)
246
+ if chapter["content_hash"] != current_hash:
247
+ item["content_hash"] = current_hash
248
+ if chapter["status"] == "accepted":
249
+ item["knowledge_status"] = "stale"
250
+ changed = True
251
+ if changed:
252
+ save_state(state)
253
+ print_status(state)
254
+ return 0
255
+
256
+
257
+ def cmd_accept(args: argparse.Namespace) -> int:
258
+ state = load_state()
259
+ item = find_chapter(state, args.chapter)
260
+ chapter = normalize_chapter(item)
261
+ path = ROOT / chapter["file"]
262
+ if not path.is_file():
263
+ raise SystemExit(f"Chapter file not found: {chapter['file']}")
264
+ item["status"] = "accepted"
265
+ item["content_hash"] = sha256_file(path)
266
+ item["knowledge_status"] = "stale"
267
+ save_state(state)
268
+ print(f"Accepted chapter {item.get('number')}: {item.get('file')}")
269
+ return 0
270
+
271
+
272
+ def cmd_mark_current(_: argparse.Namespace) -> int:
273
+ state = load_state()
274
+ for item in state.get("chapters", []):
275
+ if not isinstance(item, dict):
276
+ continue
277
+ chapter = normalize_chapter(item)
278
+ if chapter["status"] == "accepted":
279
+ item["knowledge_status"] = "current"
280
+ save_state(state)
281
+ print_status(state)
282
+ return 0
283
+
284
+
285
+ def copy_tree_contents(src: Path, dst: Path) -> list[str]:
286
+ copied: list[str] = []
287
+ if not src.is_dir():
288
+ return copied
289
+ for path in sorted(src.rglob("*")):
290
+ if not path.is_file():
291
+ continue
292
+ if "__pycache__" in path.parts or path.suffix == ".pyc":
293
+ continue
294
+ rel_path = path.relative_to(src)
295
+ target = dst / rel_path
296
+ target.parent.mkdir(parents=True, exist_ok=True)
297
+ shutil.copy2(path, target)
298
+ copied.append(rel(target))
299
+ return copied
300
+
301
+
302
+ def cmd_generate_adapters(_: argparse.Namespace) -> int:
303
+ adapters = ROOT / ".homer" / "adapters"
304
+ copied: list[str] = []
305
+ copied.extend(copy_tree_contents(adapters / "agents", ROOT / ".agents"))
306
+ copied.extend(copy_tree_contents(adapters / "codex", ROOT / ".codex"))
307
+ for path in copied:
308
+ print(f"generated {path}")
309
+ if not copied:
310
+ print("No adapter files generated")
311
+ return 0
312
+
313
+
314
+ def cmd_hook_state(_: argparse.Namespace) -> int:
315
+ state = load_state()
316
+ counts = count_statuses(state)
317
+ stale = [
318
+ normalize_chapter(item)
319
+ for item in state.get("chapters", [])
320
+ if isinstance(item, dict)
321
+ and normalize_chapter(item)["status"] == "accepted"
322
+ and normalize_chapter(item)["knowledge_status"] == "stale"
323
+ ]
324
+ print("<homer-state>")
325
+ print(f"Project: {'initialized' if STATE_PATH.is_file() else 'not initialized'}")
326
+ print(
327
+ "Chapters: "
328
+ f"planned={counts['planned']} draft={counts['draft']} accepted={counts['accepted']}"
329
+ )
330
+ if stale:
331
+ refs = ", ".join(str(item.get("number")) for item in stale[:8])
332
+ suffix = "..." if len(stale) > 8 else ""
333
+ print(f"Knowledge stale for accepted chapters: {refs}{suffix}")
334
+ print("Route: setup/import -> homer-setup; write/polish -> homer-write; accept/rebuild -> homer-sync.")
335
+ print("</homer-state>")
336
+ return 0
337
+
338
+
339
+ def count_statuses(state: dict[str, Any]) -> dict[str, int]:
340
+ counts = {"planned": 0, "draft": 0, "accepted": 0}
341
+ for item in state.get("chapters", []):
342
+ if isinstance(item, dict):
343
+ status = normalize_chapter(item)["status"]
344
+ counts[status] += 1
345
+ return counts
346
+
347
+
348
+ def print_status(state: dict[str, Any]) -> None:
349
+ counts = count_statuses(state)
350
+ print("Homer project status")
351
+ print(f"Root: {ROOT}")
352
+ print(
353
+ "Chapters: "
354
+ f"planned={counts['planned']} draft={counts['draft']} accepted={counts['accepted']}"
355
+ )
356
+ for item in state.get("chapters", []):
357
+ if not isinstance(item, dict):
358
+ continue
359
+ chapter = normalize_chapter(item)
360
+ print(
361
+ f"- {chapter['number']:03d} "
362
+ f"[{chapter['status']}/{chapter['knowledge_status']}] "
363
+ f"{chapter['file']}"
364
+ )
365
+
366
+
367
+ def build_parser() -> argparse.ArgumentParser:
368
+ parser = argparse.ArgumentParser(description="Manage Homer project state.")
369
+ sub = parser.add_subparsers(dest="command", required=True)
370
+
371
+ init = sub.add_parser("init", help="Initialize Homer directories and state.")
372
+ init.add_argument("--scan", action="store_true", help="Scan existing chapter files.")
373
+ init.add_argument(
374
+ "--status",
375
+ choices=sorted(VALID_STATUSES),
376
+ default="draft",
377
+ help="Default status for newly scanned chapters.",
378
+ )
379
+ init.set_defaults(func=cmd_init)
380
+
381
+ scan = sub.add_parser("scan", help="Scan chapter files under 正文/.")
382
+ scan.add_argument(
383
+ "--status",
384
+ choices=sorted(VALID_STATUSES),
385
+ default="draft",
386
+ help="Default status for newly scanned chapters.",
387
+ )
388
+ scan.add_argument(
389
+ "--reset-status",
390
+ action="store_true",
391
+ help="Apply --status to all scanned chapters instead of preserving existing statuses.",
392
+ )
393
+ scan.set_defaults(func=cmd_scan)
394
+
395
+ status = sub.add_parser("status", help="Show Homer state.")
396
+ status.set_defaults(func=cmd_status)
397
+
398
+ check = sub.add_parser("check", help="Update content hashes and mark edited accepted chapters stale.")
399
+ check.set_defaults(func=cmd_check)
400
+
401
+ accept = sub.add_parser("accept", help="Mark a chapter accepted and stale until knowledge rebuild.")
402
+ accept.add_argument("chapter", help="Chapter number, title, file path, or basename.")
403
+ accept.set_defaults(func=cmd_accept)
404
+
405
+ mark_current = sub.add_parser("mark-current", help="Mark accepted chapters knowledge_status=current.")
406
+ mark_current.set_defaults(func=cmd_mark_current)
407
+
408
+ generate_adapters = sub.add_parser("generate-adapters", help="Generate .agents/.codex files from .homer/adapters.")
409
+ generate_adapters.set_defaults(func=cmd_generate_adapters)
410
+
411
+ hook_state = sub.add_parser("hook-state", help="Emit compact state for Codex hooks.")
412
+ hook_state.set_defaults(func=cmd_hook_state)
413
+
414
+ return parser
415
+
416
+
417
+ def main(argv: list[str] | None = None) -> int:
418
+ parser = build_parser()
419
+ args = parser.parse_args(argv)
420
+ return args.func(args)
421
+
422
+
423
+ if __name__ == "__main__":
424
+ raise SystemExit(main())
@@ -0,0 +1,14 @@
1
+ # Homer Hard Rules
2
+
3
+ - One project equals one book.
4
+ - `设定/`, `大纲/`, and `正文/` are author-owned.
5
+ - Author-owned files are freeform and are not modified unless the user explicitly asks.
6
+ - AI-readable knowledge is JSON under `.homer/knowledge/`.
7
+ - `accepted` chapters are canon and are not edited by default.
8
+ - `draft` chapters are editable.
9
+ - Public lore and tracking are generated only from accepted chapters.
10
+ - Draft chapters are current-task context, not long-term public knowledge.
11
+ - Writing does not read full `设定/` by default.
12
+ - Hidden author settings may be read only as relevant `author-lore` slices.
13
+ - Explicit facts, inferences, candidate ideas, conflicts, and obsolete notes must stay separate.
14
+ - After writing or polishing, Homer does not auto-accept or auto-sync.
@@ -0,0 +1,34 @@
1
+ # Homer Setup
2
+
3
+ Use setup to initialize or repair a single-book Homer project.
4
+
5
+ ## Required Outputs
6
+
7
+ - `设定/`
8
+ - `大纲/`
9
+ - `正文/`
10
+ - `.homer/workflow.md`
11
+ - `.homer/spec/`
12
+ - `.homer/state/chapters.json`
13
+ - `.homer/knowledge/author-lore/`
14
+ - `.homer/knowledge/public-lore/`
15
+ - `.homer/knowledge/tracking/`
16
+ - `.homer/cache/context-packs/`
17
+ - `.homer/scripts/`
18
+ - `.homer/adapters/`
19
+ - generated `.agents/skills/` and `.codex/` adapter files when applicable
20
+
21
+ ## Existing Chapters
22
+
23
+ - Empty `正文/`: initialize an empty chapter list.
24
+ - One chapter: ask whether it is `draft` or `accepted` unless the user has already said.
25
+ - Multiple chapters: confirm the status pattern before marking them accepted.
26
+ - Imported completed or serialized works may default to `accepted` only when the user says the import is completed/published.
27
+
28
+ Do not guess batch chapter status.
29
+
30
+ ## After Setup
31
+
32
+ Run `python3 .homer/scripts/homer.py status`.
33
+
34
+ If accepted chapters exist, use `homer-sync` to rebuild public lore and tracking.
@@ -0,0 +1,48 @@
1
+ # Homer Sync
2
+
3
+ Use this flow to accept chapters and rebuild structured knowledge.
4
+
5
+ ## Chapter Acceptance
6
+
7
+ 1. Run `python3 .homer/scripts/homer.py accept <chapter>`.
8
+ 2. Read `.homer/state/chapters.json`.
9
+ 3. Read all `accepted` chapters ordered by chapter number.
10
+ 4. Rebuild `.homer/knowledge/public-lore/` from accepted chapter text.
11
+ 5. Rebuild `.homer/knowledge/tracking/` from accepted chapter text.
12
+ 6. Run `python3 .homer/scripts/homer.py mark-current`.
13
+
14
+ ## Public Lore
15
+
16
+ Public lore is reader-known knowledge only. Store items with source chapter and evidence.
17
+
18
+ Separate item types:
19
+
20
+ - `shown_fact`
21
+ - `reader_inference`
22
+ - `character_claim`
23
+ - `rumor`
24
+ - `misdirection`
25
+
26
+ ## Tracking
27
+
28
+ Tracking stores current serial continuity extracted directly from accepted chapters:
29
+
30
+ - Current story context.
31
+ - Timeline.
32
+ - Character state and location.
33
+ - Active foreshadowing.
34
+ - Patches for accepted-canon issues that should be repaired later.
35
+
36
+ Recommended files:
37
+
38
+ - `.homer/knowledge/tracking/context.json`
39
+ - `.homer/knowledge/tracking/timeline.json`
40
+ - `.homer/knowledge/tracking/character-state.json`
41
+ - `.homer/knowledge/tracking/foreshadowing.json`
42
+ - `.homer/knowledge/tracking/patches.json`
43
+
44
+ ## Author Lore
45
+
46
+ Rebuild `.homer/knowledge/author-lore/` only when requested, during setup, or when the task needs author settings.
47
+
48
+ Source is `设定/`. Keep explicit facts separate from inferences, candidates, conflicts, and obsolete notes. Do not modify `设定/` unless explicitly requested.
@@ -0,0 +1,51 @@
1
+ # Homer Write
2
+
3
+ Use this flow for drafting, continuing, expanding, polishing, or revising chapters.
4
+
5
+ ## Required Checks
6
+
7
+ 1. Read `.homer/workflow.md`.
8
+ 2. Read `.homer/spec/hard-rules.md`.
9
+ 3. Read `.homer/state/chapters.json`.
10
+ 4. Identify the target chapter and its file under `正文/`.
11
+ 5. If the target is `accepted`, do not edit unless the user explicitly asks for a canon revision.
12
+ 6. If the target is `draft`, edit that file directly.
13
+ 7. If the target is missing, create it under `正文/` and register it as `draft`.
14
+
15
+ ## Context Selection
16
+
17
+ Read minimal context:
18
+
19
+ - User's current instruction.
20
+ - Relevant chapter outline from `大纲/`.
21
+ - Relevant public lore JSON.
22
+ - Relevant tracking JSON.
23
+ - Previous or directly related accepted chapter text only when needed.
24
+ - Relevant Homer spec rules.
25
+
26
+ Do not read full `设定/` by default. When hidden author settings are needed, read only relevant `.homer/knowledge/author-lore/` slices.
27
+
28
+ ## Editing Semantics
29
+
30
+ - `polish`, `打磨`, `优化`: refine existing draft while preserving intent.
31
+ - `续写`: continue from existing material.
32
+ - `扩写`: add detail and complete underwritten parts.
33
+ - `重写`: replace only when explicitly requested.
34
+
35
+ If intent is unclear, preserve and improve or expand rather than replacing.
36
+
37
+ ## Creative Boundary
38
+
39
+ By default, Homer may improve expression, pacing, transitions, sensory detail, action, dialogue, and chapter hooks.
40
+
41
+ Do not introduce canon-changing plot, important new characters, new world rules, relationship changes, or major foreshadowing unless the user authorizes it.
42
+
43
+ ## Baseline Style
44
+
45
+ - Long-form serialized web novel rhythm.
46
+ - Mobile-friendly paragraphs.
47
+ - Each chapter should advance events.
48
+ - Avoid pure atmosphere chapters, heavy exposition, and premature hidden-lore reveal.
49
+ - Default new chapter length is 2000-3000 Chinese characters unless overridden.
50
+
51
+ After editing, run `python3 .homer/scripts/homer.py check` so hashes and stale knowledge markers update. Tell the user the chapter remains draft.
@@ -0,0 +1,4 @@
1
+ {
2
+ "schema_version": 1,
3
+ "chapters": []
4
+ }
@@ -0,0 +1,55 @@
1
+ # Homer Workflow
2
+
3
+ Homer is a single-book web-novel assistant. One working directory equals one book project.
4
+
5
+ ## Business Flows
6
+
7
+ 1. `homer-setup`
8
+ - Initialize `.homer/` infrastructure.
9
+ - Create `设定/`, `大纲/`, and `正文/` if missing.
10
+ - Scan existing chapter files and establish `.homer/state/chapters.json`.
11
+ - Generate or refresh agent adapter files from `.homer` source.
12
+
13
+ 2. `homer-write`
14
+ - Write, continue, expand, polish, or revise draft chapters.
15
+ - Edit files directly under `正文/`.
16
+ - Keep edited chapters as `draft`.
17
+ - Do not accept chapters or rebuild long-term knowledge automatically.
18
+
19
+ 3. `homer-sync`
20
+ - Accept chapters into canon.
21
+ - Rebuild `.homer/knowledge/public-lore/` from all accepted chapters.
22
+ - Rebuild `.homer/knowledge/tracking/` from all accepted chapters.
23
+ - Rebuild `.homer/knowledge/author-lore/` only when requested or needed.
24
+
25
+ ## State Model
26
+
27
+ Chapter state lives in `.homer/state/chapters.json`.
28
+
29
+ Statuses:
30
+
31
+ - `planned`: planned but not accepted canon.
32
+ - `draft`: editable working chapter.
33
+ - `accepted`: canon. Do not edit by default.
34
+
35
+ Knowledge status:
36
+
37
+ - `none`: not part of generated long-term knowledge.
38
+ - `current`: accepted chapter hash matches generated knowledge.
39
+ - `stale`: accepted chapter changed after knowledge was generated.
40
+
41
+ ## Context Rule
42
+
43
+ Only read what would cause the target chapter to be wrong if omitted.
44
+
45
+ Default writing context includes current instruction, relevant outline, public lore, tracking, and nearby accepted chapters when needed. Default writing context excludes full `设定/` and full `author-lore`.
46
+
47
+ ## Workflow State Breadcrumbs
48
+
49
+ [workflow-state:no_project]
50
+ No initialized Homer project. If the user wants setup, initialization, writing, chapter sync, or knowledge rebuild, load `homer-setup` and run setup first.
51
+ [/workflow-state:no_project]
52
+
53
+ [workflow-state:ready]
54
+ Homer project is initialized. Route setup/import/repair to `homer-setup`; writing, polishing, continuation, expansion, or revision to `homer-write`; accepting chapters, rebuilding public lore/tracking, or rebuilding author-lore indexes to `homer-sync`.
55
+ [/workflow-state:ready]
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tophtab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.