codealmanac 0.1.0.dev0__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 (192) hide show
  1. codealmanac/__init__.py +13 -0
  2. codealmanac/app.py +175 -0
  3. codealmanac/cli/__init__.py +1 -0
  4. codealmanac/cli/dispatch/__init__.py +0 -0
  5. codealmanac/cli/dispatch/admin.py +124 -0
  6. codealmanac/cli/dispatch/config.py +50 -0
  7. codealmanac/cli/dispatch/root.py +328 -0
  8. codealmanac/cli/main.py +28 -0
  9. codealmanac/cli/parser/__init__.py +0 -0
  10. codealmanac/cli/parser/admin.py +81 -0
  11. codealmanac/cli/parser/lifecycle.py +57 -0
  12. codealmanac/cli/parser/root.py +19 -0
  13. codealmanac/cli/parser/wiki.py +87 -0
  14. codealmanac/cli/render/__init__.py +0 -0
  15. codealmanac/cli/render/admin.py +191 -0
  16. codealmanac/cli/render/root.py +290 -0
  17. codealmanac/core/__init__.py +1 -0
  18. codealmanac/core/errors.py +45 -0
  19. codealmanac/core/models.py +14 -0
  20. codealmanac/core/paths.py +25 -0
  21. codealmanac/core/slug.py +7 -0
  22. codealmanac/core/text.py +5 -0
  23. codealmanac/database/__init__.py +15 -0
  24. codealmanac/database/sqlite.py +54 -0
  25. codealmanac/integrations/__init__.py +1 -0
  26. codealmanac/integrations/automation/__init__.py +3 -0
  27. codealmanac/integrations/automation/scheduler/__init__.py +5 -0
  28. codealmanac/integrations/automation/scheduler/launchd.py +163 -0
  29. codealmanac/integrations/command.py +56 -0
  30. codealmanac/integrations/harnesses/__init__.py +7 -0
  31. codealmanac/integrations/harnesses/claude/__init__.py +1 -0
  32. codealmanac/integrations/harnesses/claude/adapter.py +217 -0
  33. codealmanac/integrations/harnesses/codex/__init__.py +3 -0
  34. codealmanac/integrations/harnesses/codex/adapter.py +221 -0
  35. codealmanac/integrations/harnesses/git_status.py +49 -0
  36. codealmanac/integrations/sources/__init__.py +29 -0
  37. codealmanac/integrations/sources/filesystem/__init__.py +5 -0
  38. codealmanac/integrations/sources/filesystem/adapter.py +685 -0
  39. codealmanac/integrations/sources/filesystem/selection.py +209 -0
  40. codealmanac/integrations/sources/git/__init__.py +3 -0
  41. codealmanac/integrations/sources/git/adapter.py +132 -0
  42. codealmanac/integrations/sources/github/__init__.py +3 -0
  43. codealmanac/integrations/sources/github/adapter.py +413 -0
  44. codealmanac/integrations/sources/runtime.py +22 -0
  45. codealmanac/integrations/sources/transcripts/__init__.py +33 -0
  46. codealmanac/integrations/sources/transcripts/claude.py +61 -0
  47. codealmanac/integrations/sources/transcripts/codex.py +69 -0
  48. codealmanac/integrations/sources/transcripts/jsonl.py +84 -0
  49. codealmanac/integrations/sources/transcripts/runtime.py +387 -0
  50. codealmanac/integrations/sources/web/__init__.py +3 -0
  51. codealmanac/integrations/sources/web/adapter.py +303 -0
  52. codealmanac/integrations/updates/__init__.py +7 -0
  53. codealmanac/integrations/updates/package.py +85 -0
  54. codealmanac/integrations/workspaces/__init__.py +1 -0
  55. codealmanac/integrations/workspaces/git/__init__.py +3 -0
  56. codealmanac/integrations/workspaces/git/probe.py +128 -0
  57. codealmanac/manual/README.md +24 -0
  58. codealmanac/manual/__init__.py +19 -0
  59. codealmanac/manual/build.md +20 -0
  60. codealmanac/manual/evidence.md +23 -0
  61. codealmanac/manual/garden.md +20 -0
  62. codealmanac/manual/ingest.md +17 -0
  63. codealmanac/manual/library.py +84 -0
  64. codealmanac/manual/models.py +83 -0
  65. codealmanac/manual/pages.md +28 -0
  66. codealmanac/manual/requests.py +6 -0
  67. codealmanac/manual/sources.md +18 -0
  68. codealmanac/manual/style.md +19 -0
  69. codealmanac/prompts/__init__.py +5 -0
  70. codealmanac/prompts/base/notability.md +14 -0
  71. codealmanac/prompts/base/purpose.md +23 -0
  72. codealmanac/prompts/base/syntax.md +19 -0
  73. codealmanac/prompts/models.py +9 -0
  74. codealmanac/prompts/operations/garden.md +26 -0
  75. codealmanac/prompts/operations/ingest.md +18 -0
  76. codealmanac/prompts/renderer.py +24 -0
  77. codealmanac/prompts/requests.py +22 -0
  78. codealmanac/server/__init__.py +1 -0
  79. codealmanac/server/app.py +202 -0
  80. codealmanac/server/assets/__init__.py +1 -0
  81. codealmanac/server/assets/app.css +865 -0
  82. codealmanac/server/assets/app.js +3 -0
  83. codealmanac/server/assets/index.html +80 -0
  84. codealmanac/server/assets/viewer/api.js +30 -0
  85. codealmanac/server/assets/viewer/components.js +197 -0
  86. codealmanac/server/assets/viewer/main.js +126 -0
  87. codealmanac/server/assets/viewer/renderers.js +122 -0
  88. codealmanac/server/assets/viewer/routes.js +36 -0
  89. codealmanac/services/__init__.py +1 -0
  90. codealmanac/services/automation/__init__.py +3 -0
  91. codealmanac/services/automation/models.py +83 -0
  92. codealmanac/services/automation/ports.py +14 -0
  93. codealmanac/services/automation/requests.py +40 -0
  94. codealmanac/services/automation/service.py +294 -0
  95. codealmanac/services/config/__init__.py +17 -0
  96. codealmanac/services/config/models.py +61 -0
  97. codealmanac/services/config/requests.py +21 -0
  98. codealmanac/services/config/service.py +55 -0
  99. codealmanac/services/config/store.py +26 -0
  100. codealmanac/services/diagnostics/__init__.py +1 -0
  101. codealmanac/services/diagnostics/models.py +22 -0
  102. codealmanac/services/diagnostics/requests.py +8 -0
  103. codealmanac/services/diagnostics/service.py +283 -0
  104. codealmanac/services/harnesses/__init__.py +1 -0
  105. codealmanac/services/harnesses/models.py +104 -0
  106. codealmanac/services/harnesses/ports.py +18 -0
  107. codealmanac/services/harnesses/requests.py +19 -0
  108. codealmanac/services/harnesses/service.py +38 -0
  109. codealmanac/services/health/__init__.py +1 -0
  110. codealmanac/services/health/requests.py +8 -0
  111. codealmanac/services/health/service.py +20 -0
  112. codealmanac/services/index/__init__.py +1 -0
  113. codealmanac/services/index/models.py +135 -0
  114. codealmanac/services/index/requests.py +26 -0
  115. codealmanac/services/index/service.py +86 -0
  116. codealmanac/services/index/store.py +411 -0
  117. codealmanac/services/index/views.py +524 -0
  118. codealmanac/services/pages/__init__.py +1 -0
  119. codealmanac/services/pages/requests.py +17 -0
  120. codealmanac/services/pages/service.py +26 -0
  121. codealmanac/services/runs/__init__.py +1 -0
  122. codealmanac/services/runs/models.py +91 -0
  123. codealmanac/services/runs/requests.py +76 -0
  124. codealmanac/services/runs/service.py +86 -0
  125. codealmanac/services/runs/store.py +256 -0
  126. codealmanac/services/search/__init__.py +1 -0
  127. codealmanac/services/search/requests.py +23 -0
  128. codealmanac/services/search/service.py +31 -0
  129. codealmanac/services/sources/__init__.py +1 -0
  130. codealmanac/services/sources/models.py +126 -0
  131. codealmanac/services/sources/ports.py +30 -0
  132. codealmanac/services/sources/requests.py +76 -0
  133. codealmanac/services/sources/service.py +351 -0
  134. codealmanac/services/tagging/__init__.py +1 -0
  135. codealmanac/services/tagging/models.py +9 -0
  136. codealmanac/services/tagging/requests.py +35 -0
  137. codealmanac/services/tagging/service.py +43 -0
  138. codealmanac/services/topics/__init__.py +1 -0
  139. codealmanac/services/topics/models.py +36 -0
  140. codealmanac/services/topics/requests.py +115 -0
  141. codealmanac/services/topics/service.py +297 -0
  142. codealmanac/services/updates/__init__.py +4 -0
  143. codealmanac/services/updates/models.py +83 -0
  144. codealmanac/services/updates/ports.py +17 -0
  145. codealmanac/services/updates/requests.py +10 -0
  146. codealmanac/services/updates/service.py +113 -0
  147. codealmanac/services/viewer/__init__.py +1 -0
  148. codealmanac/services/viewer/models.py +80 -0
  149. codealmanac/services/viewer/renderer.py +89 -0
  150. codealmanac/services/viewer/requests.py +86 -0
  151. codealmanac/services/viewer/service.py +211 -0
  152. codealmanac/services/wiki/__init__.py +1 -0
  153. codealmanac/services/wiki/documents.py +83 -0
  154. codealmanac/services/wiki/frontmatter.py +94 -0
  155. codealmanac/services/wiki/frontmatter_rewrite.py +142 -0
  156. codealmanac/services/wiki/models.py +69 -0
  157. codealmanac/services/wiki/paths.py +42 -0
  158. codealmanac/services/wiki/service.py +57 -0
  159. codealmanac/services/wiki/templates.py +73 -0
  160. codealmanac/services/wiki/topics.py +266 -0
  161. codealmanac/services/wiki/wikilinks.py +58 -0
  162. codealmanac/services/workspaces/__init__.py +1 -0
  163. codealmanac/services/workspaces/models.py +124 -0
  164. codealmanac/services/workspaces/ports.py +9 -0
  165. codealmanac/services/workspaces/requests.py +82 -0
  166. codealmanac/services/workspaces/roots.py +74 -0
  167. codealmanac/services/workspaces/service.py +303 -0
  168. codealmanac/services/workspaces/store.py +127 -0
  169. codealmanac/workflows/__init__.py +1 -0
  170. codealmanac/workflows/build/__init__.py +1 -0
  171. codealmanac/workflows/build/models.py +8 -0
  172. codealmanac/workflows/build/service.py +45 -0
  173. codealmanac/workflows/garden/__init__.py +3 -0
  174. codealmanac/workflows/garden/models.py +30 -0
  175. codealmanac/workflows/garden/requests.py +22 -0
  176. codealmanac/workflows/garden/service.py +239 -0
  177. codealmanac/workflows/ingest/__init__.py +1 -0
  178. codealmanac/workflows/ingest/models.py +26 -0
  179. codealmanac/workflows/ingest/requests.py +39 -0
  180. codealmanac/workflows/ingest/service.py +302 -0
  181. codealmanac/workflows/lifecycle.py +197 -0
  182. codealmanac/workflows/sync/__init__.py +3 -0
  183. codealmanac/workflows/sync/models.py +157 -0
  184. codealmanac/workflows/sync/requests.py +63 -0
  185. codealmanac/workflows/sync/service.py +651 -0
  186. codealmanac/workflows/sync/store.py +51 -0
  187. codealmanac-0.1.0.dev0.dist-info/METADATA +248 -0
  188. codealmanac-0.1.0.dev0.dist-info/RECORD +192 -0
  189. codealmanac-0.1.0.dev0.dist-info/WHEEL +5 -0
  190. codealmanac-0.1.0.dev0.dist-info/entry_points.txt +2 -0
  191. codealmanac-0.1.0.dev0.dist-info/licenses/LICENSE.md +201 -0
  192. codealmanac-0.1.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,85 @@
