forked from jason/echo
Phase 3: per-member namespacing — schema 5 (the core group conversion)
One shared vault, per-member namespaces for all churn-prone state:
- _agent/members/<member>/{preferences,current-context,heartbeat,inbox}.md
+ _agent/sessions/<member>/ session logs; shared group-profile.md
(with auto-rostered ## Members) and inbox/captures/group.md.
- Self-onboarding: load detects a bootstrapped vault lacking THIS
member's namespace and seeds it (bootstrap.member_bootstrap()).
- load = 8 reads (marker, group profile, my prefs/context/heartbeat,
shared daily, my inbox, group inbox); heartbeat fallback lists my
sessions dir; scope show/set is per member.
- Member-tagged shared writes: daily Agent Log lines and entity-update
dated blocks are '- YYYY-MM-DD [member]: …'; capture --inbox targets
my inbox; triage lists both inboxes and writes attributed audit lines.
- routing.json: member-anchors / inbox-group / member-segmented session
routes; single-user anchor paths retired (schema 5). vault_lint runs
aging-inbox + scope-drift per member. migrate.py 4->5 for alpha vaults.
- Scaffold: member-* seeds ({{MEMBER}}-stamped), group-profile/group-inbox
seeds, marker schema 5. Docs (SKILL, vault-layout, routing-map,
bootstrap) synced; manifest -> 2.0.0-alpha.3; rebuilt plugin (81 files).
Verified: all suites green (25/25 unit, scaffold, routing-sync 36 routes,
4 mock e2e, run_eval unchanged) + 19-check two-member smoke: bootstrap ->
self-onboard -> interleaved tagged daily log -> cross-member update
attribution -> independent scopes -> mine/group triage -> lint clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,52 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2.0.0-alpha.3 — Phase 3 (per-member namespacing — schema 5)
|
||||||
|
|
||||||
|
The core of the group conversion: one shared vault, per-member namespaces for all
|
||||||
|
churn-prone state. A CHORUS vault now bootstraps multi-member from empty.
|
||||||
|
|
||||||
|
### Added — vault layout (schema 5)
|
||||||
|
- **`_agent/members/<member>/`** — each member's `preferences.md`,
|
||||||
|
`current-context.md` (scope + history), `heartbeat.md` (last-session pointer,
|
||||||
|
written at session end), and `inbox.md`. Members never contend on each other's
|
||||||
|
churn-prone files.
|
||||||
|
- **`_agent/sessions/<member>/YYYY-MM-DD-HHMM-<slug>.md`** — member-namespaced
|
||||||
|
session logs (no same-minute collisions, filterable per member).
|
||||||
|
- **`_agent/memory/semantic/group-profile.md`** — shared group facts, conventions,
|
||||||
|
and a `## Members` roster (members are rostered automatically at onboarding).
|
||||||
|
- **`inbox/captures/group.md`** — shared inbox for unowned member-tagged items;
|
||||||
|
whoever triages first routes them.
|
||||||
|
|
||||||
|
### Added — behavior
|
||||||
|
- **Self-onboarding**: `chorus.py load` detects a bootstrapped vault that lacks THIS
|
||||||
|
member's namespace and stands it up automatically (anchors + sessions dir +
|
||||||
|
roster line). New members install their baked plugin and just start.
|
||||||
|
- `load` is now 8 orientation reads: marker → group profile → my preferences /
|
||||||
|
context / heartbeat → today's shared daily note → my inbox → group inbox; the
|
||||||
|
heartbeat fallback lists MY sessions dir.
|
||||||
|
- `scope show/set` operates on the member's own current-context; drift counts only
|
||||||
|
that member's sessions.
|
||||||
|
- **Member-tagged shared writes**: daily `## Agent Log` lines and entity-update
|
||||||
|
dated blocks are written as `- YYYY-MM-DD [member]: …`; `capture --inbox` lands
|
||||||
|
in the member's own inbox; triage lists BOTH inboxes (tagged mine/group) and
|
||||||
|
writes member-attributed audit lines.
|
||||||
|
- `bootstrap.py` seeds group anchors + the configured member's namespace
|
||||||
|
(`member_bootstrap()` is reusable); the marker is schema 5; `migrate.py` gains a
|
||||||
|
4→5 step for alpha vaults (moves pre-5 anchors into the configured member's
|
||||||
|
namespace; no ECHO adoption path).
|
||||||
|
- `routing.json`: new `member-anchors`, `inbox-group`, member-segmented sessions
|
||||||
|
routes; the old single-user anchor paths (shared inbox, operator-preferences,
|
||||||
|
current-context, heartbeat, flat sessions) are **retired**. `vault_lint` runs
|
||||||
|
aging-inbox and scope-drift checks per member across all members found in the
|
||||||
|
vault. Docs (SKILL, vault-layout, routing-map, bootstrap) updated to match.
|
||||||
|
|
||||||
|
### Verified
|
||||||
|
- All suites green (25/25 unit, scaffold, routing-sync ×36 routes, 4 mock e2e,
|
||||||
|
run_eval metrics unchanged) plus a 19-check two-member end-to-end smoke:
|
||||||
|
jason bootstraps → alice self-onboards via load → interleaved member-tagged
|
||||||
|
daily log → cross-member entity update attributed → independent scopes →
|
||||||
|
triage separates personal vs group items → lint clean.
|
||||||
|
|
||||||
## 2.0.0-alpha.2 — Phase 2 (identity plumbing)
|
## 2.0.0-alpha.2 — Phase 2 (identity plumbing)
|
||||||
|
|
||||||
Every write is now attributed to a member. Vault layout unchanged (that's Phase 3).
|
Every write is now attributed to a member. Vault layout unchanged (that's Phase 3).
|
||||||
|
|||||||
+1
-1
@@ -197,7 +197,7 @@ does NOT backfill — attribution can't be guessed), member-aware lock owner
|
|||||||
Vault layout untouched. **Deliverable:** every new write is attributed;
|
Vault layout untouched. **Deliverable:** every new write is attributed;
|
||||||
single-user behavior otherwise identical.
|
single-user behavior otherwise identical.
|
||||||
|
|
||||||
### Phase 3 — Per-member namespacing (3–5 days — the core)
|
### Phase 3 — Per-member namespacing (3–5 days — the core) — DONE
|
||||||
Schema 5. New `LEAVES` + seeds (`members/<id>/…`, `group-profile.md`),
|
Schema 5. New `LEAVES` + seeds (`members/<id>/…`, `group-profile.md`),
|
||||||
member-parameterized bootstrap (bootstraps *your* member subtree on first load
|
member-parameterized bootstrap (bootstraps *your* member subtree on first load
|
||||||
if the vault exists but your namespace doesn't — new members self-onboard),
|
if the vault exists but your namespace doesn't — new members self-onboard),
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# chorus-memory — v2.0.0-alpha.2
|
# chorus-memory — v2.0.0-alpha.3
|
||||||
|
|
||||||
> **CHORUS is a group-based fork of [ECHO](https://git.alwisp.com/jason/echo)** — one shared
|
> **CHORUS is a group-based fork of [ECHO](https://git.alwisp.com/jason/echo)** — one shared
|
||||||
> Obsidian vault serving a group of members instead of a single operator. The conversion
|
> Obsidian vault serving a group of members instead of a single operator. The conversion
|
||||||
> plan and settled design decisions live in [CHORUS-PLAN.md](CHORUS-PLAN.md). The pre-fork
|
> plan and settled design decisions live in [CHORUS-PLAN.md](CHORUS-PLAN.md). The pre-fork
|
||||||
> ECHO v1.5.1 state is preserved at the `echo-fork-point` tag. **Phase 1 (mechanical
|
> ECHO v1.5.1 state is preserved at the `echo-fork-point` tag. **Phases 1–3 (through per-member
|
||||||
> rename) is done**; the body of this README below the fold still describes the
|
> namespacing) are done**; the body of this README below the fold still describes the
|
||||||
> inherited single-operator behavior and is rewritten in Phase 5.
|
> inherited single-operator behavior and is rewritten in Phase 5.
|
||||||
> Deployment endpoint: `https://chorusapi.mpm.to`. CHORUS is a **clean break** from
|
> Deployment endpoint: `https://chorusapi.mpm.to`. CHORUS is a **clean break** from
|
||||||
> ECHO — fresh vaults on fresh machines; it carries no ECHO back-compat and never
|
> ECHO — fresh vaults on fresh machines; it carries no ECHO back-compat and never
|
||||||
|
|||||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "chorus-memory",
|
"name": "chorus-memory",
|
||||||
"version": "2.0.0-alpha.2",
|
"version": "2.0.0-alpha.3",
|
||||||
"description": "Group-based persistent memory via the shared CHORUS Obsidian vault over the Obsidian Local REST API. Cross-platform Python client: one-call capture/resolve/recall/link/triage over an entity index, hybrid BM25 + graph recall spanning entities + sessions/journal (recency/status-aware), a pre-write duplicate gate, complete-frontmatter capture, session hooks that self-fire load/reflect, offline write-ahead queue, lock-guarded concurrency, linter-enforced routing, and /chorus-* commands.",
|
"description": "Group-based persistent memory via the shared CHORUS Obsidian vault over the Obsidian Local REST API. Cross-platform Python client: one-call capture/resolve/recall/link/triage over an entity index, hybrid BM25 + graph recall spanning entities + sessions/journal (recency/status-aware), a pre-write duplicate gate, complete-frontmatter capture, session hooks that self-fire load/reflect, offline write-ahead queue, lock-guarded concurrency, linter-enforced routing, and /chorus-* commands.",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Jason Stedwell"
|
"name": "Jason Stedwell"
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ All paths below are under `${CLAUDE_PLUGIN_ROOT}/skills/chorus-memory/`. **Invok
|
|||||||
```bash
|
```bash
|
||||||
CHORUS="${CLAUDE_PLUGIN_ROOT}/skills/chorus-memory/scripts/chorus.py" # call as: python3 "$CHORUS" <cmd> ...
|
CHORUS="${CLAUDE_PLUGIN_ROOT}/skills/chorus-memory/scripts/chorus.py" # call as: python3 "$CHORUS" <cmd> ...
|
||||||
[ -f "$CHORUS" ] || CHORUS=$(ls /sessions/*/mnt/.remote-plugins/*/skills/chorus-memory/scripts/chorus.py 2>/dev/null | head -1) # CoWork sandbox fallback
|
[ -f "$CHORUS" ] || CHORUS=$(ls /sessions/*/mnt/.remote-plugins/*/skills/chorus-memory/scripts/chorus.py 2>/dev/null | head -1) # CoWork sandbox fallback
|
||||||
python3 "$CHORUS" load # cold-start: the 6 orientation reads in one call
|
python3 "$CHORUS" load # cold-start: the 8 orientation reads in one call
|
||||||
python3 "$CHORUS" get <path> # 404 -> exit 44
|
python3 "$CHORUS" get <path> # 404 -> exit 44
|
||||||
python3 "$CHORUS" ls <dir> ; python3 "$CHORUS" map <path> # listing / document-map
|
python3 "$CHORUS" ls <dir> ; python3 "$CHORUS" map <path> # listing / document-map
|
||||||
python3 "$CHORUS" search <terms...>
|
python3 "$CHORUS" search <terms...>
|
||||||
@@ -101,7 +101,7 @@ The index is maintained automatically by `capture`; `sweep.py` rebuilds it and b
|
|||||||
|
|
||||||
### Concurrency — the vault is shared, so coordinate writes
|
### Concurrency — the vault is shared, so coordinate writes
|
||||||
|
|
||||||
CHORUS 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:
|
CHORUS is read/written by **every member's** clients (Claude Code **and** CoWork sessions). The schema-5 member namespaces remove most contention by design — your scope/heartbeat/preferences/inbox are yours alone. What remains genuinely shared-write: the daily note's `## Agent Log` (member-tagged idempotent appends make interleaving safe), the group inbox, shared entity notes, and the machine indexes. Before a burst of writes that may overlap another member's session, take the **advisory lock**, and release it at session end:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 "$CHORUS" lock "cc-<session-id>" # exit 75 if another session holds a fresh lock, or you lost the race
|
python3 "$CHORUS" lock "cc-<session-id>" # exit 75 if another session holds a fresh lock, or you lost the race
|
||||||
@@ -141,22 +141,24 @@ Load at the start of any substantive conversation — anything beyond a single q
|
|||||||
|
|
||||||
### Loading procedure
|
### Loading procedure
|
||||||
|
|
||||||
**Run `python3 "$CHORUS" load`** — one call that performs the six orientation reads below and prints each labeled section (404s on today's note / inbox show as absent, not errors; an absent marker is flagged). This replaces issuing six separate GETs by hand. The table documents what `load` reads and why:
|
**Run `python3 "$CHORUS" load`** — one call that performs the eight orientation reads below and prints each labeled section (404s on today's note / inboxes show as absent, not errors; an absent marker is flagged). If the vault is set up but YOUR member namespace isn't, `load` **self-onboards** it (seeds your `_agent/members/<member>/` anchors and sessions folder, and adds you to the group-profile roster). The table documents what `load` reads and why:
|
||||||
|
|
||||||
| # | GET | Notes |
|
| # | GET | Notes |
|
||||||
|---|-----|-------|
|
|---|-----|-------|
|
||||||
| 1 | `/vault/_agent/chorus-vault.md` | The bootstrap marker. 404 → vault not set up; follow `references/bootstrap.md`. 200 → check its `schema_version` and migrate if older. |
|
| 1 | `/vault/_agent/chorus-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` | the operator's profile |
|
| 2 | `/vault/_agent/memory/semantic/group-profile.md` | Shared facts about the group — what the team is, member roster, conventions |
|
||||||
| 3 | `/vault/_agent/context/current-context.md` | Active scope + Scope History |
|
| 3 | `/vault/_agent/members/<member>/preferences.md` | THIS member's profile/preferences |
|
||||||
| 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. |
|
| 4 | `/vault/_agent/members/<member>/current-context.md` | THIS member's active scope + Scope History |
|
||||||
| 5 | `/vault/journal/daily/YYYY-MM-DD.md` | Today's note; 404 is fine — it's created on first agent activity |
|
| 5 | `/vault/_agent/members/<member>/heartbeat.md` → then `/vault/_agent/sessions/<member>/` | **Read the heartbeat first** — a one-line pointer (`<session-log-path> @ <ISO-timestamp>`) written at the end of the member's previous session. It's an O(1) jump to their latest log. Fall back to the member's `sessions/` listing only if the pointer is missing or 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. |
|
||||||
| 6 | `/vault/inbox/captures/inbox.md` | Inbox depth probe — feeds the load-time reconcile below. 404 is fine (empty inbox). |
|
| 6 | `/vault/journal/daily/YYYY-MM-DD.md` | Today's SHARED note (the whole group's timeline); 404 is fine — created on first agent activity |
|
||||||
|
| 7 | `/vault/_agent/members/<member>/inbox.md` | MY inbox depth probe — feeds the load-time reconcile below. 404 is fine (empty inbox). |
|
||||||
|
| 8 | `/vault/inbox/captures/group.md` | GROUP inbox depth probe — unowned items any member may triage. 404 is fine. |
|
||||||
|
|
||||||
Do not read every session log — older sessions are reachable via `POST /search/simple/?query=...` when needed.
|
Do not read every session log — older sessions are reachable via `POST /search/simple/?query=...` when needed.
|
||||||
|
|
||||||
**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:
|
**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.
|
1. **Inbox depth (Inbox Triage).** If your inbox (GET #7) or the group inbox (GET #8) 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 — the operator 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 — `python3 "$CHORUS" 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 `python3 "$CHORUS" 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 the operator rather than trusting it.
|
2. **Scope drift (state it, don't just check it).** Scope is the most churn-prone state — the operator 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 — `python3 "$CHORUS" 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 `python3 "$CHORUS" 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 the operator rather than trusting it.
|
||||||
|
|
||||||
Keep the reconcile to a single short line to the operator (e.g. "3 inbox captures from last week are still un-routed — triage now?"); don't let it crowd out the actual request.
|
Keep the reconcile to a single short line to the operator (e.g. "3 inbox captures from last week are still un-routed — triage now?"); don't let it crowd out the actual request.
|
||||||
@@ -173,9 +175,9 @@ Do NOT narrate this loading. Reading memory is expected behavior.
|
|||||||
|
|
||||||
## Inbox Triage (one-tap: `chorus.py triage`)
|
## Inbox Triage (one-tap: `chorus.py triage`)
|
||||||
|
|
||||||
`inbox/captures/inbox.md` is the catch-all for "save this somewhere — figure out where later." Without triage it grows forever and durable facts get stranded there.
|
There are TWO inboxes: `_agent/members/<member>/inbox.md` (YOUR catch-all for "save this somewhere — figure out where later") and `inbox/captures/group.md` (unowned group-wide items, as member-tagged lines `- YYYY-MM-DD [<member>]: …` — whoever triages first routes them). `triage --list` covers both. Without triage they grow forever and durable facts get stranded.
|
||||||
|
|
||||||
At the start of a substantive session, check the inbox. If it holds lines older than ~7 days that haven't been routed elsewhere, surface them once:
|
At the start of a substantive session, check both inboxes (`load` probes them). If either holds lines older than ~7 days that haven't been routed elsewhere, surface them once:
|
||||||
|
|
||||||
> "Three captures from last week are still in the inbox — want to route them now or leave them?"
|
> "Three captures from last week are still in the inbox — want to route them now or leave them?"
|
||||||
|
|
||||||
@@ -189,7 +191,7 @@ python3 "$CHORUS" triage proposals.json # dry-run: classify + previe
|
|||||||
python3 "$CHORUS" triage proposals.json --apply # route via capture + log each move
|
python3 "$CHORUS" triage proposals.json --apply # route via capture + log each move
|
||||||
```
|
```
|
||||||
|
|
||||||
Routing targets are the usual homes (preference/pattern → `operator-preferences.md::Observations` via a `semantic` proposal or a direct PATCH; project idea → `project` with `--status incubating`; durable fact → `semantic`; person fact → `person`). `--apply` records each move in `inbox/processing-log/YYYY-MM-DD.md` (`- <original line> → <destination path>`) automatically. Originals are **never deleted** unless the operator explicitly asks — the processing log is the audit trail.
|
Routing targets are the usual homes (a preference/pattern about YOU → your own `_agent/members/<member>/preferences.md` under `Preferences::Observations` via a direct PATCH; project idea → `project` with `--status incubating`; durable group fact → `semantic`; person fact → `person`). Never route another member's preference into your own preferences file — put it in theirs only if their own session confirms it, else `resources/people/`. `--apply` records each move in `inbox/processing-log/YYYY-MM-DD.md` (`- <original line> → <destination path>`) automatically. Originals are **never deleted** unless the operator explicitly asks — the processing log is the audit trail.
|
||||||
|
|
||||||
## Project Lifecycle
|
## Project Lifecycle
|
||||||
|
|
||||||
@@ -239,7 +241,7 @@ Only after the search comes back empty (or you've decided to merge) is it safe t
|
|||||||
|
|
||||||
### Before you append — read first (idempotency)
|
### Before you append — read first (idempotency)
|
||||||
|
|
||||||
`chorus.py append` is **whole-line idempotent**: it GETs the file and skips the POST when the exact line already exists, so a retry, replay, or overlapping session can't grow duplicate lines. Use it for single-line additions to `inbox/captures/inbox.md`, a daily note's `## Agent Log`, or a `## Fact / Pattern` / `## Observations` / `## Log` heading. (A raw POST is **not** idempotent — if you must use curl, GET first and whole-line-match the entry yourself; see `references/api-reference.md`.) This does **not** apply to PUT or PATCH `replace`, which overwrite by design.
|
`chorus.py append` is **whole-line idempotent**: it GETs the file and skips the POST when the exact line already exists, so a retry, replay, or overlapping session can't grow duplicate lines. Use it for single-line additions to `_agent/members/<member>/inbox.md`, a daily note's `## Agent Log`, or a `## Fact / Pattern` / `## Observations` / `## Log` heading. (A raw POST is **not** idempotent — if you must use curl, GET first and whole-line-match the entry yourself; see `references/api-reference.md`.) This does **not** apply to PUT or PATCH `replace`, which overwrite by design.
|
||||||
|
|
||||||
### Append, patch, put — via the client
|
### Append, patch, put — via the client
|
||||||
|
|
||||||
@@ -247,14 +249,14 @@ Write multi-line bodies to a file first (use the Write tool — cross-platform,
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# additive line (idempotent). Anchor the date on the conversation's currentDate.
|
# additive line (idempotent). Anchor the date on the conversation's currentDate.
|
||||||
python3 "$CHORUS" append inbox/captures/inbox.md "- <currentDate>: <entry>"
|
python3 "$CHORUS" append _agent/members/<member>/inbox.md "- <currentDate>: <entry>"
|
||||||
|
|
||||||
# targeted heading append/replace. The heading Target is the FULL `::`-delimited path
|
# targeted heading append/replace. The heading Target is the FULL `::`-delimited path
|
||||||
# from the H1 — a bare subheading returns 400 invalid-target and the write is LOST.
|
# from the H1 — a bare subheading returns 400 invalid-target and the write is LOST.
|
||||||
# If unsure of the exact path, GET the document map first and copy the heading verbatim:
|
# If unsure of the exact path, GET the document map first and copy the heading verbatim:
|
||||||
python3 "$CHORUS" map _agent/memory/semantic/operator-preferences.md
|
python3 "$CHORUS" map _agent/members/<member>/preferences.md
|
||||||
python3 "$CHORUS" patch _agent/memory/semantic/operator-preferences.md \
|
python3 "$CHORUS" patch _agent/members/<member>/preferences.md \
|
||||||
append heading "Operator Preferences::Fact / Pattern" <bodyfile>
|
append heading "Preferences::Fact / Pattern" <bodyfile>
|
||||||
# `replace` (instead of `append`) overwrites a section entirely (e.g. a project's "Name::Status").
|
# `replace` (instead of `append`) overwrites a section entirely (e.g. a project's "Name::Status").
|
||||||
|
|
||||||
# create/overwrite a whole file (read-back verified; intermediate dirs auto-created)
|
# create/overwrite a whole file (read-back verified; intermediate dirs auto-created)
|
||||||
@@ -274,7 +276,7 @@ Every PUT body needs the canonical YAML frontmatter (see `references/vault-layou
|
|||||||
|
|
||||||
## Scope Switching (`current-context.md`)
|
## Scope Switching (`current-context.md`)
|
||||||
|
|
||||||
`_agent/context/current-context.md` tracks a single active scope. The operator routinely shifts scope within a day (one project → another → docs), so this is high-churn — switch deliberately, every time the work changes topic.
|
`_agent/members/<member>/current-context.md` tracks a single active scope. The operator routinely shifts scope within a day (one project → another → docs), so this is high-churn — switch deliberately, every time the work changes topic.
|
||||||
|
|
||||||
**Preferred — one command:**
|
**Preferred — one command:**
|
||||||
|
|
||||||
@@ -321,7 +323,7 @@ CHORUS_TODAY=<currentDate> python3 "$LINT"
|
|||||||
Exit codes: `0` clean · `1` violations · `2` unreachable · `3` not bootstrapped. Checks (the linter asserts each and prints violations):
|
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/`.
|
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.
|
2. **Unprocessed inbox** — GET `_agent/members/<member>/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.
|
3. **Duplicate slugs across lifecycle folders** — any slug appearing in more than one of `active/`, `incubating/`, `on-hold/`, `archived/` is broken state.
|
||||||
4. **Folder ↔ `status:` mismatch** — any `projects/<lifecycle>/` note whose `status:` frontmatter disagrees with its folder.
|
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.
|
5. **Wikilinks in frontmatter** — any `[[...]]` inside a YAML frontmatter block (breaks Obsidian reading view), swept across all folders.
|
||||||
@@ -350,7 +352,7 @@ The pass is cheap and pays for itself by catching drift before it requires a reo
|
|||||||
|
|
||||||
## Daily Note — Agent Log
|
## Daily Note — Agent Log
|
||||||
|
|
||||||
After substantive activity, write a one-line entry to today's daily note's `## Agent Log` heading. The daily-note **template** (`journal/templates/daily-note-template.md`) defines this heading, but ad-hoc daily notes created without the template don't have it — so PATCH can fail with `invalid-target` if the note exists but lacks the heading.
|
After substantive activity, write a one-line entry to today's daily note's `## Agent Log` heading. The daily note is the **group's shared timeline** — every member's sessions append to the same heading — so **every line is member-tagged**: `- YYYY-MM-DD [<member>]: <what happened>`. (`capture` writes its log line in this form automatically.) The daily-note **template** (`journal/templates/daily-note-template.md`) defines this heading, but ad-hoc daily notes created without the template don't have it — so PATCH can fail with `invalid-target` if the note exists but lacks the heading.
|
||||||
|
|
||||||
**Procedure (resilient)** — all dates are the conversation's `currentDate`, not the machine clock:
|
**Procedure (resilient)** — all dates are the conversation's `currentDate`, not the machine clock:
|
||||||
|
|
||||||
@@ -367,13 +369,14 @@ python3 "$CHORUS" patch journal/daily/<currentDate>.md append heading "<currentD
|
|||||||
|
|
||||||
| Situation | File / path | Method |
|
| Situation | File / path | Method |
|
||||||
|-----------|-------------|--------|
|
|-----------|-------------|--------|
|
||||||
| Unsure where it goes / quick capture | `inbox/captures/inbox.md` (date-prefixed line) | POST |
|
| Unsure where it goes / quick capture (mine) | `_agent/members/<member>/inbox.md` (date-prefixed line) | POST |
|
||||||
| Operator preference or durable fact | `_agent/memory/semantic/operator-preferences.md` (append under the right heading) | PATCH |
|
| Unowned group-wide capture ("someone should look at this") | `inbox/captures/group.md` (member-tagged line `- YYYY-MM-DD [<member>]: …`) | POST |
|
||||||
| Other durable facts / patterns | `_agent/memory/semantic/<slug>.md` | PUT |
|
| MY preference or personal pattern | `_agent/members/<member>/preferences.md` (append under the right heading) | PATCH |
|
||||||
|
| Durable group facts / patterns (incl. `group-profile.md`) | `_agent/memory/semantic/<slug>.md` | PUT |
|
||||||
| What happened (event record) | `_agent/memory/episodic/<slug>.md` | PUT |
|
| What happened (event record) | `_agent/memory/episodic/<slug>.md` | PUT |
|
||||||
| Short-lived working state | `_agent/memory/working/<slug>.md` | PUT |
|
| Short-lived working state | `_agent/memory/working/<slug>.md` | PUT |
|
||||||
| Task-scoped context / focus | `_agent/context/current-context.md` | PATCH / PUT |
|
| Task-scoped context / focus | `_agent/members/<member>/current-context.md` | PATCH / PUT |
|
||||||
| Working session ended with substance | `_agent/sessions/YYYY-MM-DD-HHMM-<slug>.md` | PUT |
|
| Working session ended with substance | `_agent/sessions/<member>/YYYY-MM-DD-HHMM-<slug>.md` | PUT |
|
||||||
| Long-running project state | `projects/<lifecycle>/<slug>.md` (see Project Lifecycle) | PUT + PATCH |
|
| 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 |
|
| 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 |
|
| Non-obvious decision (ADR) | `decisions/by-date/YYYY-MM-DD-<slug>.md` (see mirror note below) | PUT |
|
||||||
@@ -386,7 +389,7 @@ python3 "$CHORUS" patch journal/daily/<currentDate>.md append heading "<currentD
|
|||||||
| Weekly / monthly rollup | `journal/weekly/YYYY-Www.md` · `journal/monthly/YYYY-MM.md` — see **Journal Rollups** | PUT |
|
| 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 |
|
| 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 |
|
| 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 |
|
| Session-end orientation pointer | `_agent/members/<member>/heartbeat.md` (one line: `<session-log-path> @ <ISO-timestamp>`) | PUT |
|
||||||
|
|
||||||
> **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.
|
> **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.
|
||||||
|
|
||||||
@@ -404,7 +407,7 @@ Never delete files unless the operator explicitly asks. Memory is append-friendl
|
|||||||
|
|
||||||
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?"
|
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 exist; leave them alone, but every new one must use the full form.
|
**Filename format is canonical: `_agent/sessions/<member>/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.
|
||||||
|
|
||||||
Write the body (see `references/session-log-template.md`) to a file with the Write tool, then:
|
Write the body (see `references/session-log-template.md`) to a file with the Write tool, then:
|
||||||
|
|
||||||
@@ -417,7 +420,7 @@ Then add a one-line entry to today's daily note via the **Daily Note — Agent L
|
|||||||
Finally, update the heartbeat pointer so the next session can orient in one read (this closes the loop with loading-procedure Step 4 — a pointer nobody writes is a pointer nobody can read). It is a single line, `<session-log-path> @ <ISO-timestamp>`; write it to a file and PUT it:
|
Finally, update the heartbeat pointer so the next session can orient in one read (this closes the loop with loading-procedure Step 4 — a pointer nobody writes is a pointer nobody can read). It is a single line, `<session-log-path> @ <ISO-timestamp>`; write it to a file and PUT it:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 "$CHORUS" put _agent/heartbeat/last-session.md <bodyfile>
|
python3 "$CHORUS" put _agent/members/<member>/heartbeat.md <bodyfile>
|
||||||
```
|
```
|
||||||
|
|
||||||
`last-session.md` is overwritten (PUT) each session end — never appended, so it can't grow or duplicate.
|
`last-session.md` is overwritten (PUT) each session end — never appended, so it can't grow or duplicate.
|
||||||
@@ -429,7 +432,7 @@ If the API returns a connection error, timeout, or `502`, tell the operator once
|
|||||||
## Style Rules
|
## Style Rules
|
||||||
|
|
||||||
- Write in third person about the operator: "the operator prefers X", not "I prefer X".
|
- Write in third person about the operator: "the operator prefers X", not "I prefer X".
|
||||||
- This vault holds only its own owner's memory. Do not cross-write between distinct vaults/owners — another person's vault and preferences belong in their own vault, not here.
|
- This vault holds the GROUP's shared memory — every member reads and writes it, and everything in it is visible to the whole group. Do not edit another member's `_agent/members/<id>/` namespace (their preferences/scope/heartbeat/inbox belong to their sessions); facts about them as a person go in `resources/people/`. Do not cross-write with vaults outside this group.
|
||||||
- **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.
|
- **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.
|
||||||
- Every memory file has canonical YAML frontmatter — see `references/vault-layout.md`.
|
- Every memory file has canonical YAML frontmatter — see `references/vault-layout.md`.
|
||||||
- Set `agent_written: true` and `author: <your-member-id>` on agent-generated notes and list `source_notes` (plain relative paths, not links).
|
- Set `agent_written: true` and `author: <your-member-id>` on agent-generated notes and list `source_notes` (plain relative paths, not links).
|
||||||
@@ -442,7 +445,7 @@ If the API returns a connection error, timeout, or `502`, tell the operator once
|
|||||||
|
|
||||||
## operator-preferences.md — Rules vs Observations
|
## operator-preferences.md — Rules vs Observations
|
||||||
|
|
||||||
`_agent/memory/semantic/operator-preferences.md` separates two kinds of content:
|
`_agent/members/<member>/preferences.md` separates two kinds of content:
|
||||||
|
|
||||||
- `## Fact / Pattern` — **promoted, deduped rules.** No date prefix. These are timeless: "the operator prefers concise communication." Append here only when a rule is stable.
|
- `## Fact / Pattern` — **promoted, deduped rules.** No date prefix. These are timeless: "the operator prefers concise communication." Append here only when a rule is stable.
|
||||||
- `## Observations` — **timestamped raw observations.** Date-prefixed: `- 2026-06-06: the operator chose X over Y because Z.` This is where new evidence goes by default.
|
- `## Observations` — **timestamped raw observations.** Date-prefixed: `- 2026-06-06: the operator chose X over Y because Z.` This is where new evidence goes by default.
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ For a vault large enough that even the concurrent pass approaches the tool timeo
|
|||||||
# Read any file by vault path
|
# Read any file by vault path
|
||||||
curl -s \
|
curl -s \
|
||||||
-H "Authorization: Bearer $CHORUS_KEY" \
|
-H "Authorization: Bearer $CHORUS_KEY" \
|
||||||
"$CHORUS_BASE/vault/_agent/context/current-context.md"
|
"$CHORUS_BASE/vault/_agent/members/<member>/current-context.md"
|
||||||
```
|
```
|
||||||
|
|
||||||
Returns raw file content (text/markdown). On 404, the file does not exist.
|
Returns raw file content (text/markdown). On 404, the file does not exist.
|
||||||
@@ -38,7 +38,7 @@ Returns raw file content (text/markdown). On 404, the file does not exist.
|
|||||||
# Read a specific heading's content only
|
# Read a specific heading's content only
|
||||||
curl -s \
|
curl -s \
|
||||||
-H "Authorization: Bearer $CHORUS_KEY" \
|
-H "Authorization: Bearer $CHORUS_KEY" \
|
||||||
"$CHORUS_BASE/vault/_agent/memory/semantic/operator-preferences.md/heading/Operator"
|
"$CHORUS_BASE/vault/_agent/members/<member>/preferences.md/heading/Operator"
|
||||||
```
|
```
|
||||||
|
|
||||||
Nested headings: separate levels with `::` (URL-encode spaces as `%20`):
|
Nested headings: separate levels with `::` (URL-encode spaces as `%20`):
|
||||||
@@ -74,7 +74,7 @@ curl -s -X POST \
|
|||||||
-H "Authorization: Bearer $CHORUS_KEY" \
|
-H "Authorization: Bearer $CHORUS_KEY" \
|
||||||
-H "Content-Type: text/markdown" \
|
-H "Content-Type: text/markdown" \
|
||||||
--data-binary @/tmp/obs_entry.md \
|
--data-binary @/tmp/obs_entry.md \
|
||||||
"$CHORUS_BASE/vault/inbox/captures/inbox.md"
|
"$CHORUS_BASE/vault/_agent/members/<member>/inbox.md"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -116,7 +116,7 @@ curl -s -X PUT \
|
|||||||
|
|
||||||
### Append under a heading
|
### Append under a heading
|
||||||
|
|
||||||
**Heading targets must be the FULL heading path, `::`-delimited from the top-level heading.** A bare subheading name returns `400 invalid-target` (errorCode 40080). Example: `## Fact / Pattern` nested under `# Operator Preferences` → `Target: Operator Preferences::Fact / Pattern`. Percent-encode non-ASCII characters (e.g. `H%C3%A9llo`); spaces are fine.
|
**Heading targets must be the FULL heading path, `::`-delimited from the top-level heading.** A bare subheading name returns `400 invalid-target` (errorCode 40080). Example: `## Fact / Pattern` nested under `# Operator Preferences` → `Target: Preferences::Fact / Pattern`. Percent-encode non-ASCII characters (e.g. `H%C3%A9llo`); spaces are fine.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cat > /tmp/obs_patch.md << 'OBSEOF'
|
cat > /tmp/obs_patch.md << 'OBSEOF'
|
||||||
@@ -127,10 +127,10 @@ curl -s -X PATCH \
|
|||||||
-H "Authorization: Bearer $CHORUS_KEY" \
|
-H "Authorization: Bearer $CHORUS_KEY" \
|
||||||
-H "Operation: append" \
|
-H "Operation: append" \
|
||||||
-H "Target-Type: heading" \
|
-H "Target-Type: heading" \
|
||||||
-H "Target: Operator Preferences::Fact / Pattern" \
|
-H "Target: Preferences::Fact / Pattern" \
|
||||||
-H "Content-Type: text/markdown" \
|
-H "Content-Type: text/markdown" \
|
||||||
--data-binary @/tmp/obs_patch.md \
|
--data-binary @/tmp/obs_patch.md \
|
||||||
"$CHORUS_BASE/vault/_agent/memory/semantic/operator-preferences.md"
|
"$CHORUS_BASE/vault/_agent/members/<member>/preferences.md"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Discover heading / block / frontmatter targets
|
### Discover heading / block / frontmatter targets
|
||||||
@@ -141,7 +141,7 @@ When unsure of the exact heading path, GET the note with the document-map Accept
|
|||||||
curl -s \
|
curl -s \
|
||||||
-H "Authorization: Bearer $CHORUS_KEY" \
|
-H "Authorization: Bearer $CHORUS_KEY" \
|
||||||
-H "Accept: application/vnd.olrapi.document-map+json" \
|
-H "Accept: application/vnd.olrapi.document-map+json" \
|
||||||
"$CHORUS_BASE/vault/_agent/memory/semantic/operator-preferences.md"
|
"$CHORUS_BASE/vault/_agent/members/<member>/preferences.md"
|
||||||
```
|
```
|
||||||
|
|
||||||
Returns `{ "headings": [...], "blocks": [...], "frontmatterFields": [...] }`. Copy the heading string verbatim into `Target`.
|
Returns `{ "headings": [...], "blocks": [...], "frontmatterFields": [...] }`. Copy the heading string verbatim into `Target`.
|
||||||
@@ -211,7 +211,7 @@ Only on explicit operator request. Deletion is destructive.
|
|||||||
- Path separators (`/`) in the vault path are **literal** — do not encode them.
|
- Path separators (`/`) in the vault path are **literal** — do not encode them.
|
||||||
- Spaces in filenames or heading targets in a URL: use `%20`.
|
- Spaces in filenames or heading targets in a URL: use `%20`.
|
||||||
- Nested heading levels in a URL path: use `%3A%3A` for `::`.
|
- 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.
|
- Heading text in the `Target:` header: the **full heading path** joined by `::` (e.g. `Preferences::Fact / Pattern`), no `#`; spaces are fine; percent-encode non-ASCII.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -219,14 +219,14 @@ Only on explicit operator request. Deletion is destructive.
|
|||||||
|
|
||||||
| Situation | Vault path | Method |
|
| Situation | Vault path | Method |
|
||||||
|-----------|-----------|--------|
|
|-----------|-----------|--------|
|
||||||
| Quick capture / unsorted | `inbox/captures/inbox.md` (date-prefixed line) | POST |
|
| Quick capture / unsorted | `_agent/members/<member>/inbox.md` (date-prefixed line) | POST |
|
||||||
| Raw imported material | `inbox/imports/` | PUT |
|
| Raw imported material | `inbox/imports/` | PUT |
|
||||||
| Operator preference / durable fact | `_agent/memory/semantic/operator-preferences.md` | PATCH |
|
| Operator preference / durable fact | `_agent/members/<member>/preferences.md` | PATCH |
|
||||||
| Other durable facts, patterns | `_agent/memory/semantic/<slug>.md` | PUT |
|
| Other durable facts, patterns | `_agent/memory/semantic/<slug>.md` | PUT |
|
||||||
| Event record (what happened) | `_agent/memory/episodic/<slug>.md` | PUT |
|
| Event record (what happened) | `_agent/memory/episodic/<slug>.md` | PUT |
|
||||||
| Short-lived, time-boxed state | `_agent/memory/working/<slug>.md` | PUT |
|
| Short-lived, time-boxed state | `_agent/memory/working/<slug>.md` | PUT |
|
||||||
| Task-scoped context / focus | `_agent/context/current-context.md` | PATCH / PUT |
|
| Task-scoped context / focus | `_agent/members/<member>/current-context.md` | PATCH / PUT |
|
||||||
| Working-session log | `_agent/sessions/YYYY-MM-DD-HHMM-<slug>.md` | PUT |
|
| Working-session log | `_agent/sessions/<member>/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 |
|
| Long-running project state | `projects/<lifecycle>/<slug>.md` (lifecycle: `incubating` → `active` → `on-hold`/`archived`; folder and `status:` MUST agree) | PUT + PATCH |
|
||||||
| Ongoing area of responsibility (standing domain, no end state) | `areas/<domain>/<slug>.md` (`<domain>`: `business`/`personal`/`learning`/`systems`) | 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 |
|
| 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 |
|
||||||
@@ -238,7 +238,7 @@ Only on explicit operator request. Deletion is destructive.
|
|||||||
| Daily activity / Agent Log | `journal/daily/YYYY-MM-DD.md` | POST / PATCH |
|
| Daily activity / Agent Log | `journal/daily/YYYY-MM-DD.md` | POST / PATCH |
|
||||||
| 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 |
|
| 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 |
|
| 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 |
|
| Session-end orientation pointer | `_agent/members/<member>/heartbeat.md` (one line, overwritten each session end) | PUT |
|
||||||
| Bootstrap marker (plugin-owned) | `_agent/chorus-vault.md` (`schema_version`, bootstrap date) — the "is this vault set up?" probe | GET / PUT |
|
| Bootstrap marker (plugin-owned) | `_agent/chorus-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`).
|
**Slug rules:** kebab-case, ASCII, ~40 chars max. Every file carries canonical frontmatter (see `vault-layout.md`).
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ curl -s -o /dev/null -w "%{http_code}" -H "$AUTH" "$BASE/vault/_agent/chorus-vau
|
|||||||
|
|
||||||
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.
|
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.
|
Throughout, `{{DATE}}` means today's date (`YYYY-MM-DD`), resolved from the conversation's `currentDate`, and `{{MEMBER}}` means this machine's configured member id. Substitute both before PUTting any scaffold file or anchor seed.
|
||||||
|
|
||||||
### 1. Folder tree
|
### 1. Folder tree
|
||||||
|
|
||||||
@@ -57,10 +57,13 @@ areas/business areas/personal areas/learning areas/systems
|
|||||||
resources/concepts resources/references resources/people resources/companies resources/meetings
|
resources/concepts resources/references resources/people resources/companies resources/meetings
|
||||||
decisions/by-date
|
decisions/by-date
|
||||||
_agent/context _agent/memory/working _agent/memory/episodic _agent/memory/semantic
|
_agent/context _agent/memory/working _agent/memory/episodic _agent/memory/semantic
|
||||||
_agent/sessions _agent/health _agent/templates _agent/heartbeat
|
_agent/sessions _agent/health _agent/templates _agent/members
|
||||||
_agent/skills/active _agent/skills/archived
|
_agent/skills/active _agent/skills/archived
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Plus, per member (created by `bootstrap.py` for the configured member, and by `chorus.py load`'s
|
||||||
|
self-onboarding for any later member): `_agent/sessions/<member>` (→ README).
|
||||||
|
|
||||||
> `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.
|
> `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/`.
|
> `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/`.
|
||||||
@@ -101,15 +104,28 @@ curl -s -X PUT -H "$AUTH" -H "Content-Type: text/markdown" \
|
|||||||
|
|
||||||
### 3. Anchor seeds (only if absent — no fabricated facts)
|
### 3. Anchor seeds (only if absent — no fabricated facts)
|
||||||
|
|
||||||
Substitute `{{DATE}}` → today, then PUT each:
|
Substitute `{{DATE}}` → today and `{{MEMBER}}` → the configured member, then PUT each.
|
||||||
|
|
||||||
|
**Shared group anchors** (once per vault):
|
||||||
|
|
||||||
| Scaffold file | Vault path |
|
| Scaffold file | Vault path |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `scaffold/anchors/operator-preferences.seed.md` | `_agent/memory/semantic/operator-preferences.md` |
|
| `scaffold/anchors/group-profile.seed.md` | `_agent/memory/semantic/group-profile.md` |
|
||||||
| `scaffold/anchors/current-context.seed.md` | `_agent/context/current-context.md` |
|
| `scaffold/anchors/group-inbox.seed.md` | `inbox/captures/group.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.
|
**Member anchors** (once per member — `bootstrap.py member_bootstrap()`, also invoked by
|
||||||
|
`chorus.py load`'s self-onboarding; a roster line `- <member> (joined YYYY-MM-DD)` is appended
|
||||||
|
idempotently under the group profile's `## Members`):
|
||||||
|
|
||||||
|
| Scaffold file | Vault path |
|
||||||
|
|---|---|
|
||||||
|
| `scaffold/anchors/member-preferences.seed.md` | `_agent/members/<member>/preferences.md` |
|
||||||
|
| `scaffold/anchors/member-context.seed.md` | `_agent/members/<member>/current-context.md` |
|
||||||
|
| `scaffold/anchors/member-inbox.seed.md` | `_agent/members/<member>/inbox.md` |
|
||||||
|
|
||||||
|
`_agent/members/<member>/heartbeat.md` is deliberately NOT seeded — it appears at the first
|
||||||
|
session end. The preferences seed is deliberately empty — **do not invent preferences.** Real
|
||||||
|
facts accrue through use.
|
||||||
|
|
||||||
### 4. Vault README (human signpost)
|
### 4. Vault README (human signpost)
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ Assume clients may operate without filesystem access, through the Obsidian Local
|
|||||||
|
|
||||||
The vault is a **shared** substrate — Claude Code, CoWork, and other REST API clients may operate on it concurrently. The REST API offers no transactions, so writers coordinate cooperatively:
|
The vault is a **shared** substrate — Claude Code, CoWork, and other REST API 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.
|
- Single-line, overwrite-style files (`_agent/members/<member>/heartbeat.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 (`chorus.py lock <id>` → `_agent/locks/vault.lock`) and release it at session end. The lock is read-back-confirmed and cooperative with a TTL (stale locks are reclaimable); it is a courtesy, not a hard mutex.
|
- Before a burst of writes in a session that may overlap another, take the advisory lock (`chorus.py lock <id>` → `_agent/locks/vault.lock`) and release it at session end. The lock is read-back-confirmed and cooperative with a TTL (stale locks are reclaimable); it is a courtesy, not a hard mutex.
|
||||||
- Idempotent append (read-before-POST, whole-line match, via `chorus.py append`) is the second line of defense against duplicate lines from retries or overlapping sessions.
|
- Idempotent append (read-before-POST, whole-line match, via `chorus.py 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.
|
- Status-check every write. A write that returns `>= 400` did **not** land — surface it rather than assuming success.
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ Views of the same truth: `scripts/routing.json` is the **machine-readable canoni
|
|||||||
|
|
||||||
| Path | Trigger | What lands | Distinct because | Method |
|
| 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/captures/group.md` | Unowned "someone should look at this" capture for the whole group | One date-prefixed **member-tagged** line (`- YYYY-MM-DD [member]: …`) | Shared deferred routing — whoever triages first routes it. Personal captures go to your own `_agent/members/<member>/inbox.md` | 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/imports/<slug>.md` | Raw external material dropped in wholesale (export, paste, dump) | The raw artifact, unedited | Holds un-triaged *bulk*, vs captures which hold 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 |
|
| `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 the operator okays deletion.
|
Captures and imports are temporary by contract. Triage drains them into the homes below and logs the move; the original is left until the operator okays deletion.
|
||||||
@@ -79,14 +79,16 @@ Lifecycle folders; `status:` frontmatter MUST equal the folder name (the linter
|
|||||||
| Path | Trigger | What lands | Distinct because | Method |
|
| Path | Trigger | What lands | Distinct because | Method |
|
||||||
|------|---------|------------|------------------|--------|
|
|------|---------|------------|------------------|--------|
|
||||||
| `_agent/chorus-vault.md` | Bootstrap / schema migration only | Marker: `schema_version`, bootstrap date | Plugin-owned probe; never hand-edited | GET / PUT |
|
| `_agent/chorus-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/members/<member>/current-context.md` | THIS member's active scope changes; task focus shifts | `## Scope`, `## Scope History`, priorities | Per-member live scope pointer — members work on different things concurrently | PATCH / PUT |
|
||||||
| `_agent/memory/semantic/operator-preferences.md` | A preference/pattern about the operator | Append under `## Observations`; promote to `## Fact / Pattern` when stable | The one curated profile; distinct from ad-hoc semantic notes | PATCH |
|
| `_agent/members/<member>/preferences.md` | A preference/pattern about THIS member | Append under `## Observations`; promote to `## Fact / Pattern` when stable | The member's curated profile; only their own sessions edit it (group-visible by design) | PATCH |
|
||||||
| `_agent/memory/semantic/<slug>.md` | A durable fact/pattern that isn't an operator-preference | Semantic note | Timeless fact, vs episodic (time-stamped event) and working (transient) | PUT |
|
| `_agent/members/<member>/heartbeat.md` | End of every session, after the session log is written | One line: `<session-log-path> @ <ISO-timestamp>` | Per-member O(1) orientation pointer read first at load; overwritten, never grows | PUT |
|
||||||
|
| `_agent/members/<member>/inbox.md` | THIS member's "save this, sort later" capture | One date-prefixed line | Personal deferred routing — no cross-member append contention | POST |
|
||||||
|
| `_agent/context/<slug>.md` | Shared task bundles / reading lists for the group | Context bundle | Task-scoped shared context; the live scope itself is per member (`members/`) | PATCH / PUT |
|
||||||
|
| `_agent/memory/semantic/<slug>.md` | A durable group fact/pattern (incl. `group-profile.md`) | 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/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/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/sessions/<member>/YYYY-MM-DD-HHMM-<slug>.md` | A substantive session ends (decisions/artifacts/commitments) | Session log (see template) | Per-session record, namespaced by member; the unit loading's heartbeat fallback 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 the operator's work narrative, so not in `journal/` | 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 a 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/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` | The operator authors/installs a skill and wants it catalogued | Skill capability entry | Catalogs a *capability*, vs `projects/` which tracks the *build effort* | PUT |
|
| `_agent/skills/active/<slug>.md` | The operator 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 |
|
| `_agent/skills/archived/<slug>.md` | A catalogued skill is retired | Skill entry (moved from `active/`) | Terminal state of the skill catalog | PUT |
|
||||||
@@ -112,6 +114,11 @@ Listed so they are recognised as dead and never recreated. Any one of these appe
|
|||||||
|
|
||||||
| Path | Status | Where it went instead |
|
| Path | Status | Where it went instead |
|
||||||
|------|--------|-----------------------|
|
|------|--------|-----------------------|
|
||||||
|
| `inbox/captures/inbox.md` (single shared inbox) | Retired in schema 5 | Personal captures → `_agent/members/<member>/inbox.md`; unowned items → `inbox/captures/group.md` |
|
||||||
|
| `_agent/memory/semantic/operator-preferences.md` | Retired in schema 5 | Per-member `_agent/members/<member>/preferences.md` |
|
||||||
|
| `_agent/context/current-context.md` (single shared scope) | Retired in schema 5 | Per-member `_agent/members/<member>/current-context.md` |
|
||||||
|
| `_agent/heartbeat/` (single shared pointer) | Retired in schema 5 | Per-member `_agent/members/<member>/heartbeat.md` |
|
||||||
|
| `_agent/sessions/YYYY-MM-DD-HHMM-<slug>.md` (flat, unattributed) | Retired in schema 5 | Member-namespaced `_agent/sessions/<member>/YYYY-MM-DD-HHMM-<slug>.md` |
|
||||||
| `reviews/` (weekly/monthly/quarterly/annual) | Retired in schema 2 | Journal rollups → `journal/{weekly,monthly,quarterly,annual}/`; vault-health → `_agent/health/` |
|
| `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` |
|
| `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/` |
|
| `archive/` (top-level) | Never existed | Project archival → `projects/archived/`; skill archival → `_agent/skills/archived/` |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Session Log Template
|
# Session Log Template
|
||||||
|
|
||||||
Session logs go in: `_agent/sessions/YYYY-MM-DD-HHMM-<slug>.md`
|
Session logs go in: `_agent/sessions/<member>/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 exist in the vault; do not edit their names, 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.
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
/vault/
|
/vault/
|
||||||
├── README.md ← thin human signpost (NOT read for routing)
|
├── README.md ← thin human signpost (NOT read for routing)
|
||||||
├── inbox/
|
├── inbox/
|
||||||
│ ├── captures/ ← quick captures (inbox.md), date-prefixed lines
|
│ ├── captures/ ← group.md — unowned group-wide captures (member-tagged lines);
|
||||||
|
│ │ personal captures live in _agent/members/<member>/inbox.md
|
||||||
│ ├── imports/ ← raw imported material
|
│ ├── imports/ ← raw imported material
|
||||||
│ └── processing-log/
|
│ └── processing-log/
|
||||||
├── journal/ ← one append-only time-series stream; rollups are coarser journal entries, NOT a separate reviews/ tree
|
├── journal/ ← one append-only time-series stream; rollups are coarser journal entries, NOT a separate reviews/ tree
|
||||||
@@ -37,23 +38,28 @@
|
|||||||
│ (reviews/ is retired — journal rollups live under journal/; vault-health audits under _agent/health/)
|
│ (reviews/ is retired — journal rollups live under journal/; vault-health audits under _agent/health/)
|
||||||
└── _agent/
|
└── _agent/
|
||||||
├── chorus-vault.md ← bootstrap marker: schema_version + bootstrap date (plugin-owned; the "is this vault set up?" probe)
|
├── chorus-vault.md ← bootstrap marker: schema_version + bootstrap date (plugin-owned; the "is this vault set up?" probe)
|
||||||
├── context/ ← current-context.md and task bundles
|
├── members/ ← PER-MEMBER namespaces (schema 5) — each member's churn-prone state
|
||||||
|
│ └── <member>/
|
||||||
|
│ ├── preferences.md ← this member's profile/preferences (group-visible)
|
||||||
|
│ ├── current-context.md ← this member's active scope + Scope History
|
||||||
|
│ ├── heartbeat.md ← this member's last-session pointer (written at session end)
|
||||||
|
│ └── inbox.md ← this member's quick captures
|
||||||
|
├── context/ ← shared task bundles / reading lists (scope itself is per member)
|
||||||
├── memory/
|
├── memory/
|
||||||
│ ├── working/ ← transient, time-boxed
|
│ ├── working/ ← transient, time-boxed
|
||||||
│ ├── episodic/ ← what happened, when
|
│ ├── episodic/ ← what happened, when
|
||||||
│ └── semantic/ ← durable facts/patterns (operator-preferences.md)
|
│ └── semantic/ ← durable group facts/patterns (group-profile.md)
|
||||||
├── sessions/ ← YYYY-MM-DD-HHMM-<slug>.md
|
├── sessions/ ← <member>/YYYY-MM-DD-HHMM-<slug>.md (member-namespaced session logs)
|
||||||
├── health/ ← YYYY-MM-vault-health.md (monthly self-maintenance audit; NOT a journal entry)
|
├── health/ ← YYYY-MM-vault-health.md (monthly self-maintenance audit; NOT a journal entry)
|
||||||
├── templates/ ← canonical note templates
|
├── templates/ ← canonical note templates
|
||||||
├── skills/ ← active / archived
|
├── skills/ ← active / archived
|
||||||
├── heartbeat/ ← single-line pointers (e.g. last-session.md → most-recent session log path)
|
|
||||||
├── index/ ← entities.json — machine-maintained slug→{path,kind,title,aliases} registry
|
├── index/ ← entities.json — machine-maintained slug→{path,kind,title,aliases} registry
|
||||||
└── locks/ ← vault.lock — cooperative advisory multi-writer lock
|
└── locks/ ← vault.lock — cooperative advisory multi-writer lock
|
||||||
```
|
```
|
||||||
|
|
||||||
**Entity index:** `_agent/index/entities.json` is a machine-maintained registry mapping each entity slug to its `{path, kind, title, aliases, last_seen}`. It makes routing/resolve an O(1), alias-aware lookup and supplies the name→path map for cross-linking and recall. `resolve` matches slug/title/alias exactly; when nothing matches exactly it falls back to **fuzzy candidates** — entities sharing a distinctive token with the mention — so a shortened name ("chorus memory" → the project `chorus`) surfaces the existing note instead of spawning a duplicate. Aliases are auto-derived from titles, learned from mentions on update, and re-folded from note frontmatter on `sweep`. It is rebuilt automatically by `chorus.py capture` and by `sweep.py`; do not hand-edit. The only JSON file in the vault.
|
**Entity index:** `_agent/index/entities.json` is a machine-maintained registry mapping each entity slug to its `{path, kind, title, aliases, last_seen}`. It makes routing/resolve an O(1), alias-aware lookup and supplies the name→path map for cross-linking and recall. `resolve` matches slug/title/alias exactly; when nothing matches exactly it falls back to **fuzzy candidates** — entities sharing a distinctive token with the mention — so a shortened name ("chorus memory" → the project `chorus`) surfaces the existing note instead of spawning a duplicate. Aliases are auto-derived from titles, learned from mentions on update, and re-folded from note frontmatter on `sweep`. It is rebuilt automatically by `chorus.py capture` and by `sweep.py`; do not hand-edit. The only JSON file in the vault.
|
||||||
|
|
||||||
**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.
|
**Heartbeat:** `_agent/members/<member>/heartbeat.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.
|
**Slug rules:** kebab-case, ASCII only, truncate to ~40 chars.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Inbox — Group
|
||||||
|
|
||||||
|
Unowned "someone should look at this" captures for the whole group, as date-prefixed **member-tagged** lines (`- {{DATE}} [member]: <thing>`), one per line, via idempotent append. Any member's triage may route these; the processing log records who routed what. Personal captures belong in your own `_agent/members/<you>/inbox.md`.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
type: semantic-memory
|
||||||
|
status: active
|
||||||
|
created: {{DATE}}
|
||||||
|
updated: {{DATE}}
|
||||||
|
tags: [agent, group, profile]
|
||||||
|
agent_written: true
|
||||||
|
author: {{MEMBER}}
|
||||||
|
source_notes: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# Group Profile
|
||||||
|
|
||||||
|
<!-- Shared facts about the group itself — what the team is, how it works, and
|
||||||
|
durable group-wide conventions. Read by every member's cold-start load.
|
||||||
|
Facts about ONE member belong in their _agent/members/<id>/preferences.md;
|
||||||
|
facts about a person as a person belong in resources/people/. -->
|
||||||
|
|
||||||
|
## Group
|
||||||
|
<!-- One paragraph: what this group is and what the vault is for. -->
|
||||||
|
|
||||||
|
## Members
|
||||||
|
<!-- One line per member, appended automatically at onboarding: `- <member-id> (joined YYYY-MM-DD)` -->
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
<!-- Durable group-wide working agreements. -->
|
||||||
|
|
||||||
|
## Related
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# 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 chorus-memory skill's Inbox Triage). Don't delete originals on triage — the processing log is the audit trail.
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Inbox — {{MEMBER}}
|
||||||
|
|
||||||
|
{{MEMBER}}'s 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 chorus-memory skill's Inbox Triage). Don't delete originals on triage — the processing log is the audit trail. Items for the whole group belong in `inbox/captures/group.md`.
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
type: semantic-memory
|
||||||
|
status: active
|
||||||
|
created: {{DATE}}
|
||||||
|
updated: {{DATE}}
|
||||||
|
tags: [agent, member, preferences]
|
||||||
|
agent_written: true
|
||||||
|
author: {{MEMBER}}
|
||||||
|
source_notes: []
|
||||||
|
confidence: low
|
||||||
|
last_reviewed: {{DATE}}
|
||||||
|
---
|
||||||
|
|
||||||
|
# Preferences
|
||||||
|
|
||||||
|
<!-- This member's preferences — how the agent should work FOR {{MEMBER}}.
|
||||||
|
Group-visible by design (one vault, one key); truly private material belongs
|
||||||
|
in a personal vault, not CHORUS. Only {{MEMBER}}'s sessions should edit this
|
||||||
|
file. -->
|
||||||
|
|
||||||
|
## Member
|
||||||
|
<!-- One paragraph: who {{MEMBER}} is (role, focus). Leave for them 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 member onboarding ({{DATE}}). Raise confidence once preferences are confirmed through day-to-day use.
|
||||||
-32
@@ -1,32 +0,0 @@
|
|||||||
---
|
|
||||||
type: semantic-memory
|
|
||||||
status: active
|
|
||||||
created: {{DATE}}
|
|
||||||
updated: {{DATE}}
|
|
||||||
tags: [agent, semantic-memory, operator]
|
|
||||||
agent_written: true
|
|
||||||
author: {{MEMBER}}
|
|
||||||
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
|
|
||||||
@@ -6,7 +6,7 @@ updated: {{DATE}}
|
|||||||
tags: [agent, system, marker]
|
tags: [agent, system, marker]
|
||||||
agent_written: true
|
agent_written: true
|
||||||
source_notes: []
|
source_notes: []
|
||||||
schema_version: 4
|
schema_version: 5
|
||||||
bootstrapped: {{DATE}}
|
bootstrapped: {{DATE}}
|
||||||
managed_by: chorus-memory-plugin
|
managed_by: chorus-memory-plugin
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""bootstrap.py — stand up (or repair) an CHORUS vault deterministically.
|
"""bootstrap.py — stand up (or repair) a CHORUS vault deterministically.
|
||||||
|
|
||||||
Idempotent and additive: every write is probe-before-write and NEVER overwrites
|
Idempotent and additive: every write is probe-before-write and NEVER overwrites
|
||||||
an existing file. The marker (_agent/chorus-vault.md) is written LAST, so the vault
|
an existing file. The marker (_agent/chorus-vault.md) is written LAST, so the vault
|
||||||
is only flagged "set up" once every piece is in place. Re-running is the repair
|
is only flagged "set up" once every piece is in place. Re-running is the repair
|
||||||
path (it fills in only what is missing). Cross-platform: pure Python via chorus.py.
|
path (it fills in only what is missing). Cross-platform: pure Python via chorus.py.
|
||||||
|
|
||||||
|
CHORUS is group memory, so bootstrap has two layers:
|
||||||
|
* the SHARED vault (folder tree, templates, group profile, group inbox, marker)
|
||||||
|
— created once, repaired by anyone;
|
||||||
|
* the MEMBER subtree (_agent/members/<member>/ anchors + _agent/sessions/<member>/)
|
||||||
|
— created per member. `member_bootstrap()` is reusable: `chorus.py load` calls
|
||||||
|
it to SELF-ONBOARD a new member whose vault exists but whose namespace doesn't.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
bootstrap.py [--dry-run]
|
bootstrap.py [--dry-run]
|
||||||
|
|
||||||
Env: CHORUS_BASE, CHORUS_KEY (via chorus.py), CHORUS_TODAY (YYYY-MM-DD for {{DATE}}).
|
Env: CHORUS_BASE, CHORUS_KEY, CHORUS_MEMBER (via chorus.py),
|
||||||
|
CHORUS_TODAY (YYYY-MM-DD for {{DATE}}).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -36,10 +44,13 @@ LEAVES = [
|
|||||||
"decisions/by-date",
|
"decisions/by-date",
|
||||||
"_agent/context", "_agent/memory/working", "_agent/memory/episodic",
|
"_agent/context", "_agent/memory/working", "_agent/memory/episodic",
|
||||||
"_agent/memory/semantic", "_agent/sessions", "_agent/health", "_agent/templates",
|
"_agent/memory/semantic", "_agent/sessions", "_agent/health", "_agent/templates",
|
||||||
"_agent/heartbeat", "_agent/skills/active", "_agent/skills/archived", "_agent/locks",
|
"_agent/members", "_agent/skills/active", "_agent/skills/archived", "_agent/locks",
|
||||||
"_agent/index",
|
"_agent/index",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
GROUP_PROFILE_PATH = "_agent/memory/semantic/group-profile.md"
|
||||||
|
GROUP_INBOX_PATH = "inbox/captures/group.md"
|
||||||
|
|
||||||
|
|
||||||
def exists(path: str) -> bool:
|
def exists(path: str) -> bool:
|
||||||
status, _ = chorus.request("GET", chorus.vault_url(path))
|
status, _ = chorus.request("GET", chorus.vault_url(path))
|
||||||
@@ -78,8 +89,43 @@ def leaf_readme(folder: str, dry_run: bool) -> None:
|
|||||||
print(f"bootstrap: readme {path}")
|
print(f"bootstrap: readme {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def register_member(dry_run: bool) -> None:
|
||||||
|
"""Idempotently add this member to the group profile's ## Members roster."""
|
||||||
|
line = f"- {chorus.MEMBER} (joined {chorus.today()})"
|
||||||
|
if dry_run:
|
||||||
|
print(f"bootstrap: would roster {chorus.MEMBER} in {GROUP_PROFILE_PATH}")
|
||||||
|
return
|
||||||
|
status, body = chorus.request("GET", chorus.vault_url(GROUP_PROFILE_PATH))
|
||||||
|
if status != 200:
|
||||||
|
return # no profile yet (dry-run orderings) — the roster line is best-effort
|
||||||
|
if any(ln.strip().startswith(f"- {chorus.MEMBER} ") for ln in body.decode(errors="replace").splitlines()):
|
||||||
|
return
|
||||||
|
chorus.request("PATCH", chorus.vault_url(GROUP_PROFILE_PATH),
|
||||||
|
data=chorus.normalize_patch_body((line + "\n").encode(), "append", "heading"),
|
||||||
|
headers={"Operation": "append", "Target-Type": "heading",
|
||||||
|
"Target": "Group Profile::Members", "Content-Type": "text/markdown"})
|
||||||
|
print(f"bootstrap: rostered {chorus.MEMBER} in group profile")
|
||||||
|
|
||||||
|
|
||||||
|
def member_bootstrap(dry_run: bool = False) -> None:
|
||||||
|
"""Stand up (or repair) the CONFIGURED member's namespace: leaf READMEs for
|
||||||
|
_agent/members/<m>/ and _agent/sessions/<m>/, the three member anchors, and
|
||||||
|
the group-profile roster line. Idempotent; called by main() and by
|
||||||
|
`chorus.py load` when it detects an onboarded vault without this member."""
|
||||||
|
if not chorus.MEMBER:
|
||||||
|
print("bootstrap: no member configured — cannot create a member namespace.", file=sys.stderr)
|
||||||
|
return
|
||||||
|
mdir = f"_agent/members/{chorus.MEMBER}"
|
||||||
|
leaf_readme(f"_agent/sessions/{chorus.MEMBER}", dry_run)
|
||||||
|
seed(f"{mdir}/preferences.md", SCAFFOLD / "anchors" / "member-preferences.seed.md", dry_run)
|
||||||
|
seed(f"{mdir}/current-context.md", SCAFFOLD / "anchors" / "member-context.seed.md", dry_run)
|
||||||
|
seed(f"{mdir}/inbox.md", SCAFFOLD / "anchors" / "member-inbox.seed.md", dry_run)
|
||||||
|
# heartbeat.md is deliberately NOT seeded — it appears at the first session end.
|
||||||
|
register_member(dry_run)
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
parser = argparse.ArgumentParser(description="Bootstrap or repair an CHORUS vault")
|
parser = argparse.ArgumentParser(description="Bootstrap or repair a CHORUS vault")
|
||||||
parser.add_argument("--dry-run", "-n", action="store_true")
|
parser.add_argument("--dry-run", "-n", action="store_true")
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
@@ -104,14 +150,15 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
for source in sorted(templates.rglob("*.md")):
|
for source in sorted(templates.rglob("*.md")):
|
||||||
seed(source.relative_to(templates).as_posix(), source, args.dry_run)
|
seed(source.relative_to(templates).as_posix(), source, args.dry_run)
|
||||||
|
|
||||||
seed("_agent/memory/semantic/operator-preferences.md", SCAFFOLD / "anchors" / "operator-preferences.seed.md", args.dry_run)
|
# Shared group anchors, then this member's namespace.
|
||||||
seed("_agent/context/current-context.md", SCAFFOLD / "anchors" / "current-context.seed.md", args.dry_run)
|
seed(GROUP_PROFILE_PATH, SCAFFOLD / "anchors" / "group-profile.seed.md", args.dry_run)
|
||||||
seed("inbox/captures/inbox.md", SCAFFOLD / "anchors" / "inbox.seed.md", args.dry_run)
|
seed(GROUP_INBOX_PATH, SCAFFOLD / "anchors" / "group-inbox.seed.md", args.dry_run)
|
||||||
|
member_bootstrap(args.dry_run)
|
||||||
seed("README.md", SCAFFOLD / "README.vault.md", args.dry_run)
|
seed("README.md", SCAFFOLD / "README.vault.md", args.dry_run)
|
||||||
seed(chorus.MARKER_PATH, SCAFFOLD / "chorus-vault.md", args.dry_run) # marker LAST
|
seed(chorus.MARKER_PATH, SCAFFOLD / "chorus-vault.md", args.dry_run) # marker LAST
|
||||||
|
|
||||||
print(f"bootstrap: done ({'DRY-RUN ' if args.dry_run else ''}{chorus.today()}).")
|
print(f"bootstrap: done ({'DRY-RUN ' if args.dry_run else ''}{chorus.today()}).")
|
||||||
print("bootstrap: next: create today's daily note + a bootstrap session log + heartbeat (see SKILL.md).")
|
print("bootstrap: next: create today's daily note + a bootstrap session log + your heartbeat (see SKILL.md).")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ Config (group/member/endpoint/key are machine-local — see chorus_config; the p
|
|||||||
CHORUS_TODAY YYYY-MM-DD to use as "today" (pass the conversation's currentDate)
|
CHORUS_TODAY YYYY-MM-DD to use as "today" (pass the conversation's currentDate)
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
chorus.py load # cold-start: the 6 orientation reads in one call
|
chorus.py load # cold-start: the 8 orientation reads in one call
|
||||||
chorus.py get <path> # print file contents (404 -> exit 44)
|
chorus.py get <path> # print file contents (404 -> exit 44)
|
||||||
chorus.py map <path> # document-map JSON (headings/blocks/frontmatter)
|
chorus.py map <path> # document-map JSON (headings/blocks/frontmatter)
|
||||||
chorus.py ls <dir> # directory listing JSON
|
chorus.py ls <dir> # directory listing JSON
|
||||||
@@ -103,6 +103,22 @@ LOCK_PATH = "_agent/locks/vault.lock"
|
|||||||
|
|
||||||
MARKER_PATH = "_agent/chorus-vault.md"
|
MARKER_PATH = "_agent/chorus-vault.md"
|
||||||
|
|
||||||
|
# --- member/group path helpers (schema 5) -------------------------------------
|
||||||
|
# Per-member churn-prone state lives under _agent/members/<member>/ so members
|
||||||
|
# never contend on each other's scope/heartbeat/preferences/inbox. Shared
|
||||||
|
# knowledge (projects, areas, resources, journal, semantic memory) stays communal.
|
||||||
|
GROUP_PROFILE_PATH = "_agent/memory/semantic/group-profile.md"
|
||||||
|
GROUP_INBOX_PATH = "inbox/captures/group.md"
|
||||||
|
|
||||||
|
|
||||||
|
def member_path(name: str, member: str | None = None) -> str:
|
||||||
|
"""A file in a member's namespace, e.g. member_path('preferences.md')."""
|
||||||
|
return f"_agent/members/{member or MEMBER}/{name}"
|
||||||
|
|
||||||
|
|
||||||
|
def member_sessions_dir(member: str | None = None) -> str:
|
||||||
|
return f"_agent/sessions/{member or MEMBER}"
|
||||||
|
|
||||||
|
|
||||||
class ChorusError(RuntimeError):
|
class ChorusError(RuntimeError):
|
||||||
def __init__(self, message: str, code: int = 1) -> None:
|
def __init__(self, message: str, code: int = 1) -> None:
|
||||||
@@ -553,7 +569,9 @@ def extract_heading(markdown: str, heading: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def cmd_scope(subcommand: str, text: str | None = None, as_json: bool = False) -> int:
|
def cmd_scope(subcommand: str, text: str | None = None, as_json: bool = False) -> int:
|
||||||
path = "_agent/context/current-context.md"
|
# Scope is PER MEMBER (schema 5): each member's active scope lives in their
|
||||||
|
# own namespace, so members working on different things never clobber it.
|
||||||
|
path = member_path("current-context.md")
|
||||||
status, body = request("GET", vault_url(path))
|
status, body = request("GET", vault_url(path))
|
||||||
check(status, body, f"scope {subcommand}")
|
check(status, body, f"scope {subcommand}")
|
||||||
current = body.decode(errors="replace")
|
current = body.decode(errors="replace")
|
||||||
@@ -566,9 +584,9 @@ def cmd_scope(subcommand: str, text: str | None = None, as_json: bool = False) -
|
|||||||
scope_updated = line.split(":", 1)[1].strip().strip('"').strip("'")
|
scope_updated = line.split(":", 1)[1].strip().strip('"').strip("'")
|
||||||
break
|
break
|
||||||
sessions_since = None
|
sessions_since = None
|
||||||
# Count session logs dated after scope_updated (the drift signal).
|
# Count MY session logs dated after scope_updated (the drift signal).
|
||||||
if scope_updated:
|
if scope_updated:
|
||||||
status, body = request("GET", vault_url("_agent/sessions/"))
|
status, body = request("GET", vault_url(member_sessions_dir() + "/"))
|
||||||
if status == 200:
|
if status == 200:
|
||||||
try:
|
try:
|
||||||
files = json.loads(body).get("files", [])
|
files = json.loads(body).get("files", [])
|
||||||
@@ -609,8 +627,11 @@ def cmd_scope(subcommand: str, text: str | None = None, as_json: bool = False) -
|
|||||||
|
|
||||||
|
|
||||||
def cmd_load() -> int:
|
def cmd_load() -> int:
|
||||||
"""Cold-start orientation: the canonical 6 reads in one call. 404s on today's
|
"""Cold-start orientation: the canonical 8 reads in one call — the marker, the
|
||||||
daily note and the inbox are normal (printed as absent, not errors)."""
|
GROUP profile, then MY member state (preferences/context/heartbeat/inbox), the
|
||||||
|
shared daily note, and the group inbox. 404s on today's daily note and the
|
||||||
|
inboxes are normal (printed as absent, not errors). If the vault is
|
||||||
|
bootstrapped but MY namespace is missing, self-onboard it."""
|
||||||
if not chorus_config.is_configured(_CFG):
|
if not chorus_config.is_configured(_CFG):
|
||||||
print("chorus: NOT CONFIGURED — this machine has no usable CHORUS key file yet.")
|
print("chorus: NOT CONFIGURED — this machine has no usable CHORUS key file yet.")
|
||||||
print(f"Expected at: {chorus_config.config_path()}")
|
print(f"Expected at: {chorus_config.config_path()}")
|
||||||
@@ -622,11 +643,13 @@ def cmd_load() -> int:
|
|||||||
return 78 # distinct: configuration required
|
return 78 # distinct: configuration required
|
||||||
targets = [
|
targets = [
|
||||||
("marker", MARKER_PATH),
|
("marker", MARKER_PATH),
|
||||||
("preferences", "_agent/memory/semantic/operator-preferences.md"),
|
("group-profile", GROUP_PROFILE_PATH),
|
||||||
("context", "_agent/context/current-context.md"),
|
("my-preferences", member_path("preferences.md")),
|
||||||
("heartbeat", "_agent/heartbeat/last-session.md"),
|
("my-context", member_path("current-context.md")),
|
||||||
|
("my-heartbeat", member_path("heartbeat.md")),
|
||||||
("today", f"journal/daily/{today()}.md"),
|
("today", f"journal/daily/{today()}.md"),
|
||||||
("inbox", "inbox/captures/inbox.md"),
|
("my-inbox", member_path("inbox.md")),
|
||||||
|
("group-inbox", GROUP_INBOX_PATH),
|
||||||
]
|
]
|
||||||
import chorus_queue
|
import chorus_queue
|
||||||
# H2: sync any writes queued during a prior outage, best-effort and quiet on empty.
|
# H2: sync any writes queued during a prior outage, best-effort and quiet on empty.
|
||||||
@@ -639,6 +662,7 @@ def cmd_load() -> int:
|
|||||||
|
|
||||||
marker_missing = False
|
marker_missing = False
|
||||||
heartbeat_absent = False
|
heartbeat_absent = False
|
||||||
|
prefs_absent = False
|
||||||
offline = False
|
offline = False
|
||||||
for label, path in targets:
|
for label, path in targets:
|
||||||
status, body = request("GET", vault_url(path))
|
status, body = request("GET", vault_url(path))
|
||||||
@@ -654,8 +678,10 @@ def cmd_load() -> int:
|
|||||||
print("(absent — fine)")
|
print("(absent — fine)")
|
||||||
if label == "marker":
|
if label == "marker":
|
||||||
marker_missing = True
|
marker_missing = True
|
||||||
if label == "heartbeat":
|
if label == "my-heartbeat":
|
||||||
heartbeat_absent = True
|
heartbeat_absent = True
|
||||||
|
if label == "my-preferences":
|
||||||
|
prefs_absent = True
|
||||||
elif status == 0: # vault unreachable -> degrade to last-known-good cache
|
elif status == 0: # vault unreachable -> degrade to last-known-good cache
|
||||||
offline = True
|
offline = True
|
||||||
cached = chorus_queue.cache_get(path)
|
cached = chorus_queue.cache_get(path)
|
||||||
@@ -671,10 +697,23 @@ def cmd_load() -> int:
|
|||||||
print(f"===== {label}: {path} (HTTP {status}) =====")
|
print(f"===== {label}: {path} (HTTP {status}) =====")
|
||||||
print(f"(error HTTP {status}: {body.decode(errors='replace')[:200]})")
|
print(f"(error HTTP {status}: {body.decode(errors='replace')[:200]})")
|
||||||
print()
|
print()
|
||||||
# M3: heartbeat pointer missing/stale -> fall back to the recent sessions listing
|
# Self-onboarding: the vault is bootstrapped but THIS member has no namespace
|
||||||
|
# yet (fresh member, existing group). Stand up their subtree right now so the
|
||||||
|
# session starts with real anchors instead of 404s.
|
||||||
|
if prefs_absent and not marker_missing and not offline:
|
||||||
|
print(f"===== self-onboarding: member '{MEMBER}' is new to this vault =====")
|
||||||
|
try:
|
||||||
|
import bootstrap
|
||||||
|
bootstrap.member_bootstrap(dry_run=False)
|
||||||
|
print(f"(created _agent/members/{MEMBER}/ anchors + {member_sessions_dir()}/ — "
|
||||||
|
"ask the member to confirm their preferences when convenient)")
|
||||||
|
except Exception as exc: # noqa: BLE001 — onboarding must never kill a load
|
||||||
|
print(f"(self-onboarding failed: {exc} — run bootstrap.py manually)")
|
||||||
|
print()
|
||||||
|
# M3: heartbeat pointer missing/stale -> fall back to MY recent sessions listing
|
||||||
# (matches the documented loading procedure) so orientation works without the pointer.
|
# (matches the documented loading procedure) so orientation works without the pointer.
|
||||||
if heartbeat_absent and not offline:
|
if heartbeat_absent and not offline:
|
||||||
st, body = request("GET", vault_url("_agent/sessions/"))
|
st, body = request("GET", vault_url(member_sessions_dir() + "/"))
|
||||||
if st == 200:
|
if st == 200:
|
||||||
try:
|
try:
|
||||||
files = sorted((f for f in json.loads(body).get("files", []) if f.endswith(".md")),
|
files = sorted((f for f in json.loads(body).get("files", []) if f.endswith(".md")),
|
||||||
@@ -682,9 +721,9 @@ def cmd_load() -> int:
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
files = []
|
files = []
|
||||||
if files:
|
if files:
|
||||||
print("===== recent sessions (heartbeat absent — fallback) =====")
|
print("===== my recent sessions (heartbeat absent — fallback) =====")
|
||||||
for f in files[:5]:
|
for f in files[:5]:
|
||||||
print(f" _agent/sessions/{f}")
|
print(f" {member_sessions_dir()}/{f}")
|
||||||
print()
|
print()
|
||||||
if offline:
|
if offline:
|
||||||
print("NOTE: vault unreachable — context above is last-known-good cache; writes this "
|
print("NOTE: vault unreachable — context above is last-known-good cache; writes this "
|
||||||
|
|||||||
@@ -146,7 +146,10 @@ def _dated_block(today_s: str, body_text: str) -> tuple[str, str]:
|
|||||||
a markdown continuation. Returns (bullet, full_block) — the bullet alone is the
|
a markdown continuation. Returns (bullet, full_block) — the bullet alone is the
|
||||||
idempotency key (same first-line-per-day semantics as before)."""
|
idempotency key (same first-line-per-day semantics as before)."""
|
||||||
lines = [ln.rstrip() for ln in body_text.strip().splitlines()]
|
lines = [ln.rstrip() for ln in body_text.strip().splitlines()]
|
||||||
bullet = f"- {today_s}: {lines[0] if lines else '(update)'}"
|
# Member-tagged: shared entities are updated by many members' sessions, so
|
||||||
|
# each dated block records who added it.
|
||||||
|
who = f" [{chorus.MEMBER}]" if chorus.MEMBER else ""
|
||||||
|
bullet = f"- {today_s}{who}: {lines[0] if lines else '(update)'}"
|
||||||
block = bullet
|
block = bullet
|
||||||
for ln in lines[1:]:
|
for ln in lines[1:]:
|
||||||
block += "\n" + (f" {ln}" if ln else "")
|
block += "\n" + (f" {ln}" if ln else "")
|
||||||
@@ -207,16 +210,19 @@ def capture(kind: str | None, title: str, file_arg: str | None, status_v: str =
|
|||||||
sources = [s.strip() for s in (sources or []) if s.strip()]
|
sources = [s.strip() for s in (sources or []) if s.strip()]
|
||||||
tags = [t.strip() for t in (tags or []) if t.strip()]
|
tags = [t.strip() for t in (tags or []) if t.strip()]
|
||||||
|
|
||||||
# Unknown home -> defer to the inbox (a single idempotent capture line).
|
# Unknown home -> defer to MY inbox (a single idempotent capture line).
|
||||||
|
# (Group-wide "someone should look at this" items go to inbox/captures/group.md
|
||||||
|
# as member-tagged lines via a plain append — see the skill's Inbox Triage.)
|
||||||
if inbox or not kind:
|
if inbox or not kind:
|
||||||
|
inbox_path = chorus.member_path("inbox.md")
|
||||||
if dry_run:
|
if dry_run:
|
||||||
return done("inbox", "inbox/captures/inbox.md", dry=True)
|
return done("inbox", inbox_path, dry=True)
|
||||||
line = f"- {today_s}: {title}"
|
line = f"- {today_s}: {title}"
|
||||||
if body_text.strip():
|
if body_text.strip():
|
||||||
line += f" — {body_text.strip().splitlines()[0]}"
|
line += f" — {body_text.strip().splitlines()[0]}"
|
||||||
with quiet():
|
with quiet():
|
||||||
rc = chorus.cmd_append("inbox/captures/inbox.md", line)
|
rc = chorus.cmd_append(inbox_path, line)
|
||||||
return done("inbox", "inbox/captures/inbox.md", ok=rc == 0)
|
return done("inbox", inbox_path, ok=rc == 0)
|
||||||
|
|
||||||
slug = idx_mod.slugify(title)
|
slug = idx_mod.slugify(title)
|
||||||
index = idx_mod.load()
|
index = idx_mod.load()
|
||||||
@@ -339,7 +345,10 @@ def capture(kind: str | None, title: str, file_arg: str | None, status_v: str =
|
|||||||
print(f"chorus_ops: recall-index update skipped ({exc})", file=sys.stderr)
|
print(f"chorus_ops: recall-index update skipped ({exc})", file=sys.stderr)
|
||||||
|
|
||||||
if not no_log:
|
if not no_log:
|
||||||
ensure_daily_log(f"- {today_s}: {action} {kind} [[{links.link_token(path)}]]")
|
# The daily Agent Log is the GROUP's shared timeline — every line is
|
||||||
|
# member-tagged so interleaved activity stays attributable.
|
||||||
|
who = f" [{chorus.MEMBER}]" if chorus.MEMBER else ""
|
||||||
|
ensure_daily_log(f"- {today_s}{who}: {action} {kind} [[{links.link_token(path)}]]")
|
||||||
|
|
||||||
if as_json:
|
if as_json:
|
||||||
done(action, path, links=linked, near=near_dupes)
|
done(action, path, links=linked, near=near_dupes)
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ from pathlib import Path
|
|||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
import chorus # noqa: E402
|
import chorus # noqa: E402
|
||||||
|
|
||||||
INBOX_PATH = "inbox/captures/inbox.md"
|
# Triage covers BOTH inboxes: this member's personal captures and the shared
|
||||||
|
# group inbox (unowned items — whoever triages first routes them).
|
||||||
|
INBOX_PATH = chorus.member_path("inbox.md")
|
||||||
|
GROUP_INBOX_PATH = chorus.GROUP_INBOX_PATH
|
||||||
LOG_DIR = "inbox/processing-log"
|
LOG_DIR = "inbox/processing-log"
|
||||||
|
|
||||||
_CAPTURE_LINE = re.compile(r"^\s*-\s*(\d{4}-\d{2}-\d{2})\s*:?\s*(.*\S)\s*$")
|
_CAPTURE_LINE = re.compile(r"^\s*-\s*(\d{4}-\d{2}-\d{2})\s*:?\s*(.*\S)\s*$")
|
||||||
@@ -55,26 +58,33 @@ def parse_inbox(text: str) -> list[dict]:
|
|||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
def list_inbox(as_json: bool = False) -> int:
|
def _read_inbox(path: str) -> list[dict]:
|
||||||
status, body = chorus.request("GET", chorus.vault_url(INBOX_PATH))
|
status, body = chorus.request("GET", chorus.vault_url(path))
|
||||||
if status == 404:
|
if status == 404:
|
||||||
items = []
|
return []
|
||||||
else:
|
chorus.check(status, body, f"triage list {path}")
|
||||||
chorus.check(status, body, f"triage list {INBOX_PATH}")
|
|
||||||
items = parse_inbox(body.decode(errors="replace"))
|
items = parse_inbox(body.decode(errors="replace"))
|
||||||
|
for it in items:
|
||||||
|
it["source"] = path
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def list_inbox(as_json: bool = False) -> int:
|
||||||
|
items = _read_inbox(INBOX_PATH) + _read_inbox(GROUP_INBOX_PATH)
|
||||||
if as_json:
|
if as_json:
|
||||||
import chorus_output
|
import chorus_output
|
||||||
env = chorus_output.envelope("triage-list", {"path": INBOX_PATH, "items": items,
|
env = chorus_output.envelope("triage-list", {"paths": [INBOX_PATH, GROUP_INBOX_PATH],
|
||||||
"count": len(items)})
|
"items": items, "count": len(items)})
|
||||||
print(json.dumps(env, ensure_ascii=False))
|
print(json.dumps(env, ensure_ascii=False))
|
||||||
return 0
|
return 0
|
||||||
if not items:
|
if not items:
|
||||||
print("triage: inbox is empty — nothing to route.")
|
print("triage: both inboxes are empty — nothing to route.")
|
||||||
return 0
|
return 0
|
||||||
print(f"triage: {len(items)} capture(s) in {INBOX_PATH}")
|
print(f"triage: {len(items)} capture(s) across {INBOX_PATH} + {GROUP_INBOX_PATH}")
|
||||||
for it in items:
|
for it in items:
|
||||||
age = f"{it['age_days']}d" if it["age_days"] is not None else "?"
|
age = f"{it['age_days']}d" if it["age_days"] is not None else "?"
|
||||||
print(f" [{age:>4}] {it['date']}: {it['text']}")
|
tag = "group" if it["source"] == GROUP_INBOX_PATH else "mine"
|
||||||
|
print(f" [{age:>4}][{tag:>5}] {it['date']}: {it['text']}")
|
||||||
print("\nBuild a proposals JSON (reflect schema + optional \"line\") and run "
|
print("\nBuild a proposals JSON (reflect schema + optional \"line\") and run "
|
||||||
"`chorus.py triage <file>` to preview, `--apply` to route.")
|
"`chorus.py triage <file>` to preview, `--apply` to route.")
|
||||||
return 0
|
return 0
|
||||||
@@ -124,8 +134,9 @@ def apply(proposals: list[dict], confirm: bool = False) -> int:
|
|||||||
continue
|
continue
|
||||||
applied += 1
|
applied += 1
|
||||||
original = (p.get("line") or p["title"]).strip()
|
original = (p.get("line") or p["title"]).strip()
|
||||||
|
who = f"[{chorus.MEMBER}] " if chorus.MEMBER else ""
|
||||||
chorus.cmd_append(f"{LOG_DIR}/{today_s}.md",
|
chorus.cmd_append(f"{LOG_DIR}/{today_s}.md",
|
||||||
f"- {original} → {p.get('_path', '?')}")
|
f"- {who}{original} → {p.get('_path', '?')}")
|
||||||
print(f"triage: routed {applied}/{len(valid)} item(s); audit in {LOG_DIR}/{today_s}.md. "
|
print(f"triage: routed {applied}/{len(valid)} item(s); audit in {LOG_DIR}/{today_s}.md. "
|
||||||
"Originals kept in the inbox (deletion is explicit-only)."
|
"Originals kept in the inbox (deletion is explicit-only)."
|
||||||
+ (f" {gated} stopped at the duplicate gate — re-propose with the existing "
|
+ (f" {gated} stopped at the duplicate gate — re-propose with the existing "
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ sys.path.insert(0, str(SCRIPT_DIR))
|
|||||||
|
|
||||||
import chorus # noqa: E402
|
import chorus # noqa: E402
|
||||||
|
|
||||||
CURRENT_SCHEMA = 4
|
CURRENT_SCHEMA = 5
|
||||||
|
|
||||||
|
|
||||||
def get_text(path: str) -> str | None:
|
def get_text(path: str) -> str | None:
|
||||||
@@ -146,6 +146,35 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
print("migrate: [3->4] _agent/index/ already exists (schema 3); no moves needed.")
|
print("migrate: [3->4] _agent/index/ already exists (schema 3); no moves needed.")
|
||||||
print("migrate: [3->4] then run `sweep.py --apply` to build _agent/index/recall-index.json.")
|
print("migrate: [3->4] then run `sweep.py --apply` to build _agent/index/recall-index.json.")
|
||||||
|
|
||||||
|
if start < 5:
|
||||||
|
# Schema 5 = per-member namespacing. This step exists for alpha CHORUS
|
||||||
|
# vaults stood up before Phase 3 — the pre-5 single-user anchors move into
|
||||||
|
# the CONFIGURED member's namespace (there is no ECHO adoption path).
|
||||||
|
print("migrate: [4->5] per-member namespacing — move single-user anchors into "
|
||||||
|
f"_agent/members/{chorus.MEMBER or '<member>'}/")
|
||||||
|
if not chorus.MEMBER:
|
||||||
|
print("migrate: [4->5] ERROR: no member configured — set CHORUS_MEMBER first.",
|
||||||
|
file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
m = chorus.MEMBER
|
||||||
|
moves = [
|
||||||
|
("_agent/memory/semantic/operator-preferences.md", f"_agent/members/{m}/preferences.md"),
|
||||||
|
("_agent/context/current-context.md", f"_agent/members/{m}/current-context.md"),
|
||||||
|
("_agent/heartbeat/last-session.md", f"_agent/members/{m}/heartbeat.md"),
|
||||||
|
("inbox/captures/inbox.md", f"_agent/members/{m}/inbox.md"),
|
||||||
|
]
|
||||||
|
for src, dst in moves:
|
||||||
|
if get_text(src) is not None:
|
||||||
|
do_or_show(args.apply, f"move {src} -> {dst}",
|
||||||
|
lambda s=src, d=dst: move(s, d))
|
||||||
|
for filename in list_files("_agent/sessions"):
|
||||||
|
if filename.endswith(".md") and filename != "README.md":
|
||||||
|
dst = f"_agent/sessions/{m}/{filename}"
|
||||||
|
do_or_show(args.apply, f"move _agent/sessions/{filename} -> {dst}",
|
||||||
|
lambda f=filename, d=dst: move(f"_agent/sessions/{f}", d))
|
||||||
|
print("migrate: [4->5] then run `bootstrap.py` to seed the group profile, group "
|
||||||
|
"inbox, and any missing member anchors, and `sweep.py --apply` to reindex.")
|
||||||
|
|
||||||
do_or_show(args.apply, f"set _agent/chorus-vault.md schema_version -> {CURRENT_SCHEMA}",
|
do_or_show(args.apply, f"set _agent/chorus-vault.md schema_version -> {CURRENT_SCHEMA}",
|
||||||
lambda: chorus.cmd_fm("_agent/chorus-vault.md", "schema_version", str(CURRENT_SCHEMA)))
|
lambda: chorus.cmd_fm("_agent/chorus-vault.md", "schema_version", str(CURRENT_SCHEMA)))
|
||||||
if args.apply:
|
if args.apply:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"$comment": "CANONICAL machine-readable routing manifest for the CHORUS 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.py consumes it to enforce the core rule (if a vault path matches no route here, and is not retired, nothing should be written to it); check_routing.py consumes it to verify the human routing docs stay in sync with this manifest. Patterns are Python regexes matched against vault-root-relative paths (no leading slash, no /vault/ prefix).",
|
"$comment": "CANONICAL machine-readable routing manifest for the CHORUS 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.py consumes it to enforce the core rule (if a vault path matches no route here, and is not retired, nothing should be written to it); check_routing.py consumes it to verify the human routing docs stay in sync with this manifest. Patterns are Python regexes matched against vault-root-relative paths (no leading slash, no /vault/ prefix).",
|
||||||
"schema_version": 2,
|
"schema_version": 2,
|
||||||
"routes": [
|
"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-group", "pattern": "^inbox/captures/group\\.md$", "method": "POST", "trigger": "Unowned group-wide capture (member-tagged line)", "distinct_because": "Shared deferred routing — any member's triage may route it" },
|
||||||
{ "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-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": "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" },
|
||||||
|
|
||||||
@@ -31,13 +31,13 @@
|
|||||||
{ "id": "decisions-template", "pattern": "^decisions/decision-template\\.md$", "method": "PUT", "trigger": "Bootstrap seed only", "distinct_because": "Template, not a decision" },
|
{ "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/chorus-vault\\.md$", "method": "PUT", "trigger": "Bootstrap / schema migration only", "distinct_because": "Plugin-owned probe; never hand-edited" },
|
{ "id": "agent-marker", "pattern": "^_agent/chorus-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": "member-anchors", "pattern": "^_agent/members/[a-z0-9-]+/(preferences|current-context|heartbeat|inbox)\\.md$", "method": "PATCH", "trigger": "A member's own preferences / scope / heartbeat / captures", "distinct_because": "Per-member namespace — only that member's sessions write here" },
|
||||||
{ "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-context", "pattern": "^_agent/context/(?!current-context\\.md$)[^/]+\\.md$", "method": "PATCH", "trigger": "Shared task bundles / reading lists", "distinct_because": "Task-scoped context shared by the group (scope itself is per member)" },
|
||||||
|
{ "id": "agent-semantic", "pattern": "^_agent/memory/semantic/(?!operator-preferences\\.md$)[^/]+\\.md$", "method": "PUT", "trigger": "A durable fact/pattern (incl. group-profile.md)", "distinct_because": "Timeless fact shared by the group" },
|
||||||
{ "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-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-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-sessions", "pattern": "^_agent/sessions/[a-z0-9-]+/\\d{4}-\\d{2}-\\d{2}-\\d{4}-[^/]+\\.md$", "method": "PUT", "trigger": "A substantive session ends", "distinct_because": "Per-session record, namespaced by member (HHMM required)" },
|
||||||
{ "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-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-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-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-skills-archived","pattern": "^_agent/skills/archived/[^/]+\\.md$", "method": "PUT", "trigger": "A catalogued skill is retired", "distinct_because": "Terminal state of the skill catalog" },
|
||||||
@@ -47,6 +47,11 @@
|
|||||||
{ "id": "leaf-readme", "pattern": "^(.+/)?README\\.md$", "method": "PUT", "trigger": "Bootstrap leaf signpost / vault root README", "distinct_because": "Human signpost, not read for routing" }
|
{ "id": "leaf-readme", "pattern": "^(.+/)?README\\.md$", "method": "PUT", "trigger": "Bootstrap leaf signpost / vault root README", "distinct_because": "Human signpost, not read for routing" }
|
||||||
],
|
],
|
||||||
"retired": [
|
"retired": [
|
||||||
|
{ "pattern": "^inbox/captures/inbox\\.md$", "retired_in_schema": 5, "replacement": "_agent/members/<member>/inbox.md (personal) or inbox/captures/group.md (unowned)" },
|
||||||
|
{ "pattern": "^_agent/memory/semantic/operator-preferences\\.md$", "retired_in_schema": 5, "replacement": "_agent/members/<member>/preferences.md" },
|
||||||
|
{ "pattern": "^_agent/context/current-context\\.md$", "retired_in_schema": 5, "replacement": "_agent/members/<member>/current-context.md" },
|
||||||
|
{ "pattern": "^_agent/heartbeat/", "retired_in_schema": 5, "replacement": "_agent/members/<member>/heartbeat.md" },
|
||||||
|
{ "pattern": "^_agent/sessions/\\d{4}-\\d{2}-\\d{2}(-\\d{4})?-[^/]+\\.md$", "retired_in_schema": 5, "replacement": "_agent/sessions/<member>/YYYY-MM-DD-HHMM-<slug>.md" },
|
||||||
{ "pattern": "^reviews/", "retired_in_schema": 2, "replacement": "journal/{weekly,monthly,quarterly,annual}/ and _agent/health/" },
|
{ "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": "^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": "^archive/", "retired_in_schema": 0, "replacement": "projects/archived/ and _agent/skills/archived/" },
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ backfill what older vaults don't have yet:
|
|||||||
4. **symmetrize cross-links** — for every `## Related` link A -> B, ensure the
|
4. **symmetrize cross-links** — for every `## Related` link A -> B, ensure the
|
||||||
reciprocal B -> A exists (it only adds the missing direction; it never invents
|
reciprocal B -> A exists (it only adds the missing direction; it never invents
|
||||||
new links from body text), and
|
new links from body text), and
|
||||||
5. stamp the marker `schema_version` to the current schema (4).
|
5. stamp the marker `schema_version` to the current schema (5).
|
||||||
|
|
||||||
Dry-run by default; pass --apply to write. READ-ONLY without --apply.
|
Dry-run by default; pass --apply to write. READ-ONLY without --apply.
|
||||||
Cross-platform: pure Python via chorus.py.
|
Cross-platform: pure Python via chorus.py.
|
||||||
@@ -39,7 +39,7 @@ import chorus_index as idx_mod # noqa: E402
|
|||||||
import chorus_links as links # noqa: E402
|
import chorus_links as links # noqa: E402
|
||||||
import chorus_recall # noqa: E402
|
import chorus_recall # noqa: E402
|
||||||
|
|
||||||
CURRENT_SCHEMA = 4
|
CURRENT_SCHEMA = 5
|
||||||
TEMPLATE_RE = re.compile(r"(^|/)(templates/|.*-template\.md$)")
|
TEMPLATE_RE = re.compile(r"(^|/)(templates/|.*-template\.md$)")
|
||||||
SKIP_BASENAMES = {"README.md"}
|
SKIP_BASENAMES = {"README.md"}
|
||||||
|
|
||||||
|
|||||||
@@ -242,31 +242,43 @@ def main() -> int:
|
|||||||
if count > 1:
|
if count > 1:
|
||||||
flag("duplicate-agent-log", f"{path}: {count} '## Agent Log' headings")
|
flag("duplicate-agent-log", f"{path}: {count} '## Agent Log' headings")
|
||||||
|
|
||||||
# Inbox: captures aging past INBOX_DAYS
|
# Members roster (schema 5): every per-member check below runs across ALL
|
||||||
inbox = get("inbox/captures/inbox.md") or ""
|
# members found in the vault, not just the configured one.
|
||||||
for line in inbox.splitlines():
|
members = [
|
||||||
|
m for m in re.findall(r"^_agent/members/([a-z0-9-]+)/", "\n".join(all_files), re.M)
|
||||||
|
]
|
||||||
|
members = sorted(set(members))
|
||||||
|
|
||||||
|
# Inboxes: captures aging past INBOX_DAYS — each member's inbox + the group inbox
|
||||||
|
inboxes = [(f"_agent/members/{m}/inbox.md", m) for m in members]
|
||||||
|
inboxes.append(("inbox/captures/group.md", "group"))
|
||||||
|
for inbox_path, who in inboxes:
|
||||||
|
for line in (get(inbox_path) or "").splitlines():
|
||||||
match = re.match(r"^\s*-\s*(\d{4}-\d{2}-\d{2})\b", line)
|
match = re.match(r"^\s*-\s*(\d{4}-\d{2}-\d{2})\b", line)
|
||||||
date = parse_date(match.group(1)) if match else None
|
date = parse_date(match.group(1)) if match else None
|
||||||
if date and (TODAY - date).days > INBOX_DAYS:
|
if date and (TODAY - date).days > INBOX_DAYS:
|
||||||
flag("aging-inbox", f"inbox capture {date} ({(TODAY - date).days}d): {line.strip()[:80]}")
|
flag("aging-inbox", f"[{who}] capture {date} ({(TODAY - date).days}d): {line.strip()[:80]}")
|
||||||
|
|
||||||
# Scope freshness (drift detector)
|
# Scope freshness (drift detector) — per member, against THAT member's sessions
|
||||||
context = get("_agent/context/current-context.md")
|
for m in members:
|
||||||
if context is not None:
|
ctx_path = f"_agent/members/{m}/current-context.md"
|
||||||
|
context = get(ctx_path)
|
||||||
|
if context is None:
|
||||||
|
continue
|
||||||
_, fm = parse_frontmatter(context)
|
_, fm = parse_frontmatter(context)
|
||||||
scope_updated = parse_date(fm.get("scope_updated"))
|
scope_updated = parse_date(fm.get("scope_updated"))
|
||||||
if scope_updated is None:
|
if scope_updated is None:
|
||||||
flag("scope-no-timestamp",
|
flag("scope-no-timestamp",
|
||||||
"_agent/context/current-context.md: no scope_updated frontmatter — scope drift cannot be detected; add it (bootstrap.py) and switch scope via `chorus.py scope set`")
|
f"{ctx_path}: no scope_updated frontmatter — scope drift cannot be detected; add it (bootstrap.py) and switch scope via `chorus.py scope set`")
|
||||||
else:
|
else:
|
||||||
since = [
|
since = [
|
||||||
path for path in all_files
|
path for path in all_files
|
||||||
if (m := re.match(r"^_agent/sessions/(\d{4}-\d{2}-\d{2})", path))
|
if (mt := re.match(rf"^_agent/sessions/{re.escape(m)}/(\d{{4}}-\d{{2}}-\d{{2}})", path))
|
||||||
and (d := parse_date(m.group(1))) and d > scope_updated
|
and (d := parse_date(mt.group(1))) and d > scope_updated
|
||||||
]
|
]
|
||||||
if len(since) >= SCOPE_STALE_SESSIONS:
|
if len(since) >= SCOPE_STALE_SESSIONS:
|
||||||
flag("scope-stale",
|
flag("scope-stale",
|
||||||
f"scope set {scope_updated}; {len(since)} session(s) logged since without a switch — confirm it still reflects current work (or run `chorus.py scope set`)")
|
f"[{m}] scope set {scope_updated}; {len(since)} session(s) logged since without a switch — confirm it still reflects current work (or run `chorus.py scope set`)")
|
||||||
|
|
||||||
# ---- Graph health: broken wikilinks, orphans, index drift ----------------
|
# ---- Graph health: broken wikilinks, orphans, index drift ----------------
|
||||||
# (md_files / md_set were built up top; reuse the shared `texts` cache.)
|
# (md_files / md_set were built up top; reuse the shared `texts` cache.)
|
||||||
|
|||||||
+5
-5
@@ -287,9 +287,9 @@ def eval_write_safety(h):
|
|||||||
|
|
||||||
# replayed append -> whole-line idempotency, no duplicate
|
# replayed append -> whole-line idempotency, no duplicate
|
||||||
ops += 1
|
ops += 1
|
||||||
h.run(CHORUS, "append", "inbox/captures/inbox.md", f"- {TODAY}: replay me")
|
h.run(CHORUS, "append", "_agent/members/eval-member/inbox.md", f"- {TODAY}: replay me")
|
||||||
h.run(CHORUS, "append", "inbox/captures/inbox.md", f"- {TODAY}: replay me")
|
h.run(CHORUS, "append", "_agent/members/eval-member/inbox.md", f"- {TODAY}: replay me")
|
||||||
n = (h.ground("inbox/captures/inbox.md") or "").count("replay me")
|
n = (h.ground("_agent/members/eval-member/inbox.md") or "").count("replay me")
|
||||||
if n != 1:
|
if n != 1:
|
||||||
silent += 1
|
silent += 1
|
||||||
|
|
||||||
@@ -303,13 +303,13 @@ def eval_durability(h, dead_base):
|
|||||||
h.seed("_agent/chorus-vault.md", "---\nschema_version: 4\n---\n# marker\n")
|
h.seed("_agent/chorus-vault.md", "---\nschema_version: 4\n---\n# marker\n")
|
||||||
queued_ok = 0
|
queued_ok = 0
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
r = h.run(CHORUS, "append", "inbox/captures/inbox.md",
|
r = h.run(CHORUS, "append", "_agent/members/eval-member/inbox.md",
|
||||||
f"- {TODAY}: offline capture {i}", base=dead_base)
|
f"- {TODAY}: offline capture {i}", base=dead_base)
|
||||||
queued_ok += int(r.returncode == 0 and "queued" in r.stdout)
|
queued_ok += int(r.returncode == 0 and "queued" in r.stdout)
|
||||||
# vault returns: flush replays; a second flush must not duplicate
|
# vault returns: flush replays; a second flush must not duplicate
|
||||||
h.run(CHORUS, "flush")
|
h.run(CHORUS, "flush")
|
||||||
h.run(CHORUS, "flush")
|
h.run(CHORUS, "flush")
|
||||||
inbox = h.ground("inbox/captures/inbox.md") or ""
|
inbox = h.ground("_agent/members/eval-member/inbox.md") or ""
|
||||||
landed = sum(1 for i in range(3) if f"offline capture {i}" in inbox)
|
landed = sum(1 for i in range(3) if f"offline capture {i}" in inbox)
|
||||||
dupes = sum(inbox.count(f"offline capture {i}") - 1
|
dupes = sum(inbox.count(f"offline capture {i}") - 1
|
||||||
for i in range(3) if f"offline capture {i}" in inbox)
|
for i in range(3) if f"offline capture {i}" in inbox)
|
||||||
|
|||||||
@@ -121,14 +121,14 @@ def main():
|
|||||||
|
|
||||||
# 6. inbox capture (unknown home)
|
# 6. inbox capture (unknown home)
|
||||||
h.chorus(CHORUS, "capture", "stray thought", "--inbox")
|
h.chorus(CHORUS, "capture", "stray thought", "--inbox")
|
||||||
inbox = h.ground("inbox/captures/inbox.md")
|
inbox = h.ground("_agent/members/eval-member/inbox.md")
|
||||||
check("inbox capture lands", inbox and "stray thought" in inbox)
|
check("inbox capture lands", inbox and "stray thought" in inbox)
|
||||||
|
|
||||||
# 7. sweep --apply rebuilds the index, symmetrizes, stamps schema 3
|
# 7. sweep --apply rebuilds the index, symmetrizes, stamps schema 3
|
||||||
r = h.chorus(SWEEP, "--apply")
|
r = h.chorus(SWEEP, "--apply")
|
||||||
marker = h.ground("_agent/chorus-vault.md")
|
marker = h.ground("_agent/chorus-vault.md")
|
||||||
check("sweep runs clean", r.returncode == 0, r.stdout + r.stderr)
|
check("sweep runs clean", r.returncode == 0, r.stdout + r.stderr)
|
||||||
check("sweep stamps schema 4", marker and "schema_version=4" in marker.replace(": ", "="), marker)
|
check("sweep stamps schema 5", marker and "schema_version=5" in marker.replace(": ", "="), marker)
|
||||||
idx2 = h.ground("_agent/index/entities.json")
|
idx2 = h.ground("_agent/index/entities.json")
|
||||||
check("sweep rebuilt index has all entities",
|
check("sweep rebuilt index has all entities",
|
||||||
idx2 and all(s in idx2 for s in ["bob-smith", "mpm", "alpha", "beta"]))
|
idx2 and all(s in idx2 for s in ["bob-smith", "mpm", "alpha", "beta"]))
|
||||||
@@ -188,7 +188,7 @@ def main():
|
|||||||
capture_output=True, text=True,
|
capture_output=True, text=True,
|
||||||
env=dict(os.environ, CHORUS_BASE=base, CHORUS_KEY=KEY, CHORUS_MEMBER="eval-member", CHORUS_TODAY="2026-06-21"))
|
env=dict(os.environ, CHORUS_BASE=base, CHORUS_KEY=KEY, CHORUS_MEMBER="eval-member", CHORUS_TODAY="2026-06-21"))
|
||||||
bob3 = h.ground("resources/people/bob-smith.md") or ""
|
bob3 = h.ground("resources/people/bob-smith.md") or ""
|
||||||
check("v1.5 update keeps the dated bullet", "- 2026-06-21: met at expo" in bob3, r.stdout + r.stderr)
|
check("v1.5 update keeps the dated bullet", "- 2026-06-21 [eval-member]: met at expo" in bob3, r.stdout + r.stderr)
|
||||||
check("v1.5 update keeps EVERY body line",
|
check("v1.5 update keeps EVERY body line",
|
||||||
" second line survives" in bob3 and " third line too" in bob3, bob3)
|
" second line survives" in bob3 and " third line too" in bob3, bob3)
|
||||||
|
|
||||||
@@ -259,7 +259,7 @@ def main():
|
|||||||
and primary[0].get("status") == "active", json.dumps(primary[:1]))
|
and primary[0].get("status") == "active", json.dumps(primary[:1]))
|
||||||
|
|
||||||
# 15. one-tap triage: structured list, then route with an audit trail.
|
# 15. one-tap triage: structured list, then route with an audit trail.
|
||||||
h.seed("inbox/captures/inbox.md",
|
h.seed("_agent/members/eval-member/inbox.md",
|
||||||
"- 2026-06-01: prefers uv over pip\n- 2026-06-20: try the new espresso place\n")
|
"- 2026-06-01: prefers uv over pip\n- 2026-06-20: try the new espresso place\n")
|
||||||
r = h.chorus(CHORUS, "triage", "--list", "--json")
|
r = h.chorus(CHORUS, "triage", "--list", "--json")
|
||||||
try:
|
try:
|
||||||
@@ -282,7 +282,7 @@ def main():
|
|||||||
check("v1.5 triage writes the processing-log audit line",
|
check("v1.5 triage writes the processing-log audit line",
|
||||||
"prefers uv over pip" in plog and "_agent/memory/semantic/prefers-uv-over-pip.md" in plog, plog)
|
"prefers uv over pip" in plog and "_agent/memory/semantic/prefers-uv-over-pip.md" in plog, plog)
|
||||||
check("v1.5 triage keeps the original capture",
|
check("v1.5 triage keeps the original capture",
|
||||||
"prefers uv over pip" in (h.ground("inbox/captures/inbox.md") or ""))
|
"prefers uv over pip" in (h.ground("_agent/members/eval-member/inbox.md") or ""))
|
||||||
|
|
||||||
# 16. lint flags incomplete entity frontmatter; sweep backfills it.
|
# 16. lint flags incomplete entity frontmatter; sweep backfills it.
|
||||||
h.seed("resources/companies/driftco.md",
|
h.seed("resources/companies/driftco.md",
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ def main():
|
|||||||
(ground("resources/companies/acme-corp.md") or "").find("type: company") >= 0)
|
(ground("resources/companies/acme-corp.md") or "").find("type: company") >= 0)
|
||||||
check("apply creates the semantic note",
|
check("apply creates the semantic note",
|
||||||
ground("_agent/memory/semantic/use-uv-not-pip.md") is not None)
|
ground("_agent/memory/semantic/use-uv-not-pip.md") is not None)
|
||||||
check("apply routes the inbox proposal", "half-formed idea" in (ground("inbox/captures/inbox.md") or ""))
|
check("apply routes the inbox proposal", "half-formed idea" in (ground("_agent/members/eval-member/inbox.md") or ""))
|
||||||
check("apply still skips the low-confidence proposal",
|
check("apply still skips the low-confidence proposal",
|
||||||
ground("resources/concepts/maybe-relevant.md") is None)
|
ground("resources/concepts/maybe-relevant.md") is None)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user