gator-command 1.0.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.
- gator_command/__init__.py +2 -0
- gator_command/cli.py +137 -0
- gator_command/scripts/crawler.py +633 -0
- gator_command/scripts/dashboard/dashboard.css +982 -0
- gator_command/scripts/dashboard/dashboard.html +84 -0
- gator_command/scripts/dashboard/dashboard.js +419 -0
- gator_command/scripts/dashboard/views/audit.js +270 -0
- gator_command/scripts/dashboard/views/fleet.js +307 -0
- gator_command/scripts/dashboard/views/repo.js +599 -0
- gator_command/scripts/dashboard/views/settings.js +173 -0
- gator_command/scripts/dashboard/views/updates.js +308 -0
- gator_command/scripts/enforcer-prompt.md +22 -0
- gator_command/scripts/extract-claude-sessions.py +489 -0
- gator_command/scripts/extract-codex-sessions.py +477 -0
- gator_command/scripts/extract-gemini-sessions.py +410 -0
- gator_command/scripts/gator-audit.py +956 -0
- gator_command/scripts/gator-charter-draft.py +919 -0
- gator_command/scripts/gator-charter-lint.py +427 -0
- gator_command/scripts/gator-charter-verify.py +606 -0
- gator_command/scripts/gator-dashboard.py +1271 -0
- gator_command/scripts/gator-deploy.py +916 -0
- gator_command/scripts/gator-drift.py +569 -0
- gator_command/scripts/gator-enforce.py +82 -0
- gator_command/scripts/gator-fleet-intel.py +460 -0
- gator_command/scripts/gator-fleet-report.py +615 -0
- gator_command/scripts/gator-init-command-post.py +315 -0
- gator_command/scripts/gator-init.py +434 -0
- gator_command/scripts/gator-machine-id.py +153 -0
- gator_command/scripts/gator-policy-status.py +631 -0
- gator_command/scripts/gator-pulse.py +459 -0
- gator_command/scripts/gator-repo-status.py +649 -0
- gator_command/scripts/gator-session-common.py +372 -0
- gator_command/scripts/gator-session-sink.py +831 -0
- gator_command/scripts/gator-sessions.py +1244 -0
- gator_command/scripts/gator-update.py +615 -0
- gator_command/scripts/gator-version.py +38 -0
- gator_command/scripts/gator_core.py +489 -0
- gator_command/scripts/gator_remote.py +381 -0
- gator_command/scripts/gator_runtime.py +142 -0
- gator_command/scripts/gatorize-actions.sh +989 -0
- gator_command/scripts/gatorize-lib.sh +166 -0
- gator_command/scripts/gatorize-post.sh +394 -0
- gator_command/scripts/gatorize.py +1163 -0
- gator_command/scripts/gatorize.sh +185 -0
- gator_command/scripts/generate_markdown.py +212 -0
- gator_command/scripts/generate_wiki.py +424 -0
- gator_command/scripts/graph_health.py +780 -0
- gator_command/scripts/memex-lint.py +286 -0
- gator_command/scripts/memex-lint.sh +205 -0
- gator_command/scripts/memex.py +1472 -0
- gator_command/scripts/memex_formatters.py +191 -0
- gator_command/scripts/memex_state.py +236 -0
- gator_command/scripts/spawn.py +650 -0
- gator_command/templates/gator-starter/blueprints/README.md +32 -0
- gator_command/templates/gator-starter/charterignore +53 -0
- gator_command/templates/gator-starter/charters/README.md +178 -0
- gator_command/templates/gator-starter/charters/_template.md +31 -0
- gator_command/templates/gator-starter/commands/commit.md +33 -0
- gator_command/templates/gator-starter/commands/init.md +11 -0
- gator_command/templates/gator-starter/commands/update.md +5 -0
- gator_command/templates/gator-starter/constitution.md +165 -0
- gator_command/templates/gator-starter/field-guides/README.md +25 -0
- gator_command/templates/gator-starter/gator-start-up.md +119 -0
- gator_command/templates/gator-starter/procedures/charter-alignment.md +83 -0
- gator_command/templates/gator-starter/procedures/enforcer-review.md +317 -0
- gator_command/templates/gator-starter/procedures/field-guide-generation.md +176 -0
- gator_command/templates/gator-starter/procedures/knowledge-capture.md +57 -0
- gator_command/templates/gator-starter/procedures/significance-check.md +69 -0
- gator_command/templates/gator-starter/reference-notes/concierge-responses.md +535 -0
- gator_command/templates/gator-starter/reference-notes/dangerous-patterns.md +91 -0
- gator_command/templates/gator-starter/reference-notes/dashboard-operations.md +22 -0
- gator_command/templates/gator-starter/reference-notes/enforcer-configuration.md +232 -0
- gator_command/templates/gator-starter/reference-notes/example-project.md +289 -0
- gator_command/templates/gator-starter/reference-notes/failure-modes-and-self-correction.md +72 -0
- gator_command/templates/gator-starter/reference-notes/git-workflow.md +60 -0
- gator_command/templates/gator-starter/reference-notes/identity-and-ownership.md +37 -0
- gator_command/templates/gator-starter/reference-notes/refactor-approach.md +155 -0
- gator_command/templates/gator-starter/reference-notes/what-gator-requires-from-a-model.md +108 -0
- gator_command/templates/gator-starter/reference-notes/why-navigation-coding-feels-different.md +99 -0
- gator_command/templates/gator-starter/reference-notes/workflow-profiles.md +155 -0
- gator_command/templates/gator-starter/scripts/__pycache__/enforcer-review.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-approve.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-init.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-pre-commit.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-update.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/enforcer-prompt.md +55 -0
- gator_command/templates/gator-starter/scripts/enforcer-review.py +1551 -0
- gator_command/templates/gator-starter/scripts/gator-approve.py +139 -0
- gator_command/templates/gator-starter/scripts/gator-enforce.py +82 -0
- gator_command/templates/gator-starter/scripts/gator-init.py +434 -0
- gator_command/templates/gator-starter/scripts/gator-pre-commit.py +2670 -0
- gator_command/templates/gator-starter/scripts/gator-pulse.py +459 -0
- gator_command/templates/gator-starter/scripts/gator-update.py +615 -0
- gator_command/templates/gator-starter/scripts/gator-version.py +38 -0
- gator_command/templates/gator-starter/scripts/gator_core.py +487 -0
- gator_command/templates/gator-starter/scripts/hooks/__pycache__/commit-msgcpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/hooks/__pycache__/post-commitcpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/hooks/__pycache__/pre-commitcpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/hooks/commit-msg +5 -0
- gator_command/templates/gator-starter/scripts/hooks/post-commit +7 -0
- gator_command/templates/gator-starter/scripts/hooks/pre-commit +5 -0
- gator_command/templates/gator-starter/sessions/.gitignore +7 -0
- gator_command/templates/gator-starter/vault/.gitkeep +0 -0
- gator_command/templates/gator-starter/whiteboard.md +5 -0
- gator_command-1.0.0.dist-info/METADATA +122 -0
- gator_command-1.0.0.dist-info/RECORD +110 -0
- gator_command-1.0.0.dist-info/WHEEL +5 -0
- gator_command-1.0.0.dist-info/entry_points.txt +2 -0
- gator_command-1.0.0.dist-info/licenses/LICENSE +21 -0
- gator_command-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Spawn — create a new Memex repo from seed threads.
|
|
3
|
+
|
|
4
|
+
Knowledge-layer operation: selects threads, follows artifact/vault references,
|
|
5
|
+
copies the portable Memex skeleton, and mechanically repairs the subgraph.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
17
|
+
REPO_ROOT = SCRIPTS_DIR.parent.parent.parent
|
|
18
|
+
|
|
19
|
+
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
20
|
+
from generate_wiki import parse_frontmatter, parse_sections
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ── Constants ────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
# File extensions considered textual (safe to copy from vault)
|
|
26
|
+
TEXT_EXTENSIONS = {
|
|
27
|
+
".md", ".txt", ".csv", ".json", ".yaml", ".yml",
|
|
28
|
+
".py", ".sh", ".tex", ".bib", ".r", ".jl", ".toml",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Markdown link pattern: [text](path)
|
|
32
|
+
LINK_RE = re.compile(r"\[([^\]]*)\]\(([^)]+)\)")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── Dependency resolution ────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
def resolve_seed_threads(memex_dir: Path, names: list[str]) -> list[Path]:
|
|
38
|
+
"""Resolve thread names to file paths. Fails fast if any not found."""
|
|
39
|
+
from memex_state import resolve_thread, list_thread_names
|
|
40
|
+
|
|
41
|
+
paths = []
|
|
42
|
+
for name in names:
|
|
43
|
+
name = name.strip()
|
|
44
|
+
if not name:
|
|
45
|
+
continue
|
|
46
|
+
path = resolve_thread(memex_dir, name)
|
|
47
|
+
if not path:
|
|
48
|
+
candidates = list_thread_names(memex_dir)
|
|
49
|
+
print(f"Thread not found: '{name}'")
|
|
50
|
+
if candidates:
|
|
51
|
+
print(f"Available: {', '.join(candidates)}")
|
|
52
|
+
raise SystemExit(1)
|
|
53
|
+
paths.append(path)
|
|
54
|
+
return paths
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def collect_dependencies(memex_dir: Path, seed_paths: list[Path]) -> dict:
|
|
58
|
+
"""Collect artifacts and vault files referenced by seed threads."""
|
|
59
|
+
artifacts = []
|
|
60
|
+
vault_files = []
|
|
61
|
+
|
|
62
|
+
artifacts_dir = memex_dir / "artifacts"
|
|
63
|
+
vault_dir = memex_dir / "vault"
|
|
64
|
+
|
|
65
|
+
for thread_path in seed_paths:
|
|
66
|
+
text = thread_path.read_text(encoding="utf-8")
|
|
67
|
+
|
|
68
|
+
# Find inline artifact references (links containing artifacts/)
|
|
69
|
+
for match in LINK_RE.finditer(text):
|
|
70
|
+
link_target = match.group(2)
|
|
71
|
+
if "artifacts/" in link_target:
|
|
72
|
+
# Resolve relative to thread's directory
|
|
73
|
+
resolved = (thread_path.parent / link_target).resolve()
|
|
74
|
+
if resolved.exists() and resolved not in artifacts:
|
|
75
|
+
artifacts.append(resolved)
|
|
76
|
+
|
|
77
|
+
# For each artifact, check for vault references via source: frontmatter
|
|
78
|
+
for art_path in artifacts:
|
|
79
|
+
text = art_path.read_text(encoding="utf-8")
|
|
80
|
+
fm, _ = parse_frontmatter(text)
|
|
81
|
+
source = fm.get("source", "")
|
|
82
|
+
if source and "vault/" in source:
|
|
83
|
+
# source is relative to repo root
|
|
84
|
+
vault_path = REPO_ROOT / source
|
|
85
|
+
if not vault_path.exists():
|
|
86
|
+
# Try relative to memex dir
|
|
87
|
+
vault_path = memex_dir / source.replace("gator-command/", "", 1)
|
|
88
|
+
if vault_path.exists() and vault_path.suffix.lower() in TEXT_EXTENSIONS:
|
|
89
|
+
if vault_path not in vault_files:
|
|
90
|
+
vault_files.append(vault_path)
|
|
91
|
+
|
|
92
|
+
# Also check seed threads for direct vault references
|
|
93
|
+
for thread_path in seed_paths:
|
|
94
|
+
text = thread_path.read_text(encoding="utf-8")
|
|
95
|
+
for match in LINK_RE.finditer(text):
|
|
96
|
+
link_target = match.group(2)
|
|
97
|
+
if "vault/" in link_target:
|
|
98
|
+
resolved = (thread_path.parent / link_target).resolve()
|
|
99
|
+
if not resolved.exists():
|
|
100
|
+
resolved = REPO_ROOT / link_target.lstrip("./")
|
|
101
|
+
if not resolved.exists():
|
|
102
|
+
resolved = memex_dir / link_target.replace("gator-command/", "", 1)
|
|
103
|
+
if resolved.exists() and resolved.suffix.lower() in TEXT_EXTENSIONS:
|
|
104
|
+
if resolved not in vault_files:
|
|
105
|
+
vault_files.append(resolved)
|
|
106
|
+
|
|
107
|
+
# Also check for source: lines outside frontmatter (like -> Source: path)
|
|
108
|
+
for line in text.splitlines():
|
|
109
|
+
if line.strip().startswith("-> Source:") or line.strip().startswith("→ Source:"):
|
|
110
|
+
# Extract backtick-quoted path or bare path
|
|
111
|
+
path_match = re.search(r"`([^`]+)`", line)
|
|
112
|
+
if path_match:
|
|
113
|
+
source_ref = path_match.group(1)
|
|
114
|
+
elif ":" in line:
|
|
115
|
+
source_ref = line.split(":", 1)[1].strip()
|
|
116
|
+
else:
|
|
117
|
+
continue
|
|
118
|
+
if "vault/" in source_ref:
|
|
119
|
+
vault_path = REPO_ROOT / source_ref
|
|
120
|
+
if not vault_path.exists():
|
|
121
|
+
vault_path = memex_dir / source_ref.replace("gator-command/", "", 1)
|
|
122
|
+
if vault_path.exists() and vault_path.suffix.lower() in TEXT_EXTENSIONS:
|
|
123
|
+
if vault_path not in vault_files:
|
|
124
|
+
vault_files.append(vault_path)
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
"threads": seed_paths,
|
|
128
|
+
"artifacts": artifacts,
|
|
129
|
+
"vault_files": vault_files,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ── Skeleton creation ────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
GENERIC_CONSTITUTION = """\
|
|
136
|
+
# {project_name} — Constitution
|
|
137
|
+
|
|
138
|
+
A Memex for {project_name} — tracking ideas, experiments, and progress across sessions.
|
|
139
|
+
|
|
140
|
+
## What This Is
|
|
141
|
+
|
|
142
|
+
This repo is a persistence layer — it makes the LLM feel like it was there yesterday. The Memex is a small-world network of cross-referenced threads, not a hierarchical index. Navigate by entering any node and following links.
|
|
143
|
+
|
|
144
|
+
## Roles
|
|
145
|
+
|
|
146
|
+
| Role | What it does | Who runs it |
|
|
147
|
+
|------|-------------|-------------|
|
|
148
|
+
| **Chat agent** | Talks to the human. Reads and writes the Memex in-session — updates threads, creates artifacts, compresses and rotates. | Claude Code (primary session) |
|
|
149
|
+
| **Enforcer** | Audits the Memex (read-only) and produces reports and documentation renders. Does not edit Memex files. Must be a different model. | Different model (e.g., Sonnet checking Opus's work) |
|
|
150
|
+
|
|
151
|
+
## Memex Structure
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
gator-command/ ← machinery (portable, upstream-maintained, don't modify)
|
|
155
|
+
roles.yaml ← role definitions
|
|
156
|
+
policies/ ← concierge wisdom and operational guidelines
|
|
157
|
+
procedures/ ← core Memex operational procedures
|
|
158
|
+
scripts/ ← CLI and tooling
|
|
159
|
+
gator-command/ ← knowledge graph (navigable, compressed, cross-referenced)
|
|
160
|
+
mission.md ← what we're building and why (always loaded)
|
|
161
|
+
roadmap.md ← feature roadmap (always loaded)
|
|
162
|
+
issues.md ← active bugs, blockers, known fragilities (always loaded)
|
|
163
|
+
identity.md ← stable traits, background, persistent interests (always loaded)
|
|
164
|
+
inbox.md ← zero-friction capture (always checked at session open, then cleared)
|
|
165
|
+
active-threads/ ← current topics, 5-8 files (always loaded, compression-budgeted)
|
|
166
|
+
threads/ ← lightweight reference threads (NOT always loaded, navigated via links)
|
|
167
|
+
patterns/ ← recurring rhythms (always loaded)
|
|
168
|
+
artifacts/ ← deep records, synopses, reference material (NOT always loaded)
|
|
169
|
+
vault/ ← external source files (gitignored, referenced by artifacts)
|
|
170
|
+
procedures/ ← project-specific workflows (organic, PI-owned)
|
|
171
|
+
reference-notes/ ← cognitive aids (consulted situationally)
|
|
172
|
+
commit_draft.md ← session change log, used as commit message source (cleared after commit)
|
|
173
|
+
blueprints/ ← subsystem documentation (how each piece works, kept current)
|
|
174
|
+
docs/ ← project documentation
|
|
175
|
+
reports/ ← enforcer audits, design reviews
|
|
176
|
+
wiki/ ← rendered output
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Navigation**: No central index — the graph is the index. Enter through active threads, follow cross-references.
|
|
180
|
+
|
|
181
|
+
**Compression rule**: Always-loaded files should stay under 400 lines total. Depth lives in threads/, artifacts/, and cross-references.
|
|
182
|
+
|
|
183
|
+
**Graph connectivity**: Every operation must preserve all cross-references.
|
|
184
|
+
|
|
185
|
+
## Operating Levels
|
|
186
|
+
|
|
187
|
+
Every message from the human operates at one of two levels:
|
|
188
|
+
|
|
189
|
+
| Level | Signal | What the agent does |
|
|
190
|
+
|-------|--------|--------------------|
|
|
191
|
+
| **Content** | `*c` prefix (or no prefix in user mode) | Use the Memex as infrastructure — read threads, capture ideas, update hits, create artifacts. The system is furniture. |
|
|
192
|
+
| **Meta** | `*m` prefix (or no prefix in designer mode) | Operate *on* the Memex itself — modify the constitution, restructure, write procedures. The system is the object of work. |
|
|
193
|
+
|
|
194
|
+
### Operating Mode
|
|
195
|
+
|
|
196
|
+
The default level is set by `operating-mode:` in `identity.md` frontmatter:
|
|
197
|
+
|
|
198
|
+
| Mode | Default level | Override |
|
|
199
|
+
|------|--------------|----------|
|
|
200
|
+
| `designer` | Meta | `*c` prefix switches to content |
|
|
201
|
+
| `user` | Content | `*m` prefix switches to meta |
|
|
202
|
+
|
|
203
|
+
## Session Opening
|
|
204
|
+
|
|
205
|
+
**Bootstrap detection**: Before running the normal procedure, check these three signals:
|
|
206
|
+
1. `gator-command/identity.md` still contains bracket placeholders (e.g., `[Your role`)
|
|
207
|
+
2. `gator-command/inbox.md` is empty (no captured thoughts)
|
|
208
|
+
3. `gator-command/active-threads/` contains ≤ 2 threads
|
|
209
|
+
|
|
210
|
+
If all three are true, this is a **first session**. Skip the normal session-opening procedure entirely. Simply greet the human and ask what's on their mind. Everything you need to read, read silently.
|
|
211
|
+
|
|
212
|
+
**Normal sessions**: Follow the session-opening procedure: → [session-lifecycle.md](gator-command/procedures/session-lifecycle.md). Prefer the CLI path (`python gator-command/scripts/memex.py status --full --role <your-role> --format json`). Your role is specified in your entry point file.
|
|
213
|
+
|
|
214
|
+
## Thread Lifecycle
|
|
215
|
+
|
|
216
|
+
Threads move between three tiers based on activity. Full lifecycle: → [thread-lifecycle.md](gator-command/procedures/thread-lifecycle.md)
|
|
217
|
+
|
|
218
|
+
New threads should follow the template: → [_TEMPLATE.md](gator-command/active-threads/_TEMPLATE.md)
|
|
219
|
+
|
|
220
|
+
Key rules:
|
|
221
|
+
- Active threads that exceed **60 lines** must be evaluated for splitting.
|
|
222
|
+
- Every thread must carry a `## Summary` (2–4 sentences).
|
|
223
|
+
- Demotion moves threads intact — no compression, no information loss.
|
|
224
|
+
- Cross-references annotate *why* the link exists, not just that it does.
|
|
225
|
+
|
|
226
|
+
## Conventions
|
|
227
|
+
|
|
228
|
+
- **BRANCHING**: Default working branch is `dev`. Merges to `main` are human-authorized.
|
|
229
|
+
- **ARTIFACTS**: Deep records go in `gator-command/artifacts/` with date prefixes (`YYYY-MM-DD-short-title.md`).
|
|
230
|
+
- **VAULT**: External source files in `gator-command/vault/`, gitignored.
|
|
231
|
+
- **PROCEDURES**: Core procedures in `gator-command/procedures/`. Project-specific workflows in `gator-command/procedures/`.
|
|
232
|
+
- **REFERENCE NOTES**: Cognitive aids in `gator-command/reference-notes/`.
|
|
233
|
+
- **CHANGELOG**: Use `git log`. No separate changelog file.
|
|
234
|
+
- **COMMIT DRAFT**: Maintain `gator-command/commit_draft.md`. Append changes during the session.
|
|
235
|
+
- **ENFORCER INDEPENDENCE**: The enforcer must be a different model than the chat agent.
|
|
236
|
+
- **INBOX**: Capture and organization are different operations.
|
|
237
|
+
- **CLIP**: Verbatim exchange capture: `[save]` or `[clip]`. Procedure: → [clip-to-artifact.md](gator-command/procedures/clip-to-artifact.md)
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
BLANK_IDENTITY = """\
|
|
241
|
+
---
|
|
242
|
+
operating-mode: user
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
# Identity
|
|
246
|
+
|
|
247
|
+
[Your role, background, how you think, what you're working on. The agent uses this to calibrate collaboration style.]
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
BLANK_MISSION = """\
|
|
251
|
+
# Mission
|
|
252
|
+
|
|
253
|
+
## What We're Building
|
|
254
|
+
|
|
255
|
+
[What this project is and why it exists.]
|
|
256
|
+
|
|
257
|
+
## What Success Looks Like
|
|
258
|
+
|
|
259
|
+
[How you'll know it's working.]
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
BLANK_ROADMAP = """\
|
|
263
|
+
# Feature Roadmap
|
|
264
|
+
|
|
265
|
+
Last updated: {date}
|
|
266
|
+
|
|
267
|
+
## Active
|
|
268
|
+
|
|
269
|
+
| # | Feature | Status | Next step |
|
|
270
|
+
|---|---------|--------|-----------|
|
|
271
|
+
|
|
272
|
+
## Connections
|
|
273
|
+
|
|
274
|
+
→ [mission.md](mission.md) — what success looks like
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
BLANK_ISSUES = """\
|
|
278
|
+
# Issues
|
|
279
|
+
|
|
280
|
+
## Open
|
|
281
|
+
|
|
282
|
+
(none yet)
|
|
283
|
+
|
|
284
|
+
## Working
|
|
285
|
+
|
|
286
|
+
(none yet)
|
|
287
|
+
|
|
288
|
+
## Resolved
|
|
289
|
+
|
|
290
|
+
(none yet)
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
BLANK_INBOX = """\
|
|
294
|
+
# Inbox
|
|
295
|
+
|
|
296
|
+
Drop anything here. No formatting needed. The chat agent triages at session open.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def create_skeleton(target_dir: Path, project_name: str, source_repo: Path) -> None:
|
|
303
|
+
"""Create the portable Memex skeleton in target_dir."""
|
|
304
|
+
import datetime as dt
|
|
305
|
+
today = dt.date.today().isoformat()
|
|
306
|
+
|
|
307
|
+
target_dir.mkdir(parents=True, exist_ok=False)
|
|
308
|
+
|
|
309
|
+
# Directory structure
|
|
310
|
+
dirs = [
|
|
311
|
+
"gator-command/active-threads",
|
|
312
|
+
"gator-command/threads",
|
|
313
|
+
"gator-command/artifacts",
|
|
314
|
+
"gator-command/vault",
|
|
315
|
+
"gator-command/procedures",
|
|
316
|
+
"gator-command/reference-notes",
|
|
317
|
+
"gator-command/patterns",
|
|
318
|
+
"gator-command/scripts",
|
|
319
|
+
"gator-command/procedures",
|
|
320
|
+
"gator-command/policies",
|
|
321
|
+
"blueprints",
|
|
322
|
+
"docs/reports",
|
|
323
|
+
"docs/wiki",
|
|
324
|
+
]
|
|
325
|
+
for d in dirs:
|
|
326
|
+
(target_dir / d).mkdir(parents=True, exist_ok=True)
|
|
327
|
+
|
|
328
|
+
# Constitution
|
|
329
|
+
(target_dir / "constitution.md").write_text(
|
|
330
|
+
GENERIC_CONSTITUTION.format(project_name=project_name),
|
|
331
|
+
encoding="utf-8",
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# Entry point files
|
|
335
|
+
for ep in ["CLAUDE.md", "AGENTS.md", "GEMINI.md"]:
|
|
336
|
+
src = source_repo / ep
|
|
337
|
+
if src.exists():
|
|
338
|
+
shutil.copy2(src, target_dir / ep)
|
|
339
|
+
|
|
340
|
+
# Roles
|
|
341
|
+
roles_src = source_repo / "gator-command" / "roles.yaml"
|
|
342
|
+
if roles_src.exists():
|
|
343
|
+
shutil.copy2(roles_src, target_dir / "gator-command" / "roles.yaml")
|
|
344
|
+
|
|
345
|
+
# Scripts
|
|
346
|
+
for script in (source_repo / "gator-command" / "scripts").glob("*.py"):
|
|
347
|
+
shutil.copy2(script, target_dir / "gator-command" / "scripts" / script.name)
|
|
348
|
+
|
|
349
|
+
# Core procedures
|
|
350
|
+
for proc in (source_repo / "gator-command" / "procedures").glob("*.md"):
|
|
351
|
+
shutil.copy2(proc, target_dir / "gator-command" / "procedures" / proc.name)
|
|
352
|
+
|
|
353
|
+
# Policies
|
|
354
|
+
for policy in (source_repo / "gator-command" / "policies").glob("*.md"):
|
|
355
|
+
shutil.copy2(policy, target_dir / "gator-command" / "policies" / policy.name)
|
|
356
|
+
|
|
357
|
+
# Templates
|
|
358
|
+
for template_path in [
|
|
359
|
+
"gator-command/active-threads/_TEMPLATE.md",
|
|
360
|
+
"gator-command/artifacts/_TEMPLATE.md",
|
|
361
|
+
]:
|
|
362
|
+
src = source_repo / template_path
|
|
363
|
+
if src.exists():
|
|
364
|
+
shutil.copy2(src, target_dir / template_path)
|
|
365
|
+
|
|
366
|
+
blueprint_template = source_repo / "blueprints" / "_TEMPLATE.md"
|
|
367
|
+
if blueprint_template.exists():
|
|
368
|
+
shutil.copy2(blueprint_template, target_dir / "blueprints" / "_TEMPLATE.md")
|
|
369
|
+
|
|
370
|
+
# Blank top-level memex files
|
|
371
|
+
(target_dir / "gator-command" / "identity.md").write_text(BLANK_IDENTITY, encoding="utf-8")
|
|
372
|
+
(target_dir / "gator-command" / "mission.md").write_text(BLANK_MISSION, encoding="utf-8")
|
|
373
|
+
(target_dir / "gator-command" / "roadmap.md").write_text(
|
|
374
|
+
BLANK_ROADMAP.format(date=today), encoding="utf-8",
|
|
375
|
+
)
|
|
376
|
+
(target_dir / "gator-command" / "issues.md").write_text(BLANK_ISSUES, encoding="utf-8")
|
|
377
|
+
(target_dir / "gator-command" / "inbox.md").write_text(BLANK_INBOX, encoding="utf-8")
|
|
378
|
+
(target_dir / "gator-command" / "commit_draft.md").write_text("", encoding="utf-8")
|
|
379
|
+
|
|
380
|
+
# Gitignore for vault
|
|
381
|
+
(target_dir / "gator-command" / "vault" / ".gitkeep").write_text("", encoding="utf-8")
|
|
382
|
+
gitignore = target_dir / ".gitignore"
|
|
383
|
+
gitignore.write_text("gator-command/vault/**\n!gator-command/vault/.gitkeep\n", encoding="utf-8")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# ── Copy seed content ────────────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
def copy_seed_content(target_dir: Path, deps: dict, memex_dir: Path) -> list[str]:
|
|
389
|
+
"""Copy seed threads, artifacts, and vault files into the new repo."""
|
|
390
|
+
copied = []
|
|
391
|
+
|
|
392
|
+
for thread_path in deps["threads"]:
|
|
393
|
+
dest = target_dir / "gator-command" / "active-threads" / thread_path.name
|
|
394
|
+
shutil.copy2(thread_path, dest)
|
|
395
|
+
copied.append(f"gator-command/active-threads/{thread_path.name}")
|
|
396
|
+
|
|
397
|
+
for art_path in deps["artifacts"]:
|
|
398
|
+
dest = target_dir / "gator-command" / "artifacts" / art_path.name
|
|
399
|
+
shutil.copy2(art_path, dest)
|
|
400
|
+
copied.append(f"gator-command/artifacts/{art_path.name}")
|
|
401
|
+
|
|
402
|
+
for vault_path in deps["vault_files"]:
|
|
403
|
+
# Preserve subdirectory structure under vault/
|
|
404
|
+
try:
|
|
405
|
+
rel = vault_path.relative_to(memex_dir / "vault")
|
|
406
|
+
except ValueError:
|
|
407
|
+
rel = Path(vault_path.name)
|
|
408
|
+
dest = target_dir / "gator-command" / "vault" / rel
|
|
409
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
410
|
+
shutil.copy2(vault_path, dest)
|
|
411
|
+
copied.append(f"gator-command/vault/{rel}")
|
|
412
|
+
|
|
413
|
+
return copied
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# ── Mechanical graph repair ──────────────────────────────────────────────────
|
|
417
|
+
|
|
418
|
+
def repair_graph(target_dir: Path, copied_files: list[str]) -> list[str]:
|
|
419
|
+
"""Mechanically repair cross-references in copied threads and artifacts.
|
|
420
|
+
|
|
421
|
+
- Remove connections to threads that don't exist in the new repo
|
|
422
|
+
- Fix relative paths for connections that do exist
|
|
423
|
+
- Leave HTML comments noting what was pruned
|
|
424
|
+
Returns list of repair descriptions.
|
|
425
|
+
"""
|
|
426
|
+
repairs = []
|
|
427
|
+
|
|
428
|
+
# Build index of files that exist in the new repo
|
|
429
|
+
existing_stems = set()
|
|
430
|
+
existing_files = {} # stem -> relative path from active-threads
|
|
431
|
+
for f in copied_files:
|
|
432
|
+
p = Path(f)
|
|
433
|
+
existing_stems.add(p.stem)
|
|
434
|
+
existing_files[p.stem] = p.name
|
|
435
|
+
|
|
436
|
+
# Also include templates and top-level files
|
|
437
|
+
for md in (target_dir / "gator-command").rglob("*.md"):
|
|
438
|
+
existing_stems.add(md.stem)
|
|
439
|
+
|
|
440
|
+
# Process each copied thread and artifact
|
|
441
|
+
for rel_path in copied_files:
|
|
442
|
+
if not rel_path.endswith(".md"):
|
|
443
|
+
continue
|
|
444
|
+
|
|
445
|
+
file_path = target_dir / rel_path
|
|
446
|
+
text = file_path.read_text(encoding="utf-8")
|
|
447
|
+
original = text
|
|
448
|
+
file_dir = file_path.parent
|
|
449
|
+
|
|
450
|
+
# Fix connections section
|
|
451
|
+
new_lines = []
|
|
452
|
+
in_connections = False
|
|
453
|
+
for line in text.splitlines():
|
|
454
|
+
stripped = line.strip()
|
|
455
|
+
|
|
456
|
+
if stripped == "## Connections":
|
|
457
|
+
in_connections = True
|
|
458
|
+
new_lines.append(line)
|
|
459
|
+
continue
|
|
460
|
+
|
|
461
|
+
if in_connections and stripped.startswith("## "):
|
|
462
|
+
in_connections = False
|
|
463
|
+
|
|
464
|
+
if in_connections and (stripped.startswith("->") or stripped.startswith("→")):
|
|
465
|
+
# Parse the link target
|
|
466
|
+
link_match = LINK_RE.search(stripped)
|
|
467
|
+
if link_match:
|
|
468
|
+
link_text = link_match.group(1)
|
|
469
|
+
link_target = link_match.group(2)
|
|
470
|
+
target_stem = Path(link_target).stem
|
|
471
|
+
|
|
472
|
+
if target_stem not in existing_stems:
|
|
473
|
+
# Target doesn't exist — prune silently
|
|
474
|
+
# (no comment: parent thread names may be permission-sensitive)
|
|
475
|
+
repairs.append(f"PRUNED {rel_path}: 1 connection removed")
|
|
476
|
+
continue
|
|
477
|
+
else:
|
|
478
|
+
# Target exists — fix relative path if needed
|
|
479
|
+
# All seed threads are in active-threads/ now
|
|
480
|
+
if target_stem in existing_files:
|
|
481
|
+
target_file = existing_files[target_stem]
|
|
482
|
+
# Compute correct relative path
|
|
483
|
+
target_abs = None
|
|
484
|
+
for subdir in ["active-threads", "artifacts", "threads"]:
|
|
485
|
+
candidate = target_dir / "gator-command" / subdir / target_file
|
|
486
|
+
if candidate.exists():
|
|
487
|
+
target_abs = candidate
|
|
488
|
+
break
|
|
489
|
+
if target_abs:
|
|
490
|
+
import os
|
|
491
|
+
new_rel = os.path.relpath(target_abs, file_dir)
|
|
492
|
+
new_rel = Path(new_rel).as_posix()
|
|
493
|
+
# Get the annotation (everything after " — " or " - ")
|
|
494
|
+
annotation_match = re.search(
|
|
495
|
+
r" [—\-] (.+)$", stripped
|
|
496
|
+
)
|
|
497
|
+
annotation = annotation_match.group(1) if annotation_match else ""
|
|
498
|
+
arrow = "→" if "→" in stripped else "->"
|
|
499
|
+
new_line = f"{arrow} [{link_text}]({new_rel})"
|
|
500
|
+
if annotation:
|
|
501
|
+
new_line += f" — {annotation}"
|
|
502
|
+
new_lines.append(new_line)
|
|
503
|
+
if new_line != stripped:
|
|
504
|
+
repairs.append(
|
|
505
|
+
f"FIXED {rel_path}: path to {link_text}"
|
|
506
|
+
)
|
|
507
|
+
continue
|
|
508
|
+
|
|
509
|
+
# Also fix inline links outside connections
|
|
510
|
+
if not in_connections:
|
|
511
|
+
def fix_link(m):
|
|
512
|
+
lt, lp = m.group(1), m.group(2)
|
|
513
|
+
ts = Path(lp).stem
|
|
514
|
+
if ("threads/" in lp or "artifacts/" in lp) and ts not in existing_stems:
|
|
515
|
+
repairs.append(f"INLINE {rel_path}: removed link to {lt}")
|
|
516
|
+
return lt # Plain text, no link
|
|
517
|
+
return m.group(0)
|
|
518
|
+
|
|
519
|
+
line = LINK_RE.sub(fix_link, line)
|
|
520
|
+
|
|
521
|
+
new_lines.append(line)
|
|
522
|
+
|
|
523
|
+
new_text = "\n".join(new_lines)
|
|
524
|
+
# Preserve trailing newline if original had one
|
|
525
|
+
if original.endswith("\n") and not new_text.endswith("\n"):
|
|
526
|
+
new_text += "\n"
|
|
527
|
+
|
|
528
|
+
if new_text != original:
|
|
529
|
+
file_path.write_text(new_text, encoding="utf-8")
|
|
530
|
+
|
|
531
|
+
# Fix artifact source-thread paths
|
|
532
|
+
for rel_path in copied_files:
|
|
533
|
+
if "artifacts/" not in rel_path or not rel_path.endswith(".md"):
|
|
534
|
+
continue
|
|
535
|
+
file_path = target_dir / rel_path
|
|
536
|
+
text = file_path.read_text(encoding="utf-8")
|
|
537
|
+
if "source-thread:" in text:
|
|
538
|
+
# Update source-thread to point to active-threads/
|
|
539
|
+
new_text = re.sub(
|
|
540
|
+
r"(source-thread:\s*)[\./]*(?:threads|active-threads)/",
|
|
541
|
+
r"\1../active-threads/",
|
|
542
|
+
text,
|
|
543
|
+
)
|
|
544
|
+
if new_text != text:
|
|
545
|
+
file_path.write_text(new_text, encoding="utf-8")
|
|
546
|
+
repairs.append(f"FIXED {rel_path}: source-thread path")
|
|
547
|
+
|
|
548
|
+
return repairs
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
# ── Git initialization ───────────────────────────────────────────────────────
|
|
552
|
+
|
|
553
|
+
def git_init_and_commit(target_dir: Path, project_name: str) -> None:
|
|
554
|
+
"""Initialize git repo and create initial commit."""
|
|
555
|
+
subprocess.run(["git", "init"], cwd=str(target_dir), check=True,
|
|
556
|
+
capture_output=True)
|
|
557
|
+
subprocess.run(["git", "checkout", "-b", "dev"], cwd=str(target_dir),
|
|
558
|
+
check=True, capture_output=True)
|
|
559
|
+
subprocess.run(["git", "add", "-A"], cwd=str(target_dir), check=True,
|
|
560
|
+
capture_output=True)
|
|
561
|
+
subprocess.run(
|
|
562
|
+
["git", "commit", "-m", f"Spawn {project_name} from parent Memex\n\n"
|
|
563
|
+
f"Memex skeleton + seed threads from parent graph.\n"
|
|
564
|
+
f"Knowledge-layer spawn — independent repo, no upstream."],
|
|
565
|
+
cwd=str(target_dir), check=True, capture_output=True,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
# ── Main entry point (called from memex.py) ─────────────────────────────────
|
|
570
|
+
|
|
571
|
+
def run_spawn(name: str, thread_names: list[str], dry_run: bool = False,
|
|
572
|
+
fmt: str = "pretty") -> int:
|
|
573
|
+
"""Execute the spawn operation."""
|
|
574
|
+
import json
|
|
575
|
+
|
|
576
|
+
memex_dir = REPO_ROOT / "gator-command"
|
|
577
|
+
target_dir = REPO_ROOT.parent / name
|
|
578
|
+
|
|
579
|
+
if target_dir.exists() and not dry_run:
|
|
580
|
+
print(f"Target directory already exists: {target_dir}")
|
|
581
|
+
return 1
|
|
582
|
+
|
|
583
|
+
# Resolve seed threads
|
|
584
|
+
seed_paths = resolve_seed_threads(memex_dir, thread_names)
|
|
585
|
+
|
|
586
|
+
# Collect dependencies
|
|
587
|
+
deps = collect_dependencies(memex_dir, seed_paths)
|
|
588
|
+
|
|
589
|
+
if dry_run:
|
|
590
|
+
result = {
|
|
591
|
+
"command": "spawn",
|
|
592
|
+
"project": name,
|
|
593
|
+
"target": str(target_dir),
|
|
594
|
+
"dry_run": True,
|
|
595
|
+
"threads": [p.stem for p in deps["threads"]],
|
|
596
|
+
"artifacts": [p.stem for p in deps["artifacts"]],
|
|
597
|
+
"vault_files": [str(p.name) for p in deps["vault_files"]],
|
|
598
|
+
}
|
|
599
|
+
if fmt == "json":
|
|
600
|
+
print(json.dumps(result, indent=2))
|
|
601
|
+
else:
|
|
602
|
+
print(f" Spawn: {name}")
|
|
603
|
+
print(f" Target: {target_dir}")
|
|
604
|
+
print(f" Threads: {', '.join(p.stem for p in deps['threads'])}")
|
|
605
|
+
print(f" Artifacts: {', '.join(p.stem for p in deps['artifacts'])}")
|
|
606
|
+
print(f" Vault files: {', '.join(p.name for p in deps['vault_files'])}")
|
|
607
|
+
print(f"\n Dry run — no files created.")
|
|
608
|
+
return 0
|
|
609
|
+
|
|
610
|
+
# Create skeleton
|
|
611
|
+
print(f" Creating skeleton: {target_dir}")
|
|
612
|
+
create_skeleton(target_dir, name, REPO_ROOT)
|
|
613
|
+
|
|
614
|
+
# Copy seed content
|
|
615
|
+
print(f" Copying seed content...")
|
|
616
|
+
copied = copy_seed_content(target_dir, deps, memex_dir)
|
|
617
|
+
for c in copied:
|
|
618
|
+
print(f" + {c}")
|
|
619
|
+
|
|
620
|
+
# Repair graph
|
|
621
|
+
print(f" Repairing graph...")
|
|
622
|
+
repairs = repair_graph(target_dir, copied)
|
|
623
|
+
for r in repairs:
|
|
624
|
+
print(f" {r}")
|
|
625
|
+
|
|
626
|
+
# Git init
|
|
627
|
+
print(f" Initializing git repo...")
|
|
628
|
+
git_init_and_commit(target_dir, name)
|
|
629
|
+
|
|
630
|
+
if fmt == "json":
|
|
631
|
+
result = {
|
|
632
|
+
"command": "spawn",
|
|
633
|
+
"project": name,
|
|
634
|
+
"target": str(target_dir),
|
|
635
|
+
"threads": [p.stem for p in deps["threads"]],
|
|
636
|
+
"artifacts": [p.stem for p in deps["artifacts"]],
|
|
637
|
+
"vault_files": [str(p.name) for p in deps["vault_files"]],
|
|
638
|
+
"repairs": repairs,
|
|
639
|
+
}
|
|
640
|
+
print(json.dumps(result, indent=2))
|
|
641
|
+
else:
|
|
642
|
+
print(f"\n ✓ Spawned {name} at {target_dir}")
|
|
643
|
+
print(f" {len(deps['threads'])} thread(s), "
|
|
644
|
+
f"{len(deps['artifacts'])} artifact(s), "
|
|
645
|
+
f"{len(deps['vault_files'])} vault file(s)")
|
|
646
|
+
print(f" {len(repairs)} graph repair(s)")
|
|
647
|
+
print(f" Branch: dev")
|
|
648
|
+
print(f"\n cd {target_dir} && claude")
|
|
649
|
+
|
|
650
|
+
return 0
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Blueprints
|
|
2
|
+
|
|
3
|
+
Per-feature flow maps — how features work end-to-end, referencing modules, functions, and charters.
|
|
4
|
+
|
|
5
|
+
Blueprints are optional. Create one when a feature or subsystem needs an architect-facing flow map.
|
|
6
|
+
|
|
7
|
+
## Structure
|
|
8
|
+
|
|
9
|
+
Each blueprint describes one feature:
|
|
10
|
+
|
|
11
|
+
```markdown
|
|
12
|
+
# Blueprint: <Feature Name>
|
|
13
|
+
|
|
14
|
+
## What It Does
|
|
15
|
+
[1-2 sentences]
|
|
16
|
+
|
|
17
|
+
## Flow
|
|
18
|
+
1. **<Module>** → `function_name()` — what happens
|
|
19
|
+
→ [charter-name](../charters/charter-name.md)
|
|
20
|
+
|
|
21
|
+
## Key Charters
|
|
22
|
+
[Charters involved in this flow]
|
|
23
|
+
|
|
24
|
+
## Invariants
|
|
25
|
+
[What must be true for this to work]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Master Blueprint
|
|
29
|
+
|
|
30
|
+
Once you have several blueprints, create `system.md` — the architectural overview that links to individual feature blueprints.
|
|
31
|
+
|
|
32
|
+
See the [charters README](../charters/README.md) for the notation system used in function references.
|