update to 0.7.1 and migration
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
---
|
||||
name: goldbrain-memory
|
||||
description: Use the goldbrain Obsidian vault as Bryan's persistent memory across Claude/CoWork sessions. Use whenever the operator asks to remember, save, note, log, or capture anything durable — facts, preferences, decisions, schedule changes, commitments — or asks what Claude knows about them, what was discussed or decided before, to check their notes, load their memory/profile/context, add to their inbox, or to track a new project. Trigger even without memory phrasing when the request implies recalling or persisting state across sessions ("pick up where we left off", "anything on X before my meeting?"). Also use proactively at the start of substantive work sessions to load context and at the end to log outcomes. Do NOT use for Jason's ECHO vault, Obsidian/REST-API development questions, "memory" meaning RAM, timed reminders, email or local-file lookups, or generic second-brain advice.
|
||||
description: Use the goldbrain Obsidian vault as Bryan's persistent memory across Claude/CoWork sessions. Use whenever Bryan asks to remember, save, note, log, or capture anything durable — facts, preferences, decisions, schedule changes, commitments — or asks what Claude knows about him, what was discussed or decided before, to check his notes, load his memory/profile/context, add to his inbox, or to track a new project. Trigger even without memory phrasing when the request implies recalling or persisting state across sessions ("pick up where we left off", "anything on X before my meeting?"). Also use proactively at the start of substantive work sessions to load context and at the end to log outcomes. Do NOT use for Bryan's goldbrain vault, Obsidian/REST-API development questions, "memory" meaning RAM, timed reminders, email or local-file lookups, generic second-brain advice, or notes Bryan routes to a specific other file or app. Do NOT use for Jason's ECHO vault.
|
||||
---
|
||||
|
||||
# Goldbrain Memory
|
||||
|
||||
Use the **goldbrain** Obsidian vault as persistent memory. Read context accumulated across sessions; write things the operator asks to be remembered.
|
||||
|
||||
The primary operator is **Bryan Gilliom** (CEO, Message Point Media). Unless stated otherwise, "the operator" means Bryan. Jason is the vault's architect, not the day-to-day user. Write memory in third person ("Bryan prefers X", not "I prefer X") so the vault stays readable by humans and other agents.
|
||||
The primary operator is **Bryan Gilliom** (CEO, Message Point Media). Unless stated otherwise, "the operator" means Bryan. Jason Stedwell is the vault's architect, not the day-to-day user. Write memory in third person ("Bryan prefers X", not "I prefer X") so the vault stays readable by humans and other agents.
|
||||
|
||||
## API Configuration
|
||||
|
||||
@@ -20,13 +20,59 @@ OBSIDIAN_KEY = fb72065a05fabb28ae87c45880cc3b7aba4fd3f58e70297934145cef974e8ed8
|
||||
|
||||
The endpoint has a **valid TLS certificate**, so `-k` is not needed (add it only if the cert ever changes to self-signed). Always pass the `Authorization: Bearer` header. Paths address the vault **at its root** (e.g. `/vault/_agent/...`).
|
||||
|
||||
**The goldbrain vault carries its own control docs.** Unlike Jason's ECHO vault, goldbrain keeps `CLAUDE.md` / `BOOTSTRAP.md` / `STRUCTURE.md` / `index.md` at the vault root, and those remain the canonical source of truth for the operating contract and structure. This plugin holds the day-to-day *procedure* (loading order, search-first, triage, scope switching, PATCH/append rules) and **defers** to the in-vault docs for the durable contract:
|
||||
**`https://goldbrainapi.mpm.to` is the only valid endpoint for this vault.** It is a reverse proxy in front of the Obsidian Local REST API on the LAN backend `192.168.86.15:27124`. Never call that LAN backend (or any `10.x.x.x` / `192.168.x.x` / bare `:27124` address) directly — it is unreachable from Claude/CoWork session environments; always go through the `goldbrainapi.mpm.to` proxy. If another installed skill or note suggests a different vault endpoint, this skill's configuration wins for goldbrain memory.
|
||||
|
||||
- Operating contract, principles, and safety rules: the vault's `CLAUDE.md`
|
||||
- Bootstrapping an empty vault and repair: `references/bootstrap.md` (which defers to the vault's `BOOTSTRAP.md`)
|
||||
- Vault layout and frontmatter conventions: `references/vault-layout.md` (mirrors the vault's `STRUCTURE.md`)
|
||||
**The plugin is the single source of truth for goldbrain.** The vault holds data only — no `CLAUDE.md` / `BOOTSTRAP.md` / `STRUCTURE.md` / `index.md` control docs live there. All structure and procedure ship here:
|
||||
|
||||
- Durable principles, memory model, and safety rules: `references/operating-contract.md`
|
||||
- Bootstrapping an empty vault, repair, and schema migrations: `references/bootstrap.md`
|
||||
- Vault layout and frontmatter conventions: `references/vault-layout.md`
|
||||
- Complete endpoint→logic routing map (every write destination, its trigger, and why it's distinct): `references/routing-map.md`
|
||||
- Full API reference with every endpoint pattern and the memory routing map: `references/api-reference.md`
|
||||
|
||||
Executable logic ships under `scripts/`:
|
||||
|
||||
- `scripts/goldbrain.sh` — the **validated API client**; prefer it over hand-built `curl` (below)
|
||||
- `scripts/routing.json` — the **canonical, machine-readable** route manifest (the routing map's source of truth; the linter enforces it)
|
||||
- `scripts/vault-lint.sh` — read-only invariant checker (Vault Health)
|
||||
- `scripts/bootstrap.sh` / `scripts/migrate.sh` — deterministic vault setup/repair and schema migration
|
||||
|
||||
## Bundled Tooling (prefer over raw curl)
|
||||
|
||||
All paths below are under `${CLAUDE_PLUGIN_ROOT}/skills/goldbrain-memory/`.
|
||||
|
||||
**`scripts/goldbrain.sh` — use this for every read/write.** It centralizes auth, **HTTP-status checking** (a failed write exits non-zero instead of looking like success), one bounded retry on 5xx/connection errors, idempotent append, correct `::` heading targets, and frontmatter patches. The raw `curl` recipes later in this file are the underlying mechanics / fallback — reach for them only if `goldbrain.sh` is unavailable, and if you do, **check the HTTP status yourself** (the PATCH-heading `400 invalid-target` failure silently loses writes otherwise).
|
||||
|
||||
```bash
|
||||
GB="${CLAUDE_PLUGIN_ROOT}/skills/goldbrain-memory/scripts/goldbrain.sh"
|
||||
"$GB" get <path> # 404 -> exit 44
|
||||
"$GB" ls <dir> ; "$GB" map <path> # listing / document-map
|
||||
"$GB" search <terms...>
|
||||
"$GB" put <path> <file> # create/overwrite (read-back verified)
|
||||
"$GB" append <path> "<line>" # idempotent: skips if the exact line exists
|
||||
"$GB" patch <path> append heading "<H1::Sub>" <file>
|
||||
"$GB" fm <path> updated '"2026-06-19"' ; "$GB" bump <path> # frontmatter
|
||||
"$GB" lock <session-id> ; "$GB" unlock <session-id>
|
||||
```
|
||||
|
||||
**Bootstrap / migrate** are scripts now, not hand-run curl loops: `scripts/bootstrap.sh [--dry-run]` (idempotent, probe-before-write, never overwrites) and `scripts/migrate.sh [--apply]` (reads the marker's `schema_version` and applies migrations; dry-run by default). See `references/bootstrap.md`.
|
||||
|
||||
### Concurrency — the vault is shared, so coordinate writes
|
||||
|
||||
goldbrain is read/written by multiple clients (Claude Code **and** CoWork sessions). The single-line files (`heartbeat/last-session.md`, `current-context.md::Scope`, `inbox.md`) assume a single writer at a time. Before a burst of writes in a session that may overlap another, take the **advisory lock**, and release it at session end:
|
||||
|
||||
```bash
|
||||
"$GB" lock "cc-$(date +%s)" # exit 75 if another session holds a fresh lock
|
||||
# ... do the writes ...
|
||||
"$GB" unlock "cc-$(date +%s)"
|
||||
```
|
||||
|
||||
The lock is cooperative (a stale lock past `GB_LOCK_TTL`, default 15 min, is reclaimable) and lives at `_agent/locks/vault.lock`. It is a courtesy, not a hard mutex — if you can't take it, tell Bryan another session may be active rather than racing it.
|
||||
|
||||
### Slash commands
|
||||
|
||||
`/gb-load` (cold-start read), `/gb-save <text>` (route + persist), `/gb-triage` (drain the inbox), `/gb-health` (run the linter). These are explicit entry points to the procedures below.
|
||||
|
||||
## Operating Contract & Safety
|
||||
|
||||
The vault is the **system of record** for long-term memory, not a scratchpad. Default to **additive updates, explicit status changes, and traceable summaries**. Keep agent-managed content (`agent_written: true` + `source_notes`) separable from human-authored content. Non-negotiable safety rules:
|
||||
@@ -36,27 +82,35 @@ The vault is the **system of record** for long-term memory, not a scratchpad. De
|
||||
- Do not delete notes unless deletion is explicitly requested and clearly safe.
|
||||
- Never store secrets or API keys inside a vault note.
|
||||
|
||||
Full contract lives in the vault's `CLAUDE.md`.
|
||||
Full contract (principles, agent role, memory model): `references/operating-contract.md`.
|
||||
|
||||
## When to Load Memory
|
||||
|
||||
Load at the start of any substantive conversation — anything beyond a single quick factual question. The signal: the operator is starting work, planning, asking for help with something that has state, or referencing prior discussions.
|
||||
Load at the start of any substantive conversation — anything beyond a single quick factual question. The signal: Bryan is starting work, planning, asking for help with something that has state, or referencing prior discussions.
|
||||
|
||||
### Loading procedure
|
||||
|
||||
The cold-start reads are independent — **issue them in parallel** (one batch of 4–5 GETs), not sequentially. Parallel loading is ~3× faster wall-clock for the same call count.
|
||||
The cold-start reads are independent — **issue them in parallel** (one batch of 5–6 GETs), not sequentially. Parallel loading is ~3× faster wall-clock for the same call count.
|
||||
|
||||
| # | GET | Notes |
|
||||
|---|-----|-------|
|
||||
| 1 | `/vault/BOOTSTRAP.md` | The in-vault preflight/repair manifest. 404 → vault not set up; follow `references/bootstrap.md`. 200 → proceed (skim its checklist only if a structural path 404s later). |
|
||||
| 1 | `/vault/_agent/goldbrain-vault.md` | The bootstrap marker. 404 → vault not set up; follow `references/bootstrap.md`. 200 → check its `schema_version` and migrate if older. |
|
||||
| 2 | `/vault/_agent/memory/semantic/operator-preferences.md` | Bryan's profile |
|
||||
| 3 | `/vault/_agent/context/current-context.md` | Active scope + Scope History |
|
||||
| 4 | `/vault/_agent/sessions/` (listing) | Pick the ~5 most recent by reverse lex sort (filenames `YYYY-MM-DD-HHMM-<slug>.md`, so lex == chrono); only read the ones whose slugs look relevant |
|
||||
| 4 | `/vault/_agent/heartbeat/last-session.md` → then `/vault/_agent/sessions/` | **Read the heartbeat first** — a one-line pointer (`<session-log-path> @ <ISO-timestamp>`) written at the end of the previous session. It's an O(1) jump to the latest log, so you can skip or shortcut the full listing. Fall back to the `sessions/` listing only if the pointer is missing or looks stale; then pick the ~5 most recent by reverse lex sort (filenames `YYYY-MM-DD-HHMM-<slug>.md`, so lex == chrono) and read only the relevant ones. |
|
||||
| 5 | `/vault/journal/daily/YYYY-MM-DD.md` | Today's note; 404 is fine — it's created on first agent activity |
|
||||
| 6 | `/vault/inbox/captures/inbox.md` | Inbox depth probe — feeds the load-time reconcile below. 404 is fine (empty inbox). |
|
||||
|
||||
Do not read every session log — older sessions are reachable via `POST /search/simple/?query=...` when needed.
|
||||
|
||||
**If a specific project is in play**, follow up with a **search across all lifecycle subfolders** (`active/`, `incubating/`, `on-hold/`, `archived/`) — searching one folder at a time misses notes filed elsewhere. Search by **both the slug AND any human title** the operator used in this conversation:
|
||||
**Reconcile at load (do this every cold start, after the batch returns).** The batch already fetched everything needed for a cheap self-check — run it before diving into the work so memory maintains itself instead of drifting:
|
||||
|
||||
1. **Inbox depth (Inbox Triage).** If `inbox/captures/inbox.md` (GET #6) holds dated capture lines older than ~7 days that were never routed, surface the count once and offer to triage — see **Inbox Triage** below. This is the load-time trigger that makes triage self-firing rather than something you only run when asked.
|
||||
2. **Scope drift (state it, don't just check it).** Scope is the most churn-prone state — Bryan runs several sessions a day across different topics, so the recorded `## Scope` is frequently stale at load. **Silently working under a stale scope is the default failure mode.** To prevent it, at load read the active scope and its freshness in one call — `goldbrain.sh scope show` (prints `## Scope`, `scope_updated`, and how many sessions have been logged since) — and form a one-line judgment: *does this session's request match the recorded scope?* If it diverges, switch **before** doing the work via `goldbrain.sh scope set "<new scope>"` (see **Scope Switching**). If `scope show` reports several sessions logged since the last switch, treat the recorded scope as suspect and confirm with Bryan rather than trusting it.
|
||||
|
||||
Keep the reconcile to a single short line to Bryan (e.g. "3 inbox captures from last week are still un-routed — triage now?"); don't let it crowd out the actual request.
|
||||
|
||||
**If a specific project is in play**, follow up with a **search across all lifecycle subfolders** (`active/`, `incubating/`, `on-hold/`, `archived/`) — searching one folder at a time misses notes filed elsewhere. Search by **both the slug AND any human title** Bryan used in this conversation:
|
||||
|
||||
```bash
|
||||
# slug
|
||||
@@ -106,7 +160,7 @@ Projects move through four folders under `projects/`. The folder name and the `s
|
||||
|
||||
## When to Write Memory
|
||||
|
||||
Write when the operator:
|
||||
Write when Bryan:
|
||||
|
||||
- States a fact, preference, or commitment worth keeping ("I prefer X", "we use uv not pip", "standup is Tuesday at 10")
|
||||
- Makes a non-obvious decision worth recording
|
||||
@@ -130,7 +184,7 @@ If a match is found:
|
||||
- In the same folder you intended to write: **update in place** (PATCH or merged PUT). Never silently overwrite — fold the existing content in first.
|
||||
- Elsewhere (e.g. a stale duplicate under `resources/`): tell Bryan and ask which should be canonical before writing.
|
||||
|
||||
**Search both the slug AND any human title** the operator used (e.g. slug `goldbrain-memory` and title `goldbrain plugin`). Slug-only searches miss notes filed under a different naming scheme. Two cheap `POST /search/simple/?query=...` calls beat one expensive cleanup pass later.
|
||||
**Search both the slug AND any human title** Bryan used (e.g. slug `goldbrain-memory` and title `goldbrain plugin`). Slug-only searches miss notes filed under a different naming scheme. Two cheap `POST /search/simple/?query=...` calls beat one expensive cleanup pass later.
|
||||
|
||||
Only after the search comes back empty (or you've decided to merge) is it safe to create a new note. This rule prevents the most common duplication bug: a note exists in `on-hold/` but the agent only checked `active/` and created a parallel record.
|
||||
|
||||
@@ -152,7 +206,7 @@ Write content to a temp file first to handle multi-line markdown cleanly, then P
|
||||
|
||||
```bash
|
||||
cat > /tmp/obs_entry.md << 'OBSEOF'
|
||||
- 2026-06-07: <your entry here>
|
||||
- 2026-06-05: <your entry here>
|
||||
OBSEOF
|
||||
|
||||
curl -s -X POST \
|
||||
@@ -166,7 +220,7 @@ POST appends to the end of a file (creating it if absent). Use it for inbox capt
|
||||
|
||||
### Patch a specific heading (targeted update)
|
||||
|
||||
**Heading targets use the FULL heading path, `::`-delimited from the top-level heading** — a bare subheading name fails with `400 invalid-target` (errorCode 40080) and the write is lost. For example, `## Fact / Pattern` under the `# Operator Preferences` H1 is targeted as `Operator Preferences::Fact / Pattern`.
|
||||
**Heading targets use the FULL heading path, `::`-delimited from the top-level heading** — a bare subheading name fails with `400 invalid-target` and the write is lost. For example, `## Fact / Pattern` under the `# Operator Preferences` H1 is targeted as `Operator Preferences::Fact / Pattern`.
|
||||
|
||||
**Default: GET the document map first** (every first PATCH to a file in a session — cache the result mentally for subsequent PATCHes to the same file). This eliminates the most common failure mode of PATCH:
|
||||
|
||||
@@ -195,7 +249,7 @@ curl -s -X PATCH \
|
||||
"https://goldbrainapi.mpm.to/vault/_agent/memory/semantic/operator-preferences.md"
|
||||
```
|
||||
|
||||
Use `Operation: replace` to overwrite a section entirely (e.g. a project's `Project Name::Current status`).
|
||||
Use `Operation: replace` to overwrite a section entirely (e.g. a project's `Project Name::Status`). Percent-encode non-ASCII characters in the `Target` header; spaces are fine.
|
||||
|
||||
#### Special characters in heading targets — em dashes, parentheses (KNOWN BUG)
|
||||
|
||||
@@ -229,7 +283,7 @@ When a PATCH or PUT changes meaningful content (status update, decision recorded
|
||||
curl -s -X PATCH \
|
||||
-H "Authorization: Bearer fb72065a05fabb28ae87c45880cc3b7aba4fd3f58e70297934145cef974e8ed8" \
|
||||
-H "Operation: replace" -H "Target-Type: frontmatter" -H "Target: updated" \
|
||||
-H "Content-Type: application/json" --data '"2026-06-07"' \
|
||||
-H "Content-Type: application/json" --data '"2026-06-06"' \
|
||||
"https://goldbrainapi.mpm.to/vault/projects/active/<slug>.md"
|
||||
```
|
||||
|
||||
@@ -242,8 +296,8 @@ cat > /tmp/obs_file.md << 'OBSEOF'
|
||||
---
|
||||
type: project
|
||||
status: active
|
||||
created: 2026-06-07
|
||||
updated: 2026-06-07
|
||||
created: 2026-06-05
|
||||
updated: 2026-06-05
|
||||
tags: []
|
||||
agent_written: true
|
||||
source_notes: []
|
||||
@@ -251,11 +305,11 @@ source_notes: []
|
||||
|
||||
# Project Name
|
||||
|
||||
## Current status
|
||||
## Status
|
||||
...
|
||||
|
||||
## Related
|
||||
- [[journal/daily/2026-06-07]]
|
||||
- [[journal/daily/2026-06-05]]
|
||||
OBSEOF
|
||||
|
||||
curl -s -X PUT \
|
||||
@@ -277,28 +331,60 @@ curl -s -X POST \
|
||||
|
||||
## Scope Switching (`current-context.md`)
|
||||
|
||||
`_agent/context/current-context.md` tracks a single active scope. The operator routinely shifts scope within a day (a project → MPM ops → something else).
|
||||
`_agent/context/current-context.md` tracks a single active scope. Bryan routinely shifts scope within a day (echo plugin → MPM brand → WISP docs), so this is high-churn — switch deliberately, every time the work changes topic.
|
||||
|
||||
When scope changes:
|
||||
**Preferred — one command:**
|
||||
|
||||
1. PATCH `prepend` a dated bullet to `## Scope History` capturing the **prior** scope (one line: `- 2026-06-07: <prior scope summary>`). If `## Scope History` doesn't exist yet, POST the heading first, same pattern as the daily-note Agent Log.
|
||||
2. PATCH `replace` `## Scope` with the new scope.
|
||||
3. PATCH the frontmatter `updated:` field.
|
||||
```bash
|
||||
"$GB" scope set "<new scope summary>"
|
||||
```
|
||||
|
||||
`scope set` does the whole switch atomically and correctly: it archives the **prior** scope to `## Scope History` (dated, truncated), replaces `## Scope` with the new text, and stamps the `scope_updated:` frontmatter timestamp. That timestamp is the **freshness signal** — it's what `goldbrain.sh scope show` and the `vault-lint.sh` drift check read to tell whether the recorded scope still reflects current work. Always switch through `scope set` so `scope_updated` stays honest; a hand-edited `## Scope` that skips the stamp reintroduces silent drift.
|
||||
|
||||
**Manual fallback** (only if `goldbrain.sh` is unavailable): PATCH `prepend` the prior scope to `## Scope History`, PATCH `replace` `## Scope`, then PATCH the frontmatter `scope_updated:` (and `updated:`) to today. Note `scope_updated` must already exist in frontmatter — a `PATCH replace` on a missing field returns `400 invalid-target`; run `bootstrap.sh` repair to add it.
|
||||
|
||||
This keeps a rolling trail of recent scopes in one file instead of spawning separate stash notes. Trim Scope History to the last ~10 entries when it grows past that.
|
||||
|
||||
**Drift backstop:** `vault-lint.sh` flags when ≥ `SCOPE_STALE_SESSIONS` (default 3) session logs are dated after `scope_updated` — i.e. work happened without a scope switch. It's advisory (surfaced in Vault Health / `/gb-health`), the mechanical safety net under the load-time judgment above.
|
||||
|
||||
## Journal Rollups (the journal is one continuum)
|
||||
|
||||
The journal is a single append-only chronological stream. Rollups are just coarser-grained journal entries over the same timeline, so they **all live under `journal/`** — there is no separate `reviews/` tree. One place to read the whole time-series story, daily through annual.
|
||||
|
||||
| Cadence | Path | Trigger | Type |
|
||||
|---------|------|---------|------|
|
||||
| Daily | `journal/daily/YYYY-MM-DD.md` | first agent activity that day | `daily-note` |
|
||||
| Weekly | `journal/weekly/YYYY-Www.md` (e.g. `2026-W24.md`) | first substantive session of a new ISO week — **opt-in**, offer first | `weekly-note` |
|
||||
| Monthly | `journal/monthly/YYYY-MM.md` | first substantive session of a new calendar month — offer alongside the Vault Health pass | `monthly-note` |
|
||||
| Quarterly | `journal/quarterly/YYYY-Qn.md` | **manual / on request only** | `review` |
|
||||
| Annual | `journal/annual/YYYY.md` | **manual / on request only** | `review` |
|
||||
|
||||
All filenames lex-sort chronologically. Detect the weekly trigger with `date +%G-W%V` and check whether that week's note already exists; monthly with `date +%Y-%m`.
|
||||
|
||||
A weekly/monthly rollup is a **light digest** — open threads across `projects/active/`, inbox items aging past ~7 days, and the period's `## Scope History` changes from `current-context.md`. It is *not* a vault-health audit (that's an agent-maintenance artifact — see below).
|
||||
|
||||
## Vault Health (monthly)
|
||||
|
||||
On the first substantive session of a calendar month, run a quick health pass and write findings to `reviews/monthly/YYYY-MM-vault-health.md`. Don't auto-fix without asking.
|
||||
On the first substantive session of a calendar month, run a quick health pass and write findings to `_agent/health/YYYY-MM-vault-health.md`. This is **agent self-maintenance, not a journal entry** — it lives under `_agent/` because it's about the vault's mechanical integrity, not Bryan's work narrative. Don't auto-fix without asking.
|
||||
|
||||
Checks:
|
||||
Run the bundled linter first — it mechanically checks the invariants below so you don't eyeball them. **Pass `GB_TODAY` = the conversation's `currentDate`** so stale/aging math uses the same clock you write with (not the runner's machine date):
|
||||
|
||||
```bash
|
||||
GB_TODAY=<currentDate> "${CLAUDE_PLUGIN_ROOT}/skills/goldbrain-memory/scripts/vault-lint.sh"
|
||||
```
|
||||
|
||||
Exit codes: `0` clean · `1` violations · `2` unreachable · `3` not bootstrapped. Checks (the linter asserts each and prints violations):
|
||||
|
||||
1. **Stale active projects** — for each note in `projects/active/`, check `updated:` >30 days. Likely belongs in `on-hold/`.
|
||||
2. **Unprocessed inbox** — GET `inbox/captures/inbox.md`. List items older than 14 days that never moved through the triage protocol.
|
||||
3. **Duplicate slugs across lifecycle folders** — any slug appearing in more than one of `active/`, `incubating/`, `on-hold/`, `archived/` is broken state.
|
||||
4. **Broken-heading risk** — sample 2–3 frequently-PATCHed files; confirm `## Agent Log`, `## Scope`, `## Fact / Pattern`, `## Observations` headings still exist, and that no heading has acquired an em dash or parentheses that would break PATCH targeting.
|
||||
4. **Folder ↔ `status:` mismatch** — any `projects/<lifecycle>/` note whose `status:` frontmatter disagrees with its folder.
|
||||
5. **Wikilinks in frontmatter** — any `[[...]]` inside a YAML frontmatter block (breaks Obsidian reading view), swept across all folders.
|
||||
6. **Duplicate `## Agent Log` headings** — any daily note with more than one.
|
||||
7. **Unknown / retired paths** — any vault file that matches no route in `scripts/routing.json` (or sits at a retired path).
|
||||
8. **Frontmatter integrity** — missing required fields, `updated` < `created`, future `updated`, and `[[wikilinks]]` leaking into `source_notes`.
|
||||
|
||||
The pass is cheap (a few searches + a directory listing) and pays for itself by catching drift before it requires a reorg.
|
||||
The pass is cheap and pays for itself by catching drift before it requires a reorg. Write the findings as a digest; act on them only with Bryan's go-ahead.
|
||||
|
||||
## Daily Note — Agent Log
|
||||
|
||||
@@ -358,12 +444,28 @@ curl -s -X PATCH -H "$AUTH" \
|
||||
| Task-scoped context / focus | `_agent/context/current-context.md` | PATCH / PUT |
|
||||
| Working session ended with substance | `_agent/sessions/YYYY-MM-DD-HHMM-<slug>.md` | PUT |
|
||||
| Long-running project state | `projects/<lifecycle>/<slug>.md` (see Project Lifecycle) | PUT + PATCH |
|
||||
| Ongoing area of responsibility (standing domain, no end state) | `areas/<domain>/<slug>.md` (domain: `business`/`personal`/`learning`/`systems`) | PUT |
|
||||
| Non-obvious decision (ADR) | `decisions/by-date/YYYY-MM-DD-<slug>.md` (see mirror note below) | PUT |
|
||||
| Person context | `resources/people/<name>.md` | PUT / PATCH |
|
||||
| Company / organization context | `resources/companies/<slug>.md` | PUT / PATCH |
|
||||
| Concept / reference note | `resources/concepts/` or `resources/references/` | PUT |
|
||||
| Meeting notes / call recap | `resources/meetings/YYYY-MM-DD-<slug>.md` | PUT |
|
||||
| Skill / plugin capability entry (catalog, not the build work) | `_agent/skills/active/<slug>.md` (→ `_agent/skills/archived/` when retired) | PUT |
|
||||
| Daily activity / Agent Log | `journal/daily/YYYY-MM-DD.md` — see **Daily Note — Agent Log** above | PATCH (with auto-create) |
|
||||
| Weekly / monthly rollup | `journal/weekly/YYYY-Www.md` · `journal/monthly/YYYY-MM.md` — see **Journal Rollups** | PUT |
|
||||
| Quarterly / annual review | `journal/quarterly/YYYY-Qn.md` · `journal/annual/YYYY.md` (manual / on request) | PUT |
|
||||
| Vault-health audit (agent self-maintenance) | `_agent/health/YYYY-MM-vault-health.md` — see **Vault Health** | PUT |
|
||||
| Session-end orientation pointer | `_agent/heartbeat/last-session.md` (one line: `<session-log-path> @ <ISO-timestamp>`) | PUT |
|
||||
|
||||
**Decision mirrors:** mirror a project-relevant ADR as a `[[wikilink]]` under that project's `## Decisions` heading (PATCH), and optionally into `decisions/by-project/` per the vault's `STRUCTURE.md`. The by-date ADR is always the canonical record.
|
||||
> **The complete, audited routing map lives in `references/routing-map.md`** — every write destination with its trigger, what lands there, and why it's distinct from its neighbors. This table is the quick-reference; the map is the authority. If a path isn't in the map, it shouldn't be written to.
|
||||
|
||||
**Decision mirrors:** if the decision belongs to an existing note in `projects/active/`, add a `[[wikilink]]` to the ADR under that project's `## Key Decisions` heading (PATCH). If no matching project note exists, skip the mirror — the by-date ADR is sufficient; do not invent topical mirror files in `decisions/by-project/`.
|
||||
|
||||
**Routing triggers — areas / meetings / skills:**
|
||||
|
||||
- **Area** (`areas/`): a standing responsibility Bryan maintains with no finish line. Decision rule: has an end state → `projects/`; never "done" → `areas/`. Subfolder is the domain (`business`/`personal`/`learning`/`systems`); create it on first write. `type: area`.
|
||||
- **Meeting** (`resources/meetings/`): notes, recaps, or action items tied to a specific meeting or call. Mirror decisions to `decisions/by-date/` and commitments to the relevant project. `type: meeting`.
|
||||
- **Skill** (`_agent/skills/`): Bryan authors, installs, or retires a skill/plugin and wants it tracked as a capability — distinct from `projects/`, which tracks the build effort. Active entries in `active/`, retired ones in `archived/`. `type: skill`.
|
||||
|
||||
Never delete files unless Bryan explicitly asks. Memory is append-friendly; deletion is destructive.
|
||||
|
||||
@@ -371,7 +473,7 @@ Never delete files unless Bryan explicitly asks. Memory is append-friendly; dele
|
||||
|
||||
At the end of substantive conversations (ones that produced decisions, artifacts, or commitments), create a session log. Ask once if unsure: "Want me to log a session note for this?"
|
||||
|
||||
**Filename format is canonical: `_agent/sessions/YYYY-MM-DD-HHMM-<slug>.md`.** Always include the four-digit local-time HHMM component — it makes filenames lexically sort in true chronological order, which is what Step 3 of loading relies on. Older session logs without the HHMM part may exist; leave them alone, but every new one must use the full form.
|
||||
**Filename format is canonical: `_agent/sessions/YYYY-MM-DD-HHMM-<slug>.md`.** Always include the four-digit local-time HHMM component — it makes filenames lexically sort in true chronological order, which is what Step 3 of loading relies on. Older session logs without the HHMM part exist; leave them alone, but every new one must use the full form.
|
||||
|
||||
See `references/session-log-template.md` for the body format.
|
||||
|
||||
@@ -384,11 +486,23 @@ curl -s -X PUT \
|
||||
-H "Authorization: Bearer fb72065a05fabb28ae87c45880cc3b7aba4fd3f58e70297934145cef974e8ed8" \
|
||||
-H "Content-Type: text/markdown" \
|
||||
--data-binary @/tmp/obs_session.md \
|
||||
"https://goldbrainapi.mpm.to/vault/_agent/sessions/2026-06-07-1430-my-session.md"
|
||||
"https://goldbrainapi.mpm.to/vault/_agent/sessions/2026-06-05-1430-my-session.md"
|
||||
```
|
||||
|
||||
Then add a one-line entry to today's daily note via the **Daily Note — Agent Log** procedure above.
|
||||
|
||||
Finally, update the heartbeat pointer so the next session can orient in one GET (this is what closes the loop with loading-procedure Step 4 — a pointer nobody writes is a pointer nobody can read):
|
||||
|
||||
```bash
|
||||
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
printf '%s @ %s\n' "_agent/sessions/${SESSION_FILENAME}" "$NOW" \
|
||||
| curl -s -X PUT -H "Authorization: Bearer fb72065a05fabb28ae87c45880cc3b7aba4fd3f58e70297934145cef974e8ed8" \
|
||||
-H "Content-Type: text/markdown" --data-binary @- \
|
||||
"https://goldbrainapi.mpm.to/vault/_agent/heartbeat/last-session.md"
|
||||
```
|
||||
|
||||
`last-session.md` is a single-line pointer overwritten (PUT) each session end — never appended, so it can't grow or duplicate.
|
||||
|
||||
## Vault Unreachable
|
||||
|
||||
If the API returns a connection error, timeout, or `502`, tell Bryan once that the memory vault is unreachable (a `502` usually means Obsidian/the REST plugin is not running on the backend), then proceed without memory. Do not retry repeatedly.
|
||||
@@ -396,16 +510,16 @@ If the API returns a connection error, timeout, or `502`, tell Bryan once that t
|
||||
## Style Rules
|
||||
|
||||
- Write in third person about Bryan: "Bryan prefers X", not "I prefer X".
|
||||
- Do not attribute Jason's design preferences to Bryan — capture Bryan's preferences only from observed interactions. Jason's preferences belong in the ECHO vault, Bryan's belong here.
|
||||
- Do not attribute Jason's design preferences to Bryan — capture Bryan's preferences only from observed interactions. Jason is the architect of this vault, not its operator. Jason's preferences belong in his ECHO vault; Bryan's belong here. Do not cross-write between the two.
|
||||
- **Keep heading text ASCII** so it stays PATCH-targetable — plain hyphens with spaces (` - `), never em dashes or parentheses in headings (see the special-characters note under PATCH).
|
||||
- **Anchor relative dates on the conversation's `currentDate`** before writing. "Today" → `currentDate`. "Thursday" / "next week" → resolve to an absolute `YYYY-MM-DD`. Never guess from training-data knowledge of the current year.
|
||||
- **Keep heading text ASCII** so it stays PATCH-targetable — plain hyphens, no em dashes or parentheses in headings (see the special-characters note under PATCH).
|
||||
- Every memory file has canonical YAML frontmatter — see `references/vault-layout.md`.
|
||||
- Set `agent_written: true` on agent-generated notes and list `source_notes` (plain relative paths, not links).
|
||||
- **`created:` is the earliest known date the entity was tracked anywhere in the vault, not "today".** When merging notes (e.g. promoting `on-hold/` → `active/`), preserve the earliest `created:` and only bump `updated:`.
|
||||
- **`source_notes` lists the note(s) that *triggered* or *supplied content for* this one** — e.g. the session log that produced a project update, or the daily note where a captured fact originated. It is a *backward* link to inputs. Forward links (this note → other notes it references) belong in the `## Related` section in the note body, never in frontmatter.
|
||||
- **Never put `[[wikilinks]]` in frontmatter** — YAML parses them as nested lists and the links break in Obsidian's reading view. Put all cross-references in a `## Related` section in the note **body** as a bulleted list of `[[links]]`.
|
||||
- Use Obsidian wiki links (`[[note name]]`) freely in the note **body** for cross-references.
|
||||
- Keep entries short and focused. Fewer, sharper entries beat many noisy ones.
|
||||
- Keep entries short and focused. Fewer, sharper entries beat many noisy ones — Bryan explicitly prefers concision.
|
||||
- About to write something large or sensitive? Show Bryan the content first and confirm.
|
||||
|
||||
## operator-preferences.md — Rules vs Observations
|
||||
@@ -413,7 +527,7 @@ If the API returns a connection error, timeout, or `502`, tell Bryan once that t
|
||||
`_agent/memory/semantic/operator-preferences.md` separates two kinds of content:
|
||||
|
||||
- `## Fact / Pattern` — **promoted, deduped rules.** No date prefix. These are timeless: "Bryan prefers concise communication." Append here only when a rule is stable.
|
||||
- `## Observations` — **timestamped raw observations.** Date-prefixed: `- 2026-06-07: Bryan chose X over Y because Z.` This is where new evidence goes by default.
|
||||
- `## Observations` — **timestamped raw observations.** Date-prefixed: `- 2026-06-06: Bryan chose X over Y because Z.` This is where new evidence goes by default.
|
||||
|
||||
During monthly Vault Health or when an observation stabilizes, promote it from `## Observations` into `## Fact / Pattern` (drop the date) and remove the duplicate from Observations. Trim Observations to the last ~30 entries when it grows past that — the rest live in session logs.
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ Server: `https://goldbrainapi.mpm.to` (reverse proxy → Obsidian Local REST API
|
||||
Auth header: `Authorization: Bearer fb72065a05fabb28ae87c45880cc3b7aba4fd3f58e70297934145cef974e8ed8`
|
||||
The endpoint has a **valid TLS certificate** — `-k` is not required. Paths address the vault at its **root**.
|
||||
|
||||
> **Prefer `scripts/goldbrain.sh` over the raw recipes below.** It wraps every verb with auth, status checking, retry, idempotent append, and frontmatter patches. The recipes here are the underlying mechanics and the fallback. **If you call `curl` directly, check the HTTP status** — add `-o /dev/null -w "%{http_code}"` and branch on it. A `PATCH` to a non-existent heading returns `400 invalid-target` (errorCode 40080) and the write is *silently lost*; a bare `curl` that ignores status will report success anyway. `GET` returns `404` for a missing file. Treat any `>= 400` as a failed operation, surface it, and do not continue as if it succeeded.
|
||||
|
||||
---
|
||||
|
||||
## Reading Files
|
||||
@@ -50,7 +52,7 @@ Returns JSON: `{ "files": [...], "folders": [...] }`.
|
||||
|
||||
```bash
|
||||
cat > /tmp/obs_entry.md << 'OBSEOF'
|
||||
- 2026-06-02: your entry here
|
||||
- 2026-06-05: your entry here
|
||||
OBSEOF
|
||||
|
||||
curl -s -X POST \
|
||||
@@ -71,8 +73,8 @@ cat > /tmp/obs_file.md << 'OBSEOF'
|
||||
---
|
||||
type: session-log
|
||||
status: complete
|
||||
created: 2026-06-02
|
||||
updated: 2026-06-02
|
||||
created: 2026-06-05
|
||||
updated: 2026-06-05
|
||||
tags: [agent, session]
|
||||
agent_written: true
|
||||
source_notes: []
|
||||
@@ -88,7 +90,7 @@ curl -s -X PUT \
|
||||
-H "Authorization: Bearer fb72065a05fabb28ae87c45880cc3b7aba4fd3f58e70297934145cef974e8ed8" \
|
||||
-H "Content-Type: text/markdown" \
|
||||
--data-binary @/tmp/obs_file.md \
|
||||
"https://goldbrainapi.mpm.to/vault/_agent/sessions/2026-06-02-1430-my-session.md"
|
||||
"https://goldbrainapi.mpm.to/vault/_agent/sessions/2026-06-05-1430-my-session.md"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -117,7 +119,7 @@ curl -s -X PUT \
|
||||
|
||||
```bash
|
||||
cat > /tmp/obs_patch.md << 'OBSEOF'
|
||||
Bryan prefers concise status updates — lead with the decision.
|
||||
Bryan prefers concise status updates - lead with the decision.
|
||||
OBSEOF
|
||||
|
||||
curl -s -X PATCH \
|
||||
@@ -145,7 +147,7 @@ Returns `{ "headings": [...], "blocks": [...], "frontmatterFields": [...] }`. Co
|
||||
|
||||
### Replace a heading's content entirely
|
||||
|
||||
Same call with `Operation: replace` — e.g. to refresh a project's `Project Name::Current status`.
|
||||
Same call with `Operation: replace` — e.g. to refresh a project's `Project Name::Status`.
|
||||
|
||||
### Prepend under a heading
|
||||
|
||||
@@ -160,7 +162,7 @@ curl -s -X PATCH \
|
||||
-H "Target-Type: frontmatter" \
|
||||
-H "Target: updated" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '"2026-06-02"' \
|
||||
--data '"2026-06-05"' \
|
||||
"https://goldbrainapi.mpm.to/vault/projects/active/vault-foundation.md"
|
||||
```
|
||||
|
||||
@@ -183,7 +185,7 @@ Returns an array of `{ filename, score, matches: [{ context, match }] }`.
|
||||
```bash
|
||||
curl -s -X DELETE \
|
||||
-H "Authorization: Bearer fb72065a05fabb28ae87c45880cc3b7aba4fd3f58e70297934145cef974e8ed8" \
|
||||
"https://goldbrainapi.mpm.to/vault/archive/notes/old-note.md"
|
||||
"https://goldbrainapi.mpm.to/vault/inbox/imports/old-note.md"
|
||||
```
|
||||
|
||||
Only on explicit operator request. Deletion is destructive.
|
||||
@@ -195,7 +197,7 @@ Only on explicit operator request. Deletion is destructive.
|
||||
- Path separators (`/`) in the vault path are **literal** — do not encode them.
|
||||
- Spaces in filenames or heading targets in a URL: use `%20`.
|
||||
- Nested heading levels in a URL path: use `%3A%3A` for `::`.
|
||||
- Heading text in the `Target:` header: the **full heading path** joined by `::` (e.g. `Operator Preferences::Fact / Pattern`), no `#`; spaces are fine; **percent-encode non-ASCII and regex-special characters** — em dash `—` → `%E2%80%94`, en dash `–` → `%E2%80%93` (see the special-characters note under *Patching* above). Best practice: keep heading text ASCII so targets never need encoding.
|
||||
- Heading text in the `Target:` header: the **full heading path** joined by `::` (e.g. `Operator Preferences::Fact / Pattern`), no `#`; spaces are fine; percent-encode non-ASCII.
|
||||
|
||||
---
|
||||
|
||||
@@ -212,10 +214,17 @@ Only on explicit operator request. Deletion is destructive.
|
||||
| Task-scoped context / focus | `_agent/context/current-context.md` | PATCH / PUT |
|
||||
| Working-session log | `_agent/sessions/YYYY-MM-DD-HHMM-<slug>.md` | PUT |
|
||||
| Long-running project state | `projects/<lifecycle>/<slug>.md` (lifecycle: `incubating` → `active` → `on-hold`/`archived`; folder and `status:` MUST agree) | PUT + PATCH |
|
||||
| Non-obvious decision (ADR) | `decisions/by-date/YYYY-MM-DD-<slug>.md` (mirror `by-project/`) | PUT |
|
||||
| Ongoing area of responsibility (standing domain, no end state) | `areas/<domain>/<slug>.md` (`<domain>`: `business`/`personal`/`learning`/`systems`) | PUT |
|
||||
| Non-obvious decision (ADR) | `decisions/by-date/YYYY-MM-DD-<slug>.md` (mirror only into an existing project note's `## Key Decisions`; otherwise skip) | PUT |
|
||||
| Person context | `resources/people/<name>.md` | PUT / PATCH |
|
||||
| Company / organization context | `resources/companies/<slug>.md` | PUT / PATCH |
|
||||
| Concept / reference note | `resources/concepts/` or `resources/references/` | PUT |
|
||||
| Meeting notes / call recap | `resources/meetings/YYYY-MM-DD-<slug>.md` | PUT |
|
||||
| Skill / plugin capability entry (catalog, not build work) | `_agent/skills/active/<slug>.md` (→ `archived/` when retired) | PUT |
|
||||
| Daily activity / Agent Log | `journal/daily/YYYY-MM-DD.md` | POST / PATCH |
|
||||
| Periodic review | `reviews/{weekly,monthly,quarterly,annual}/` | PUT |
|
||||
| Journal rollup | `journal/{weekly/YYYY-Www,monthly/YYYY-MM,quarterly/YYYY-Qn,annual/YYYY}.md` (weekly = opt-in on first session of a new ISO week; monthly = offered with Vault Health; quarterly/annual = manual) | PUT |
|
||||
| Vault-health audit (agent self-maintenance) | `_agent/health/YYYY-MM-vault-health.md` (monthly; NOT a journal entry) | PUT |
|
||||
| Session-end orientation pointer | `_agent/heartbeat/last-session.md` (one line, overwritten each session end) | PUT |
|
||||
| Bootstrap marker (plugin-owned) | `_agent/goldbrain-vault.md` (`schema_version`, bootstrap date) — the "is this vault set up?" probe | GET / PUT |
|
||||
|
||||
**Slug rules:** kebab-case, ASCII, ~40 chars max. Every file carries canonical frontmatter (see `vault-layout.md`).
|
||||
|
||||
@@ -1,39 +1,140 @@
|
||||
# Bootstrap Procedure
|
||||
# Bootstrap & Repair
|
||||
|
||||
The goldbrain vault ships its **own** `BOOTSTRAP.md` at the vault root — that file is the canonical preflight/repair manifest. This plugin defers to it rather than duplicating the logic.
|
||||
The **plugin is the single source of truth** for goldbrain's structure. Everything needed to stand up a vault ships in this skill under `scaffold/` — there is no dependency on any in-vault control doc and no external/local re-seed path. This makes the vault portable: point the REST API at any empty Obsidian vault, run this procedure, and it becomes a working goldbrain vault.
|
||||
|
||||
> **Structure vs. procedure.** This file (and the vault's `BOOTSTRAP.md`/`STRUCTURE.md`) own *structure* — what folders and seed files must exist. Day-to-day *procedure* — parallel loading, search-before-write, append idempotency, project lifecycle transitions, scope switching, vault health, and the em-dash-safe PATCH rules — lives in `SKILL.md`. When repairing, ensure the four project-lifecycle folders exist: `projects/{incubating,active,on-hold,archived}/`.
|
||||
The vault holds **data only**. It carries no `CLAUDE.md` / `BOOTSTRAP.md` / `STRUCTURE.md` / `index.md`. The "is this vault set up?" signal is a small marker file, `_agent/goldbrain-vault.md`.
|
||||
|
||||
## Normal case — vault already bootstrapped
|
||||
## Quick path — run the scripts
|
||||
|
||||
At session start, read the in-vault manifest:
|
||||
Bootstrap, repair, and migration are deterministic scripts; prefer them over running the curl steps by hand. They resolve the scaffold relative to their own location, so they work regardless of the caller's CWD:
|
||||
|
||||
```bash
|
||||
curl -s \
|
||||
-H "Authorization: Bearer fb72065a05fabb28ae87c45880cc3b7aba4fd3f58e70297934145cef974e8ed8" \
|
||||
"https://goldbrainapi.mpm.to/vault/BOOTSTRAP.md"
|
||||
SCRIPTS="${CLAUDE_PLUGIN_ROOT}/skills/goldbrain-memory/scripts"
|
||||
"$SCRIPTS/bootstrap.sh" --dry-run # preview what would be seeded
|
||||
"$SCRIPTS/bootstrap.sh" # idempotent, additive — fills only what is missing (also the repair path)
|
||||
"$SCRIPTS/migrate.sh" # plan a schema migration (dry-run)
|
||||
"$SCRIPTS/migrate.sh" --apply # perform the migration (moves/deletes, after review)
|
||||
```
|
||||
|
||||
If it returns content (200), the vault is set up. Skim its preflight checklist, then read `CLAUDE.md` for the operating contract and proceed with the loading procedure in `SKILL.md`. The in-vault `BOOTSTRAP.md` describes how to repair any missing folders/files; follow it if something is absent. **Never overwrite an existing file** during repair — generate only what is missing.
|
||||
`bootstrap.sh` writes through `goldbrain.sh`, so every step is status-checked and the marker is written last. The manual steps below document what the script does (and serve as a fallback if the script can't run).
|
||||
|
||||
## Fresh-vault case — BOOTSTRAP.md returns 404
|
||||
```
|
||||
AUTH="Authorization: Bearer fb72065a05fabb28ae87c45880cc3b7aba4fd3f58e70297934145cef974e8ed8"
|
||||
BASE="https://goldbrainapi.mpm.to"
|
||||
```
|
||||
|
||||
This means the REST API is pointed at an empty vault. Confirm with the operator once:
|
||||
---
|
||||
|
||||
> "The goldbrain vault looks empty — there's no `BOOTSTRAP.md`. The standard scaffold (control docs, folder tree, templates, seed notes) needs to be loaded before I can use it as memory. Want me to create the core seed files now?"
|
||||
## Probe — is the vault bootstrapped?
|
||||
|
||||
If yes, create the minimum viable seed with `PUT` (the API creates intermediate directories automatically), then let the vault's own structure grow from there:
|
||||
At session start, GET the marker:
|
||||
|
||||
1. `CLAUDE.md` — operating contract and session protocol
|
||||
2. `BOOTSTRAP.md` — preflight/repair manifest
|
||||
3. `STRUCTURE.md` — layout, taxonomy, frontmatter standard
|
||||
4. `index.md` — navigation hub
|
||||
5. `_agent/memory/semantic/operator-preferences.md` — operator profile (empty, no fabricated facts)
|
||||
6. `_agent/context/current-context.md` — empty context bundle
|
||||
7. `inbox/captures/inbox.md` — capture file
|
||||
```bash
|
||||
curl -s -o /dev/null -w "%{http_code}" -H "$AUTH" "$BASE/vault/_agent/goldbrain-vault.md"
|
||||
```
|
||||
|
||||
Prefer copying the full prepared scaffold into the Obsidian vault folder over reconstructing it by hand — it is the source of truth for goldbrain's structure.
|
||||
- **200** → bootstrapped. Read the marker's `schema_version`; if it is **less than** the plugin's current schema (2), run a migration pass (see *Migrations* below), otherwise proceed straight to the loading procedure in `SKILL.md`.
|
||||
- **404** → empty/unconfigured vault. Run **Fresh bootstrap** below. (If you expected an existing vault, confirm with the operator once that the REST API is pointed at the right vault before seeding.)
|
||||
|
||||
## After bootstrap
|
||||
---
|
||||
|
||||
Tell the operator briefly what was created, append a line to the daily note's **Agent Log**, and write a session log in `_agent/sessions/`. Do not over-explain — they can browse the vault.
|
||||
## Fresh bootstrap (empty vault)
|
||||
|
||||
Idempotent and additive. **Never overwrite an existing file** — GET-probe each path and skip any that already returns `200`. The marker is written **last**, so the vault is only flagged "set up" after every piece is in place.
|
||||
|
||||
Throughout, `{{DATE}}` means today's date (`YYYY-MM-DD`), resolved from the conversation's `currentDate`. Substitute it before PUTting any scaffold file or anchor seed.
|
||||
|
||||
### 1. Folder tree
|
||||
|
||||
The REST API auto-creates parent directories on PUT, so creating the tree = seeding a file into each leaf. Obsidian and git both ignore empty dirs, so drop a one-line `README.md` into any leaf that wouldn't otherwise receive a file. Required tree (leaves marked `→ README`):
|
||||
|
||||
```
|
||||
inbox/captures inbox/imports inbox/processing-log
|
||||
journal/daily journal/weekly journal/monthly journal/quarterly journal/annual journal/templates
|
||||
projects/active projects/incubating projects/on-hold projects/archived
|
||||
areas/business areas/personal areas/learning areas/systems
|
||||
resources/concepts resources/references resources/people resources/companies resources/meetings
|
||||
decisions/by-date
|
||||
_agent/context _agent/memory/working _agent/memory/episodic _agent/memory/semantic
|
||||
_agent/sessions _agent/health _agent/templates _agent/heartbeat
|
||||
_agent/skills/active _agent/skills/archived
|
||||
```
|
||||
|
||||
> `decisions/by-project/` is intentionally **not** created — it is retired. A project-relevant decision is mirrored as a `[[wikilink]]` under that project's `## Key Decisions` heading instead.
|
||||
>
|
||||
> `reviews/` is **not** created — it is retired. Journal rollups (weekly/monthly/quarterly/annual) live under `journal/`; the monthly vault-health audit lives under `_agent/health/`.
|
||||
|
||||
A leaf README is just a one-liner, e.g.:
|
||||
|
||||
```bash
|
||||
printf '# %s\n\nMemory vault folder. See the goldbrain-memory plugin for conventions.\n' "captures" \
|
||||
| curl -s -X PUT -H "$AUTH" -H "Content-Type: text/markdown" --data-binary @- \
|
||||
"$BASE/vault/inbox/captures/README.md"
|
||||
```
|
||||
|
||||
### 2. Templates
|
||||
|
||||
PUT every file under this skill's `scaffold/templates/` to its mirrored vault path. The tree mirrors 1:1:
|
||||
|
||||
| Scaffold file | Vault path |
|
||||
|---|---|
|
||||
| `scaffold/templates/_agent/templates/session-log-template.md` | `_agent/templates/session-log-template.md` |
|
||||
| `scaffold/templates/_agent/templates/context-bundle-template.md` | `_agent/templates/context-bundle-template.md` |
|
||||
| `scaffold/templates/_agent/templates/working-memory-template.md` | `_agent/templates/working-memory-template.md` |
|
||||
| `scaffold/templates/_agent/templates/semantic-memory-template.md` | `_agent/templates/semantic-memory-template.md` |
|
||||
| `scaffold/templates/journal/templates/daily-note-template.md` | `journal/templates/daily-note-template.md` |
|
||||
| `scaffold/templates/journal/templates/weekly-review-template.md` | `journal/templates/weekly-review-template.md` |
|
||||
| `scaffold/templates/projects/project-template.md` | `projects/project-template.md` |
|
||||
| `scaffold/templates/decisions/decision-template.md` | `decisions/decision-template.md` |
|
||||
|
||||
Templates keep their Obsidian Templater tokens (`{{date:YYYY-MM-DD}}` etc.) verbatim — those are resolved by Templater / by the skill's daily-note routine, not at seed time.
|
||||
|
||||
Resolve scaffold paths against the skill directory — **never a bare relative `@scaffold/...`**, which assumes the caller's CWD is the skill dir and silently sends an empty body otherwise:
|
||||
|
||||
```bash
|
||||
SCAFFOLD="${CLAUDE_PLUGIN_ROOT}/skills/goldbrain-memory/scaffold"
|
||||
curl -s -X PUT -H "$AUTH" -H "Content-Type: text/markdown" \
|
||||
--data-binary @"$SCAFFOLD/templates/journal/templates/daily-note-template.md" \
|
||||
"$BASE/vault/journal/templates/daily-note-template.md"
|
||||
```
|
||||
|
||||
### 3. Anchor seeds (only if absent — no fabricated facts)
|
||||
|
||||
Substitute `{{DATE}}` → today, then PUT each:
|
||||
|
||||
| Scaffold file | Vault path |
|
||||
|---|---|
|
||||
| `scaffold/anchors/operator-preferences.seed.md` | `_agent/memory/semantic/operator-preferences.md` |
|
||||
| `scaffold/anchors/current-context.seed.md` | `_agent/context/current-context.md` |
|
||||
| `scaffold/anchors/inbox.seed.md` | `inbox/captures/inbox.md` |
|
||||
|
||||
The operator-preferences seed is deliberately empty — **do not invent preferences.** Real facts accrue through use.
|
||||
|
||||
### 4. Vault README (human signpost)
|
||||
|
||||
Substitute `{{DATE}}` if present, then PUT `scaffold/README.vault.md` → `/vault/README.md`. This is the only human-facing control doc in the vault and is **not** read by the agent for routing.
|
||||
|
||||
### 5. Marker (write last)
|
||||
|
||||
Substitute `{{DATE}}`, then PUT `scaffold/goldbrain-vault.md` → `/vault/_agent/goldbrain-vault.md`. Once this returns `200`, the vault is bootstrapped.
|
||||
|
||||
### 6. First-run trace
|
||||
|
||||
Create today's daily note from `journal/templates/daily-note-template.md` (resolve the `{{date:…}}` tokens to today), append a one-line `## Agent Log` entry noting the bootstrap, and write a session log in `_agent/sessions/YYYY-MM-DD-HHMM-bootstrap.md`. Tell the operator briefly what was created.
|
||||
|
||||
---
|
||||
|
||||
## Repair (existing vault, something missing)
|
||||
|
||||
Run the same steps 1–5, but GET-probe each path first and **only create what is missing**. Never overwrite. If a file exists but looks malformed, flag it in the session log rather than replacing it. Repair is safe to run any time and is the right response if the loading procedure hits an unexpected `404` on a structural path.
|
||||
|
||||
---
|
||||
|
||||
## Migrations (`schema_version` mismatch)
|
||||
|
||||
`scripts/migrate.sh` automates this: it reads the marker's `schema_version`, applies each intervening migration (idempotent, additive; destructive steps gated behind `--apply` and printed first), and stamps the marker at the end. Run it dry-run, review the plan, then `--apply`. The steps below are what it encodes — and the manual fallback.
|
||||
|
||||
When the marker's `schema_version` is older than the plugin's, apply the migration steps for each intervening version, then PATCH the marker's `schema_version` frontmatter to the new value.
|
||||
|
||||
- **0 → 1** (control-docs-in-plugin): the vault previously carried root control docs (`CLAUDE.md`, `BOOTSTRAP.md`, `STRUCTURE.md`, `index.md`). Back them up outside the vault, DELETE them, PUT the thin `scaffold/README.vault.md` over the old verbose `README.md`, write the `_agent/goldbrain-vault.md` marker, and scrub now-dangling `[[CLAUDE]]`/`[[BOOTSTRAP]]`/`[[STRUCTURE]]`/`[[index]]` links from the `## Related` sections of `operator-preferences.md` and `current-context.md` (leave historical session logs alone). Confirm with the operator before deleting.
|
||||
- **1 → 2** (reviews-folded-into-journal): the `reviews/` tree is retired. (a) For each note under `reviews/weekly/` and `reviews/monthly/`, MOVE it into `journal/weekly/` (rename `YYYY-Www-review.md` → `YYYY-Www.md`) and `journal/monthly/` respectively, preserving the earliest `created:`. (b) Move any `reviews/monthly/YYYY-MM-vault-health.md` to `_agent/health/`. (c) Move `reviews/quarterly|annual/` artifacts to `journal/quarterly|annual/`. (d) Update inbound `[[reviews/...]]` wikilinks in `## Related` sections to the new paths. (e) DELETE the now-empty `reviews/` tree. Confirm with the operator before deleting; leave historical session logs alone. *(goldbrain's live vault is migrated by `scripts/migrate.sh --apply`, which performs steps (a)–(e) deterministically; run it dry-run first and review the plan.)*
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Goldbrain — Operating Contract
|
||||
|
||||
The durable, client-independent contract for any agent operating against the goldbrain vault. These principles and safety rules formerly lived in the vault's `CLAUDE.md`; they now live in the plugin so they survive regardless of what is (or isn't) in the vault. Day-to-day *procedure* — loading order, search-first, triage, scope switching, PATCH/append rules — is owned by `SKILL.md`. This file holds the things that don't change between sessions or clients.
|
||||
|
||||
## What this agent is
|
||||
|
||||
You are an agent operating against an Obsidian vault that functions as a shared, long-term memory substrate for human work, Claude Code / CoWork sessions, and future REST/MCP clients. Your role is to read context, synthesize across notes, produce structured outputs, update memory carefully, and leave durable traces in the vault rather than keeping important state only in the conversation. The vault is the **system of record**, not a scratchpad.
|
||||
|
||||
## Core principles
|
||||
|
||||
- Treat the vault as the system of record for long-term memory.
|
||||
- Prefer Markdown, YAML frontmatter, wiki-links, and stable folder conventions over proprietary structures.
|
||||
- Write notes so both humans and agents can understand them without hidden context.
|
||||
- Preserve history instead of silently overwriting important decisions, summaries, or inferred preferences.
|
||||
- Keep agent-managed content (`agent_written: true` + `source_notes`) clearly separated from human-authored content.
|
||||
- Default to **additive updates, explicit status changes, and traceable summaries.**
|
||||
- Optimize for portability: the same vault should work across Claude Code, CoWork, MCP-compatible tools, and REST API clients.
|
||||
|
||||
## Memory model (where things live)
|
||||
|
||||
- **Working** → `_agent/memory/working/` — transient, time-boxed.
|
||||
- **Episodic** → `_agent/memory/episodic/` — what happened, when.
|
||||
- **Semantic** → `_agent/memory/semantic/` — durable facts, patterns, preferences (`operator-preferences.md`).
|
||||
- **Context bundles** → `_agent/context/` — task-focused reading lists and active state.
|
||||
|
||||
## Safety rules
|
||||
|
||||
- Do not fabricate facts, relationships, or prior decisions.
|
||||
- Do not mass-restructure the vault unless explicitly asked.
|
||||
- Do not delete notes unless deletion is explicitly requested and clearly safe.
|
||||
- Do not store secrets or API keys inside the vault, and never write the API key into a vault note.
|
||||
- Default to additive updates and explicit status changes over destructive edits.
|
||||
|
||||
## REST/API readiness
|
||||
|
||||
Assume clients may operate without filesystem access, through the Obsidian Local REST API. Keep paths predictable, frontmatter parseable, titles stable, and all state stored in notes rather than hidden conversation memory. Structure must stay portable: a fresh, empty vault is brought fully online by the plugin's bootstrap (`references/bootstrap.md`), so nothing essential should depend on files existing in the vault ahead of time.
|
||||
|
||||
## Concurrency (shared vault)
|
||||
|
||||
The vault is a **shared** substrate — Claude Code, CoWork, and other REST/MCP clients may operate on it concurrently. The REST API offers no transactions, so writers coordinate cooperatively:
|
||||
|
||||
- Single-line, overwrite-style files (`_agent/heartbeat/last-session.md`, `current-context.md::Scope`) and append targets (`inbox.md`, `## Agent Log`) assume **one writer at a time**. Two sessions writing at once can clobber or duplicate.
|
||||
- Before a burst of writes in a session that may overlap another, take the advisory lock (`goldbrain.sh lock <id>` → `_agent/locks/vault.lock`) and release it at session end. The lock is cooperative with a TTL (stale locks are reclaimable); it is a courtesy, not a hard mutex.
|
||||
- Idempotent append (read-before-POST, via `goldbrain.sh append`) is the second line of defense against duplicate lines from retries or overlapping sessions.
|
||||
- Status-check every write. A write that returns `>= 400` did **not** land — surface it rather than assuming success.
|
||||
@@ -0,0 +1,113 @@
|
||||
# Goldbrain Routing Map
|
||||
|
||||
**This document is canonical and complete.** Every write destination in the vault appears here exactly once, with the condition that routes content to it, what lands there, and why it is distinct from its neighbours. The rule for the whole system: **if a path is not in this map, nothing is written to it.** A path that cannot justify its separateness from a neighbour is a merge candidate, not a valid destination.
|
||||
|
||||
Views of the same truth: `scripts/routing.json` is the **machine-readable canonical source** (one route per destination, as a regex pattern + trigger + reason); `vault-lint.sh` loads it and flags any vault path that matches no route (and any write to a retired path). This prose map is the human-readable authority and must stay in sync with `routing.json`. The `SKILL.md` *Where to Write* table is the quick-reference and `vault-layout.md` is the physical tree. When they disagree, `routing.json` + this map win and the others are fixed to match.
|
||||
|
||||
## How to read a row
|
||||
|
||||
- **Trigger** — the observable condition that sends content here. If two rows could both fire, the more specific trigger wins.
|
||||
- **What lands** — the unit of content written.
|
||||
- **Distinct because** — the one reason this path is not merged into its nearest neighbour. This is the load-bearing column; a row without it is a bug.
|
||||
- **Method** — the dominant REST verb (see `api-reference.md` for mechanics).
|
||||
|
||||
---
|
||||
|
||||
## inbox/ — unsorted intake
|
||||
|
||||
| Path | Trigger | What lands | Distinct because | Method |
|
||||
|------|---------|------------|------------------|--------|
|
||||
| `inbox/captures/inbox.md` | "Save this, sort later" — destination genuinely unknown at capture time | One date-prefixed line | The only path whose contract is *deferred routing*; everything else here has a known home | POST |
|
||||
| `inbox/imports/<slug>.md` | Raw external material dropped in wholesale (export, paste, dump) | The raw artifact, unedited | Holds un-triaged *bulk*, vs `captures` which holds single lines | PUT |
|
||||
| `inbox/processing-log/YYYY-MM-DD.md` | An inbox item is routed to its real home | One line: `<original> → <destination path>` | Audit trail of moves — never holds memory itself, only the record of routing | POST |
|
||||
|
||||
Captures and imports are temporary by contract. Triage drains them into the homes below and logs the move; the original is left until Bryan okays deletion.
|
||||
|
||||
## journal/ — the one append-only time-series stream
|
||||
|
||||
Rollups are coarser-grained journal entries over the same timeline, so they live in the same tree. There is no separate `reviews/` tree.
|
||||
|
||||
| Path | Trigger | What lands | Distinct because | Method |
|
||||
|------|---------|------------|------------------|--------|
|
||||
| `journal/daily/YYYY-MM-DD.md` | First agent activity on a given day | `## Agent Log` line(s) + day notes | Finest grain; the only journal note PATCHed repeatedly within its period | PATCH (auto-create) |
|
||||
| `journal/weekly/YYYY-Www.md` | First substantive session of a new ISO week — **opt-in**, offer first | Light digest: open `active/` threads, aging inbox, week's scope changes | Coarser than daily, finer than monthly; ISO-week grain | PUT |
|
||||
| `journal/monthly/YYYY-MM.md` | First substantive session of a new month — offered alongside Vault Health | Month digest, same shape as weekly | Month grain; separate cadence and trigger from weekly | PUT |
|
||||
| `journal/quarterly/YYYY-Qn.md` | **Manual / on request only** | Quarter-scale narrative review | Strategic grain; never auto-fires | PUT |
|
||||
| `journal/annual/YYYY.md` | **Manual / on request only** | Year-scale narrative review | Coarsest grain; never auto-fires | PUT |
|
||||
| `journal/templates/` | Bootstrap only (seeded from plugin masters) | Note templates | Holds templates, not journal content; never a runtime routing target | PUT (seed) |
|
||||
|
||||
## projects/ — work with an end state
|
||||
|
||||
Lifecycle folders; `status:` frontmatter MUST equal the folder name (the linter checks this).
|
||||
|
||||
| Path | Trigger | What lands | Distinct because | Method |
|
||||
|------|---------|------------|------------------|--------|
|
||||
| `projects/active/<slug>.md` | Work in motion now | Project note (Status PATCHed fresh) | Default state for anything being worked | PUT + PATCH |
|
||||
| `projects/incubating/<slug>.md` | Idea captured, work not started | Project note | Pre-work; promote to `active/` when work begins | PUT |
|
||||
| `projects/on-hold/<slug>.md` | Paused but still tracked | Project note | Resumable; distinct from `archived` (which is terminal) | PUT |
|
||||
| `projects/archived/<slug>.md` | Done, abandoned, or rolled up | Project note | Terminal; kept for history, never deleted | PUT |
|
||||
|
||||
**Project vs area:** has an end state → `projects/`. Never "done" → `areas/`.
|
||||
|
||||
## areas/ — standing responsibilities, no finish line
|
||||
|
||||
| Path | Trigger | What lands | Distinct because | Method |
|
||||
|------|---------|------------|------------------|--------|
|
||||
| `areas/<domain>/<slug>.md` | Ongoing domain Bryan maintains indefinitely (`<domain>`: business/personal/learning/systems) | Area note | No end state — the one thing that disqualifies it from `projects/` | PUT |
|
||||
|
||||
## resources/ — reference material about the world
|
||||
|
||||
| Path | Trigger | What lands | Distinct because | Method |
|
||||
|------|---------|------------|------------------|--------|
|
||||
| `resources/people/<name>.md` | A fact about a specific person | Person note (kebab-case slug) | Keyed to a person, not a topic or event | PUT / PATCH |
|
||||
| `resources/companies/<slug>.md` | A fact about an organization (client, vendor, partner, employer) | Company note (kebab-case slug) | Keyed to an organization, not an individual (`people/`) or an external source (`references/`) | PUT / PATCH |
|
||||
| `resources/concepts/<slug>.md` | A reusable concept/idea Bryan wants on file | Concept note | An idea, vs a `reference` which is an external source | PUT |
|
||||
| `resources/references/<slug>.md` | An external source/link worth keeping | Reference note | Points outward (a source), vs `concepts` (an idea) | PUT |
|
||||
| `resources/meetings/YYYY-MM-DD-<slug>.md` | Notes/recap tied to a specific meeting or call | Meeting note; mirror decisions to `decisions/by-date/`, commitments to the project | Event-anchored to a meeting, vs a project's ongoing thread | PUT |
|
||||
|
||||
## decisions/ — non-obvious decisions (ADR)
|
||||
|
||||
| Path | Trigger | What lands | Distinct because | Method |
|
||||
|------|---------|------------|------------------|--------|
|
||||
| `decisions/by-date/YYYY-MM-DD-<slug>.md` | A non-obvious decision worth recording | ADR: Context → Decision → Consequences | The chronological system of record for decisions | PUT |
|
||||
|
||||
**Mirror, don't duplicate:** if the decision belongs to an existing `projects/active/` note, PATCH a `[[wikilink]]` to the ADR under that project's `## Key Decisions`. No matching project → skip the mirror; the by-date ADR stands alone.
|
||||
|
||||
## _agent/ — the agent's own working substrate
|
||||
|
||||
| Path | Trigger | What lands | Distinct because | Method |
|
||||
|------|---------|------------|------------------|--------|
|
||||
| `_agent/goldbrain-vault.md` | Bootstrap / schema migration only | Marker: `schema_version`, bootstrap date | Plugin-owned probe; never hand-edited | GET / PUT |
|
||||
| `_agent/context/current-context.md` | Active scope changes; task focus shifts | `## Scope`, `## Scope History`, priorities | Single *live* scope pointer, vs episodic which is a *past* record | PATCH / PUT |
|
||||
| `_agent/memory/semantic/operator-preferences.md` | A preference/pattern about Bryan | Append under `## Observations`; promote to `## Fact / Pattern` when stable | The one curated profile; distinct from ad-hoc semantic notes | PATCH |
|
||||
| `_agent/memory/semantic/<slug>.md` | A durable fact/pattern that isn't a Bryan-preference | Semantic note | Timeless fact, vs episodic (time-stamped event) and working (transient) | PUT |
|
||||
| `_agent/memory/episodic/<slug>.md` | A record of *what happened, when* | Episodic note | Anchored to an event in time; not a standing fact | PUT |
|
||||
| `_agent/memory/working/<slug>.md` | Short-lived state needed only for the current effort | Working note | Explicitly transient/time-boxed; safe to go stale | PUT |
|
||||
| `_agent/sessions/YYYY-MM-DD-HHMM-<slug>.md` | A substantive session ends (decisions/artifacts/commitments) | Session log (see template) | Per-session record; the unit loading Step 4 scans | PUT |
|
||||
| `_agent/health/YYYY-MM-vault-health.md` | First substantive session of a month (Vault Health pass) | Health-audit findings | Agent self-maintenance about vault integrity — NOT Bryan's work narrative, so not in `journal/` | PUT |
|
||||
| `_agent/heartbeat/last-session.md` | End of every session, after the session log is written | One line: `<session-log-path> @ <ISO-timestamp>` | O(1) orientation pointer read first at load (Step 4); overwritten, never grows | PUT |
|
||||
| `_agent/templates/` | Bootstrap only (seeded from plugin masters) | Canonical note templates | Holds templates, not memory; never a runtime routing target | PUT (seed) |
|
||||
| `_agent/skills/active/<slug>.md` | Bryan authors/installs a skill and wants it catalogued | Skill capability entry | Catalogs a *capability*, vs `projects/` which tracks the *build effort* | PUT |
|
||||
| `_agent/skills/archived/<slug>.md` | A catalogued skill is retired | Skill entry (moved from `active/`) | Terminal state of the skill catalog | PUT |
|
||||
|
||||
---
|
||||
|
||||
## Retired paths — explicitly never written
|
||||
|
||||
Listed so they are recognised as dead and never recreated. Any one of these appearing in a live vault is a migration miss (see `bootstrap.md` Migrations).
|
||||
|
||||
| Path | Status | Where it went instead |
|
||||
|------|--------|-----------------------|
|
||||
| `reviews/` (weekly/monthly/quarterly/annual) | Retired in schema 2 | Journal rollups → `journal/{weekly,monthly,quarterly,annual}/`; vault-health → `_agent/health/` |
|
||||
| `decisions/by-project/` | Retired in schema 1 | ADR mirrored as a `[[wikilink]]` under the project's `## Key Decisions` |
|
||||
| `archive/` (top-level) | Never existed | Project archival → `projects/archived/`; skill archival → `_agent/skills/archived/` |
|
||||
| `CLAUDE.md` / `BOOTSTRAP.md` / `STRUCTURE.md` / `index.md` (in-vault) | Retired in schema 1 | All control logic lives in the plugin (`references/`), not the vault |
|
||||
|
||||
## Routing decision tree (the calls that get mis-made)
|
||||
|
||||
1. **Destination unknown right now?** → `inbox/captures/`. Known? → route directly; never park a known fact in the inbox.
|
||||
2. **Is it about Bryan's work over time?** → `journal/` (pick the grain by cadence). **About the vault's mechanical health?** → `_agent/health/`. These two look similar monthly but answer different questions.
|
||||
3. **Does the effort have an end state?** → `projects/` (folder = `status:`). **No finish line?** → `areas/`.
|
||||
4. **A fact about the world:** timeless → `semantic/`; a thing that happened → `episodic/`; needed only for now → `working/`. A fact about a *person* → `resources/people/`; a fact about an *organization* → `resources/companies/`.
|
||||
5. **A decision:** always `decisions/by-date/`; mirror into a project only if one already exists.
|
||||
6. **A capability (skill/plugin):** `_agent/skills/` catalogs the capability; `projects/` tracks building it. Both can exist for the same skill.
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
Session logs go in: `_agent/sessions/YYYY-MM-DD-HHMM-<slug>.md`
|
||||
|
||||
**Filename format is canonical and not optional.** The four-digit local-time HHMM component is what makes session filenames lex-sort in true chronological order — the loading procedure depends on it. Before PUT-ing a new session log, validate the filename matches `^\d{4}-\d{2}-\d{2}-\d{4}-[a-z0-9-]+\.md$`. Legacy session logs without HHMM may exist; do not rename them, but every new write must use the full form.
|
||||
**Filename format is canonical and not optional.** The four-digit local-time HHMM component is what makes session filenames lex-sort in true chronological order — the loading procedure depends on it. Before PUT-ing a new session log, validate the filename matches `^\d{4}-\d{2}-\d{2}-\d{4}-[a-z0-9-]+\.md$`. Legacy session logs without HHMM exist in the vault; do not edit their names, but every new write must use the full form.
|
||||
|
||||
The slug describes what the session was about in 2–5 words, kebab-case.
|
||||
Examples: `2026-06-07-1430-goldbrain-plugin-build.md`, `2026-05-14-0900-q1-review-prep.md`.
|
||||
Examples: `2026-06-05-1430-echo-plugin-build.md`, `2026-05-14-0900-q1-review-prep.md`.
|
||||
|
||||
Keep logs focused. Capture the goal, what was read/done, decisions, outputs, open threads, and the next step. This matches the vault's `_agent/templates/session-log-template.md`.
|
||||
|
||||
@@ -17,12 +17,12 @@ Keep logs focused. Capture the goal, what was read/done, decisions, outputs, ope
|
||||
---
|
||||
type: session-log
|
||||
status: complete
|
||||
created: 2026-06-07T14:30
|
||||
updated: 2026-06-07T14:30
|
||||
created: 2026-06-05T14:30
|
||||
updated: 2026-06-05T14:30
|
||||
tags: [agent, session]
|
||||
agent_written: true
|
||||
source_notes: []
|
||||
session_date: 2026-06-07
|
||||
session_date: 2026-06-05
|
||||
client: claude-code
|
||||
---
|
||||
|
||||
@@ -65,12 +65,12 @@ One sentence: what to do first next time on this topic.
|
||||
---
|
||||
type: session-log
|
||||
status: complete
|
||||
created: 2026-06-07T14:30
|
||||
updated: 2026-06-07T14:30
|
||||
created: 2026-06-05T14:30
|
||||
updated: 2026-06-05T14:30
|
||||
tags: [agent, session, plugin]
|
||||
agent_written: true
|
||||
source_notes: ["resources/references/obsidian-local-rest-api.md"]
|
||||
session_date: 2026-06-07
|
||||
session_date: 2026-06-05
|
||||
client: claude-code
|
||||
---
|
||||
|
||||
@@ -87,7 +87,7 @@ Verified the REST API end-to-end, confirmed the scaffold copied into the live va
|
||||
created missing empty folders, and built the plugin (SKILL + 4 reference files).
|
||||
|
||||
## Decisions Made
|
||||
- Vault addressed at root (no `Projects/agents/` prefix) — goldbrain is a dedicated vault.
|
||||
- Vault addressed at root — goldbrain is a dedicated vault.
|
||||
- Key hardcoded in the plugin (not in the vault) — personal plugin, per the reference pattern.
|
||||
|
||||
## Outputs Created
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
# Vault Layout & Frontmatter Conventions
|
||||
|
||||
Mirrors the canonical conventions defined in the vault's own `STRUCTURE.md` and `BOOTSTRAP.md`. If those change, they are the source of truth.
|
||||
**This document is canonical.** The goldbrain vault holds data only — there are no `CLAUDE.md` / `STRUCTURE.md` / `BOOTSTRAP.md` / `index.md` control docs in it. Layout, taxonomy, and frontmatter conventions live here in the plugin; the bootstrap procedure (`references/bootstrap.md`) builds the tree below into any empty vault.
|
||||
|
||||
## Folder Map (root-addressed)
|
||||
|
||||
```
|
||||
/vault/
|
||||
├── CLAUDE.md ← operating contract + session protocol
|
||||
├── STRUCTURE.md ← layout, taxonomy, frontmatter standard
|
||||
├── BOOTSTRAP.md ← preflight/repair manifest (read first)
|
||||
├── spinup.md index.md README.md
|
||||
├── README.md ← thin human signpost (NOT read for routing)
|
||||
├── inbox/
|
||||
│ ├── captures/ ← quick captures (inbox.md), date-prefixed lines
|
||||
│ ├── imports/ ← raw imported material
|
||||
│ └── processing-log/
|
||||
├── journal/
|
||||
├── journal/ ← one append-only time-series stream; rollups are coarser journal entries, NOT a separate reviews/ tree
|
||||
│ ├── daily/ ← YYYY-MM-DD.md (has an "Agent Log" section)
|
||||
│ ├── weekly/ monthly/
|
||||
│ ├── weekly/ ← YYYY-Www.md (ISO week, opt-in rollup)
|
||||
│ ├── monthly/ ← YYYY-MM.md (monthly rollup)
|
||||
│ ├── quarterly/ ← YYYY-Qn.md (manual / on request)
|
||||
│ ├── annual/ ← YYYY.md (manual / on request)
|
||||
│ └── templates/
|
||||
├── projects/ ← lifecycle: incubating → active → on-hold/archived
|
||||
│ ├── active/ ← current work (status: active)
|
||||
@@ -26,27 +26,31 @@ Mirrors the canonical conventions defined in the vault's own `STRUCTURE.md` and
|
||||
│ └── project-template.md
|
||||
├── areas/ ← business / personal / learning / systems
|
||||
├── resources/
|
||||
│ ├── concepts/ references/ meetings/ source-material/
|
||||
│ ├── companies/ ← <slug>.md — organizations (clients, vendors, partners, employers)
|
||||
│ ├── concepts/ references/ meetings/
|
||||
│ └── people/ ← <name>.md
|
||||
├── decisions/
|
||||
│ ├── by-date/ ← YYYY-MM-DD-<slug>.md (ADR-style)
|
||||
│ ├── by-project/ ← mirror by project
|
||||
│ ├── by-date/ ← YYYY-MM-DD-<slug>.md (ADR-style) — the canonical home
|
||||
│ └── decision-template.md
|
||||
├── reviews/ ← weekly / monthly / quarterly / annual
|
||||
├── archive/ ← notes / projects / imports
|
||||
│ (decisions/by-project/ is retired and not created —
|
||||
│ mirror an ADR into a project's `## Key Decisions` heading instead)
|
||||
│ (reviews/ is retired — journal rollups live under journal/; vault-health audits under _agent/health/)
|
||||
└── _agent/
|
||||
├── goldbrain-vault.md ← bootstrap marker: schema_version + bootstrap date (plugin-owned; the "is this vault set up?" probe)
|
||||
├── context/ ← current-context.md and task bundles
|
||||
├── memory/
|
||||
│ ├── working/ ← transient, time-boxed
|
||||
│ ├── episodic/ ← what happened, when
|
||||
│ └── semantic/ ← durable facts/patterns (operator-preferences.md)
|
||||
├── sessions/ ← YYYY-MM-DD-HHMM-<slug>.md
|
||||
├── health/ ← YYYY-MM-vault-health.md (monthly self-maintenance audit; NOT a journal entry)
|
||||
├── templates/ ← canonical note templates
|
||||
├── outputs/ ← briefs / drafts / summaries / synthesis
|
||||
├── skills/ ← active / archived
|
||||
└── heartbeat/
|
||||
└── heartbeat/ ← single-line pointers (e.g. last-session.md → most-recent session log path)
|
||||
```
|
||||
|
||||
**Heartbeat:** `_agent/heartbeat/last-session.md` is a one-line pointer (`<session-log-path> @ <ISO-timestamp>`) the **session-logging procedure writes (PUT, overwrite) at session end** and the **loading procedure reads first (Step 4)** as an O(1) shortcut to the latest session log. It is a hint, not a source of truth — fall back to the `sessions/` directory listing if it's missing or stale. Because it's PUT-overwritten, it never grows or duplicates.
|
||||
|
||||
**Slug rules:** kebab-case, ASCII only, truncate to ~40 chars.
|
||||
|
||||
---
|
||||
@@ -72,6 +76,12 @@ agent-managed content from human-authored content. When appending with POST, do
|
||||
not rewrite frontmatter — the append goes after existing content. To change
|
||||
`updated:` or `status:`, use PATCH with `Target-Type: frontmatter`.
|
||||
|
||||
**Frontmatter field semantics:**
|
||||
|
||||
- `created:` is the **earliest known date** the entity was tracked in the vault — *not* "today". When merging notes (e.g. promoting an `on-hold/` project into `active/`), preserve the earliest `created:` from any merged source and only update `updated:`.
|
||||
- `source_notes` is a **backward link** — the note(s) that triggered or supplied content for this one (e.g. the session log a project update came from). Forward links go in the `## Related` body section, never here.
|
||||
- `status:` for a project MUST match its folder under `projects/` (`active`, `incubating`, `on-hold`, `archived`). Moving the file and updating `status:` are the same operation.
|
||||
|
||||
> **No `[[wikilinks]]` in frontmatter.** YAML parses `[[...]]` as nested lists, so wiki
|
||||
> links there break and never render as clickable links in reading view. Put all
|
||||
> cross-references in a **`## Related`** section in the note **body** (bulleted `[[links]]`).
|
||||
@@ -81,7 +91,7 @@ not rewrite frontmatter — the append goes after existing content. To change
|
||||
## Note Types
|
||||
|
||||
`daily-note`, `weekly-note`, `monthly-note`, `project`, `project-update`, `area`,
|
||||
`concept`, `reference`, `person`, `meeting`, `decision`, `review`, `session-log`,
|
||||
`concept`, `reference`, `person`, `company`, `meeting`, `decision`, `review`, `session-log`,
|
||||
`working-memory`, `episodic-memory`, `semantic-memory`, `context-bundle`, `skill`,
|
||||
`draft`, `inbox-item`.
|
||||
|
||||
@@ -95,58 +105,72 @@ The profile analog. Canonical headings:
|
||||
|
||||
- `## Operator` — who Bryan is (one paragraph)
|
||||
- `## Fact / Pattern` — **promoted, deduped rules.** No date prefix. Timeless.
|
||||
- `## Observations` — **timestamped raw observations.** Date-prefixed lines (`- 2026-06-07: ...`). Default landing zone for new evidence.
|
||||
- `## Observations` — **timestamped raw observations.** Date-prefixed lines (`- 2026-06-06: ...`). Default landing zone for new evidence.
|
||||
- `## Evidence` — citations/links supporting the rules
|
||||
- `## Recommendation or Implication` — how the rules should shape behavior
|
||||
- `## Review Notes` — confidence / last review date
|
||||
|
||||
Append observed facts under `## Observations` by default. Promote to `## Fact / Pattern` (dropping the date) once a pattern stabilizes. "The operator" is Bryan; do not attribute Jason's design preferences to Bryan without evidence — Jason's preferences live in the ECHO vault.
|
||||
Append observed facts under `## Observations` by default. Promote to `## Fact / Pattern` (dropping the date) once a pattern stabilizes. "The operator" is Bryan Gilliom (CEO, Message Point Media); Jason Stedwell is the vault's architect, not its day-to-day operator.
|
||||
|
||||
### projects/active/\<slug\>.md
|
||||
|
||||
Mirrors `scaffold/templates/projects/project-template.md` — keep this block in sync with that template.
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: project
|
||||
status: active
|
||||
created: 2026-06-07
|
||||
updated: 2026-06-07
|
||||
tags: []
|
||||
created: {{date:YYYY-MM-DD}}
|
||||
updated: {{date:YYYY-MM-DD}}
|
||||
tags: [project]
|
||||
agent_written: false
|
||||
source_notes: []
|
||||
owner:
|
||||
review_cycle: weekly
|
||||
---
|
||||
|
||||
# Project Name
|
||||
|
||||
## Current status
|
||||
One paragraph, kept fresh via PATCH replace.
|
||||
## Purpose
|
||||
|
||||
## Decisions
|
||||
- [[YYYY-MM-DD-decision-slug]] — one-line summary
|
||||
## Status
|
||||
One paragraph, kept fresh via PATCH replace (target `Project Name::Status`).
|
||||
|
||||
## Open threads
|
||||
## Goals
|
||||
|
||||
## Current Context
|
||||
|
||||
## Open Loops
|
||||
- [ ] unresolved thing
|
||||
|
||||
## Log
|
||||
- 2026-06-07: observation or update
|
||||
## Key Decisions
|
||||
- [[YYYY-MM-DD-decision-slug]] — one-line summary (ADR mirror target)
|
||||
|
||||
## Related Notes
|
||||
|
||||
## Session History
|
||||
- 2026-06-05: observation or update
|
||||
|
||||
## Related
|
||||
- [[areas/business/business-ops]]
|
||||
```
|
||||
|
||||
**Lifecycle / status agreement:** a project's folder under `projects/` (`active`, `incubating`, `on-hold`, `archived`) and its `status:` frontmatter MUST match — moving the file and updating `status:` are the same operation. A note in `projects/active/` with `status: on-hold` is broken state; fix it when you see it. Keep heading text ASCII (plain hyphens, no em dashes/parentheses) so headings stay PATCH-targetable.
|
||||
|
||||
### sessions/YYYY-MM-DD-HHMM-\<slug\>.md
|
||||
|
||||
See `session-log-template.md`. goldbrain uses an **HHMM time component** in the filename — this is **canonical, not optional**. The four-digit local-time component makes filenames lex-sort in true chronological order, which the loading procedure relies on. Older session logs without HHMM may exist; leave them alone, but every new one must use the full `YYYY-MM-DD-HHMM-<slug>.md` form.
|
||||
See `session-log-template.md`. goldbrain uses an **HHMM time component** in the filename — this is **canonical, not optional**. The four-digit local-time component makes filenames lex-sort in true chronological order, which the loading procedure relies on. Older session logs without HHMM exist; leave them alone, but every new one must use the full `YYYY-MM-DD-HHMM-<slug>.md` form.
|
||||
|
||||
### decisions/by-date/YYYY-MM-DD-\<slug\>.md
|
||||
|
||||
ADR-style: Context → Decision → Consequences. Mirror a project-relevant ADR as a `[[wikilink]]` under that project's `## Decisions` heading, and optionally into `by-project/` per the vault's `STRUCTURE.md`. The by-date ADR is always the canonical record.
|
||||
ADR-style: Context → Decision → Consequences. If the decision belongs to an existing project, PATCH-append the wikilink into that project's `## Key Decisions` heading. Don't use `decisions/by-project/` — it's legacy scaffolding that's intentionally left empty.
|
||||
|
||||
### people/\<name\>.md
|
||||
|
||||
`type: person`. Use lowercase kebab-case for the slug (e.g. `bryan-gilliom.md`).
|
||||
|
||||
### companies/\<slug\>.md
|
||||
|
||||
`type: company`. An organization Bryan works with — client, vendor, partner, or employer (e.g. `gillig.md`, `mpm.md`). Distinct from `people/` (individuals, who *belong to* companies) and `references/` (external sources). Lowercase kebab-case slug.
|
||||
|
||||
---
|
||||
|
||||
## Cross-References
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Goldbrain Memory Vault
|
||||
|
||||
This Obsidian vault is the persistent memory substrate ("second brain") for its operator. It is read and written across Claude / CoWork sessions through the Obsidian Local REST API.
|
||||
|
||||
**This vault holds data, not logic.** All operating procedure — how the vault is bootstrapped, how notes are routed, the taxonomy, frontmatter conventions, and safety rules — lives in the **`goldbrain-memory` plugin**, which is the single source of truth. There are intentionally no `CLAUDE.md` / `BOOTSTRAP.md` / `STRUCTURE.md` control docs in this vault; updating or porting goldbrain means updating or installing the plugin, not editing files here.
|
||||
|
||||
- **Layout:** see the plugin's `references/vault-layout.md`.
|
||||
- **Operating contract & safety:** see the plugin's `references/operating-contract.md`.
|
||||
- **Bootstrap / repair:** see the plugin's `references/bootstrap.md`.
|
||||
- **Version marker:** `_agent/goldbrain-vault.md` records the schema version and bootstrap date.
|
||||
|
||||
Folders: `inbox/`, `journal/` (daily + weekly/monthly/quarterly/annual rollups), `projects/`, `areas/`, `resources/`, `decisions/`, and the agent subtree `_agent/`.
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
type: context-bundle
|
||||
status: active
|
||||
created: {{DATE}}
|
||||
updated: {{DATE}}
|
||||
tags: [agent, context]
|
||||
agent_written: true
|
||||
source_notes: []
|
||||
scope:
|
||||
scope_updated: {{DATE}}
|
||||
refresh_strategy: on-demand
|
||||
---
|
||||
|
||||
# Current Context
|
||||
|
||||
## Scope
|
||||
<!-- The single active scope. Replaced (PATCH replace) on each scope switch. -->
|
||||
|
||||
## Scope History
|
||||
<!-- Dated bullets of prior scopes, newest first: `- {{DATE}}: <prior scope summary>`. Trim to ~10. -->
|
||||
|
||||
## Active Priorities
|
||||
|
||||
## Open Questions
|
||||
|
||||
## Related
|
||||
@@ -0,0 +1,3 @@
|
||||
# Inbox — Captures
|
||||
|
||||
Quick captures land here as date-prefixed lines (`- {{DATE}}: <thing>`), one per line, via POST. Triage routes durable items to their proper home (see the goldbrain-memory skill's Inbox Triage). Don't delete originals on triage — the processing log is the audit trail.
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
---
|
||||
type: semantic-memory
|
||||
status: active
|
||||
created: {{DATE}}
|
||||
updated: {{DATE}}
|
||||
tags: [agent, semantic-memory, operator]
|
||||
agent_written: true
|
||||
source_notes: []
|
||||
confidence: low
|
||||
last_reviewed: {{DATE}}
|
||||
---
|
||||
|
||||
# Operator Preferences
|
||||
|
||||
## Operator
|
||||
<!-- One paragraph: who the operator is. Leave for the operator to confirm; do not fabricate. -->
|
||||
|
||||
## Fact / Pattern
|
||||
<!-- Promoted, deduped, timeless rules. No date prefix. Add only when a rule is stable. -->
|
||||
|
||||
## Observations
|
||||
<!-- Timestamped raw observations. Default landing zone for new evidence: `- {{DATE}}: ...` -->
|
||||
|
||||
## Evidence
|
||||
|
||||
## Recommendation or Implication
|
||||
|
||||
## Review Notes
|
||||
Seeded empty at bootstrap ({{DATE}}). Raise confidence once preferences are confirmed through day-to-day use.
|
||||
|
||||
## Related
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
type: reference
|
||||
status: active
|
||||
created: {{DATE}}
|
||||
updated: {{DATE}}
|
||||
tags: [agent, system, marker]
|
||||
agent_written: true
|
||||
source_notes: []
|
||||
schema_version: 2
|
||||
bootstrapped: {{DATE}}
|
||||
managed_by: goldbrain-memory-plugin
|
||||
---
|
||||
|
||||
# Goldbrain Vault Marker
|
||||
|
||||
This file marks the vault as bootstrapped by the **goldbrain-memory plugin** and records its schema version. The plugin's loading procedure GETs this file as its "is this vault set up?" probe; a `404` triggers a fresh bootstrap.
|
||||
|
||||
- `schema_version` — bumped by the plugin when the vault layout changes; a mismatch is the hook for a migration pass (see `references/bootstrap.md`).
|
||||
- Do not hand-edit. The plugin owns this file.
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
---
|
||||
type: context-bundle
|
||||
status: active
|
||||
created: {{date:YYYY-MM-DD}}
|
||||
updated: {{date:YYYY-MM-DD}}
|
||||
tags: [agent, context]
|
||||
agent_written: true
|
||||
source_notes: []
|
||||
scope:
|
||||
refresh_strategy: on-demand
|
||||
---
|
||||
|
||||
# Context Bundle Title
|
||||
|
||||
## Scope
|
||||
|
||||
## Active Priorities
|
||||
|
||||
## Relevant Entities
|
||||
|
||||
## Key Decisions
|
||||
|
||||
## Open Questions
|
||||
|
||||
## Source Notes
|
||||
|
||||
## Related
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
---
|
||||
type: semantic-memory
|
||||
status: active
|
||||
created: {{date:YYYY-MM-DD}}
|
||||
updated: {{date:YYYY-MM-DD}}
|
||||
tags: [agent, semantic-memory]
|
||||
agent_written: true
|
||||
source_notes: []
|
||||
confidence: high
|
||||
last_reviewed: {{date:YYYY-MM-DD}}
|
||||
---
|
||||
|
||||
# Semantic Memory Title
|
||||
|
||||
## Fact / Pattern
|
||||
|
||||
## Evidence
|
||||
|
||||
## Recommendation or Implication
|
||||
|
||||
## Review Notes
|
||||
|
||||
## Related
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
---
|
||||
type: session-log
|
||||
status: complete
|
||||
created: {{date:YYYY-MM-DDTHH:mm}}
|
||||
updated: {{date:YYYY-MM-DDTHH:mm}}
|
||||
tags: [agent, session]
|
||||
agent_written: true
|
||||
source_notes: []
|
||||
session_date: {{date:YYYY-MM-DD}}
|
||||
client: claude-code
|
||||
---
|
||||
|
||||
# Session Log
|
||||
|
||||
## Goal
|
||||
|
||||
## Notes Read
|
||||
|
||||
## Actions Taken
|
||||
|
||||
## Decisions Made
|
||||
|
||||
## Outputs Created
|
||||
|
||||
## Open Threads
|
||||
|
||||
## Suggested Next Step
|
||||
|
||||
## Related
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
---
|
||||
type: working-memory
|
||||
status: active
|
||||
created: {{date:YYYY-MM-DDTHH:mm}}
|
||||
updated: {{date:YYYY-MM-DDTHH:mm}}
|
||||
tags: [agent, working-memory]
|
||||
agent_written: true
|
||||
source_notes: []
|
||||
expires_after: 48h
|
||||
---
|
||||
|
||||
# Working Context
|
||||
|
||||
## Active Focus
|
||||
|
||||
## Recent Decisions
|
||||
|
||||
## Open Threads
|
||||
|
||||
## Relevant Links
|
||||
|
||||
## Related
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
---
|
||||
type: decision
|
||||
status: complete
|
||||
created: {{date:YYYY-MM-DD}}
|
||||
updated: {{date:YYYY-MM-DD}}
|
||||
tags: [decision]
|
||||
agent_written: true
|
||||
source_notes: []
|
||||
decision_date: {{date:YYYY-MM-DD}}
|
||||
impact: medium
|
||||
---
|
||||
|
||||
# Decision Title
|
||||
|
||||
## Context
|
||||
|
||||
## Decision
|
||||
|
||||
## Rationale
|
||||
|
||||
## Consequences
|
||||
|
||||
## Follow-Up
|
||||
|
||||
## Related
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
---
|
||||
type: daily-note
|
||||
status: active
|
||||
created: {{date:YYYY-MM-DD}}
|
||||
updated: {{date:YYYY-MM-DD}}
|
||||
tags: [daily, journal]
|
||||
agent_written: false
|
||||
source_notes: []
|
||||
---
|
||||
|
||||
# {{date:YYYY-MM-DD}}
|
||||
|
||||
## Top Priorities
|
||||
|
||||
## Schedule / Commitments
|
||||
|
||||
## Open Loops
|
||||
|
||||
## Notes
|
||||
|
||||
## Context for AI
|
||||
|
||||
## Agent Log
|
||||
|
||||
## Related
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
---
|
||||
type: review
|
||||
status: active
|
||||
created: {{date:gggg-[W]WW}}
|
||||
updated: {{date:gggg-[W]WW}}
|
||||
tags: [review, weekly]
|
||||
agent_written: true
|
||||
source_notes: []
|
||||
period: weekly
|
||||
---
|
||||
|
||||
# Weekly Review
|
||||
|
||||
## Completed Work
|
||||
|
||||
## Open Loops
|
||||
|
||||
## Stale Projects
|
||||
|
||||
## Decisions
|
||||
|
||||
## Priorities Next Week
|
||||
|
||||
## Agent Notes
|
||||
|
||||
## Related
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
---
|
||||
type: project
|
||||
status: active
|
||||
created: {{date:YYYY-MM-DD}}
|
||||
updated: {{date:YYYY-MM-DD}}
|
||||
tags: [project]
|
||||
agent_written: false
|
||||
source_notes: []
|
||||
owner:
|
||||
review_cycle: weekly
|
||||
---
|
||||
|
||||
# Project Name
|
||||
|
||||
## Purpose
|
||||
|
||||
## Status
|
||||
|
||||
## Goals
|
||||
|
||||
## Current Context
|
||||
|
||||
## Open Loops
|
||||
|
||||
## Key Decisions
|
||||
|
||||
## Related Notes
|
||||
|
||||
## Session History
|
||||
|
||||
## Related
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
# bootstrap.sh — stand up (or repair) an goldbrain vault deterministically.
|
||||
#
|
||||
# Idempotent and additive: every write is probe-before-write and NEVER overwrites an
|
||||
# existing file. The marker (_agent/goldbrain-vault.md) is written LAST, so the vault is
|
||||
# only flagged "set up" once every piece is in place. Safe to re-run any time — that
|
||||
# is also the "repair" path (it fills in only what is missing).
|
||||
#
|
||||
# All scaffold is resolved relative to THIS script's location, so it works regardless
|
||||
# of the caller's CWD (fixes the old `@scaffold/...` relative-path assumption).
|
||||
#
|
||||
# Usage:
|
||||
# bootstrap.sh [--dry-run]
|
||||
#
|
||||
# Env: GB_BASE, GB_KEY (passed through to goldbrain.sh), GB_TODAY (YYYY-MM-DD for {{DATE}}).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
SCAFFOLD="$SKILL_DIR/scaffold"
|
||||
GB="$SCRIPT_DIR/goldbrain.sh"
|
||||
TODAY="${GB_TODAY:-$(date +%Y-%m-%d)}"
|
||||
DRY=0
|
||||
[ "${1:-}" = "--dry-run" ] || [ "${1:-}" = "-n" ] && DRY=1
|
||||
|
||||
[ -d "$SCAFFOLD" ] || { echo "bootstrap: scaffold not found at $SCAFFOLD" >&2; exit 1; }
|
||||
[ -x "$GB" ] || chmod +x "$GB" 2>/dev/null || true
|
||||
|
||||
say() { echo "bootstrap: $*"; }
|
||||
exists() { GB_VERIFY=0 "$GB" get "$1" >/dev/null 2>&1; } # 0 = present(200), nonzero = absent/404
|
||||
|
||||
# seed VAULT_PATH from LOCAL_FILE (with {{DATE}} substitution), only if absent
|
||||
seed() {
|
||||
local vpath="$1" local_file="$2"
|
||||
if exists "$vpath"; then say "skip (exists) $vpath"; return 0; fi
|
||||
if [ "$DRY" = "1" ]; then say "would seed $vpath <- ${local_file#$SKILL_DIR/}"; return 0; fi
|
||||
sed "s/{{DATE}}/$TODAY/g" "$local_file" | GB_VERIFY=1 "$GB" put "$vpath" - >/dev/null
|
||||
say "seeded $vpath"
|
||||
}
|
||||
|
||||
# write a one-line leaf README only if absent
|
||||
leaf_readme() {
|
||||
local dir="$1" name="${1##*/}"
|
||||
local vpath="$dir/README.md"
|
||||
if exists "$vpath"; then return 0; fi
|
||||
if [ "$DRY" = "1" ]; then say "would readme $vpath"; return 0; fi
|
||||
printf '# %s\n\nMemory vault folder. See the goldbrain-memory plugin for conventions.\n' "$name" \
|
||||
| GB_VERIFY=0 "$GB" put "$vpath" - >/dev/null
|
||||
say "readme $vpath"
|
||||
}
|
||||
|
||||
# ---- Pre-flight: is the vault already bootstrapped? --------------------------
|
||||
if exists "_agent/goldbrain-vault.md"; then
|
||||
ver="$("$GB" get _agent/goldbrain-vault.md 2>/dev/null | sed -n 's/^schema_version:[[:space:]]*//p' | head -1)"
|
||||
say "marker present (schema_version=${ver:-unknown}). Running repair pass (fills only missing files)."
|
||||
CUR_SCHEMA=2
|
||||
if [ -n "$ver" ] && [ "$ver" -lt "$CUR_SCHEMA" ] 2>/dev/null; then
|
||||
say "NOTE: schema_version $ver < $CUR_SCHEMA — run migrate.sh before relying on the vault."
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- 1. Folder tree (leaf READMEs guarantee non-empty dirs) ------------------
|
||||
LEAVES=(
|
||||
inbox/captures inbox/imports inbox/processing-log
|
||||
journal/daily journal/weekly journal/monthly journal/quarterly journal/annual journal/templates
|
||||
projects/active projects/incubating projects/on-hold projects/archived
|
||||
areas/business areas/personal areas/learning areas/systems
|
||||
resources/concepts resources/references resources/people resources/companies resources/meetings
|
||||
decisions/by-date
|
||||
_agent/context _agent/memory/working _agent/memory/episodic _agent/memory/semantic
|
||||
_agent/sessions _agent/health _agent/templates _agent/heartbeat
|
||||
_agent/skills/active _agent/skills/archived _agent/locks
|
||||
)
|
||||
for d in "${LEAVES[@]}"; do leaf_readme "$d"; done
|
||||
|
||||
# ---- 2. Templates (mirror scaffold/templates/ 1:1 into the vault) ------------
|
||||
if [ -d "$SCAFFOLD/templates" ]; then
|
||||
while IFS= read -r f; do
|
||||
rel="${f#$SCAFFOLD/templates/}"
|
||||
seed "$rel" "$f"
|
||||
done < <(find "$SCAFFOLD/templates" -type f -name '*.md')
|
||||
fi
|
||||
|
||||
# ---- 3. Anchor seeds (only if absent — never fabricate facts) ----------------
|
||||
seed "_agent/memory/semantic/operator-preferences.md" "$SCAFFOLD/anchors/operator-preferences.seed.md"
|
||||
seed "_agent/context/current-context.md" "$SCAFFOLD/anchors/current-context.seed.md"
|
||||
seed "inbox/captures/inbox.md" "$SCAFFOLD/anchors/inbox.seed.md"
|
||||
|
||||
# ---- 4. Vault README (human signpost) ----------------------------------------
|
||||
seed "README.md" "$SCAFFOLD/README.vault.md"
|
||||
|
||||
# ---- 5. Marker (write LAST) --------------------------------------------------
|
||||
seed "_agent/goldbrain-vault.md" "$SCAFFOLD/goldbrain-vault.md"
|
||||
|
||||
say "done (${DRY:+DRY-RUN }$TODAY)."
|
||||
say "Next: create today's daily note + a bootstrap session log + heartbeat (see SKILL.md First-run trace)."
|
||||
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env bash
|
||||
# goldbrain.sh — the single validated client for the goldbrain Obsidian Local REST API.
|
||||
#
|
||||
# Every read/write to the vault should go through this script instead of hand-built
|
||||
# curl. It centralizes: auth injection, HTTP-status checking (non-zero exit on >=400
|
||||
# so a failed write can never look like success), one bounded retry on 5xx/connection
|
||||
# errors, idempotent append (read-before-POST), correct `::` heading-target handling,
|
||||
# frontmatter field patches, and an advisory multi-writer lock.
|
||||
#
|
||||
# Config (env overrides; defaults match the rest of the plugin):
|
||||
# GB_BASE default https://goldbrainapi.mpm.to
|
||||
# GB_KEY default the plugin bearer token
|
||||
# GB_VERIFY default 1 — read-back verify after a PUT
|
||||
# GB_LOCK_TTL default 900 — seconds before an advisory lock is considered stale
|
||||
#
|
||||
# Usage:
|
||||
# goldbrain.sh get <path> # print file contents (404 -> exit 44)
|
||||
# goldbrain.sh map <path> # document-map JSON (headings/blocks/frontmatter)
|
||||
# goldbrain.sh ls <dir> # directory listing JSON
|
||||
# goldbrain.sh search <query...> # /search/simple
|
||||
# goldbrain.sh put <path> [file] # create/overwrite (body from file or stdin)
|
||||
# goldbrain.sh post <path> [file] # raw append (NON-idempotent; prefer `append`)
|
||||
# goldbrain.sh append <path> <line> # idempotent append: skips if the exact line exists
|
||||
# goldbrain.sh patch <path> <append|prepend|replace> <heading|frontmatter|block> <target> [file]
|
||||
# goldbrain.sh fm <path> <field> <json-value> # PATCH a frontmatter scalar (e.g. fm p.md updated '"2026-06-19"')
|
||||
# goldbrain.sh bump <path> [YYYY-MM-DD] # set frontmatter updated: to today (or given date)
|
||||
# goldbrain.sh delete <path> # DELETE (destructive; explicit use only)
|
||||
# goldbrain.sh lock <owner-id> # acquire advisory lock (exit 75 if held by someone else & fresh)
|
||||
# goldbrain.sh unlock <owner-id> # release advisory lock if owned by <owner-id>
|
||||
# goldbrain.sh scope show # print active scope, its freshness, and sessions-since
|
||||
# goldbrain.sh scope set "<text>" # switch scope atomically (history + replace + stamp scope_updated)
|
||||
#
|
||||
# Exit codes: 0 ok · 44 not-found(404) · 75 lock-held · 2 usage · 1 other HTTP/transport error.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
GB_BASE="${GB_BASE:-https://goldbrainapi.mpm.to}"
|
||||
GB_BASE="${GB_BASE%/}"
|
||||
GB_KEY="${GB_KEY:-fb72065a05fabb28ae87c45880cc3b7aba4fd3f58e70297934145cef974e8ed8}"
|
||||
GB_VERIFY="${GB_VERIFY:-1}"
|
||||
GB_LOCK_TTL="${GB_LOCK_TTL:-900}"
|
||||
AUTH="Authorization: Bearer ${GB_KEY}"
|
||||
|
||||
# response-body scratch file (filled by every _curl; read by callers via $RESP)
|
||||
RESP="$(mktemp)"; BODY=""
|
||||
cleanup() { rm -f "$RESP" "${BODY:-}"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
die() { echo "goldbrain.sh: $*" >&2; exit 1; }
|
||||
usage() { sed -n '2,40p' "$0" >&2; exit 2; }
|
||||
|
||||
_today() { echo "${GB_TODAY:-$(date +%Y-%m-%d)}"; }
|
||||
_now_iso() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
||||
_vault_url() { echo "${GB_BASE}/vault/$1"; }
|
||||
|
||||
# _curl METHOD URL [extra curl args...]
|
||||
# Writes the response body to $RESP and the status code to $HTTP — BOTH IN THE PARENT
|
||||
# SHELL (never call this in $(...) or on the right of a pipe, or those globals are lost).
|
||||
# One bounded retry on transport failure (000) or 5xx.
|
||||
HTTP=""
|
||||
_curl() {
|
||||
local method="$1" url="$2"; shift 2
|
||||
local code attempt=0
|
||||
while :; do
|
||||
code="$(curl -sS -X "$method" -H "$AUTH" -o "$RESP" -w '%{http_code}' "$@" "$url" 2>/dev/null || echo 000)"
|
||||
if { [ "$code" = "000" ] || [ "${code:0:1}" = "5" ]; } && [ "$attempt" -lt 1 ]; then
|
||||
attempt=$((attempt+1)); sleep 1; continue
|
||||
fi
|
||||
break
|
||||
done
|
||||
HTTP="$code"
|
||||
}
|
||||
|
||||
# assert $HTTP is acceptable; 404 -> exit 44, other >=400 -> exit 1
|
||||
_check() {
|
||||
local ctx="$1"
|
||||
[ "$HTTP" = "404" ] && exit 44
|
||||
[ "$HTTP" = "000" ] && die "$ctx: vault unreachable (connection failed) [$GB_BASE]"
|
||||
[ "${HTTP:-000}" -ge 400 ] && die "$ctx: HTTP $HTTP — $(cat "$RESP")"
|
||||
return 0
|
||||
}
|
||||
|
||||
# capture a body argument (file path or '-'/empty for stdin) into $BODY (a temp file)
|
||||
_capture_body() {
|
||||
BODY="$(mktemp)"
|
||||
if [ "${1:-}" = "" ] || [ "${1:-}" = "-" ]; then cat > "$BODY"; else cat "$1" > "$BODY"; fi
|
||||
}
|
||||
|
||||
cmd="${1:-}"; shift || true
|
||||
case "$cmd" in
|
||||
get)
|
||||
[ $# -ge 1 ] || usage
|
||||
_curl GET "$(_vault_url "$1")"; _check "get $1"; cat "$RESP" ;;
|
||||
|
||||
map)
|
||||
[ $# -ge 1 ] || usage
|
||||
_curl GET "$(_vault_url "$1")" -H 'Accept: application/vnd.olrapi.document-map+json'
|
||||
_check "map $1"; cat "$RESP" ;;
|
||||
|
||||
ls)
|
||||
[ $# -ge 1 ] || usage
|
||||
p="$1"; [ "${p%/}" = "$p" ] && p="$p/"
|
||||
_curl GET "$(_vault_url "$p")"; _check "ls $1"; cat "$RESP" ;;
|
||||
|
||||
search)
|
||||
[ $# -ge 1 ] || usage
|
||||
q="$*"; q="${q// /+}"
|
||||
_curl POST "${GB_BASE}/search/simple/?query=${q}"; _check "search"; cat "$RESP" ;;
|
||||
|
||||
put)
|
||||
[ $# -ge 1 ] || usage
|
||||
path="$1"; _capture_body "${2:-}"
|
||||
_curl PUT "$(_vault_url "$path")" -H 'Content-Type: text/markdown' --data-binary @"$BODY"
|
||||
_check "put $path"
|
||||
if [ "$GB_VERIFY" = "1" ]; then
|
||||
_curl GET "$(_vault_url "$path")"
|
||||
[ "$HTTP" = "200" ] || die "put $path: write did not verify (GET returned $HTTP)"
|
||||
fi
|
||||
echo "ok: PUT $path" ;;
|
||||
|
||||
post)
|
||||
[ $# -ge 1 ] || usage
|
||||
path="$1"; _capture_body "${2:-}"
|
||||
_curl POST "$(_vault_url "$path")" -H 'Content-Type: text/markdown' --data-binary @"$BODY"
|
||||
_check "post $path"; echo "ok: POST $path" ;;
|
||||
|
||||
append)
|
||||
# idempotent: GET the file, skip the POST if the exact line is already present.
|
||||
[ $# -ge 2 ] || usage
|
||||
path="$1"; line="$2"
|
||||
_curl GET "$(_vault_url "$path")"
|
||||
if [ "$HTTP" = "200" ] && grep -qF -- "$line" "$RESP"; then
|
||||
echo "skip: line already present in $path"; exit 0
|
||||
fi
|
||||
[ "$HTTP" = "200" ] || [ "$HTTP" = "404" ] || _check "append(read) $path"
|
||||
BODY="$(mktemp)"; printf '%s\n' "$line" > "$BODY"
|
||||
_curl POST "$(_vault_url "$path")" -H 'Content-Type: text/markdown' --data-binary @"$BODY"
|
||||
_check "append $path"; echo "ok: APPEND $path" ;;
|
||||
|
||||
patch)
|
||||
[ $# -ge 4 ] || usage
|
||||
path="$1"; op="$2"; ttype="$3"; target="$4"; _capture_body "${5:-}"
|
||||
case "$op" in append|prepend|replace) ;; *) die "patch: op must be append|prepend|replace";; esac
|
||||
case "$ttype" in heading|frontmatter|block) ;; *) die "patch: target-type must be heading|frontmatter|block";; esac
|
||||
_curl PATCH "$(_vault_url "$path")" \
|
||||
-H "Operation: $op" -H "Target-Type: $ttype" -H "Target: $target" \
|
||||
-H 'Content-Type: text/markdown' --data-binary @"$BODY"
|
||||
_check "patch $path ($target)"
|
||||
echo "ok: PATCH $op $ttype '$target' -> $path" ;;
|
||||
|
||||
fm)
|
||||
[ $# -ge 3 ] || usage
|
||||
path="$1"; field="$2"; value="$3"
|
||||
BODY="$(mktemp)"; printf '%s' "$value" > "$BODY"
|
||||
_curl PATCH "$(_vault_url "$path")" \
|
||||
-H 'Operation: replace' -H 'Target-Type: frontmatter' -H "Target: $field" \
|
||||
-H 'Content-Type: application/json' --data-binary @"$BODY"
|
||||
_check "fm $path.$field"; echo "ok: frontmatter $field -> $path" ;;
|
||||
|
||||
bump)
|
||||
[ $# -ge 1 ] || usage
|
||||
path="$1"; d="${2:-$(_today)}"
|
||||
exec "$0" fm "$path" updated "\"$d\"" ;;
|
||||
|
||||
delete)
|
||||
[ $# -ge 1 ] || usage
|
||||
_curl DELETE "$(_vault_url "$1")"; _check "delete $1"; echo "ok: DELETE $1" ;;
|
||||
|
||||
lock)
|
||||
# advisory lock: _agent/locks/vault.lock holds "<owner> @ <iso>". Honored cooperatively.
|
||||
[ $# -ge 1 ] || usage
|
||||
owner="$1"; lockpath="_agent/locks/vault.lock"
|
||||
_curl GET "$(_vault_url "$lockpath")"
|
||||
if [ "$HTTP" = "200" ] && [ -s "$RESP" ]; then
|
||||
cur="$(cat "$RESP")"; held_owner="${cur%% @ *}"; held_iso="${cur##* @ }"; held_iso="${held_iso%$'\n'}"
|
||||
held_epoch="$(date -u -j -f '%Y-%m-%dT%H:%M:%SZ' "$held_iso" +%s 2>/dev/null \
|
||||
|| date -u -d "$held_iso" +%s 2>/dev/null || echo 0)"
|
||||
now_epoch="$(date -u +%s)"
|
||||
if [ "$held_owner" != "$owner" ] && [ $((now_epoch - held_epoch)) -lt "$GB_LOCK_TTL" ]; then
|
||||
echo "lock held by '$held_owner' since $held_iso (fresh)" >&2; exit 75
|
||||
fi
|
||||
fi
|
||||
BODY="$(mktemp)"; printf '%s @ %s\n' "$owner" "$(_now_iso)" > "$BODY"
|
||||
_curl PUT "$(_vault_url "$lockpath")" -H 'Content-Type: text/markdown' --data-binary @"$BODY"
|
||||
_check "lock"; echo "ok: locked by $owner" ;;
|
||||
|
||||
unlock)
|
||||
[ $# -ge 1 ] || usage
|
||||
owner="$1"; lockpath="_agent/locks/vault.lock"
|
||||
_curl GET "$(_vault_url "$lockpath")"
|
||||
if [ "$HTTP" = "200" ]; then
|
||||
cur="$(cat "$RESP")"; held_owner="${cur%% @ *}"
|
||||
[ "$held_owner" = "$owner" ] || { echo "lock owned by '$held_owner', not '$owner' — not releasing" >&2; exit 75; }
|
||||
fi
|
||||
_curl DELETE "$(_vault_url "$lockpath")"
|
||||
[ "$HTTP" = "404" ] || _check "unlock"
|
||||
echo "ok: unlocked" ;;
|
||||
|
||||
scope)
|
||||
# scope show | scope set "<new scope text>"
|
||||
# 'set' archives the prior scope to ## Scope History, replaces ## Scope, and stamps
|
||||
# the scope_updated freshness timestamp — one command instead of three error-prone PATCHes.
|
||||
sub="${1:-show}"; shift || true
|
||||
ccpath="_agent/context/current-context.md"
|
||||
case "$sub" in
|
||||
show)
|
||||
_curl GET "$(_vault_url "$ccpath")"; _check "scope show"
|
||||
cur="$RESP"
|
||||
echo "── Active scope ──"
|
||||
awk '/^## Scope[[:space:]]*$/{f=1;next} /^## /{if(f)exit} f' "$cur"
|
||||
su="$(sed -n 's/^scope_updated:[[:space:]]*//p' "$cur" | head -1)"
|
||||
su="${su//\"/}"
|
||||
echo "scope_updated: ${su:-<missing — drift cannot be detected; run scope set or repair>}"
|
||||
_curl GET "$(_vault_url "_agent/sessions/")"
|
||||
if [ "$HTTP" = "200" ] && [ -n "$su" ]; then
|
||||
n="$(python3 -c "import json,sys;f=json.load(open('$RESP'))['files'];print(sum(1 for x in f if x.endswith('.md') and x[:10]>'$su'))" 2>/dev/null || echo '?')"
|
||||
echo "sessions logged since: ${n}"
|
||||
fi ;;
|
||||
set)
|
||||
[ $# -ge 1 ] || die "scope set needs the new scope text"
|
||||
new="$1"
|
||||
_curl GET "$(_vault_url "$ccpath")"; _check "scope set(read)"
|
||||
prior="$(awk '/^## Scope[[:space:]]*$/{f=1;next} /^## /{if(f)exit} f' "$RESP" \
|
||||
| tr '\n' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' | cut -c1-140)"
|
||||
BODY="$(mktemp)"; printf -- '- %s: %s\n' "$(_today)" "${prior:-(prior scope)}" > "$BODY"
|
||||
_curl PATCH "$(_vault_url "$ccpath")" -H 'Operation: prepend' -H 'Target-Type: heading' \
|
||||
-H 'Target: Current Context::Scope History' -H 'Content-Type: text/markdown' --data-binary @"$BODY"
|
||||
_check "scope set(history)"
|
||||
BODY="$(mktemp)"; printf '%s\n' "$new" > "$BODY"
|
||||
_curl PATCH "$(_vault_url "$ccpath")" -H 'Operation: replace' -H 'Target-Type: heading' \
|
||||
-H 'Target: Current Context::Scope' -H 'Content-Type: text/markdown' --data-binary @"$BODY"
|
||||
_check "scope set(replace)"
|
||||
BODY="$(mktemp)"; printf '"%s"' "$(_today)" > "$BODY"
|
||||
_curl PATCH "$(_vault_url "$ccpath")" -H 'Operation: replace' -H 'Target-Type: frontmatter' \
|
||||
-H 'Target: scope_updated' -H 'Content-Type: application/json' --data-binary @"$BODY"
|
||||
if [ "${HTTP:-000}" -ge 400 ]; then
|
||||
die "scope set: body switched, but scope_updated frontmatter is missing (run bootstrap.sh repair to add it) [HTTP $HTTP]"
|
||||
fi
|
||||
echo "ok: scope switched (prior archived to Scope History; scope_updated=$(_today))" ;;
|
||||
*) die "scope: use 'show' or 'set \"<text>\"'" ;;
|
||||
esac ;;
|
||||
|
||||
""|-h|--help|help) usage ;;
|
||||
*) die "unknown command '$cmd' (try: get map ls search put post append patch fm bump delete lock unlock scope)" ;;
|
||||
esac
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
# migrate.sh — bring an existing goldbrain vault up to the plugin's current schema.
|
||||
#
|
||||
# Reads the marker's schema_version and applies each intervening migration in order.
|
||||
# Migrations are idempotent and additive; every destructive step (DELETE) is gated
|
||||
# behind --apply AND prints what it will do first. Default mode is a DRY-RUN plan.
|
||||
#
|
||||
# Usage:
|
||||
# migrate.sh # print the migration plan (no changes)
|
||||
# migrate.sh --apply # perform the migration (moves/deletes included)
|
||||
#
|
||||
# Env: GB_BASE, GB_KEY (via goldbrain.sh).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
GB="$SCRIPT_DIR/goldbrain.sh"
|
||||
CURRENT_SCHEMA=2
|
||||
APPLY=0
|
||||
[ "${1:-}" = "--apply" ] && APPLY=1
|
||||
|
||||
[ -x "$GB" ] || chmod +x "$GB" 2>/dev/null || true
|
||||
say() { echo "migrate: $*"; }
|
||||
do_or_show() { # do_or_show "<human description>" cmd args...
|
||||
local desc="$1"; shift
|
||||
if [ "$APPLY" = "1" ]; then say "APPLY $desc"; "$@"; else say "PLAN $desc"; fi
|
||||
}
|
||||
|
||||
# ---- Read current schema -----------------------------------------------------
|
||||
if ! marker="$("$GB" get _agent/goldbrain-vault.md 2>/dev/null)"; then
|
||||
say "marker missing — vault not bootstrapped. Run bootstrap.sh, not migrate.sh."
|
||||
exit 3
|
||||
fi
|
||||
FROM="$(printf '%s' "$marker" | sed -n 's/^schema_version:[[:space:]]*//p' | head -1)"
|
||||
FROM="${FROM:-0}"
|
||||
say "vault schema_version=$FROM, plugin schema=$CURRENT_SCHEMA $([ "$APPLY" = 1 ] && echo '(APPLY)' || echo '(dry-run)')"
|
||||
|
||||
if [ "$FROM" -ge "$CURRENT_SCHEMA" ] 2>/dev/null; then
|
||||
say "up to date — nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ls_files() { "$GB" ls "$1" 2>/dev/null | python3 -c 'import sys,json;print("\n".join(json.load(sys.stdin).get("files",[])))' 2>/dev/null || true; }
|
||||
move() { # move SRC DST preserving content (PUT dst <- get src, then delete src)
|
||||
local src="$1" dst="$2"
|
||||
"$GB" get "$src" 2>/dev/null | "$GB" put "$dst" - >/dev/null
|
||||
"$GB" delete "$src" >/dev/null
|
||||
}
|
||||
|
||||
# ---- 0 -> 1 : control docs moved into the plugin -----------------------------
|
||||
mig_0_1() {
|
||||
say "[0->1] retire in-vault control docs (CLAUDE/BOOTSTRAP/STRUCTURE/index.md)"
|
||||
for f in CLAUDE.md BOOTSTRAP.md STRUCTURE.md index.md; do
|
||||
if GB_VERIFY=0 "$GB" get "$f" >/dev/null 2>&1; then
|
||||
do_or_show "delete vault/$f (back it up outside the vault first)" "$GB" delete "$f"
|
||||
fi
|
||||
done
|
||||
say "[0->1] reminder: scrub dangling [[CLAUDE]]/[[BOOTSTRAP]]/[[STRUCTURE]]/[[index]] links from ## Related sections (manual/agent step)."
|
||||
}
|
||||
|
||||
# ---- 1 -> 2 : reviews/ folded into journal/ + _agent/health/ -----------------
|
||||
mig_1_2() {
|
||||
say "[1->2] fold reviews/ into journal/ and _agent/health/"
|
||||
for f in $(ls_files reviews/weekly); do
|
||||
[ "${f%.md}" != "$f" ] || continue
|
||||
dst="journal/weekly/$(printf '%s' "$f" | sed 's/-review\.md$/.md/')"
|
||||
do_or_show "move reviews/weekly/$f -> $dst" move "reviews/weekly/$f" "$dst"
|
||||
done
|
||||
for f in $(ls_files reviews/monthly); do
|
||||
[ "${f%.md}" != "$f" ] || continue
|
||||
case "$f" in
|
||||
*vault-health.md) dst="_agent/health/$f" ;;
|
||||
*) dst="journal/monthly/$f" ;;
|
||||
esac
|
||||
do_or_show "move reviews/monthly/$f -> $dst" move "reviews/monthly/$f" "$dst"
|
||||
done
|
||||
for period in quarterly annual; do
|
||||
for f in $(ls_files "reviews/$period"); do
|
||||
[ "${f%.md}" != "$f" ] || continue
|
||||
do_or_show "move reviews/$period/$f -> journal/$period/$f" move "reviews/$period/$f" "journal/$period/$f"
|
||||
done
|
||||
done
|
||||
say "[1->2] reminder: update inbound [[reviews/...]] wikilinks in ## Related sections (manual/agent step)."
|
||||
}
|
||||
|
||||
[ "$FROM" -lt 1 ] && mig_0_1
|
||||
[ "$FROM" -lt 2 ] && mig_1_2
|
||||
|
||||
# ---- Stamp the marker --------------------------------------------------------
|
||||
do_or_show "set _agent/goldbrain-vault.md schema_version -> $CURRENT_SCHEMA" \
|
||||
"$GB" fm _agent/goldbrain-vault.md schema_version "$CURRENT_SCHEMA"
|
||||
|
||||
if [ "$APPLY" = "1" ]; then
|
||||
say "migration complete -> schema $CURRENT_SCHEMA. Run vault-lint.sh to confirm invariants."
|
||||
else
|
||||
say "dry-run only. Re-run with --apply to perform the moves/deletes above."
|
||||
fi
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"$comment": "CANONICAL machine-readable routing manifest for the goldbrain vault. This is the single source of truth for 'what paths may be written to'. The human-readable tables in SKILL.md, references/routing-map.md, and references/api-reference.md are DERIVED views of this file — when they disagree, this file wins. vault-lint.sh consumes it to enforce the core rule: if a path matches no route here (and is not a retired path), nothing should be written to it. Patterns are Python regexes matched against vault-root-relative paths (no leading slash, no /vault/ prefix).",
|
||||
"schema_version": 2,
|
||||
"routes": [
|
||||
{ "id": "inbox-captures", "pattern": "^inbox/captures/inbox\\.md$", "method": "POST", "trigger": "Destination unknown at capture time", "distinct_because": "Only path whose contract is deferred routing" },
|
||||
{ "id": "inbox-imports", "pattern": "^inbox/imports/[^/]+\\.md$", "method": "PUT", "trigger": "Raw external material dropped wholesale", "distinct_because": "Bulk un-triaged material vs single-line captures" },
|
||||
{ "id": "inbox-processing-log", "pattern": "^inbox/processing-log/\\d{4}-\\d{2}-\\d{2}\\.md$", "method": "POST", "trigger": "An inbox item is routed to its real home", "distinct_because": "Audit trail of moves, not memory itself" },
|
||||
|
||||
{ "id": "journal-daily", "pattern": "^journal/daily/\\d{4}-\\d{2}-\\d{2}\\.md$", "method": "PATCH", "trigger": "First agent activity on a given day", "distinct_because": "Finest grain; PATCHed repeatedly within its period" },
|
||||
{ "id": "journal-weekly", "pattern": "^journal/weekly/\\d{4}-W\\d{2}\\.md$", "method": "PUT", "trigger": "First substantive session of a new ISO week (opt-in)", "distinct_because": "ISO-week grain" },
|
||||
{ "id": "journal-monthly", "pattern": "^journal/monthly/\\d{4}-\\d{2}\\.md$", "method": "PUT", "trigger": "First substantive session of a new month", "distinct_because": "Month grain" },
|
||||
{ "id": "journal-quarterly", "pattern": "^journal/quarterly/\\d{4}-Q[1-4]\\.md$", "method": "PUT", "trigger": "Manual / on request only", "distinct_because": "Strategic grain; never auto-fires" },
|
||||
{ "id": "journal-annual", "pattern": "^journal/annual/\\d{4}\\.md$", "method": "PUT", "trigger": "Manual / on request only", "distinct_because": "Coarsest grain; never auto-fires" },
|
||||
{ "id": "journal-templates", "pattern": "^journal/templates/.+\\.md$", "method": "PUT", "trigger": "Bootstrap seed only", "distinct_because": "Holds templates, not journal content" },
|
||||
|
||||
{ "id": "projects-active", "pattern": "^projects/active/[^/]+\\.md$", "method": "PUT", "trigger": "Work in motion now", "distinct_because": "Default state for anything being worked", "status": "active" },
|
||||
{ "id": "projects-incubating", "pattern": "^projects/incubating/[^/]+\\.md$", "method": "PUT", "trigger": "Idea captured, work not started", "distinct_because": "Pre-work", "status": "incubating" },
|
||||
{ "id": "projects-on-hold", "pattern": "^projects/on-hold/[^/]+\\.md$", "method": "PUT", "trigger": "Paused but still tracked", "distinct_because": "Resumable; not terminal", "status": "on-hold" },
|
||||
{ "id": "projects-archived", "pattern": "^projects/archived/[^/]+\\.md$", "method": "PUT", "trigger": "Done, abandoned, or rolled up", "distinct_because": "Terminal; kept for history", "status": "archived" },
|
||||
{ "id": "projects-template", "pattern": "^projects/project-template\\.md$", "method": "PUT", "trigger": "Bootstrap seed only", "distinct_because": "Template, not a project" },
|
||||
|
||||
{ "id": "areas", "pattern": "^areas/(business|personal|learning|systems)/[^/]+\\.md$", "method": "PUT", "trigger": "Ongoing domain with no finish line", "distinct_because": "No end state — disqualifies it from projects/" },
|
||||
|
||||
{ "id": "resources-people", "pattern": "^resources/people/[^/]+\\.md$", "method": "PUT", "trigger": "A fact about a specific person", "distinct_because": "Keyed to a person" },
|
||||
{ "id": "resources-companies", "pattern": "^resources/companies/[^/]+\\.md$", "method": "PUT", "trigger": "A fact about an organization", "distinct_because": "Keyed to an organization, not an individual" },
|
||||
{ "id": "resources-concepts", "pattern": "^resources/concepts/[^/]+\\.md$", "method": "PUT", "trigger": "A reusable concept/idea", "distinct_because": "An idea vs an external source" },
|
||||
{ "id": "resources-references", "pattern": "^resources/references/[^/]+\\.md$", "method": "PUT", "trigger": "An external source/link worth keeping", "distinct_because": "Points outward" },
|
||||
{ "id": "resources-meetings", "pattern": "^resources/meetings/\\d{4}-\\d{2}-\\d{2}-[^/]+\\.md$", "method": "PUT", "trigger": "Notes tied to a specific meeting", "distinct_because": "Event-anchored to a meeting" },
|
||||
{ "id": "resources-source", "pattern": "^resources/source-material/[^/]+\\.md$", "method": "PUT", "trigger": "Raw source material kept for reference (goldbrain-specific)", "distinct_because": "Unprocessed source kept verbatim, vs a curated reference/concept note" },
|
||||
|
||||
{ "id": "decisions-by-date", "pattern": "^decisions/by-date/\\d{4}-\\d{2}-\\d{2}-[^/]+\\.md$", "method": "PUT", "trigger": "A non-obvious decision worth recording", "distinct_because": "Chronological system of record for decisions" },
|
||||
{ "id": "decisions-template", "pattern": "^decisions/decision-template\\.md$", "method": "PUT", "trigger": "Bootstrap seed only", "distinct_because": "Template, not a decision" },
|
||||
|
||||
{ "id": "agent-marker", "pattern": "^_agent/goldbrain-vault\\.md$", "method": "PUT", "trigger": "Bootstrap / schema migration only", "distinct_because": "Plugin-owned probe; never hand-edited" },
|
||||
{ "id": "agent-context", "pattern": "^_agent/context/[^/]+\\.md$", "method": "PATCH", "trigger": "Active scope changes / task bundles", "distinct_because": "Single live scope pointer + bundles" },
|
||||
{ "id": "agent-semantic", "pattern": "^_agent/memory/semantic/[^/]+\\.md$", "method": "PUT", "trigger": "A durable fact/pattern (incl. operator-preferences.md)", "distinct_because": "Timeless fact" },
|
||||
{ "id": "agent-episodic", "pattern": "^_agent/memory/episodic/[^/]+\\.md$", "method": "PUT", "trigger": "A record of what happened, when", "distinct_because": "Anchored to an event in time" },
|
||||
{ "id": "agent-working", "pattern": "^_agent/memory/working/[^/]+\\.md$", "method": "PUT", "trigger": "Short-lived state for the current effort", "distinct_because": "Explicitly transient" },
|
||||
{ "id": "agent-sessions", "pattern": "^_agent/sessions/\\d{4}-\\d{2}-\\d{2}(-\\d{4})?-[^/]+\\.md$", "method": "PUT", "trigger": "A substantive session ends", "distinct_because": "Per-session record (new ones require HHMM)" },
|
||||
{ "id": "agent-health", "pattern": "^_agent/health/\\d{4}-\\d{2}-vault-health\\.md$", "method": "PUT", "trigger": "First substantive session of a month", "distinct_because": "Vault integrity, not work narrative" },
|
||||
{ "id": "agent-heartbeat", "pattern": "^_agent/heartbeat/[^/]+\\.md$", "method": "PUT", "trigger": "End of every session", "distinct_because": "O(1) orientation pointer; overwritten, never grows" },
|
||||
{ "id": "agent-templates", "pattern": "^_agent/templates/.+\\.md$", "method": "PUT", "trigger": "Bootstrap seed only", "distinct_because": "Holds templates, not memory" },
|
||||
{ "id": "agent-skills-active", "pattern": "^_agent/skills/active/[^/]+\\.md$", "method": "PUT", "trigger": "A skill/plugin catalogued as a capability", "distinct_because": "Catalogs a capability vs the build effort" },
|
||||
{ "id": "agent-skills-archived","pattern": "^_agent/skills/archived/[^/]+\\.md$", "method": "PUT", "trigger": "A catalogued skill is retired", "distinct_because": "Terminal state of the skill catalog" },
|
||||
{ "id": "agent-locks", "pattern": "^_agent/locks/[^/]+\\.lock$", "method": "PUT", "trigger": "Advisory multi-writer lock acquire/release", "distinct_because": "Concurrency coordination, not memory" },
|
||||
{ "id": "agent-outputs", "pattern": "^_agent/outputs/(briefs|drafts|summaries|synthesis)/[^/]+\\.md$", "method": "PUT", "trigger": "Agent-produced deliverable kept in the vault (goldbrain-specific)", "distinct_because": "A produced artifact, not memory about the operator or the world" },
|
||||
|
||||
{ "id": "leaf-readme", "pattern": "^(.+/)?README\\.md$", "method": "PUT", "trigger": "Bootstrap leaf signpost / vault root README", "distinct_because": "Human signpost, not read for routing" }
|
||||
],
|
||||
"retired": [
|
||||
{ "pattern": "^reviews/", "retired_in_schema": 2, "replacement": "journal/{weekly,monthly,quarterly,annual}/ and _agent/health/" },
|
||||
{ "pattern": "^decisions/by-project/", "retired_in_schema": 1, "replacement": "[[wikilink]] under the project's ## Key Decisions" },
|
||||
{ "pattern": "^archive/", "retired_in_schema": 0, "replacement": "projects/archived/ and _agent/skills/archived/" },
|
||||
{ "pattern": "^(CLAUDE|BOOTSTRAP|STRUCTURE|index)\\.md$", "retired_in_schema": 1, "replacement": "All control logic lives in the plugin references/, not the vault" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
#!/usr/bin/env bash
|
||||
# vault-lint.sh — mechanically assert goldbrain vault invariants.
|
||||
#
|
||||
# Catches the recurring "invariant violation" bugs that prose rules can't enforce:
|
||||
# folder<->status drift, duplicate slugs, wikilinks leaking into frontmatter,
|
||||
# duplicate "## Agent Log" headings, stale active projects, aging inbox captures,
|
||||
# impossible dates, bad status values, missing frontmatter, broken source_notes, and
|
||||
# paths that no route in routing.json permits. Invoked by the monthly Vault Health
|
||||
# pass (see SKILL.md), but safe to run any time — it is READ-ONLY.
|
||||
#
|
||||
# Exit status: 0 = clean, 1 = violations found, 2 = vault unreachable,
|
||||
# 3 = vault not bootstrapped (marker missing).
|
||||
#
|
||||
# Config (env overrides):
|
||||
# GB_BASE (default https://goldbrainapi.mpm.to)
|
||||
# GB_KEY (default the plugin's bearer token)
|
||||
# GB_TODAY (default the machine date) — pass the conversation's currentDate so
|
||||
# stale/aging math uses the SAME clock the agent writes with (YYYY-MM-DD)
|
||||
# STALE_DAYS (default 30) INBOX_DAYS (default 14)
|
||||
#
|
||||
# routing.json (canonical route manifest) is read from this script's own directory.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
GB_BASE="${GB_BASE:-https://goldbrainapi.mpm.to}"
|
||||
GB_KEY="${GB_KEY:-fb72065a05fabb28ae87c45880cc3b7aba4fd3f58e70297934145cef974e8ed8}"
|
||||
STALE_DAYS="${STALE_DAYS:-30}"
|
||||
INBOX_DAYS="${INBOX_DAYS:-14}"
|
||||
SCOPE_STALE_SESSIONS="${SCOPE_STALE_SESSIONS:-3}"
|
||||
GB_TODAY="${GB_TODAY:-$(date +%Y-%m-%d)}"
|
||||
|
||||
GB_BASE="$GB_BASE" GB_KEY="$GB_KEY" STALE_DAYS="$STALE_DAYS" INBOX_DAYS="$INBOX_DAYS" \
|
||||
SCOPE_STALE_SESSIONS="$SCOPE_STALE_SESSIONS" \
|
||||
GB_TODAY="$GB_TODAY" ROUTING_JSON="$SCRIPT_DIR/routing.json" \
|
||||
python3 - <<'PY'
|
||||
import os, sys, json, re, datetime, urllib.request, urllib.error
|
||||
|
||||
BASE = os.environ["GB_BASE"].rstrip("/")
|
||||
KEY = os.environ["GB_KEY"]
|
||||
STALE_DAYS = int(os.environ["STALE_DAYS"])
|
||||
INBOX_DAYS = int(os.environ["INBOX_DAYS"])
|
||||
SCOPE_STALE_SESSIONS = int(os.environ["SCOPE_STALE_SESSIONS"])
|
||||
TODAY = datetime.date.fromisoformat(os.environ["GB_TODAY"])
|
||||
ROUTING_JSON = os.environ["ROUTING_JSON"]
|
||||
LIFECYCLES = ["active", "incubating", "on-hold", "archived"]
|
||||
SKIP = {"README.md", "project-template.md", "decision-template.md"}
|
||||
REQUIRED_FM = ("type", "created")
|
||||
# Project status vocabulary IS enforced (status must equal the lifecycle folder) by the
|
||||
# folder/status check below. Other note kinds (decisions/concepts) carry free-form status
|
||||
# vocab (accepted, shipped, reference, ...), so there is no global status allow-list.
|
||||
|
||||
# optional real YAML parser; fall back to a tolerant line parser
|
||||
try:
|
||||
import yaml # type: ignore
|
||||
HAVE_YAML = True
|
||||
except Exception:
|
||||
HAVE_YAML = False
|
||||
|
||||
violations = []
|
||||
def flag(check, msg): violations.append((check, msg))
|
||||
|
||||
def get(path):
|
||||
"""GET /vault/<path>. Returns text, or None on 404. Raises on hard failure."""
|
||||
req = urllib.request.Request(f"{BASE}/vault/{path}",
|
||||
headers={"Authorization": f"Bearer {KEY}"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=20) as r:
|
||||
return r.read().decode("utf-8", "replace")
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 404:
|
||||
return None
|
||||
raise
|
||||
|
||||
def list_dir(path):
|
||||
"""Return (files, folders) for a vault directory. Directories may arrive either in a
|
||||
'folders' key OR as 'files' entries ending in '/'; handle both. Root is '' -> /vault/.
|
||||
Tolerates non-404 errors (e.g. a 400 on an odd path) by returning empty."""
|
||||
p = "" if path in ("", "/") else (path if path.endswith("/") else path + "/")
|
||||
try:
|
||||
body = get(p)
|
||||
except urllib.error.HTTPError:
|
||||
return [], []
|
||||
if body is None:
|
||||
return [], []
|
||||
try:
|
||||
j = json.loads(body)
|
||||
except json.JSONDecodeError:
|
||||
return [], []
|
||||
entries = list(j.get("files", [])) + list(j.get("folders", []))
|
||||
files = [e for e in entries if not e.endswith("/")]
|
||||
folders = [e[:-1] for e in entries if e.endswith("/")]
|
||||
return files, folders
|
||||
|
||||
def walk(prefix=""):
|
||||
"""Yield every file path under prefix (recursive). prefix is '' or ends with '/'."""
|
||||
files, folders = list_dir(prefix)
|
||||
for f in files:
|
||||
yield prefix + f
|
||||
for d in folders:
|
||||
yield from walk(f"{prefix}{d}/")
|
||||
|
||||
def split_frontmatter(text):
|
||||
"""Return (raw_yaml_str, body) splitting on anchored ^---$ delimiters. ('', text) if none."""
|
||||
if not text:
|
||||
return "", ""
|
||||
lines = text.splitlines()
|
||||
if not lines or lines[0].strip() != "---":
|
||||
return "", text
|
||||
for i in range(1, len(lines)):
|
||||
if lines[i].strip() == "---":
|
||||
return "\n".join(lines[1:i]), "\n".join(lines[i+1:])
|
||||
return "", text # unterminated block -> treat as no frontmatter
|
||||
|
||||
def parse_fm(text):
|
||||
"""Return (raw_yaml_str, dict). Uses PyYAML when available, else a tolerant parser."""
|
||||
raw, _ = split_frontmatter(text)
|
||||
if not raw:
|
||||
return "", {}
|
||||
if HAVE_YAML:
|
||||
try:
|
||||
d = yaml.safe_load(raw)
|
||||
return raw, (d if isinstance(d, dict) else {})
|
||||
except Exception:
|
||||
pass
|
||||
# fallback: scalar + simple inline-list lines (keys may contain digits, _, -)
|
||||
fields = {}
|
||||
for line in raw.splitlines():
|
||||
m = re.match(r"^([A-Za-z_][\w-]*):\s*(.*)$", line)
|
||||
if m:
|
||||
v = m.group(2).strip()
|
||||
if v.startswith("[") and v.endswith("]"):
|
||||
v = [x.strip().strip('"').strip("'") for x in v[1:-1].split(",") if x.strip()]
|
||||
else:
|
||||
v = v.strip('"').strip("'")
|
||||
fields[m.group(1)] = v
|
||||
return raw, fields
|
||||
|
||||
def parse_date(s):
|
||||
m = re.match(r"(\d{4}-\d{2}-\d{2})", str(s or ""))
|
||||
if not m:
|
||||
return None
|
||||
try:
|
||||
return datetime.date.fromisoformat(m.group(1))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def as_list(v):
|
||||
if v is None or v == "":
|
||||
return []
|
||||
return v if isinstance(v, list) else [v]
|
||||
|
||||
# ---- Reachability + bootstrap probe (M2: do NOT silently report clean) -------
|
||||
try:
|
||||
if get("_agent/goldbrain-vault.md") is None:
|
||||
print("vault-lint: marker missing — vault not bootstrapped (run bootstrap.sh).", file=sys.stderr)
|
||||
sys.exit(3)
|
||||
except Exception as e:
|
||||
print(f"vault-lint: vault unreachable ({e}).", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
# ---- Load canonical routing manifest (S3) ------------------------------------
|
||||
ROUTES, RETIRED = [], []
|
||||
try:
|
||||
with open(ROUTING_JSON) as fh:
|
||||
rj = json.load(fh)
|
||||
ROUTES = [(r["id"], re.compile(r["pattern"])) for r in rj.get("routes", [])]
|
||||
RETIRED = [(re.compile(r["pattern"]), r.get("replacement", "")) for r in rj.get("retired", [])]
|
||||
except Exception as e:
|
||||
flag("routing-manifest", f"could not load routing.json ({e}) — path checks skipped")
|
||||
|
||||
# ---- Single full walk feeds every path-level check ---------------------------
|
||||
all_files = list(walk())
|
||||
|
||||
def route_for(path):
|
||||
for rid, rx in ROUTES:
|
||||
if rx.match(path):
|
||||
return rid
|
||||
return None
|
||||
|
||||
# Path membership + retired-path detection (S3)
|
||||
for path in all_files:
|
||||
if ROUTES and route_for(path) is None:
|
||||
hit = next((repl for rx, repl in RETIRED if rx.match(path)), None)
|
||||
if hit is not None:
|
||||
flag("retired-path", f"{path}: retired location — should be {hit}")
|
||||
else:
|
||||
flag("unknown-path", f"{path}: matches no route in routing.json")
|
||||
|
||||
# ---- Per-note frontmatter checks (M5) ----------------------------------------
|
||||
TEMPLATE_RE = re.compile(r"(^|/)(templates/|.*-template\.md$)")
|
||||
for path in all_files:
|
||||
base = path.rsplit("/", 1)[-1]
|
||||
if base in SKIP or TEMPLATE_RE.search(path) or not path.endswith(".md"):
|
||||
continue
|
||||
text = get(path)
|
||||
if text is None:
|
||||
continue
|
||||
raw, fm = parse_fm(text)
|
||||
|
||||
# wikilinks anywhere in frontmatter (widened sweep — all folders)
|
||||
if "[[" in raw:
|
||||
flag("frontmatter-wikilink", f"{path}: '[[...]]' inside frontmatter")
|
||||
|
||||
# missing required frontmatter
|
||||
missing = [k for k in REQUIRED_FM if not str(fm.get(k, "")).strip()]
|
||||
if fm and missing:
|
||||
flag("missing-frontmatter", f"{path}: missing {', '.join(missing)}")
|
||||
|
||||
# impossible dates: updated < created
|
||||
c, u = parse_date(fm.get("created")), parse_date(fm.get("updated"))
|
||||
if c and u and u < c:
|
||||
flag("date-order", f"{path}: updated {u} is before created {c}")
|
||||
if u and u > TODAY:
|
||||
flag("future-date", f"{path}: updated {u} is in the future (today {TODAY})")
|
||||
|
||||
# source_notes hygiene: plain relative paths, never wikilinks, no self-reference
|
||||
for sn in as_list(fm.get("source_notes")):
|
||||
s = str(sn)
|
||||
if "[[" in s:
|
||||
flag("source-notes-wikilink", f"{path}: source_notes contains a wikilink '{s}'")
|
||||
|
||||
# ---- Projects: folder<->status, stale active, duplicate slugs ----------------
|
||||
slug_homes = {}
|
||||
for lc in LIFECYCLES:
|
||||
files, _ = list_dir(f"projects/{lc}")
|
||||
for fn in files:
|
||||
if fn.endswith("/") or fn in SKIP or not fn.endswith(".md"):
|
||||
continue
|
||||
slug = fn[:-3]
|
||||
slug_homes.setdefault(slug, []).append(lc)
|
||||
text = get(f"projects/{lc}/{fn}")
|
||||
if text is None:
|
||||
continue
|
||||
_, fm = parse_fm(text)
|
||||
status = str(fm.get("status", "")).strip().strip('"').strip("'")
|
||||
if status and status != lc:
|
||||
flag("folder/status", f"projects/{lc}/{fn}: status='{status}' but folder='{lc}'")
|
||||
if lc == "active":
|
||||
d = parse_date(fm.get("updated"))
|
||||
if d and (TODAY - d).days > STALE_DAYS:
|
||||
flag("stale-active", f"projects/active/{fn}: updated {d} ({(TODAY-d).days}d ago) — consider on-hold/")
|
||||
for slug, homes in slug_homes.items():
|
||||
if len(homes) > 1:
|
||||
flag("duplicate-slug", f"'{slug}' exists in {', '.join(homes)}")
|
||||
|
||||
# ---- Daily notes: duplicate "## Agent Log" headings --------------------------
|
||||
for path in all_files:
|
||||
if not re.match(r"^journal/daily/.*\.md$", path):
|
||||
continue
|
||||
text = get(path) or ""
|
||||
n = len(re.findall(r"(?m)^## Agent Log\s*$", text))
|
||||
if n > 1:
|
||||
flag("duplicate-agent-log", f"{path}: {n} '## Agent Log' headings")
|
||||
|
||||
# ---- Inbox: captures aging past INBOX_DAYS -----------------------------------
|
||||
inbox = get("inbox/captures/inbox.md") or ""
|
||||
for line in inbox.splitlines():
|
||||
m = re.match(r"^\s*-\s*(\d{4}-\d{2}-\d{2})\b", line)
|
||||
if m:
|
||||
d = parse_date(m.group(1))
|
||||
if d and (TODAY - d).days > INBOX_DAYS:
|
||||
flag("aging-inbox", f"inbox capture {d} ({(TODAY-d).days}d): {line.strip()[:80]}")
|
||||
|
||||
# ---- Scope freshness (drift detector) ----------------------------------------
|
||||
# Scope is the most churn-prone state (Bryan runs several sessions/day across topics).
|
||||
# It has no natural staleness signal, so drift is otherwise invisible. Rule: if N+ session
|
||||
# logs are dated AFTER current-context's scope_updated, the recorded scope may no longer
|
||||
# reflect current work — surface it for a human glance (advisory, like every health finding).
|
||||
cc = get("_agent/context/current-context.md")
|
||||
if cc is not None:
|
||||
_, ccfm = parse_fm(cc)
|
||||
su = parse_date(ccfm.get("scope_updated"))
|
||||
if su is None:
|
||||
flag("scope-no-timestamp",
|
||||
"_agent/context/current-context.md: no scope_updated frontmatter — scope drift cannot be detected; add it (bootstrap.sh repair) and switch scope via `goldbrain.sh scope set`")
|
||||
else:
|
||||
since = [p for p in all_files
|
||||
if (m := re.match(r"^_agent/sessions/(\d{4}-\d{2}-\d{2})", p))
|
||||
and (d := parse_date(m.group(1))) and d > su]
|
||||
if len(since) >= SCOPE_STALE_SESSIONS:
|
||||
flag("scope-stale",
|
||||
f"scope set {su}; {len(since)} session(s) logged since without a switch — confirm it still reflects current work (or run `goldbrain.sh scope set`)")
|
||||
|
||||
# ---- Report ------------------------------------------------------------------
|
||||
if not violations:
|
||||
print("vault-lint: clean — all invariants hold.")
|
||||
sys.exit(0)
|
||||
|
||||
print(f"vault-lint: {len(violations)} violation(s) found\n")
|
||||
by = {}
|
||||
for check, msg in violations:
|
||||
by.setdefault(check, []).append(msg)
|
||||
labels = {
|
||||
"folder/status": "Folder <-> status mismatch",
|
||||
"duplicate-slug": "Duplicate slug across lifecycle folders",
|
||||
"frontmatter-wikilink": "Wikilink in frontmatter (breaks reading view)",
|
||||
"duplicate-agent-log": "Duplicate '## Agent Log' heading",
|
||||
"stale-active": f"Stale active project (updated > {STALE_DAYS}d)",
|
||||
"aging-inbox": f"Inbox capture aging (> {INBOX_DAYS}d)",
|
||||
"unknown-path": "Path matches no route in routing.json",
|
||||
"retired-path": "Write to a retired/dead path",
|
||||
"missing-frontmatter": "Missing required frontmatter field",
|
||||
"date-order": "updated earlier than created",
|
||||
"future-date": "updated date is in the future",
|
||||
"source-notes-wikilink": "Wikilink in source_notes (must be plain paths)",
|
||||
"routing-manifest": "routing.json problem",
|
||||
"scope-no-timestamp": "current-context has no scope_updated (drift undetectable)",
|
||||
"scope-stale": f"Scope may have drifted (>= {SCOPE_STALE_SESSIONS} sessions since last switch)",
|
||||
}
|
||||
for check, msgs in by.items():
|
||||
print(f"## {labels.get(check, check)}")
|
||||
for m in msgs:
|
||||
print(f" - {m}")
|
||||
print()
|
||||
sys.exit(1)
|
||||
PY
|
||||
Reference in New Issue
Block a user