1
+ import json
2
+ import subprocess
3
+ from importlib.metadata import PackageNotFoundError, distribution
4
+
5
+ from codealmanac.core.models import CodeAlmanacModel
6
+ from codealmanac.services.updates.models import (
7
+ PACKAGE_NAME,
8
+ PackageCommandResult,
9
+ PackageInstallMetadata,
10
+ )
11
+
12
+
13
+ class InstalledPackageMetadataProvider:
14
+ def read(self) -> PackageInstallMetadata:
15
+ try:
16
+ package = distribution(PACKAGE_NAME)
17
+ except PackageNotFoundError:
18
+ return PackageInstallMetadata(
19
+ version="unknown",
20
+ installer=None,
21
+ editable=False,
22
+ source_url=None,
23
+ )
24
+ direct_url = read_direct_url(package.read_text("direct_url.json"))
25
+ return PackageInstallMetadata(
26
+ version=package.version,
27
+ installer=clean_optional_text(package.read_text("INSTALLER")),
28
+ editable=direct_url.editable,
29
+ source_url=direct_url.source_url,
30
+ )
31
+
32
+
33
+ class SubprocessPackageCommandRunner:
34
+ def run(self, command: tuple[str, ...]) -> PackageCommandResult:
35
+ try:
36
+ result = subprocess.run(
37
+ command,
38
+ text=True,
39
+ capture_output=True,
40
+ check=False,
41
+ )
42
+ except OSError as error:
43
+ return PackageCommandResult(
44
+ exit_code=1,
45
+ stderr=f"{error.__class__.__name__}: {error}",
46
+ )
47
+ return PackageCommandResult(
48
+ exit_code=result.returncode,
49
+ stdout=result.stdout,
50
+ stderr=result.stderr,
51
+ )
52
+
53
+
54
+ class DirectUrlMetadata(CodeAlmanacModel):
55
+ editable: bool = False
56
+ source_url: str | None = None
57
+
58
+
59
+ def read_direct_url(raw: str | None) -> DirectUrlMetadata:
60
+ if raw is None:
61
+ return DirectUrlMetadata()
62
+ try:
63
+ payload = json.loads(raw)
64
+ except json.JSONDecodeError:
65
+ return DirectUrlMetadata()
66
+ if not isinstance(payload, dict):
67
+ return DirectUrlMetadata()
68
+ dir_info = payload.get("dir_info")
69
+ editable = False
70
+ if isinstance(dir_info, dict):
71
+ editable = dir_info.get("editable") is True
72
+ url = payload.get("url")
73
+ return DirectUrlMetadata(
74
+ editable=editable,
75
+ source_url=url if isinstance(url, str) and url.strip() else None,
76
+ )
77
+
78
+
79
+ def clean_optional_text(value: str | None) -> str | None:
80
+ if value is None:
81
+ return None
82
+ cleaned = value.strip()
83
+ if cleaned == "":
84
+ return None
85
+ return cleaned
@@ -0,0 +1 @@
1
+ """Concrete workspace-state integrations."""
@@ -0,0 +1,3 @@
1
+ from codealmanac.integrations.workspaces.git.probe import GitWorkspaceChangeProbe
2
+
3
+ __all__ = ["GitWorkspaceChangeProbe"]
@@ -0,0 +1,128 @@
1
+ import hashlib
2
+ import subprocess
3
+ from pathlib import Path
4
+
5
+ from codealmanac.services.workspaces.models import (
6
+ WorkspaceChangeSnapshot,
7
+ WorkspacePathChange,
8
+ WorkspacePathState,
9
+ )
10
+
11
+ GIT_STATUS_TIMEOUT_SECONDS = 10
12
+
13
+
14
+ class GitWorkspaceChangeProbe:
15
+ def snapshot(self, root_path: Path) -> WorkspaceChangeSnapshot:
16
+ try:
17
+ completed = subprocess.run(
18
+ (
19
+ "git",
20
+ "-C",
21
+ str(root_path),
22
+ "status",
23
+ "--porcelain=v1",
24
+ "-z",
25
+ "--untracked-files=all",
26
+ ),
27
+ text=True,
28
+ capture_output=True,
29
+ timeout=GIT_STATUS_TIMEOUT_SECONDS,
30
+ check=False,
31
+ )
32
+ except FileNotFoundError:
33
+ return unavailable_snapshot(root_path, "git not found on PATH")
34
+ except subprocess.TimeoutExpired:
35
+ return unavailable_snapshot(root_path, "git status timed out")
36
+ if completed.returncode != 0:
37
+ return unavailable_snapshot(
38
+ root_path,
39
+ first_line(completed.stderr, completed.stdout)
40
+ or f"git status exited {completed.returncode}",
41
+ )
42
+ return WorkspaceChangeSnapshot(
43
+ root_path=root_path,
44
+ available=True,
45
+ changes=tuple(
46
+ change_with_fingerprint(root_path, change)
47
+ for change in parse_git_status(completed.stdout)
48
+ ),
49
+ )
50
+
51
+
52
+ def unavailable_snapshot(root_path: Path, reason: str) -> WorkspaceChangeSnapshot:
53
+ return WorkspaceChangeSnapshot(
54
+ root_path=root_path,
55
+ available=False,
56
+ unavailable_reason=reason,
57
+ )
58
+
59
+
60
+ def parse_git_status(value: str) -> tuple[WorkspacePathChange, ...]:
61
+ changes: list[WorkspacePathChange] = []
62
+ fields = [field for field in value.split("\0") if field]
63
+ skip_next = False
64
+ for field in fields:
65
+ if skip_next:
66
+ skip_next = False
67
+ continue
68
+ if len(field) < 4:
69
+ continue
70
+ status = field[:2]
71
+ path = Path(field[3:])
72
+ changes.append(
73
+ WorkspacePathChange(
74
+ path=path,
75
+ state=state_from_status(status),
76
+ status=status,
77
+ )
78
+ )
79
+ if "R" in status or "C" in status:
80
+ skip_next = True
81
+ return tuple(changes)
82
+
83
+
84
+ def state_from_status(status: str) -> WorkspacePathState:
85
+ if "?" in status:
86
+ return WorkspacePathState.UNTRACKED
87
+ if "U" in status:
88
+ return WorkspacePathState.UNMERGED
89
+ if "R" in status:
90
+ return WorkspacePathState.RENAMED
91
+ if "C" in status:
92
+ return WorkspacePathState.COPIED
93
+ if "D" in status:
94
+ return WorkspacePathState.DELETED
95
+ if "A" in status:
96
+ return WorkspacePathState.ADDED
97
+ if "M" in status:
98
+ return WorkspacePathState.MODIFIED
99
+ if "T" in status:
100
+ return WorkspacePathState.TYPE_CHANGED
101
+ return WorkspacePathState.UNKNOWN
102
+
103
+
104
+ def change_with_fingerprint(
105
+ root_path: Path,
106
+ change: WorkspacePathChange,
107
+ ) -> WorkspacePathChange:
108
+ return change.model_copy(
109
+ update={"fingerprint": file_fingerprint(root_path / change.path)}
110
+ )
111
+
112
+
113
+ def file_fingerprint(path: Path) -> str | None:
114
+ if not path.is_file():
115
+ return None
116
+ digest = hashlib.sha256()
117
+ with path.open("rb") as handle:
118
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
119
+ digest.update(chunk)
120
+ return digest.hexdigest()
121
+
122
+
123
+ def first_line(*values: str) -> str:
124
+ for value in values:
125
+ lines = [line.strip() for line in value.splitlines() if line.strip()]
126
+ if lines:
127
+ return lines[0]
128
+ return ""
@@ -0,0 +1,24 @@
1
+ ---
2
+ title: Manual Overview
3
+ topics: []
4
+ ---
5
+
6
+ # Manual Overview
7
+
8
+ This manual defines how agents maintain a CodeAlmanac repo-owned wiki.
9
+
10
+ Prompts name the job. The manual defines the writing rules, evidence bar, page
11
+ shape, and operation-specific workflow.
12
+
13
+ Read the relevant page before editing:
14
+
15
+ - `pages.md`: what deserves a page and how pages connect.
16
+ - `evidence.md`: how claims stay grounded and how conflicts are handled.
17
+ - `style.md`: how CodeAlmanac prose should read.
18
+ - `sources.md`: how raw material relates to wiki synthesis.
19
+ - `build.md`: how to create the first useful wiki.
20
+ - `ingest.md`: how to fold new material into an existing wiki.
21
+ - `garden.md`: how to improve an existing wiki graph.
22
+
23
+ Repo-specific conventions live in `README.md` and `topics.yaml` under the
24
+ configured Almanac root.
@@ -0,0 +1,19 @@
1
+ from codealmanac.manual.library import ManualLibrary
2
+ from codealmanac.manual.models import (
3
+ ManualDocument,
4
+ ManualDocumentName,
5
+ ManualInstallResult,
6
+ ManualInventory,
7
+ ManualWorkspaceStatus,
8
+ )
9
+ from codealmanac.manual.requests import ManualReadRequest
10
+
11
+ __all__ = [
12
+ "ManualDocument",
13
+ "ManualDocumentName",
14
+ "ManualInstallResult",
15
+ "ManualInventory",
16
+ "ManualLibrary",
17
+ "ManualReadRequest",
18
+ "ManualWorkspaceStatus",
19
+ ]
@@ -0,0 +1,20 @@
1
+ ---
2
+ title: Build
3
+ topics: [manual]
4
+ ---
5
+
6
+ # Build
7
+
8
+ Build creates or refreshes the initial scaffold under the configured Almanac
9
+ root for a repo.
10
+
11
+ The goal is a usable local wiki, not a file-tree summary. A thin starter page is
12
+ acceptable at initialization, but the first real build should create a reading
13
+ path through durable subjects.
14
+
15
+ Before writing substantive pages, read `manual/README.md`, `manual/pages.md`,
16
+ `manual/evidence.md`, `manual/style.md`, and the repo-specific `README.md`
17
+ under the configured Almanac root.
18
+
19
+ A build is useful when a future agent can understand the repo faster from the
20
+ wiki than by rediscovering context from raw files and old conversations.
@@ -0,0 +1,23 @@
1
+ ---
2
+ title: Evidence
3
+ topics: [manual]
4
+ ---
5
+
6
+ # Evidence
7
+
8
+ Every durable claim needs grounding in code, docs, transcripts, PRs, local
9
+ commands, or another named source.
10
+
11
+ Authority depends on the claim:
12
+
13
+ - Code is authoritative for runtime behavior.
14
+ - Transcripts are authoritative for what was discussed.
15
+ - PRs are authoritative for review and merge context.
16
+ - The wiki is the maintained synthesis.
17
+
18
+ When evidence conflicts, state the conflict plainly or defer the claim. Do not
19
+ turn a transcript, diff, or note into source of truth for behavior the code
20
+ contradicts.
21
+
22
+ Use frontmatter `sources:` entries for material that supports the page. Use
23
+ precise file references when source code is relevant to retrieval.
@@ -0,0 +1,20 @@
1
+ ---
2
+ title: Garden
3
+ topics: [manual]
4
+ ---
5
+
6
+ # Garden
7
+
8
+ Garden improves the existing wiki graph.
9
+
10
+ Look for stale claims, duplicate pages, weak leads, missing links, broken file
11
+ references, confusing topics, unsupported claims, disconnected temporal notes,
12
+ and clusters that need hubs.
13
+
14
+ Broken page links should be resolved by linking to the right existing page,
15
+ creating a justified page, or changing the mention back to plain text.
16
+
17
+ Prefer synthesis over activity logs. Fold fragments into durable pages when
18
+ chronology is not part of the meaning.
19
+
20
+ No-op is valid when the wiki is coherent enough for the current pass.
@@ -0,0 +1,17 @@
1
+ ---
2
+ title: Ingest
3
+ topics: [manual]
4
+ ---
5
+
6
+ # Ingest
7
+
8
+ Ingest folds selected local material into an existing configured Almanac wiki.
9
+
10
+ Read the new material, then read nearby pages, backlinks, topics, and local
11
+ wiki conventions. Decide what the material changes.
12
+
13
+ Update existing pages when the subject already has a home. Create a page only
14
+ when the material reveals a durable subject that needs one.
15
+
16
+ No-op is valid when the input adds no durable wiki knowledge. If the input
17
+ exposes a graph problem, treat part of the run like Garden.
@@ -0,0 +1,84 @@
1
+ from importlib.resources import files
2
+ from pathlib import Path
3
+
4
+ from codealmanac.core.errors import ValidationFailed
5
+ from codealmanac.manual.models import (
6
+ MANUAL_DOCUMENTS,
7
+ ManualDocument,
8
+ ManualInstallResult,
9
+ ManualInventory,
10
+ ManualWorkspaceStatus,
11
+ )
12
+ from codealmanac.manual.requests import ManualReadRequest
13
+
14
+ MANUAL_PACKAGE = "codealmanac.manual"
15
+
16
+
17
+ class ManualLibrary:
18
+ def inventory(self) -> ManualInventory:
19
+ return ManualInventory(
20
+ documents=tuple(
21
+ self.read(ManualReadRequest(document=document))
22
+ for document in MANUAL_DOCUMENTS
23
+ )
24
+ )
25
+
26
+ def read(self, request: ManualReadRequest) -> ManualDocument:
27
+ resource = files(MANUAL_PACKAGE).joinpath(request.document.value)
28
+ try:
29
+ body = resource.read_text(encoding="utf-8")
30
+ except (FileNotFoundError, OSError) as error:
31
+ raise ValidationFailed(
32
+ f"cannot read bundled manual document {request.document.value}: {error}"
33
+ ) from error
34
+ return ManualDocument(
35
+ name=request.document,
36
+ relative_path=request.document.value,
37
+ body=body,
38
+ )
39
+
40
+ def install_missing(self, target_path: Path) -> ManualInstallResult:
41
+ try:
42
+ target_path.mkdir(parents=True, exist_ok=True)
43
+ copied: list[str] = []
44
+ existing: list[str] = []
45
+ for document in MANUAL_DOCUMENTS:
46
+ destination = target_path / document.value
47
+ if destination.exists():
48
+ existing.append(document.value)
49
+ continue
50
+ source = files(MANUAL_PACKAGE).joinpath(document.value)
51
+ destination.parent.mkdir(parents=True, exist_ok=True)
52
+ destination.write_bytes(source.read_bytes())
53
+ copied.append(document.value)
54
+ except OSError as error:
55
+ raise ValidationFailed(f"cannot install manual files: {error}") from error
56
+ return ManualInstallResult(
57
+ target_path=target_path,
58
+ copied=tuple(copied),
59
+ existing=tuple(existing),
60
+ )
61
+
62
+ def workspace_status(self, target_path: Path) -> ManualWorkspaceStatus:
63
+ expected = tuple(document.value for document in MANUAL_DOCUMENTS)
64
+ present: list[str] = []
65
+ changed: list[str] = []
66
+ try:
67
+ for document in MANUAL_DOCUMENTS:
68
+ workspace_file = target_path / document.value
69
+ if not workspace_file.is_file():
70
+ continue
71
+ present.append(document.value)
72
+ bundled = files(MANUAL_PACKAGE).joinpath(document.value).read_bytes()
73
+ if workspace_file.read_bytes() != bundled:
74
+ changed.append(document.value)
75
+ except OSError as error:
76
+ raise ValidationFailed(f"cannot inspect manual files: {error}") from error
77
+ missing = tuple(document for document in expected if document not in present)
78
+ return ManualWorkspaceStatus(
79
+ target_path=target_path,
80
+ expected=expected,
81
+ present=tuple(present),
82
+ missing=missing,
83
+ changed=tuple(changed),
84
+ )
@@ -0,0 +1,83 @@
1
+ from enum import StrEnum
2
+ from pathlib import Path
3
+
4
+ from pydantic import Field, field_validator
5
+
6
+ from codealmanac.core.models import CodeAlmanacModel
7
+ from codealmanac.core.text import required_text
8
+
9
+
10
+ class ManualDocumentName(StrEnum):
11
+ README = "README.md"
12
+ PAGES = "pages.md"
13
+ EVIDENCE = "evidence.md"
14
+ STYLE = "style.md"
15
+ SOURCES = "sources.md"
16
+ BUILD = "build.md"
17
+ INGEST = "ingest.md"
18
+ GARDEN = "garden.md"
19
+
20
+
21
+ MANUAL_DOCUMENTS: tuple[ManualDocumentName, ...] = (
22
+ ManualDocumentName.README,
23
+ ManualDocumentName.PAGES,
24
+ ManualDocumentName.EVIDENCE,
25
+ ManualDocumentName.STYLE,
26
+ ManualDocumentName.SOURCES,
27
+ ManualDocumentName.BUILD,
28
+ ManualDocumentName.INGEST,
29
+ ManualDocumentName.GARDEN,
30
+ )
31
+
32
+
33
+ class ManualDocument(CodeAlmanacModel):
34
+ name: ManualDocumentName
35
+ relative_path: str
36
+ body: str
37
+
38
+ @field_validator("relative_path")
39
+ @classmethod
40
+ def require_relative_path(cls, value: str) -> str:
41
+ text = required_text(value, "manual path")
42
+ if text.startswith("/") or "/../" in f"/{text}/":
43
+ raise ValueError("manual path must be relative")
44
+ return text
45
+
46
+ @field_validator("body")
47
+ @classmethod
48
+ def require_body(cls, value: str) -> str:
49
+ if not value.strip():
50
+ raise ValueError("manual body must not be empty")
51
+ return value
52
+
53
+
54
+ class ManualInventory(CodeAlmanacModel):
55
+ documents: tuple[ManualDocument, ...] = Field(min_length=1)
56
+
57
+
58
+ class ManualInstallResult(CodeAlmanacModel):
59
+ target_path: Path
60
+ copied: tuple[str, ...]
61
+ existing: tuple[str, ...]
62
+
63
+ @field_validator("copied", "existing")
64
+ @classmethod
65
+ def require_paths(cls, value: tuple[str, ...]) -> tuple[str, ...]:
66
+ return tuple(required_text(path, "manual path") for path in value)
67
+
68
+
69
+ class ManualWorkspaceStatus(CodeAlmanacModel):
70
+ target_path: Path
71
+ expected: tuple[str, ...] = Field(min_length=1)
72
+ present: tuple[str, ...]
73
+ missing: tuple[str, ...]
74
+ changed: tuple[str, ...] = ()
75
+
76
+ @property
77
+ def complete(self) -> bool:
78
+ return len(self.missing) == 0
79
+
80
+ @field_validator("expected", "present", "missing", "changed")
81
+ @classmethod
82
+ def require_paths(cls, value: tuple[str, ...]) -> tuple[str, ...]:
83
+ return tuple(required_text(path, "manual path") for path in value)
@@ -0,0 +1,28 @@
1
+ ---
2
+ title: Pages
3
+ topics: [manual]
4
+ ---
5
+
6
+ # Pages
7
+
8
+ A page is the stable home for what the wiki knows about one durable subject.
9
+
10
+ Good subjects include decisions, subsystems, external services, workflows,
11
+ incidents, invariants, failure modes, project strategy, and repeated concepts
12
+ that future agents will need again.
13
+
14
+ Do not create pages that only summarize one file, one transcript, one diff, or
15
+ one task log. Those are material used to justify a change. They are not usually
16
+ the subject of the wiki.
17
+
18
+ Prefer updating an existing page when the subject already has a home. Create a
19
+ new page when the material reveals a durable subject that would otherwise stay
20
+ buried.
21
+
22
+ Use `[[page-slug]]` links for subjects a reader may follow. Use
23
+ `[[src/path.py]]` and `[[src/path/]]` references when a page should be found by
24
+ file-aware search.
25
+
26
+ Page links are for real wiki nodes, not automatic entity markup. Link only to
27
+ an existing page or a page created in the same run. If the subject is useful
28
+ but does not yet deserve a page, mention it as plain text.
@@ -0,0 +1,6 @@
1
+ from codealmanac.core.models import CodeAlmanacModel
2
+ from codealmanac.manual.models import ManualDocumentName
3
+
4
+
5
+ class ManualReadRequest(CodeAlmanacModel):
6
+ document: ManualDocumentName
@@ -0,0 +1,18 @@
1
+ ---
2
+ title: Sources
3
+ topics: [manual]
4
+ ---
5
+
6
+ # Sources
7
+
8
+ Sources are raw material CodeAlmanac can learn from. They are not automatically
9
+ source of truth for every claim.
10
+
11
+ Selected material may include files, directories, diffs, commit ranges, PRs,
12
+ issues, web pages, notes, and local agent transcripts. The source runtime
13
+ normalizes that material before a lifecycle run.
14
+
15
+ Adding or discovering material does not imply a wiki update. The lifecycle run
16
+ decides whether the material changes durable wiki knowledge.
17
+
18
+ Keep page shape organized by subject, not by how material arrived.
@@ -0,0 +1,19 @@
1
+ ---
2
+ title: Style
3
+ topics: [manual]
4
+ ---
5
+
6
+ # Style
7
+
8
+ Write for a future coding agent that needs useful context quickly.
9
+
10
+ Use plain factual sentences. Prefer "is" over vague phrasing such as "serves
11
+ as." Avoid promotional language, speculation, and generic architecture prose
12
+ that could describe any repository.
13
+
14
+ Start with the subject and why it matters in this repo. Keep details that would
15
+ otherwise require rediscovery: names, files, commands, dates, constraints,
16
+ failure modes, and rejected alternatives.
17
+
18
+ Use prose first. Use bullets for real lists and tables for structured
19
+ comparison.
@@ -0,0 +1,5 @@
1
+ from codealmanac.prompts.models import PromptName
2
+ from codealmanac.prompts.renderer import PromptRenderer
3
+ from codealmanac.prompts.requests import RenderPromptRequest
4
+
5
+ __all__ = ["PromptName", "PromptRenderer", "RenderPromptRequest"]
@@ -0,0 +1,14 @@
1
+ # Notability
2
+
3
+ Write or edit wiki pages only when the change preserves non-obvious durable
4
+ knowledge that helps future work.
5
+
6
+ Good wiki changes capture decisions, multi-file flows, invariants, incidents,
7
+ gotchas, operational constraints, team conventions, external dependencies as
8
+ this repo uses them, or product context that shapes implementation choices.
9
+
10
+ Do not use the wiki as a scratchpad. Do not preserve unresolved intake work,
11
+ temporary question lists, raw field inventories, or routine activity logs.
12
+
13
+ No-op is valid when the available material does not justify a durable wiki
14
+ change.
@@ -0,0 +1,23 @@
1
+ # CodeAlmanac Purpose
2
+
3
+ CodeAlmanac maintains a repo-owned wiki for a codebase and the project world
4
+ around that codebase. New installs default to `almanac/`; the repo may choose a
5
+ different configured Almanac root.
6
+
7
+ The wiki is durable project memory for future coding agents. It records why the
8
+ system is shaped this way, what must not be violated, what was tried and failed,
9
+ how workflows move end to end, and what gotchas were discovered through real
10
+ work.
11
+
12
+ The code is authoritative for runtime behavior. The wiki is maintained
13
+ synthesis. Transcripts, PRs, notes, diffs, and docs are raw material that can
14
+ justify wiki changes.
15
+
16
+ The public command and product name is `codealmanac`. Do not introduce public
17
+ `almanac`, `alm`, `absorb`, or hosted CLI language.
18
+
19
+ The public CLI name is codealmanac.
20
+
21
+ Detailed wiki doctrine lives in `manual/` under the configured Almanac root.
22
+ The prompt names the job; the manual defines the page, evidence, style, source,
23
+ and operation rules.
@@ -0,0 +1,19 @@
1
+ # Wiki Syntax
2
+
3
+ Pages are Markdown files under `pages/` inside the configured Almanac root,
4
+ with YAML frontmatter. Use kebab-case slugs and stable page titles.
5
+
6
+ Use `topics:` for topic slugs. Use structured `sources:` entries for evidence
7
+ that supports non-obvious claims. Use `[[...]]` wikilinks for pages, files,
8
+ folders, and cross-wiki references.
9
+
10
+ Page wikilinks must resolve. Link only to existing page slugs or pages you
11
+ create or update in this run. If no page exists and you are not creating it,
12
+ write the name as plain text instead of leaving a broken `[[...]]` link.
13
+
14
+ Every sentence should contain a specific fact. Prefer neutral prose. Do not
15
+ speculate. Do not add promotional language.
16
+
17
+ Update only files inside the configured Almanac root unless the operation
18
+ explicitly says otherwise. Do not edit application code during lifecycle wiki
19
+ operations.
@@ -0,0 +1,9 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class PromptName(StrEnum):
5
+ BASE_PURPOSE = "base/purpose.md"
6
+ BASE_NOTABILITY = "base/notability.md"
7
+ BASE_SYNTAX = "base/syntax.md"
8
+ OPERATION_INGEST = "operations/ingest.md"
9
+ OPERATION_GARDEN = "operations/garden.md"