create-egregore 0.19.33 → 0.20.1
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.
- package/assets/egregore-launcher.js +141 -14
- package/bin/cli.js +19 -3
- package/lib/connect-runtime.js +4 -2
- package/lib/local.js +15 -1
- package/lib/prime-runtime.js +208 -0
- package/lib/setup.js +66 -5
- package/package.json +4 -3
- package/runtime/codex/.codex/config.toml +3 -0
- package/runtime/codex/.codex/skills/archive/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/branch/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/checkup/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/commit/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/contribute/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/delete-user/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/ingest/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/ingest-corpus/SKILL.md +29 -0
- package/runtime/codex/.codex/skills/ingest-notion/SKILL.md +29 -0
- package/runtime/codex/.codex/skills/ingest-user-interview/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/issue/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/me/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/meeting/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/note/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/notion-connect/SKILL.md +29 -0
- package/runtime/codex/.codex/skills/onboarding/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/pr/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/pull/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/push/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/reflect/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/review-pr/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/save/SKILL.md +3 -1
- package/runtime/codex/.codex/skills/slack-connect/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/summon/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/sync-repos/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/teams-connect/SKILL.md +29 -0
- package/runtime/codex/.codex/skills/telegram-connect/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/telemetry/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/test/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/tutorial/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/update/SKILL.md +1 -1
- package/runtime/codex/.codex/spec-manifest.json +5 -5
- package/runtime/codex/AGENTS.md +41 -35
- package/runtime/codex/bin/activity-data.sh +4 -8
- package/runtime/codex/bin/agent.sh +17 -5
- package/runtime/codex/bin/attendant.sh +13 -0
- package/runtime/codex/bin/autosave.sh +27 -1
- package/runtime/codex/bin/codex-render-spec.mjs +33 -32
- package/runtime/codex/bin/codex-session-start.sh +3 -1
- package/runtime/codex/bin/codex-sync-skills.sh +6 -1
- package/runtime/codex/bin/graph-batch.sh +25 -6
- package/runtime/codex/bin/graph-op.sh +5 -4
- package/runtime/codex/bin/graph.sh +31 -5
- package/runtime/codex/bin/handoff-pr-backfill.sh +1 -1
- package/runtime/codex/bin/handoff-run.sh +8 -3
- package/runtime/codex/bin/handoff-save-egregore.sh +19 -7
- package/runtime/codex/bin/ingest.py +341 -10
- package/runtime/codex/bin/lib/context.sh +26 -1
- package/runtime/codex/bin/lib/git-message.sh +37 -0
- package/runtime/codex/bin/lib/git-sync.sh +10 -2
- package/runtime/codex/bin/lib/greeting.sh +4 -1
- package/runtime/codex/bin/lib/worktree-links.sh +25 -0
- package/runtime/codex/bin/session-autosave.sh +7 -1
- package/runtime/codex/bin/settings.sh +1 -1
- package/runtime/codex/manifest.json +10 -1
- package/runtime/pi/.codex/config.toml +3 -0
- package/runtime/pi/.codex/skills/archive/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/branch/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/checkup/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/commit/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/contribute/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/delete-user/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/ingest/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/ingest-corpus/SKILL.md +29 -0
- package/runtime/pi/.codex/skills/ingest-notion/SKILL.md +29 -0
- package/runtime/pi/.codex/skills/ingest-user-interview/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/issue/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/me/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/meeting/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/note/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/notion-connect/SKILL.md +29 -0
- package/runtime/pi/.codex/skills/onboarding/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/pr/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/pull/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/push/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/reflect/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/review-pr/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/save/SKILL.md +3 -1
- package/runtime/pi/.codex/skills/slack-connect/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/summon/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/sync-repos/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/teams-connect/SKILL.md +29 -0
- package/runtime/pi/.codex/skills/telegram-connect/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/telemetry/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/test/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/tutorial/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/update/SKILL.md +1 -1
- package/runtime/pi/.codex/spec-manifest.json +5 -5
- package/runtime/pi/.pi/APPEND_SYSTEM.md +57 -33
- package/runtime/pi/.pi/extensions/egregore.ts +17 -6
- package/runtime/pi/.pi/spec-manifest.json +2 -2
- package/runtime/pi/AGENTS.md +41 -35
- package/runtime/pi/bin/activity-data.sh +4 -8
- package/runtime/pi/bin/agent.sh +17 -5
- package/runtime/pi/bin/attendant.sh +13 -0
- package/runtime/pi/bin/autosave.sh +27 -1
- package/runtime/pi/bin/codex-render-spec.mjs +33 -32
- package/runtime/pi/bin/codex-session-start.sh +3 -1
- package/runtime/pi/bin/codex-sync-skills.sh +6 -1
- package/runtime/pi/bin/graph-batch.sh +25 -6
- package/runtime/pi/bin/graph-op.sh +5 -4
- package/runtime/pi/bin/graph.sh +31 -5
- package/runtime/pi/bin/handoff-pr-backfill.sh +1 -1
- package/runtime/pi/bin/handoff-run.sh +8 -3
- package/runtime/pi/bin/handoff-save-egregore.sh +19 -7
- package/runtime/pi/bin/ingest.py +341 -10
- package/runtime/pi/bin/lib/context.sh +26 -1
- package/runtime/pi/bin/lib/git-message.sh +37 -0
- package/runtime/pi/bin/lib/git-sync.sh +10 -2
- package/runtime/pi/bin/lib/greeting.sh +4 -1
- package/runtime/pi/bin/lib/worktree-links.sh +25 -0
- package/runtime/pi/bin/pi-branch-policy.mjs +138 -1
- package/runtime/pi/bin/pi-product-workflows.mjs +35 -7
- package/runtime/pi/bin/pi-render-spec.mjs +52 -26
- package/runtime/pi/bin/session-autosave.sh +7 -1
- package/runtime/pi/bin/settings.sh +1 -1
- package/runtime/pi/manifest.json +10 -1
- package/runtime/prime/.claude/context/notification-consent.md +66 -0
- package/runtime/prime/.codex/config.toml +20 -0
- package/runtime/prime/.codex/hooks/branch-guard.js +461 -0
- package/runtime/prime/.codex/hooks/search-hint.js +67 -0
- package/runtime/prime/.codex/hooks.json +30 -0
- package/runtime/prime/.codex/skills/activity/SKILL.md +82 -0
- package/runtime/prime/.codex/skills/announce/SKILL.md +67 -0
- package/runtime/prime/.codex/skills/archive/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/branch/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/checkup/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/commit/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/contribute/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/create-skill/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/dark-mode/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/dashboard/SKILL.md +59 -0
- package/runtime/prime/.codex/skills/deep-reflect/SKILL.md +109 -0
- package/runtime/prime/.codex/skills/delete-user/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/emissary/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/handoff/SKILL.md +166 -0
- package/runtime/prime/.codex/skills/harvest/QUESTION_PALETTE.md +76 -0
- package/runtime/prime/.codex/skills/harvest/SKILL.md +131 -0
- package/runtime/prime/.codex/skills/ingest/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/ingest-corpus/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/ingest-notion/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/ingest-user-interview/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/invite/SKILL.md +44 -0
- package/runtime/prime/.codex/skills/issue/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/me/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/meeting/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/note/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/notion-connect/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/onboarding/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/pr/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/pull/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/push/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/quest/SKILL.md +80 -0
- package/runtime/prime/.codex/skills/reflect/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/review-pr/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/save/SKILL.md +127 -0
- package/runtime/prime/.codex/skills/slack-connect/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/summon/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/sync-repos/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/teams-connect/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/telegram-connect/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/telemetry/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/test/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/the-spiral/SKILL.md +84 -0
- package/runtime/prime/.codex/skills/tui-design/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/tutorial/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/update/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/view/SKILL.md +156 -0
- package/runtime/prime/.codex/skills/view/agents/openai.yaml +4 -0
- package/runtime/prime/.codex/skills/view/assets/compose-scaffold.html +221 -0
- package/runtime/prime/.codex/skills/visual-explain/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/wrap/SKILL.md +58 -0
- package/runtime/prime/.codex/spec-manifest.json +132 -0
- package/runtime/prime/.prime/agent/APPEND_SYSTEM.md +416 -0
- package/runtime/prime/.prime/agent/extensions/egregore.ts +460 -0
- package/runtime/prime/.prime/agent/settings.json +4 -0
- package/runtime/prime/.prime/agent/skills/egregore/SKILL.md +34 -0
- package/runtime/prime/.prime/agent/skills/egregore/pyproject.toml +13 -0
- package/runtime/prime/.prime/agent/skills/egregore/src/egregore/__init__.py +156 -0
- package/runtime/prime/.prime/agent/spec-manifest.json +12 -0
- package/runtime/prime/AGENTS.md +488 -0
- package/runtime/prime/bin/activity-data.sh +238 -0
- package/runtime/prime/bin/agent.sh +787 -0
- package/runtime/prime/bin/attendant.sh +707 -0
- package/runtime/prime/bin/autosave.sh +112 -0
- package/runtime/prime/bin/capture-reconcile.sh +69 -0
- package/runtime/prime/bin/capture-run.sh +341 -0
- package/runtime/prime/bin/codex-install-skills.sh +78 -0
- package/runtime/prime/bin/codex-render-spec.mjs +435 -0
- package/runtime/prime/bin/codex-session-start.sh +265 -0
- package/runtime/prime/bin/codex-skill-render.mjs +347 -0
- package/runtime/prime/bin/codex-sync-skills.sh +237 -0
- package/runtime/prime/bin/connect-refresh.sh +33 -0
- package/runtime/prime/bin/dashboard-data.sh +178 -0
- package/runtime/prime/bin/graph-batch.sh +79 -0
- package/runtime/prime/bin/graph-op.sh +1102 -0
- package/runtime/prime/bin/graph-wal.sh +229 -0
- package/runtime/prime/bin/graph.sh +223 -0
- package/runtime/prime/bin/handoff-pr-backfill.sh +86 -0
- package/runtime/prime/bin/handoff-run.sh +676 -0
- package/runtime/prime/bin/handoff-save-egregore.sh +224 -0
- package/runtime/prime/bin/index-handoff.sh +322 -0
- package/runtime/prime/bin/ingest-graph.sh +80 -0
- package/runtime/prime/bin/ingest-surface.html +495 -0
- package/runtime/prime/bin/ingest.py +1011 -0
- package/runtime/prime/bin/ingest.sh +279 -0
- package/runtime/prime/bin/ingest_graph.py +856 -0
- package/runtime/prime/bin/ingest_surface.py +544 -0
- package/runtime/prime/bin/lib/artifact-id.sh +46 -0
- package/runtime/prime/bin/lib/config.sh +116 -0
- package/runtime/prime/bin/lib/context.sh +621 -0
- package/runtime/prime/bin/lib/dashboard-artifact.sh +242 -0
- package/runtime/prime/bin/lib/ensure-develop.sh +183 -0
- package/runtime/prime/bin/lib/git-message.sh +37 -0
- package/runtime/prime/bin/lib/git-sync.sh +414 -0
- package/runtime/prime/bin/lib/greeting.sh +519 -0
- package/runtime/prime/bin/lib/hash.sh +13 -0
- package/runtime/prime/bin/lib/identity.sh +213 -0
- package/runtime/prime/bin/lib/metrics.sh +374 -0
- package/runtime/prime/bin/lib/noncode.sh +31 -0
- package/runtime/prime/bin/lib/permissions.sh +106 -0
- package/runtime/prime/bin/lib/time.sh +56 -0
- package/runtime/prime/bin/lib/worktree-links.sh +79 -0
- package/runtime/prime/bin/notify.sh +636 -0
- package/runtime/prime/bin/person.py +630 -0
- package/runtime/prime/bin/person.sh +448 -0
- package/runtime/prime/bin/pi-branch-policy.mjs +312 -0
- package/runtime/prime/bin/pi-product-workflows.mjs +394 -0
- package/runtime/prime/bin/prime-render-spec.mjs +147 -0
- package/runtime/prime/bin/prime-session-start.sh +24 -0
- package/runtime/prime/bin/publish-artifact.sh +271 -0
- package/runtime/prime/bin/publish-references.sh +144 -0
- package/runtime/prime/bin/repo-state.sh +96 -0
- package/runtime/prime/bin/search.sh +240 -0
- package/runtime/prime/bin/session-autosave.sh +215 -0
- package/runtime/prime/bin/session-log.sh +199 -0
- package/runtime/prime/bin/settings.sh +304 -0
- package/runtime/prime/bin/telemetry.sh +391 -0
- package/runtime/prime/bin/worktree-create.sh +128 -0
- package/runtime/prime/bin/worktree-remove.sh +50 -0
- package/runtime/prime/bin/worktree.sh +198 -0
- package/runtime/prime/manifest.json +181 -0
package/runtime/pi/bin/ingest.py
CHANGED
|
@@ -23,10 +23,12 @@ from datetime import datetime, timezone
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
from xml.etree import ElementTree
|
|
25
25
|
|
|
26
|
-
sys.path.
|
|
26
|
+
sys.path.append(str(Path(__file__).resolve().parent))
|
|
27
|
+
import catalogue as catalogue_module # noqa: E402
|
|
27
28
|
import doc_segment # noqa: E402 (same-directory module, path set above)
|
|
28
29
|
import pdf_extract # noqa: E402
|
|
29
30
|
import source_profile # noqa: E402
|
|
31
|
+
import text_repair # noqa: E402
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
ROOT = Path(__file__).resolve().parent.parent
|
|
@@ -152,6 +154,65 @@ def chunks(text: str, limit: int = 7000) -> list[str]:
|
|
|
152
154
|
return result
|
|
153
155
|
|
|
154
156
|
|
|
157
|
+
# Catalogue columns that carry provenance, by what they mean rather than by
|
|
158
|
+
# what any one organisation calls them. A profile's `map` already renames its
|
|
159
|
+
# own columns to these, so no further configuration is needed.
|
|
160
|
+
CATALOGUE_URL_FIELDS = ("url", "source_url", "link")
|
|
161
|
+
CATALOGUE_INSTITUTION_FIELDS = ("institution", "publisher", "source_type", "source")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def catalogue_provenance(catalogued: dict) -> tuple:
|
|
165
|
+
"""The URL and institution a catalogue records for a document.
|
|
166
|
+
|
|
167
|
+
The resolver has always accepted these and ranks both above a filename,
|
|
168
|
+
because a filename is what someone happened to save a file as. Ingest
|
|
169
|
+
passed only the filename, so the stronger evidence sitting in the row it
|
|
170
|
+
had just read went unused.
|
|
171
|
+
|
|
172
|
+
Measured on a real archive that was not a marginal loss. Seventy articles
|
|
173
|
+
downloaded from two journal indexes were saved under bare article titles,
|
|
174
|
+
carrying no publisher token to match. The profile had rules for both
|
|
175
|
+
indexes and the catalogue named the index for every one of them, and still
|
|
176
|
+
nothing matched — the one input that could have identified them was the one
|
|
177
|
+
input never passed.
|
|
178
|
+
"""
|
|
179
|
+
if not catalogued:
|
|
180
|
+
return "", ""
|
|
181
|
+
url = next((str(catalogued[f]) for f in CATALOGUE_URL_FIELDS if catalogued.get(f)), "")
|
|
182
|
+
institution = next(
|
|
183
|
+
(str(catalogued[f]) for f in CATALOGUE_INSTITUTION_FIELDS if catalogued.get(f)), "")
|
|
184
|
+
return url, institution
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def find_publisher_profile() -> Path | None:
|
|
188
|
+
"""Where this instance's publisher profile is, if it has one.
|
|
189
|
+
|
|
190
|
+
The profile is what assigns a zone and a trust tier to each document, and
|
|
191
|
+
zone is a hard retrieval boundary. Ingesting without it produces a corpus
|
|
192
|
+
where every document is unplaced, so every later question is refused — a
|
|
193
|
+
failure that looks like an empty archive rather than a missing setting.
|
|
194
|
+
|
|
195
|
+
So this looks in the places a profile actually lives rather than in one
|
|
196
|
+
fixed path. An instance keeps its ontology under memory/knowledge/tools/,
|
|
197
|
+
not beside its intake metadata, and requiring an environment variable to
|
|
198
|
+
bridge that gap means the common case is the broken one.
|
|
199
|
+
"""
|
|
200
|
+
override = os.environ.get("EGREGORE_PUBLISHER_PROFILE")
|
|
201
|
+
if override:
|
|
202
|
+
path = Path(override)
|
|
203
|
+
return path if path.is_file() else None
|
|
204
|
+
|
|
205
|
+
candidates = [metadata_root() / "publisher-profile.json"]
|
|
206
|
+
memory = Path("memory")
|
|
207
|
+
if memory.is_dir():
|
|
208
|
+
candidates.extend(sorted(memory.glob("knowledge/tools/*/publisher-profile.json")))
|
|
209
|
+
candidates.append(memory / "publisher-profile.json")
|
|
210
|
+
for path in candidates:
|
|
211
|
+
if path.is_file():
|
|
212
|
+
return path
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
|
|
155
216
|
def publisher_profile() -> dict | None:
|
|
156
217
|
"""Load the instance's publisher profile, if one is configured.
|
|
157
218
|
|
|
@@ -159,9 +220,8 @@ def publisher_profile() -> dict | None:
|
|
|
159
220
|
operator set: deriving a region is an inference, and an inference must not
|
|
160
221
|
silently replace a stated fact.
|
|
161
222
|
"""
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if not path.is_file():
|
|
223
|
+
path = find_publisher_profile()
|
|
224
|
+
if path is None:
|
|
165
225
|
return None
|
|
166
226
|
try:
|
|
167
227
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
@@ -223,8 +283,31 @@ def add_path(args: argparse.Namespace) -> dict:
|
|
|
223
283
|
existing_manifest = load_manifest(directory)
|
|
224
284
|
previous = {d["id"]: d for d in existing_manifest.get("documents", [])}
|
|
225
285
|
now = datetime.now(timezone.utc).isoformat()
|
|
286
|
+
profile_path = find_publisher_profile()
|
|
226
287
|
profile = publisher_profile()
|
|
288
|
+
if profile is None:
|
|
289
|
+
# Said once, plainly, because the alternative is a corpus that ingests
|
|
290
|
+
# cleanly and then refuses every question with no indication why.
|
|
291
|
+
print("ingest: no publisher-profile.json found — documents will carry no zone "
|
|
292
|
+
"or trust tier, and a zoneless document can never be served. Put one "
|
|
293
|
+
"under memory/knowledge/tools/<ontology>/ or set "
|
|
294
|
+
"EGREGORE_PUBLISHER_PROFILE.", file=sys.stderr)
|
|
295
|
+
catalogue = catalogue_module.load(
|
|
296
|
+
profile or {}, base=profile_path.parent if profile_path else None
|
|
297
|
+
)
|
|
298
|
+
repair_map = text_repair.load_mapping(profile or {})
|
|
227
299
|
ingested = unchanged = metadata_updated = skipped = pruned = 0
|
|
300
|
+
# How often progress is written. The manifest used to be saved once, after
|
|
301
|
+
# every file, so an interrupted run lost everything it had extracted — on a
|
|
302
|
+
# 921-document folder that was two and a half hours of OCR with nothing to
|
|
303
|
+
# show. Documents already on disk were re-extracted because nothing recorded
|
|
304
|
+
# them. Writing periodically means an interruption costs at most this many.
|
|
305
|
+
checkpoint_every = int(os.environ.get("EGREGORE_INGEST_CHECKPOINT", "25") or 0)
|
|
306
|
+
since_checkpoint = 0
|
|
307
|
+
# Read before the loop rather than after it, so a checkpoint writes the real
|
|
308
|
+
# deletion records instead of dropping them. Pruning still happens later; a
|
|
309
|
+
# mid-run save simply carries forward what the previous run recorded.
|
|
310
|
+
tombstones = existing_manifest.get("tombstones", [])
|
|
228
311
|
errors: list[dict] = []
|
|
229
312
|
seen_paths: set[str] = set()
|
|
230
313
|
|
|
@@ -252,6 +335,15 @@ def add_path(args: argparse.Namespace) -> dict:
|
|
|
252
335
|
extraction = {"kind": "local", "extractor": extractor} if extractor else None
|
|
253
336
|
if extraction and detail:
|
|
254
337
|
extraction["pages"] = detail
|
|
338
|
+
# Fix characters the document's own font encoding got wrong, before
|
|
339
|
+
# anything downstream indexes or quotes them.
|
|
340
|
+
repaired = None
|
|
341
|
+
if text and repair_map:
|
|
342
|
+
outcome = text_repair.repair(text, repair_map)
|
|
343
|
+
if outcome.replaced:
|
|
344
|
+
text = outcome.text
|
|
345
|
+
repaired = outcome.as_report()
|
|
346
|
+
|
|
255
347
|
if error or text is None or not text.strip():
|
|
256
348
|
skipped += 1
|
|
257
349
|
errors.append({"path": relative, "error": error or "empty extracted text"})
|
|
@@ -283,6 +375,9 @@ def add_path(args: argparse.Namespace) -> dict:
|
|
|
283
375
|
continue
|
|
284
376
|
# Segment before chunking so no passage straddles two works. A document
|
|
285
377
|
# holding a single work yields one segment, and chunking is unchanged.
|
|
378
|
+
if repaired and extraction:
|
|
379
|
+
extraction["repair"] = repaired
|
|
380
|
+
|
|
286
381
|
segments = doc_segment.segment(text)
|
|
287
382
|
document_chunks: list[tuple[str, doc_segment.Segment]] = []
|
|
288
383
|
for seg in segments:
|
|
@@ -314,12 +409,37 @@ def add_path(args: argparse.Namespace) -> dict:
|
|
|
314
409
|
# is absent filters nothing. A derived value is marked as derived: anything
|
|
315
410
|
# downstream can tell it apart from one a person stated, and weigh it by the
|
|
316
411
|
# confidence recorded alongside. An operator's value is never overwritten.
|
|
412
|
+
# Evidence strongest first: what the operator stated, then what the
|
|
413
|
+
# organisation's own catalogue records, then what can be inferred from
|
|
414
|
+
# the document's origin. A weaker source never overwrites a stronger one.
|
|
317
415
|
provenance = None
|
|
318
416
|
doc_boundaries = dict(boundaries)
|
|
417
|
+
catalogued = catalogue_module.lookup(catalogue, relative)
|
|
418
|
+
|
|
419
|
+
if catalogued:
|
|
420
|
+
provenance = {"from_catalogue": {k: v for k, v in catalogued.items()}}
|
|
421
|
+
for field_name, value in catalogued.items():
|
|
422
|
+
if field_name in ("zone", "crop", "topic", "customer", "jurisdiction", "project"):
|
|
423
|
+
if field_name not in doc_boundaries:
|
|
424
|
+
doc_boundaries[field_name] = value
|
|
425
|
+
provenance["applied_boundary"] = field_name
|
|
426
|
+
if catalogued.get("year"):
|
|
427
|
+
structure.setdefault("publication_year", catalogued["year"])
|
|
428
|
+
structure["year_source"] = structure.get("year_source") or "catalogue"
|
|
429
|
+
if catalogued.get("tier"):
|
|
430
|
+
provenance["tier"] = catalogued["tier"]
|
|
431
|
+
provenance["confidence"] = "high"
|
|
432
|
+
|
|
319
433
|
if profile:
|
|
320
|
-
|
|
434
|
+
catalogue_url, catalogue_institution = catalogue_provenance(catalogued)
|
|
435
|
+
resolved = source_profile.resolve(
|
|
436
|
+
profile, url=catalogue_url, institution=catalogue_institution,
|
|
437
|
+
filename=relative)
|
|
321
438
|
if resolved.publisher_class or resolved.notes:
|
|
322
|
-
|
|
439
|
+
inferred = resolved.as_dict()
|
|
440
|
+
# Keep the inference visible even when the catalogue answered,
|
|
441
|
+
# so a disagreement between the two is inspectable.
|
|
442
|
+
provenance = {**inferred, **(provenance or {})} if provenance else inferred
|
|
323
443
|
key = source_profile.boundary_key(profile)
|
|
324
444
|
if key and resolved.zone and key not in doc_boundaries:
|
|
325
445
|
doc_boundaries[key] = resolved.zone
|
|
@@ -362,12 +482,18 @@ def add_path(args: argparse.Namespace) -> dict:
|
|
|
362
482
|
"status": "unverified",
|
|
363
483
|
"ingested_at": now,
|
|
364
484
|
}
|
|
485
|
+
since_checkpoint += 1
|
|
486
|
+
if checkpoint_every and since_checkpoint >= checkpoint_every:
|
|
487
|
+
# A crash between the document write and this one costs re-extraction
|
|
488
|
+
# of at most `checkpoint_every` files, never the whole run.
|
|
489
|
+
write_manifest(directory, source_id, org, args, boundaries, base, now,
|
|
490
|
+
previous, tombstones, errors)
|
|
491
|
+
since_checkpoint = 0
|
|
365
492
|
if existing and existing.get("content_hash") == content_hash:
|
|
366
493
|
metadata_updated += 1
|
|
367
494
|
else:
|
|
368
495
|
ingested += 1
|
|
369
496
|
|
|
370
|
-
tombstones = existing_manifest.get("tombstones", [])
|
|
371
497
|
if args.prune:
|
|
372
498
|
if not input_path.is_dir():
|
|
373
499
|
raise ValueError("--prune requires a directory snapshot")
|
|
@@ -389,11 +515,23 @@ def add_path(args: argparse.Namespace) -> dict:
|
|
|
389
515
|
# Current documents always win over historical deletion records.
|
|
390
516
|
tombstones = [row for row in tombstones if row["id"] not in previous]
|
|
391
517
|
|
|
518
|
+
write_manifest(directory, source_id, org, args, boundaries, base, now,
|
|
519
|
+
previous, tombstones, errors)
|
|
520
|
+
return {"org": org, "source": source_id, "ingested": ingested, "unchanged": unchanged, "metadata_updated": metadata_updated, "skipped": skipped, "pruned": pruned, "documents": len(previous), "metadata_root": str(directory), "data_root": str(docs_dir)}
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def write_manifest(directory, source_id, org, args, boundaries, base, now,
|
|
524
|
+
previous, tombstones, errors) -> None:
|
|
525
|
+
"""Persist progress. Called periodically during a run and once at the end.
|
|
526
|
+
|
|
527
|
+
One writer for both so a checkpoint and a completed run cannot record the
|
|
528
|
+
corpus differently.
|
|
529
|
+
"""
|
|
392
530
|
source_record = {
|
|
393
531
|
"id": source_id,
|
|
394
532
|
"org": org,
|
|
395
|
-
"name": args
|
|
396
|
-
"kind": args
|
|
533
|
+
"name": getattr(args, "name", None) or source_id,
|
|
534
|
+
"kind": getattr(args, "kind", None),
|
|
397
535
|
"boundaries": boundaries,
|
|
398
536
|
"root_hint": base.name,
|
|
399
537
|
"updated_at": now,
|
|
@@ -410,7 +548,6 @@ def add_path(args: argparse.Namespace) -> dict:
|
|
|
410
548
|
"errors": errors,
|
|
411
549
|
},
|
|
412
550
|
)
|
|
413
|
-
return {"org": org, "source": source_id, "ingested": ingested, "unchanged": unchanged, "metadata_updated": metadata_updated, "skipped": skipped, "pruned": pruned, "documents": len(previous), "metadata_root": str(directory), "data_root": str(docs_dir)}
|
|
414
551
|
|
|
415
552
|
|
|
416
553
|
def cmd_add(args: argparse.Namespace) -> int:
|
|
@@ -418,6 +555,191 @@ def cmd_add(args: argparse.Namespace) -> int:
|
|
|
418
555
|
return 0
|
|
419
556
|
|
|
420
557
|
|
|
558
|
+
def rewrite_frontmatter(path: Path, updates: dict) -> bool:
|
|
559
|
+
"""Replace named frontmatter fields in place, leaving the text untouched.
|
|
560
|
+
|
|
561
|
+
A document's placement lives in two files — the manifest record and this
|
|
562
|
+
document's own frontmatter — and a reader may consult either. Updating one
|
|
563
|
+
and not the other produces a corpus that disagrees with itself, which is
|
|
564
|
+
worse than the stale value it replaced.
|
|
565
|
+
|
|
566
|
+
Only the named lines are rewritten. The body is never reparsed, so nothing
|
|
567
|
+
that survived extraction can be lost by a maintenance pass.
|
|
568
|
+
"""
|
|
569
|
+
try:
|
|
570
|
+
text = path.read_text(encoding="utf-8")
|
|
571
|
+
except OSError:
|
|
572
|
+
return False
|
|
573
|
+
lines = text.split("\n")
|
|
574
|
+
if not lines or lines[0].strip() != "---":
|
|
575
|
+
return False
|
|
576
|
+
try:
|
|
577
|
+
close = next(i for i in range(1, len(lines)) if lines[i].strip() == "---")
|
|
578
|
+
except StopIteration:
|
|
579
|
+
return False
|
|
580
|
+
remaining = dict(updates)
|
|
581
|
+
for i in range(1, close):
|
|
582
|
+
key = lines[i].split(":", 1)[0].strip()
|
|
583
|
+
if key in remaining:
|
|
584
|
+
lines[i] = f"{key}: {json.dumps(remaining.pop(key), ensure_ascii=False)}"
|
|
585
|
+
# A field the original write omitted is appended rather than dropped, so a
|
|
586
|
+
# document ingested before a field existed still gains it.
|
|
587
|
+
for key, value in remaining.items():
|
|
588
|
+
lines.insert(close, f"{key}: {json.dumps(value, ensure_ascii=False)}")
|
|
589
|
+
close += 1
|
|
590
|
+
atomic_text(path, "\n".join(lines))
|
|
591
|
+
return True
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def reresolve_source(args: argparse.Namespace) -> dict:
|
|
595
|
+
"""Re-derive placement for documents already ingested.
|
|
596
|
+
|
|
597
|
+
Placement is written once, at ingest, and never revisited. That is right
|
|
598
|
+
for content — the text of a document does not change because a rule did —
|
|
599
|
+
but wrong for the rules themselves. Correcting a publisher profile changed
|
|
600
|
+
nothing about the corpus it governs: `add` skips on the content hash and
|
|
601
|
+
returns before it ever consults the profile, so it reported every document
|
|
602
|
+
unchanged while the corrected rule went unapplied. The only way to apply a
|
|
603
|
+
fix was to re-extract thousands of documents, redoing OCR to recompute a
|
|
604
|
+
field that depends on nothing but the filename.
|
|
605
|
+
|
|
606
|
+
So this re-runs the resolver alone. No file is reopened, no text is
|
|
607
|
+
re-extracted; the stored path is all the resolver ever needed.
|
|
608
|
+
|
|
609
|
+
Evidence precedence is the same as at ingest, and is the reason this is
|
|
610
|
+
safe to run at any time: a value the operator stated, or one the
|
|
611
|
+
organisation's own catalogue recorded, is never overwritten by an
|
|
612
|
+
inference. Only a placement this resolver derived — or the absence of one —
|
|
613
|
+
is open to revision.
|
|
614
|
+
"""
|
|
615
|
+
config = load_config()
|
|
616
|
+
source_id = slug(args.source)
|
|
617
|
+
directory = source_dir(source_id)
|
|
618
|
+
manifest = load_manifest(directory)
|
|
619
|
+
records = manifest.get("documents") or []
|
|
620
|
+
if not records:
|
|
621
|
+
return {"source": source_id, "error": "no manifest, or no documents in it",
|
|
622
|
+
"metadata_root": str(directory)}
|
|
623
|
+
|
|
624
|
+
profile = publisher_profile()
|
|
625
|
+
if profile is None:
|
|
626
|
+
return {"source": source_id,
|
|
627
|
+
"error": "no publisher-profile.json found — nothing to resolve against"}
|
|
628
|
+
key = source_profile.boundary_key(profile)
|
|
629
|
+
if not key:
|
|
630
|
+
return {"source": source_id, "error": "profile declares no boundary key"}
|
|
631
|
+
|
|
632
|
+
docs_dir = data_root() / "sources" / source_id / "documents"
|
|
633
|
+
placed = rezoned = identified = unchanged = protected = missing = 0
|
|
634
|
+
changes: list = []
|
|
635
|
+
|
|
636
|
+
for record in records:
|
|
637
|
+
relative = record.get("source_path")
|
|
638
|
+
if not relative:
|
|
639
|
+
unchanged += 1
|
|
640
|
+
continue
|
|
641
|
+
boundaries = dict(record.get("boundaries") or {})
|
|
642
|
+
provenance = dict(record.get("provenance") or {})
|
|
643
|
+
catalogued = provenance.get("from_catalogue") or {}
|
|
644
|
+
|
|
645
|
+
# The same evidence the original ingest had, re-read from the record so
|
|
646
|
+
# a document identified only by its catalogue row resolves here too.
|
|
647
|
+
catalogue_url, catalogue_institution = catalogue_provenance(catalogued)
|
|
648
|
+
resolved = source_profile.resolve(
|
|
649
|
+
profile, url=catalogue_url, institution=catalogue_institution,
|
|
650
|
+
filename=relative)
|
|
651
|
+
|
|
652
|
+
# Placement and identification are decided separately. A document can
|
|
653
|
+
# become identifiable — a journal index naming its publisher — without
|
|
654
|
+
# that saying anything about which region it applies to, and reporting
|
|
655
|
+
# only the placement would call that document unchanged when the thing
|
|
656
|
+
# that decides how far to trust it has just been settled.
|
|
657
|
+
current = boundaries.get(key)
|
|
658
|
+
# Anything the operator stated or the catalogue recorded outranks an
|
|
659
|
+
# inference and is left exactly as it is.
|
|
660
|
+
derived_here = (provenance.get("derived") is True
|
|
661
|
+
and provenance.get("applied_boundary") == key)
|
|
662
|
+
zone_open = not ((current and not derived_here) or catalogued.get(key))
|
|
663
|
+
zone_changes = bool(zone_open and resolved.zone and resolved.zone != current)
|
|
664
|
+
|
|
665
|
+
# A tier the catalogue stated is likewise not ours to revise.
|
|
666
|
+
class_open = not catalogued.get("tier")
|
|
667
|
+
class_changes = bool(
|
|
668
|
+
class_open and resolved.publisher_class
|
|
669
|
+
and (resolved.publisher_class != provenance.get("publisher_class")
|
|
670
|
+
or resolved.tier != provenance.get("tier")))
|
|
671
|
+
|
|
672
|
+
if not zone_changes and not class_changes:
|
|
673
|
+
if not zone_open and not class_open:
|
|
674
|
+
protected += 1
|
|
675
|
+
elif current and not zone_open:
|
|
676
|
+
protected += 1
|
|
677
|
+
else:
|
|
678
|
+
unchanged += 1
|
|
679
|
+
continue
|
|
680
|
+
|
|
681
|
+
# The fresh resolution replaces the recorded inference whenever either
|
|
682
|
+
# half moved. Refreshing only on a class change once left a re-zoned
|
|
683
|
+
# document carrying the confidence and notes of the resolution it no
|
|
684
|
+
# longer had — a record that reported an inferred national zone with
|
|
685
|
+
# the confidence of a precise one.
|
|
686
|
+
updated_provenance = dict(provenance)
|
|
687
|
+
updated_provenance.update(resolved.as_dict())
|
|
688
|
+
if class_changes:
|
|
689
|
+
identified += 1
|
|
690
|
+
if catalogued:
|
|
691
|
+
updated_provenance["from_catalogue"] = catalogued
|
|
692
|
+
if zone_changes:
|
|
693
|
+
updated_provenance["applied_boundary"] = key
|
|
694
|
+
updated_provenance["derived"] = True
|
|
695
|
+
boundaries[key] = resolved.zone
|
|
696
|
+
if current:
|
|
697
|
+
rezoned += 1
|
|
698
|
+
else:
|
|
699
|
+
placed += 1
|
|
700
|
+
|
|
701
|
+
changes.append({"id": record["id"], "source_path": relative,
|
|
702
|
+
"zone": {"from": current, "to": boundaries.get(key)},
|
|
703
|
+
"class": {"from": provenance.get("publisher_class"),
|
|
704
|
+
"to": updated_provenance.get("publisher_class")},
|
|
705
|
+
"matched_by": resolved.matched_by})
|
|
706
|
+
|
|
707
|
+
if args.dry_run:
|
|
708
|
+
continue
|
|
709
|
+
record["boundaries"] = boundaries
|
|
710
|
+
record["provenance"] = updated_provenance
|
|
711
|
+
file_path = record.get("file_path")
|
|
712
|
+
target = (data_root() / file_path) if file_path else (docs_dir / f"{record['id']}.md")
|
|
713
|
+
if not rewrite_frontmatter(target, {"boundaries": boundaries,
|
|
714
|
+
"provenance": updated_provenance}):
|
|
715
|
+
missing += 1
|
|
716
|
+
|
|
717
|
+
if not args.dry_run and (placed or rezoned or identified):
|
|
718
|
+
manifest["documents"] = sorted(records, key=lambda d: d["id"])
|
|
719
|
+
atomic_json(directory / "manifest.json", manifest)
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
"org": slug(config.get("slug") or "egregore"),
|
|
723
|
+
"source": source_id,
|
|
724
|
+
"dry_run": bool(args.dry_run),
|
|
725
|
+
"documents": len(records),
|
|
726
|
+
"placed": placed,
|
|
727
|
+
"rezoned": rezoned,
|
|
728
|
+
"identified": identified,
|
|
729
|
+
"unchanged": unchanged,
|
|
730
|
+
"protected": protected,
|
|
731
|
+
"frontmatter_not_found": missing,
|
|
732
|
+
"examples": changes[: max(0, args.examples)],
|
|
733
|
+
"metadata_root": str(directory),
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def cmd_reresolve(args: argparse.Namespace) -> int:
|
|
738
|
+
result = reresolve_source(args)
|
|
739
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
740
|
+
return 1 if result.get("error") else 0
|
|
741
|
+
|
|
742
|
+
|
|
421
743
|
def selection_relative_path(value: object) -> str:
|
|
422
744
|
if not isinstance(value, str) or not value or "\x00" in value or "\\" in value:
|
|
423
745
|
raise ValueError("selection contains an invalid relative path")
|
|
@@ -643,6 +965,15 @@ def parser() -> argparse.ArgumentParser:
|
|
|
643
965
|
add.add_argument("--boundary", action="append", default=[])
|
|
644
966
|
add.add_argument("--prune", action="store_true", help="tombstone files absent from this authoritative directory snapshot")
|
|
645
967
|
add.set_defaults(func=cmd_add)
|
|
968
|
+
reresolve = sub.add_parser(
|
|
969
|
+
"reresolve",
|
|
970
|
+
help="re-derive placement from the current profile, without re-reading any document")
|
|
971
|
+
reresolve.add_argument("--source", required=True, help="source id to re-resolve")
|
|
972
|
+
reresolve.add_argument("--dry-run", action="store_true",
|
|
973
|
+
help="report what would change and write nothing")
|
|
974
|
+
reresolve.add_argument("--examples", type=int, default=10,
|
|
975
|
+
help="how many changed documents to list (default 10)")
|
|
976
|
+
reresolve.set_defaults(func=cmd_reresolve)
|
|
646
977
|
add_selection = sub.add_parser("add-selection")
|
|
647
978
|
add_selection.add_argument("manifest")
|
|
648
979
|
add_selection.set_defaults(func=cmd_add_selection)
|
|
@@ -370,6 +370,29 @@ fi
|
|
|
370
370
|
JSON="[]"
|
|
371
371
|
RICH="[]"
|
|
372
372
|
if [ -d "$SCRIPT_DIR/memory/handoffs" ]; then
|
|
373
|
+
# One definition everywhere: in connected mode the list comes from the
|
|
374
|
+
# graph's open-handoffs named read (open = pending/unread/read/claimed,
|
|
375
|
+
# matched by name/github/aliases) — the same definition the dashboard
|
|
376
|
+
# uses. The file grep below stays as the local-mode/offline fallback.
|
|
377
|
+
ADDRESSED=""
|
|
378
|
+
if [ "${LOCAL_MODE:-false}" != "true" ]; then
|
|
379
|
+
_GRAPH_LIST=$(bash "$SCRIPT_DIR/bin/graph-op.sh" open-handoffs "$AUTHOR" 5 2>/dev/null || true)
|
|
380
|
+
if echo "$_GRAPH_LIST" | jq -e '.values | length > 0' >/dev/null 2>&1; then
|
|
381
|
+
ADDRESSED=$(echo "$_GRAPH_LIST" | jq -r '.values[][0]' 2>/dev/null | awk '!seen[$0]++' | while IFS= read -r _SID; do
|
|
382
|
+
# Two on-disk layouts: dated subdirs (YYYY-MM/DD-slug.md, the id's
|
|
383
|
+
# own shape) and flat files at the handoffs root. Probe both, else
|
|
384
|
+
# a graph-listed handoff silently vanishes from the greeting.
|
|
385
|
+
_MONTH="${_SID:0:7}"
|
|
386
|
+
_REST="${_SID:8}"
|
|
387
|
+
_F="$SCRIPT_DIR/memory/handoffs/$_MONTH/$_REST.md"
|
|
388
|
+
if [ -f "$_F" ]; then
|
|
389
|
+
echo "$_F"
|
|
390
|
+
elif [ -f "$SCRIPT_DIR/memory/handoffs/$_SID.md" ]; then
|
|
391
|
+
echo "$SCRIPT_DIR/memory/handoffs/$_SID.md"
|
|
392
|
+
fi
|
|
393
|
+
done | head -5)
|
|
394
|
+
fi
|
|
395
|
+
fi
|
|
373
396
|
# Match github username, display name, github_name first word, and people-file name
|
|
374
397
|
# Handoff files may use "to: name", "**To**: name", or "To: name"
|
|
375
398
|
_DISPLAY=$(jq -r '.display_name // empty' "$STATE_FILE" 2>/dev/null)
|
|
@@ -387,7 +410,9 @@ fi
|
|
|
387
410
|
echo "$_GREP_PAT" | grep -qF "$_VARIANT" && continue
|
|
388
411
|
_GREP_PAT="$_GREP_PAT\|[Tt]o[*]*: *$_VARIANT\|[Tt]o[*]*:$_VARIANT"
|
|
389
412
|
done
|
|
390
|
-
|
|
413
|
+
if [ -z "$ADDRESSED" ]; then
|
|
414
|
+
ADDRESSED=$(grep -rli "$_GREP_PAT" "$SCRIPT_DIR/memory/handoffs/" 2>/dev/null | sort -r | head -5 || true)
|
|
415
|
+
fi
|
|
391
416
|
JSON="["
|
|
392
417
|
RICH="["
|
|
393
418
|
FIRST=true
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# git-message.sh — shared commit/PR message mechanics, sourced by composers.
|
|
3
|
+
# Spec: .claude/context/commit-format.md + .claude/context/pr-format.md.
|
|
4
|
+
#
|
|
5
|
+
# Provides:
|
|
6
|
+
# egregore_session_trailer <hub_dir>
|
|
7
|
+
# Print "Egregore-Session: <id>" when <hub_dir>/.egregore-session-id
|
|
8
|
+
# exists; print nothing otherwise. Never fails.
|
|
9
|
+
# egregore_commit <git_dir> <hub_dir> <message> [git-commit flags…]
|
|
10
|
+
# git -C <git_dir> commit with the session trailer appended as the
|
|
11
|
+
# final paragraph (a valid git trailer block) when an id exists.
|
|
12
|
+
# egregore_pr_skeleton <what> <why> <verification> <footer>
|
|
13
|
+
# The one source of the skeleton PR body shape.
|
|
14
|
+
|
|
15
|
+
egregore_session_trailer() {
|
|
16
|
+
local sid
|
|
17
|
+
sid="$(cat "${1:-.}/.egregore-session-id" 2>/dev/null || true)"
|
|
18
|
+
[ -n "$sid" ] && printf 'Egregore-Session: %s' "$sid"
|
|
19
|
+
return 0
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
egregore_commit() {
|
|
23
|
+
local git_dir="$1" hub_dir="$2" msg="$3"
|
|
24
|
+
shift 3
|
|
25
|
+
local trailer
|
|
26
|
+
trailer="$(egregore_session_trailer "$hub_dir")"
|
|
27
|
+
if [ -n "$trailer" ]; then
|
|
28
|
+
git -C "$git_dir" commit -m "$msg" -m "$trailer" "$@"
|
|
29
|
+
else
|
|
30
|
+
git -C "$git_dir" commit -m "$msg" "$@"
|
|
31
|
+
fi
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
egregore_pr_skeleton() {
|
|
35
|
+
printf '## What\n%s\n\n## Why\n%s\n\n## Verification\n%s\n\n%s\n' \
|
|
36
|
+
"$1" "$2" "$3" "$4"
|
|
37
|
+
}
|
|
@@ -185,7 +185,7 @@ _apply_framework_update() {
|
|
|
185
185
|
FRAMEWORK_UPDATED_COUNT=$(printf '%s\n' "$_changed" | grep -c . || echo 0)
|
|
186
186
|
FRAMEWORK_UPDATED_FILES=$(printf '%s\n' "$_changed" | awk '{print $NF}' | head -3 | tr '\n' ' ')
|
|
187
187
|
git add bin/ .claude/ loom/ CLAUDE.md skills/ 2>/dev/null
|
|
188
|
-
EGREGORE_FRAMEWORK_UPDATE=1 git commit -m "
|
|
188
|
+
EGREGORE_FRAMEWORK_UPDATE=1 git commit -m "chore(sync): update framework from upstream" --quiet 2>/dev/null || true
|
|
189
189
|
FRAMEWORK_UPDATED="true"
|
|
190
190
|
fi
|
|
191
191
|
}
|
|
@@ -214,6 +214,14 @@ fi
|
|
|
214
214
|
# Named setup_develop for continuity; it operates on $BASE_BRANCH, which is
|
|
215
215
|
# "develop" by default and whatever egregore.json's base_branch says otherwise.
|
|
216
216
|
setup_develop() {
|
|
217
|
+
# Detached HEAD (CI merge-ref checkouts, bisects): never create or switch
|
|
218
|
+
# branches here — `git checkout -b $BASE_BRANCH origin/$BASE_BRANCH` would
|
|
219
|
+
# silently replace the checked-out tree with the base branch. This is how
|
|
220
|
+
# CI jobs that invoke session-start machinery ended up testing develop
|
|
221
|
+
# instead of the PR under test. Syncing means updating the base-branch ref,
|
|
222
|
+
# not claiming HEAD; on a detached HEAD there is nothing safe to do.
|
|
223
|
+
[ -n "$(git branch --show-current 2>/dev/null)" ] || return 0
|
|
224
|
+
|
|
217
225
|
# Ensure the base branch exists locally
|
|
218
226
|
if ! git show-ref --verify --quiet "refs/heads/$BASE_BRANCH" 2>/dev/null; then
|
|
219
227
|
if git show-ref --verify --quiet "refs/remotes/origin/$BASE_BRANCH" 2>/dev/null; then
|
|
@@ -267,7 +275,7 @@ setup_develop() {
|
|
|
267
275
|
# Save uncommitted work so nothing is lost (guarded: never auto-commit
|
|
268
276
|
# stray submodule pointers from sibling repos in the checkout)
|
|
269
277
|
git_add_guarded
|
|
270
|
-
git commit -m "
|
|
278
|
+
git commit -m "chore(autosave): save uncommitted work from $CURRENT_BRANCH" --quiet 2>/dev/null || true
|
|
271
279
|
SAVED_BRANCH="$CURRENT_BRANCH"
|
|
272
280
|
fi
|
|
273
281
|
|
|
@@ -188,7 +188,10 @@ fi
|
|
|
188
188
|
# Staged auto-saves awaiting the user's merge (the /autosave consent gate).
|
|
189
189
|
# Live gh query so the line persists until the PRs actually merge — the
|
|
190
190
|
# ledger cursor above only reports once. Fail-soft when gh/network is out.
|
|
191
|
-
|
|
191
|
+
# Shown only while the autosave experiment is enabled: stale PRs from a
|
|
192
|
+
# disabled experiment should not nag every greeting.
|
|
193
|
+
AUTOSAVE_ON=$(jq -r '.autosave_enabled // false' "$STATE_FILE" 2>/dev/null)
|
|
194
|
+
if [ "$AUTOSAVE_ON" = "true" ] && command -v gh >/dev/null 2>&1; then
|
|
192
195
|
GH_SELF=$(jq -r '.github_username // empty' "$STATE_FILE" 2>/dev/null)
|
|
193
196
|
if [ -n "$GH_SELF" ]; then
|
|
194
197
|
WAITING_AS=$(gh pr list --state open --author "$GH_SELF" --limit 20 \
|
|
@@ -52,3 +52,28 @@ egregore_link_shared_state() {
|
|
|
52
52
|
fi
|
|
53
53
|
done
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
# Resolve the .env that governs a checkout. A worktree can be missing its .env
|
|
57
|
+
# symlink (created before .env existed, or by a runtime that skipped worktree
|
|
58
|
+
# setup) — self-heal the links, then fall back to the main checkout's .env so
|
|
59
|
+
# credential loading never silently comes up empty. Prints the usable path, or
|
|
60
|
+
# nothing when neither checkout has one. Always returns 0.
|
|
61
|
+
egregore_env_file() {
|
|
62
|
+
local project_dir="${1:?project dir required}"
|
|
63
|
+
if [ ! -f "$project_dir/.env" ] && [ -f "$project_dir/.git" ]; then
|
|
64
|
+
egregore_link_shared_state "$project_dir" 2>/dev/null || true
|
|
65
|
+
fi
|
|
66
|
+
if [ -f "$project_dir/.env" ]; then
|
|
67
|
+
echo "$project_dir/.env"
|
|
68
|
+
return 0
|
|
69
|
+
fi
|
|
70
|
+
if [ -f "$project_dir/.git" ]; then
|
|
71
|
+
local main_dir
|
|
72
|
+
main_dir="$(egregore_main_project_dir "$project_dir" 2>/dev/null || true)"
|
|
73
|
+
if [ -n "$main_dir" ] && [ -f "$main_dir/.env" ]; then
|
|
74
|
+
echo "$main_dir/.env"
|
|
75
|
+
return 0
|
|
76
|
+
fi
|
|
77
|
+
fi
|
|
78
|
+
return 0
|
|
79
|
+
}
|