Commits
Per-owner identity files (self.md, agency.md, partner.md, awareness.md,
pulse.md, pulse_output.md, identity_pulse.md, history.jsonl, news/*) were
being written into a repo-tracked sol/ directory and committed/pushed to
the public AGPL repo by talent/heartbeat.md. This is an active privacy
leak that violates sol pbc Article IV.
Five coupled moves in one pass:
1. git rm -r sol/ at repo root; /sol/ added to .gitignore.
2. {journal}/sol/ → {journal}/identity/ everywhere (code, tests, fixtures,
docs); ensure_sol_directory → ensure_identity_directory.
3. New write_identity() helper in think/identity.py — single write path
with per-directory fcntl.LOCK_EX, atomic tmpfile + os.replace, 0o600
perms, hash-based history.jsonl audit log (no diffs).
4. talent/heartbeat.md: deleted "Path notes" block; Step 6 is now a no-op
close. Other talents (awareness_tender, pulse, naming) updated to use
identity/ paths and stripped of any commit/push instructions.
5. think/prompts.py: removed SOL_DIR and the repo-root read branch;
template vars are now $identity_* loaded only from {journal}/identity/.
Acceptance greps all return 0. New tests cover write_identity()
atomicity, lock serialization, history schema, 0o600 mode, and missing-
file first writes. CLI surface (sol call identity ...) unchanged.
No data migration code (Jer is sole user, resets identity on deploy).
No backward-compat shims. Clean break.
Sprint 4 cleanup 1 (req_myu5gwm7 Lode 4xvyyikt). Previously
merged via hopper but blocked by known refine-stage stuck-lode
false-positive; applied manually from the lode worktree.
Path A — talent/sense.md: contamination guard now explicitly ties
role: attendee to meeting_detected: true for the same segment.
When meeting_detected: false, every Person must be role: mentioned
even if they spoke, were quoted, or referenced in transcript
(closes podcast / press conference / lecture leak).
Path B — talent/participation.py: post-processor clamps attendee
to mentioned when no contributing sense segment had
meeting_detected: true. Defensive net catching any Path A prompt
leakage. Logs at warning level with count + activity id for
observability.
Tests: 3 new cases in test_sense_contamination_guard.py covering
clamp positive (podcast with speakers → all mentioned), negative
pass-through (real meeting unchanged), idempotence + logging.
make test: 3254 passed, 1 skipped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Post-Sprint-4 trivials bundled:
- remove dead occurrence/has_extraction detection in settings routes
plus the now-unreachable Extract toggle UI branch in workspace.html,
and drop the corresponding extract/has_extraction keys from the
generators.json baseline; refresh adjacent docstring/copy that
referenced the removed field
- normalize the schedule generator description to the post-Sprint-3
"anticipated activity records" phrasing in talent/schedule.md and
the three API baselines that mirror it
- fix stale "calendar" in the talent/journal/references/config.md
apps.order example (now "activities")
- replace retired "occurrence" hook value with live "schedule" in
the _format_tags test data
- document the sense.json/sense.md dual-write coupling to
think/cluster.py's per-segment talents/**/*.md glob
The activity-anticipation dispatcher only loaded the current local day's
records, so a pre-alert for an early-morning D+1 activity was silently
missed when local_now was late on day D (and symmetric on the other side).
Scan yesterday/today/tomorrow gated by _ACTIVITY_ANTICIPATION_CROSSDAY_WINDOW_MINUTES
(default 120), and build start_dt from each record's own day.
Co-Authored-By: Codex <noreply@openai.com>
Sprint 4 added dict cadences ({"type": "activity-anticipation", "offset_minutes": -30}) and wired the supervisor dispatcher to handle them, but the CLI validator _validate_routine_cadence was left behind and hard-rejected every non-string cadence. That broke `sol call routines create --template meeting-prep`, forcing manual routines.json edits.
Extend the validator to accept dict cadences with a known `type`, mirroring the dispatcher's int coercion for `offset_minutes`. Add reciprocal sync comments in both the validator and the dispatcher so future cadence types get updated in both places. New tests cover the meeting-prep template, a string override of a dict default, and the missing-type / bad-offset rejection paths.
Co-Authored-By: OpenAI Codex <codex@openai.com>
The cogitate `activities_review` talent prompt has been instructing the
LLM to emit `participation[]` on the stdin payload, but the CLI's
`create` handler only read `title`/`activity`/`description`/`details` —
silently dropping the participation field on every new activity.
Extend `apps/activities/call.py::create_record` to:
- Validate an optional `participation[]` per Sprint 1's schema (role ∈
{attendee, mentioned}, source ∈ {voice, speaker_label, transcript,
screen, other}, string name required). Fail fast on the first bad
entry with an indexed stderr error and exit 1 before any record is
written.
- Resolve each entry's `entity_id` read-only via `load_entities` +
`find_matching_entity`, mirroring `talent/participation.py::post_process`.
Unmatched names store `entity_id: null`. Never writes to any entity
file.
- Persist `participation` on the new record only when the caller
provided the key (absent stays absent; empty `[]` is stored as `[]`).
- Append `"participation"` to `edits[0].fields` when the caller opted in.
Eight new CLI-level tests cover the happy path, absent vs empty, every
validation branch, the read-only resolver invariant (st_size +
st_mtime_ns snapshot), and the no-write-on-rejection contract.
No change to `update`, the cogitate prompt, `think/activities.py`, or
`talent/participation.py`.
Sprint 4 Lode D. Surgical scrub of stale references left over from Lodes A-C3c.
Runtime was already migrated; this lode removes dead-pointer debris so readers
and prompts do not follow commands, files, or features that no longer exist.
Docs:
- APPS.md: drop occurrence-hook as a live option; mark the V6 audit retired; reference implementations now cite only `talent/schedule.py`.
- THINK.md, CORTEX.md: align the hook example and Active Facet Detection description with live behavior (`think/facets.py::get_active_facets`).
- talent/journal/references/{captures,cli,config,facets,storage}.md: frame events as historical; delete the `events` subcommand section and dead `extract` config knob; add activities alongside events; swap `flow` examples for live talent names.
Talent prompts / skill (surgical swap-or-drop, no style rewrites):
- skills/solstone/SKILL.md, triage.md, joke_bot.md, decisionalizer.md, think/planner.md, apps/entities/talent/{entity_assist,entities}.md, apps/todos/talent/{daily,weekly}.md: replace `sol call journal events` with either `sol call activities list --source anticipated` (future-facing) or `sol call journal search -a meetings` (historical); drop it where no literal equivalent applies.
- think/planner.md: drop `get_events` from the tool list and replace examples with `search_journal` agent filters.
Code:
- think/tools/search.py: drop `flow` from live-agent examples in the docstring; swap `agent="flow"` to `agent="meetings"`; keep `agent="event"` as the historical example.
- convey/templates/app.html: simplify the command-hint branch to check only `sol call activities list`.
Tests + fixtures:
- tests/baselines/api/activities/day-events.json: delete the orphan baseline (endpoint removed in Lode C1).
- make update-api-baselines is zero-delta; live baselines were regenerated in `9a5d800b`. Historical `flow` / `knowledge_graph` entries in `tokens/usage.json` come from fixture talent logs and remain out of scope.
- tests/fixtures/journal/sol/briefing.md: align with the morning briefing template (`calendar_events` to `anticipated_activities`).
Keep-list invariants intact:
- `think/formatters.py:142` `facets/*/events/*.jsonl` registry entry.
- `think/event_formatter.py::format_events`.
- `think/indexer/journal.py:~250` glob.
- `tests/test_formatters.py` and `tests/test_journal_index.py` event/flow fixture coverage.
Follow-ups (flagged, not fixed):
- `apps/settings/routes.py:435-441, 931-935`: dead `hook == "occurrence"` branches and orphan `extract` field handling.
- `talent/schedule.md:5`: schedule output still says "structured anticipation records"; this leaks into three baselines but was outside Lode D scope.
3238 passed, 1 skipped, 1 warning.
- Wire {"type": "activity-anticipation", "offset_minutes": N}
cadences into think.routines.check() so routines like
meeting-prep fire at start + offset (±60s) for visible
anticipated activities across all facets on the current
local day.
- _run_routine now accepts an optional
trigger_context={"activity": ..., "facet": ...} and injects
an ## Upcoming Activity block (title, type, facet,
start/end, description, details, attendee names) before the
"Execute this routine now." line; other callers are unchanged.
- Dedup is in-memory only (_fired_triggers, pruned at 2 days
per check() tick). State does not survive supervisor restarts
— acceptable since re-firing a past trigger is worse than
missing a restart-window fire.
- Unknown dict cadence types (e.g. legacy {"type": "event"})
are silently skipped with one INFO log per routine_id via
_logged_unknown_cadence; string-cron behavior is untouched.
- Known limitations: (1) anticipated activities carry no
timezone field — the routine's timezone is assumed. (2)
negative offsets that cross midnight (e.g. a 00:15 activity
with offset_minutes=-30) will miss their trigger because the
dispatcher only scans the activity-day file. (3) dedup is
written before dispatch, so a _run_routine failure will not
auto-retry.
- Follow-up: think/tools/routines.py::_validate_routine_cadence
still rejects non-string cadences, so sol call routines
create --template meeting-prep fails until that validation is
extended to accept dict cadences (and the manual routines run
path updated to supply trigger context).
Sprint 4 Lode C3 partial — surgical rename pass for the two template-level
terminology updates that fit under the VPE playbook's "targeted surgical
fix" carve-out (pure text replacements, no design judgment, smaller than
the scoping overhead).
talent/morning_briefing.md:
- "calendar events" → "anticipated activities" across instructions, gap
examples, and the coverage-preamble example.
- YAML frontmatter source counter renamed
`sources.calendar_events` → `sources.anticipated_activities`.
- Prose references to "calendar event title" + "calendar events are
self-evident" updated to the activity vocabulary. Sprint 3 already
switched the data source to `sol call activities list --source anticipated`;
this commit aligns the prompt text with the actual data flow.
routines/templates/meeting-prep.md:
- default_cadence switched from the legacy
`{"type": "event", "trigger": "calendar", "offset_minutes": -30}`
(dead spec — never dispatched) to
`{"type": "activity-anticipation", "offset_minutes": -30}`.
- Introductory prose updated from "Upcoming Event" → "Upcoming Activity".
The activity-anticipation cadence implementation in think/routines.py is
NOT in this commit — it is a substantive feature requiring a hopper lode.
The original C3 hop (5sh7y7va) got stuck in the known hopper gated-state
false-positive (see extro pulse.md: "Hopper live server bounce needed")
and was killed without commits. Until the cadence dispatcher is
implemented, meeting-prep with this new cadence will stay dormant — same
end state as today, where the old event/calendar cadence never fired.
Follow-up: re-scope the cadence implementation as a fresh hop lode after
the hopper server is bounced (commit 2030d96 activates the false-positive
fix).
Sprint 4 status after this commit:
Lode A (9a5d800b) — shipped
Lode B (930f1d36) — shipped
Lode C1 (53762ba9) — shipped
Lode C2 (cb034248) — shipped
Lode C3 (this commit, partial) — templates updated directly; cadence
implementation deferred
Lode D — pending (docs + baselines scrub)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Bump SCHEMA_VERSION from 3 to 4 so per-day {day}/stats.json caches are invalidated on the next `sol think` and legacy events-agent labels (`meetings`/`decisions`/`followups`) do not linger in cached bars.
- Switch the dashboard pipeline to read `facets/*/activities/{day}.jsonl` via `load_activity_records`, `estimate_duration_minutes`, and `segment_parse`; bar categories now reflect activity types (`meeting`, `coding`, ...) while the per-day cache key `agent_data` is retained for cache-format compatibility and is now keyed by activity type.
- FOUNDER-REVIEW GATE: Do NOT deploy to /data/solstone or the Mac Mini until founder has pulled this branch, run `sol think --day <recent>`, and visually confirmed the rebuilt Activities chart and heatmap render correctly with activity-type labels.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint 4 Lode C1. Completes retirement of legacy reader surfaces started
in 9a5d800b (A) and 930f1d36 (B); last-known-good for revert: 930f1d36.
- apps/home: rename `_collect_events` → `_collect_anticipated_activities`;
drop `get_events` call; rename pulse context key `events` →
`anticipated_activities`; update template iteration + empty-state copy.
- apps/activities: delete `/api/day/<day>/events` route + `activities_day_events`;
replace `activities_stats` with activities-sourced stub returning
`{YYYYMMDD: {facet: count}}` via `calendar.monthrange` + `load_activity_records`.
- apps/activities/_day.html: template surgery — replace `#eventsView` with
`#timelineView`, extract timeline rendering into new `renderTimeline()`,
delete event modal + `openModal`, delete all-day event block, delete
processedEvents/column-layout/legend code, drop `/api/day/<day>/events`
fetch. File: 1470 → 974 LOC.
- convey/static/month-picker.js: tooltip wording `events` → `items`.
- think/journal_stats.py: drop `facets/*/events/*.jsonl` mtime block from
`_get_day_mtime`.
- think/indexer/journal.py: delete `get_events`. `_find_signal_files`
event glob retained — historical event files remain searchable via
`sol call journal search`.
- think/event_formatter.py: delete `get_month_event_counts`; `format_events`
remains as the only public export. Registry entry at
`think/formatters.py:142` unchanged.
- tests: delete `tests/test_events.py` (5 tests); delete
`test_get_events*` from `test_journal_index.py` (2 tests); delete
`test_facet_event_mtime_invalidates_cache` from `test_journal_stats.py`
(1 test, covered deleted functionality); rewrite Pulse collector test
in `test_home_yesterdays_processing.py` (anticipated-only assertion);
update monkeypatches in `test_home_skills.py` + `test_home_routines.py`;
add one route-level stats stub test in `tests/test_app_activities.py`;
remove `day-events` entry from `tests/verify_api.py`; regenerate
`tests/baselines/api/activities/stats-month.json` for the new shape.
Keep-list invariants intact:
- `think/formatters.py:142` event formatter registry entry
- `think/event_formatter.py::format_events`
- `think/indexer/journal.py::_find_signal_files` event glob (line ~250)
- `tests/test_formatters.py`
Deferred to Lode D: `think/planner.md` references to `get_events` at
lines 19, 46, 82 (live prompt text, but no runtime tool-wrapper exists —
misleading but non-breaking); wider docs/baselines scrub.
Test count: 3234 passed, 1 skipped (net −7 tests from the migration:
−5 `test_events.py`, −2 `test_journal_index.py::test_get_events*`,
−1 `test_journal_stats.py::test_facet_event_mtime_invalidates_cache`,
−1 rewritten-in-place in `test_home_yesterdays_processing.py`,
+1 new `test_app_activities.py` stats route test, +1 existing
`test_home_yesterdays_processing.py` rewrite counted above).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Continues sprint 4 events retirement (parent req_ouq77ho6) after lode A
(commit 9a5d800b). Narrows the events reader surface area:
- git-mv think/events.py -> think/event_formatter.py (preserves history)
- update formatter registry tuple to point at think.event_formatter
- update apps/activities/routes.py:105 (sole remaining live consumer of
get_month_event_counts) to import from think.event_formatter
- remove `sol call journal events` subcommand from think/tools/call.py
- remove get_events() LLM tool from think/tools/search.py
- drop get_events from think/indexer/__init__.py public re-exports
- update 16 test imports across tests/test_formatters.py and tests/test_events.py
- delete 7 stale tests/test_call.py tests that exercised the removed CLI subcommand
Deliberately preserved (still has live consumers):
- think/indexer/journal.py::get_events (called by apps/home + apps/activities)
- think/event_formatter.py::get_month_event_counts (called by calendar_stats)
- think/indexer/journal.py glob facets/*/events/*.jsonl (historical search)
- tests/test_events.py body and tests/test_journal_index.py::test_get_events*
Test baseline shifts from 3249/1 (lode A) to 3242/1 — the seven removed
tests directly exercised the deleted CLI handler and have no replacement
target. make ci passes at 3242 passed / 1 skipped.
Out-of-scope followups:
- Lode C: migrate apps/home and apps/activities consumers off get_events /
get_month_event_counts; then delete both functions and their tests.
- Lode D: scrub stale references in think/planner.md, talent/*.md,
skills/solstone/SKILL.md, convey/templates/app.html, and API baselines.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Three independent test-infra hardening changes bundled into one pass, all
surfaced by the install-service load diagnosis (req_4655ikra):
- Delete the order-dependent soundfile stub in tests/conftest.py. The real
library is already a top-level dep; no test relied on the fake b"fLaCfake"
payload.
- Hoist `from faster_whisper.vad import VadOptions, get_speech_timestamps`
to module scope in observe/vad.py, eliminating the partial-module cascade
risk under timeout. Retarget the 8 @patch decorators in tests/test_vad.py
to observe.vad.get_speech_timestamps accordingly. sol --help is unaffected
(transcribe is lazy-loaded via the COMMANDS registry).
- Fix 5 tests in tests/test_sense.py that leaked into tests/fixtures/journal
via observe/sense.day_path() and post-test subprocess races. Each test now
overrides _SOLSTONE_JOURNAL_OVERRIDE to tmp_path via monkeypatch (covers
both think.runner._get_journal_path and observe.sense.day_path code paths).
- Add submit_mock to test_supervisor_schedule.py::test_excludes_today to
prevent it from spawning a real background `sol think` task that keeps
writing to the fixture journal after the test returns.
Validated: two concurrent `pytest tests/ --ignore=tests/integration` runs
complete green with no FileNotFoundError races; `make test` leaves
tests/fixtures/journal/ byte-for-byte unchanged (empty find-snapshot diff).
Sprint 4 Lode A for parent req_ouq77ho6.
Retire the occurrence/event producer side by deleting talent/timeline.md,
talent/flow.md, talent/knowledge_graph.md, talent/occurrence.md, and
talent/occurrence.py.
Scrub the occurrence-producing frontmatter from talent/meetings.md,
talent/decisions.md, talent/followups.md, and talent/messaging.md by
removing the occurrences metadata and hook.post=occurrence contract.
Create think/_extraction_utils.py with only log_extraction_failure,
move think/talents.py to import it from there, and delete think/hooks.py.
Trim tests/test_output_hooks.py per triage:
- Kept the generic post-hook, pre-hook, and template-var coverage
(~lines 50-306 and ~594-end).
- Deleted the 3 occurrence post-process tests
(drop-26-participants / keep-25 / keep-non-meeting-large).
- Deleted the 5 write_events_jsonl tests because they depended on the
deleted talent/occurrence.py or think/hooks.write_events_jsonl.
Remove the stale think/hooks.py stealth-writes row from
docs/coding-standards.md.
Refresh the API baselines and 5 collateral tests to reflect the deleted
talents and the scrubbed frontmatter on the surviving activity talents.
talent/schedule.md and talent/schedule.py were verified untouched,
correcting the CPO spec error called out in section 3.
make ci now passes at 3249 passed / 1 skipped, which is -8 from the
Sprint 3 baseline of 3257 tests.
Out-of-scope followups for the remaining lodes:
- Lode B: think/events.py rename and formatters registry cleanup.
- Lode C: read-side cleanup in apps/, stats, and briefing surfaces.
- Lode D: remaining docs/baselines scrub.
Replaces the schedule 2-pass flow (markdown → anticipation post-hook re-extracts
JSON) with a single-pass JSON-output talent processed by a new schedule post-hook
that writes activity records with source: "anticipated" directly to
facets/{facet}/activities/{target_date}.jsonl.
Key changes:
- New talent/schedule.py post-hook parses JSON result, validates per-item, and
writes via append_activity_record (atomic fcntl-locked dedup by stable ID).
- New dedup_anticipation helper + ANTICIPATION_FUZZY_THRESHOLD (0.85) in
think/activities.py supersedes pending rows whose title drifted between runs.
- Cancelled calendar events captured with cancelled: true and hidden: true.
- Home today-card merges anticipated activities into events and suppresses them
from the recent-activity strip to avoid double display.
- activities list gains --source {anticipated,user,cogitate} filter.
- Sweep replaces 16 stale sol call calendar references across talents, skills,
and routine templates with sol call activities list equivalents.
- Deletes talent/anticipation.{md,py}; removes anticipation from settings
extraction-exposure tuples; updates fixtures, docs, and API baselines.
Closes Sprint 3 of the schedule→activities refactor (req_hfwtzduq). Parallel
investigation of the schedule dup-bomb (req_b46jht3a) is closed by the
structural fix (atomic ID-dedup under fcntl lock).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint 2 Part B (req_izc53wlf, founder-approved 2026-04-18). Adds the daily
cogitate talent that reads per-span narrative .md files and creates missed
activity records (in-person meetings, phone calls, brief interactions).
Wires two new template vars into _build_prompt_context:
- $facet: bare facet name (e.g., \"work\")
- $activity_md_dir: {journal}/facets/{facet}/activities/{day}/
Both vars are populated only when a facet is dispatched with a day, matching
production semantics. The existing $facets (plural rich markdown) variable is
unchanged.
The talent slots in at priority 30, between events agents (priority 10) and
entity analysis (priority 40+). multi_facet: true fans out per active facet.
Note: the founder-approved prompt instructs the talent to include a
`participation` field on `sol call activities create` payloads, but the CLI
does not currently ingest that field — it falls through silently to
`active_entities: []`. Tracked as a follow-up; prompt body intentionally
matches the approved source verbatim.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Use a simple case-insensitive substring match against active_entities because there is no existing CLI entity-filter precedent in-tree.
Split `make ci` into `make install-checks` (fast deterministic gates —
format, ruff, rename gate, layer-hygiene, mypy) and `make verify`
(install-checks + tests). `make ci` still exists and runs the full
sequence; `install-service` upgrade now gates on `install-checks`
instead of the full `ci` suite, so tests can't flake under real
service load and abort the upgrade.
Raise global pytest timeout from 5s to 15s and add
@pytest.mark.timeout(30) to test_make_skills_idempotent, which is
subprocess-heavy and occasionally slow on a quiet box.
Co-authored-by: Codex <codex@openai.com>
Introduce a per-span participation talent that produces structured
participation records (role, source, confidence, context, entity_id)
for each activity, consolidating per-segment Sense entity drafts.
Three coordinated changes:
1. Extend talent/sense.md entities schema with role (attendee|mentioned) and source (voice|speaker_label|transcript|screen|other), plus a prompt-level contamination guard for screen-only names and tool/app UI.
2. Add talent/participation.md (quality-judge, activity-scheduled,
tier 3) and talent/participation.py post-hook. The hook parses
the LLM JSON, resolves entities read-only via
think.entities.matching.find_matching_entity, and atomically
merges participation + participation_confidence onto the
activity record.
3. Generalize think/activities.update_record_description into
update_record_fields, preserving the atomic tempfile+rename
write. update_record_description becomes a thin wrapper.
Sense drafts reach participation via think/sense_splitter.py, which
now also writes a sense.md summary alongside sense.json. This was
the only viable load path because think/cluster.py only discovers
talents/**/*.md — sense.json is invisible to the existing load
mechanism. The alternative (widening cluster) was rejected as out
of scope.
active_entities on activity records is preserved unchanged for
backward compatibility. The state-machine flatten in
think/activity_state_machine.py is untouched.
The entity resolver is read-only: it calls find_matching_entity
against already-loaded entities and injects entity_id (or null) per
participation entry. It never writes under facets/{facet}/entities/.
Tests cover: Sense schema, contamination guard prose, participation
frontmatter/prompt, resolver file-count invariance, contamination
regression at the plumbing layer, and atomic activity-record merge
including malformed-JSON early return.
Co-authored-by: Codex <codex@openai.com>
Closes the gap left by the dream→think rename: existing journals' config/schedules.json
still has stale `cmd: ["sol","dream","--weekly","-v"]` entries that now fail with
"Unknown command: dream". This one-shot maint task, run by the supervisor's
run_pending_tasks() before Callosum and scheduler start, rewrites cmd[1] from
"dream" to "think" for any schedule entry whose cmd starts with ["sol","dream"].
- Matches only dict entries with a list-typed cmd of length ≥ 2 starting with
["sol","dream"]; preserves all other fields and top-level scalars.
- Atomic write via tempfile.mkstemp + Path.replace, mirroring
think/scheduler.py:register_defaults(). tmp file cleaned up on BaseException.
- No-op on missing file, empty file, malformed JSON, or already-clean config
(exits 0 with an informative skipped_reason summary).
- --dry-run reports the planned rewrite without writing.
Follow-up to fc3a189e — the rename scope called for updating the
sol.py help group label from 'Think (daily processing)' to
'Think (AI processing)' alongside the dream → think rename; this
one-liner was missed by the lode.
Clean-break rename of the background-analysis pipeline. No aliases, no
compat shims: `sol dream` is gone, `think.dream` is unimportable.
- Subcommand `sol dream` → `sol think`
- Module `think/dream.py` → `think/thinking.py` (asymmetric by design:
verb is `think`, module noun is `thinking`, class is
`ThinkingJSONLWriter`)
- Callosum tract `"dream"` → `"think"`
- Health sidecar filenames drop `_dream`: `{ref}_{mode}.jsonl` and
`{ref}_{mode}.log` (no `_think` replacement)
- `apps/entities/events.py` dead `dream.generators_completed` listener
removed (handler subscribed to an event the pipeline never emitted)
Pre-rename journal logs under `journal/**/*_dream.{log,jsonl}` are left
untouched as historical artifacts (accepted history blackout).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lifts the deep-merge algorithm from apps/speakers/bootstrap.py:merge_names
into think/entities/merge.py as a slug-keyed primitive merge_entity(...),
exposed via sol call entities merge SOURCE_SLUG TARGET_SLUG. Default-safe
(--no-commit); --commit required to mutate. Reduces sol call speakers
merge-names to a name-resolving shim that preserves its existing contract
including the any-principal rejection.
Policy deltas vs. the original merge_names:
- source-principal + target-not: transfers principal to target
- both-principal: error, no mutation
- --no-keep-source-as-aka: skips adding source display name to target.aka
while still merging source's own aka list
- rejects the merge if any other entity's aka list references the source
slug or display name, listing every offending entity id
Lifts _dedupe_akas / _dedupe_emails / _dedupe_observations from
think/merge.py:_merge_entities into think/entities/merge.py so merge_journals
and merge_entity share one implementation.
Extracts voiceprint I/O (load_existing_voiceprint_keys,
save_voiceprints_batch, save_voiceprints_safely, normalize_embedding,
load_entity_voiceprints_file) into think/entities/voiceprints.py so the
entity merge primitive does not reach up into apps/speakers.
Audit-log line appended to journal/logs/entity-merges.jsonl on successful
commit only; dry-run prints a plan JSON with would_* counts and zero disk
mutation.
Updates the L2 write-owner table in docs/coding-standards.md.
The talents-rename lode (commit 0050be8c) left a read-side shim
accepting legacy agent.* chronicle event names through a stated
2026-05-01 sunset. That sunset assumed chronicle JSONL would be
purged by think/retention.py before the cutoff — not true.
Chronicle events are retained indefinitely, so the shim would
have lived forever as-is. Removing it now as a clean break
rather than carrying dead code.
think/pipeline_health.py and apps/home/routes.py drop the
dual-value set-membership checks against {"agent.X", "talent.X"}
in favor of single-value == comparisons on talent.* — idiomatic
with the surrounding branches in each file.
scripts/gate_agents_rename.py drops its SHIM_FILES / SHIM_WINDOW
suppression for those two files; iter_lines() collapses to a
plain enumerate. The gate now scans them as ordinary production
code with zero new allowlist entries.
Three tests that existed solely to enforce the shim (and whose
names announced as much) are deleted:
- test_newsletter_attempts_accept_legacy_agent_fail
- test_failure_reader_accepts_legacy_and_new_names
- test_reader_accepts_legacy_and_new_event_names
Pre-rename agent.* chronicle events on disk become ignored —
acceptable per founder decision.
Co-authored-by: Codex <codex@openai.com>
Two unrelated-to-shim-removal pre-existing flakes surfaced during
baseline `make ci` verification and blocked the shim-removal lode.
Both are test-only fixes.
test_entity_intelligence was depending on a gitignored, shared
tests/fixtures/journal/indexer/journal.sqlite whose contents were
only populated if an earlier test happened to run scan_journal
against that fixture first. Replaced the autouse fixture with the
repo-standard journal_copy + scan_journal(..., full=True) pattern
so each test builds its own deterministic index.
test_cortex_client::test_wait_for_agents_missed_event_recovery had
a race between unlinking {use_id}_active.jsonl and writing
{use_id}.jsonl. Under make ci's coverage overhead, wait_for_uses()
could observe the gap and return empty completed. Reordered the
two ops so the terminal file exists before the active marker is
removed — matches the real production invariant relied on by
_find_use_file().
Co-authored-by: Codex <codex@openai.com>
Swap "run with no flags" from destructive to preview across four CLI
surfaces, so defaults are safe per docs/coding-standards.md §L4/§L5.
- apps/speakers/call.py: bootstrap, resolve-names, attribute-segment,
backfill, seed-from-imports now take --commit (default False) instead
of --dry-run. Wrappers translate to dry_run=not commit. For
attribute-segment, --save/--no-save and --accumulate/--no-accumulate
only fire when --commit is also passed.
- apps/settings/call.py: keys validate is read-only by default; pass
--cache-result to persist providers.key_validation. Stdout shape is
unchanged.
- apps/todos/call.py: remove check-nudges; add list-nudges-due (pure
read, supports --json) and dispatch-nudges (old write path intact).
Shared _due_nudges() enumerates due-and-unnotified items.
- apps/import/call.py: rename resolve-facet -> resolve-staged-facet,
replace positional action with mutually-exclusive --apply / --skip.
Python fn renamed to apply_staged_facet (no read-verb segments).
Layer-hygiene ALLOWLIST is now empty; the import-resolve-facet entry
and its TODO comment are gone. docs/SOLCLI.md and talent docs
(review.md, review_facets.md) updated to new command names and flags.
Python API signatures unchanged. No shims/aliases. No changes to
confirm-owner, talent/speaker_attribution.py, apps/settings/routes.py,
keys_set/keys_clear, or resolve-entity/config/config-all.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prevent future sessions from extrapolating the 7-day default to chronicle
JSONL or other derived journal data. The talents-rename shim sunset was
reasoned from exactly this misreading.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mechanical relocation out of think/importers/shared.py into a new
think/entities/seeding.py module. No behavior change — function body
is byte-for-byte identical.
- Five importer modules now import seed_entities from
think.entities.seeding: kindle, documents, granola, ics, obsidian.
- Three function-scoped test imports in tests/test_importer_granola.py
updated to the new path.
- Mock/patch targets in test_importer_obsidian_sync.py and
test_importer_documents.py intentionally unchanged: they patch the
caller's namespace, which is still valid after the move.
- Removed the "think/importers/shared.py": "V2" allowlist entry in
scripts/check_layer_hygiene.py (the file no longer contains a
domain-write function). Kept the generic allowlist framing.
Restores the L2 invariant that only think/entities/saving.py and
apps/entities/call.py own entity-write code paths by making the
write-through-a-domain-call visible at every importer callsite.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract `consolidate_segment_entities` out of `think/indexer/journal.py`
into `think/entities/consolidation.py` as `consolidate_detected_entities`.
Insert a `find_matching_entity` gate (threshold 85) before any new slug
is written, and route all writes through `save_journal_entity` instead
of a direct `atomic_write`. Remove the implicit call from `scan_journal`
so it is pure with respect to `journal/entities/` state. Expose the
operation as `sol call entities consolidate [--full]`.
Also relocates `is_noise_entity` + `_NOISE_ENTITY_RE` to
`think/entities/core.py` so the new module does not cross into
`think.indexer`, and updates `apps/graph/routes.py` to follow.
This closes V1 in the layer-hygiene allowlist. Historical duplicates
(8 Jeremie-variant entities on the live journal) are NOT cleaned up
here — a separate merge pass will handle them.
Regression coverage: `test_scan_journal_is_pure_wrt_entity_state` in
`tests/test_journal_index.py` seeds a detection file and snapshots
`journal/entities/` across two full scans. Five unit/CLI tests in
`tests/test_entities_consolidation.py` cover create, fuzzy-skip,
unrelated-create, idempotence, and CLI dispatch.
Parent plan: vpe/workspace/plan-bundle-a-entity-write-ownership.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Delete `get_or_create_journal_entity` and replace with explicit
`create_journal_entity` alongside the existing reader `load_journal_entity`.
Every caller now composes the two at the call site so writes are visible
in the caller's own source:
load_journal_entity(id) or create_journal_entity(id, ...)
This is the L2 foundation of the entity-write-ownership refactor.
Rewritten call sites:
- think/importers/shared.py (seed_entities)
- apps/speakers/bootstrap.py (bootstrap_voiceprints)
- apps/speakers/discovery.py (identify_cluster — reuses existing load)
- apps/speakers/owner.py (confirm_owner_candidate)
- apps/entities/call.py (attach_entity)
- think/entities/saving.py (_save_entities_attached)
- warn on JSONL sidecar failures in think/talents.py and think/dream.py
- surface finish-log scan, facet metadata, dedup, and settings/sol log read failures
- keep exception narrowing scoped to the audited sites only
- drop the record_exchange entity-extraction call
- remove the private _extract_entities helper entirely
- trim the stale record_exchange docstring note
`sol dream --refresh` re-fires post_process on existing outputs; call
out the natural-key-dedup and atomic-replace patterns, and cite V6 from
the 2026-04-17 layer-violations audit as the cautionary example.
Route both observer metadata writers (save_observer, increment_stat)
through atomic_write so a crash between truncate and rename cannot leave
the observer JSON empty or partial. The observer JSON holds the remote
pairing auth key; truncation there bricks sync until the remote device is
re-enrolled. Final-path 0o600 permissions and the existing return /
exception contracts are preserved.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Codify the layer-hygiene invariants from the April 2026 solstone
layer-violations audit as a new "Layer Hygiene" section in
docs/coding-standards.md: read paths pure, domain writes owned by one
module per domain, infrastructure modules never mutate cross-cutting
state, CLI read-verbs stay read-only.
Add scripts/check_layer_hygiene.py — low-bar grep lint that warns when
code in think/{indexer,importers,search,graph}/ or a read-verb handler
in apps/*/call.py performs a direct write against journal/entities,
journal/facets, or journal/observations. Known audit-flagged files
(V1 think/indexer/journal.py, V2 think/importers/shared.py) are
allowlisted with TODOs that disappear as Bundle A lands. First run
surfaced apps/import/call.py's resolve-facet command as a new
read-verb-name / write-body conflict — allowlisted for CPO/VPE
disposition.
Wired into make ci between gate-agents-rename and mypy.
make install is now repo-local dev env only and no longer writes to
user-level aliases or skills. make install-service becomes the single
smart install-or-upgrade path that owns ~/.local/bin/sol, the global
solstone skill, and the background service.
add think.install_guard to enforce alias ownership before any
user-level mutation. cross-repo, dangling, not-a-symlink, and worktree
states hard-fail without a bypass. remove the upgrade target, make
uninstall an ambiguity error, and align the docs with the new install
split.
journal/AGENTS.md is the mandatory entry point and already carries the
identity hydration rule. Duplicating it in the skill invited drift with
no real coverage gain.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update repository docs and inline code comments to point at the journal skill and
its split reference files instead of the old docs/JOURNAL.md monolith.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove the journal seeding maint task now that the journal template is committed,
update the root agent guidance, drop the dead rename-gate allowlist, and replace
seeder coverage with journal skill/template tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Point skill installation at the journal-scoped agent directories, clean up the
legacy repo-root skill dirs, and keep reruns idempotent without rewriting
unchanged symlinks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Commit the journal-side agent stub and permanent skill symlinks so journal
checkouts can resolve the shared journal skill without relying on a maint task.
Update ignore rules to keep the template files tracked while leaving runtime
journal data ignored.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the journal layout and CLI material out of docs/JOURNAL.md into the
journal skill package, with dedicated reference files for configuration,
facets, captures, logs, storage, and CLI usage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove --basetemp=/var/tmp/pytest-solstone from pyproject.toml addopts
and add TMPDIR=/var/tmp to the Makefile instead.
The --basetemp flag pinned every pytest run to one absolute path; pytest
wipes that path on startup (documented behavior), so concurrent runs
(two hop lodes, or a lode + local make ci) destroyed each other's tmp
state mid-test. Symptom was mysterious vanishing-file errors and the
need for manual `rm -rf /var/tmp/pytest-solstone` rituals.
TMPDIR=/var/tmp gets tests onto disk (preserving the original intent of
fb0bba28 — /tmp is tmpfs and filled RAM repeatedly) while pytest's
default per-run isolation (/var/tmp/pytest-of-jer/pytest-N/) handles
concurrency correctly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Land the pre-release agents -> talents vocabulary rename in one coordinated
commit while the surface area is still cheap to change and there are no
external consumers to preserve. This keeps the product language coherent
before release and avoids shipping a half-renamed runtime, API, and journal
layout.
Key changes:
- Split the old `think/agents.py` surface into `think/talents.py` for runtime
execution and `think/providers_cli.py` for the standalone `sol providers
check` health-check CLI.
- Remove `sol agents`; add `sol providers check` for provider health, and have
Cortex spawn talents via `[sys.executable, "-m", TALENT_EXECUTION_MODULE]`
instead of shelling out through `sol`.
- Apply the vocabulary split consistently: `talent` means config/prompt
definition, `use` means one invocation (`agent_id` -> `use_id`,
`running_agents` -> `running_uses`, `agent_requests` -> `use_requests`,
and related helpers/attrs).
- Rename wire/runtime events: dream chronicle `agent.{fail,dispatch,complete,skip}`
-> `talent.*`, Callosum `agent_{started,completed}` -> `talent_*`, and
`agent_updated` -> `talent_updated`.
- Rename the day route from `/api/agents/<day>` to `/api/talents/<day>` and
return both `uses` (invocations) and `talents` (config metadata) in the
payload.
- Rename the journal layout from `<journal>/agents/` to `<journal>/talents/`
across root/day/segment/health locations and add
`apps/sol/maint/004_rename_agents_to_talents.py` for automatic live-journal
migration on supervisor startup.
- Bump the stats schema from v2 to v3 and require top-level `"talents"`.
- Keep the only allowed compatibility shim in `think/pipeline_health.py` and
`apps/home/routes.py`, where historical dream chronicle readers accept both
legacy `agent.*` and new `talent.*` event names through 2026-05-01.
- Add `scripts/gate_agents_rename.py` and wire it into `make ci` to prevent
reintroduction of legacy rename patterns.
Out-of-scope followups:
- Decompose `think/talents.py` (1,300+ lines) into submodules.
- Restructure `think/talent.py` + `think/talent_cli.py` into a `think/talent/`
package.
- Remove the historical read-side shims after the 2026-05-01 sunset.
Operator verification after supervisor restart on a live journal:
- Migration script runs at startup and renames pre-existing `<journal>/agents/`
dirs to `<journal>/talents/`.
- Cortex spawn logs show `[sys.executable, -m, think.talents]`.
- `sol providers check` reports all providers (Anthropic, OpenAI, Google,
Ollama).
- `sol agents` fails with unknown command.
- Home yesterdays-card renders on a journal that has both pre- and post-rename
chronicle events.
- `make ci` is green and the rename gate passes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a facet-registry gate in hooks.py that consults get_facets() once per write call and skips events with unknown or non-canonical facets with a WARNING log.
Add a megameeting guard in occurrence.py that rejects meeting events with more than 25 participants with a WARNING log.
Tighten the meetings.md and timeline.md prompts so orgs, projects, tools, and topics are not lifted into structured participants.
Document the existing muted-facet silent-disable behavior in docs/JOURNAL.md for agents such as entity_observer.
Add 8 focused tests in tests/test_output_hooks.py and prepend the worktree root in tests/conftest.py so pytest exercises local code.
Defensive guards against pollution patterns surfaced in the CPO journal-data-quality investigation.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Add the yesterday-processing pulse card, wire its server-side context, and cover the rendering and copy rules with focused home tests and fixtures.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Adds an optional `cwd` field on talent frontmatter with two legal values (`"journal"` default, `"repo"` escape hatch) for `type: cogitate` prompts. Threads the resolved absolute path through `prepare_config` and each provider's `CLIRunner`, and also through the cortex-side `Popen` for `sol agents`, so every cogitate subprocess lands in the correct working directory. Validation is centralized in `_validate_cwd` (`think/talent.py`) and runs at both `get_talent_configs` and `get_agent`. `talent/coder.md` opts into `cwd: "repo"`; all other cogitate talents default to `"journal"`. Generators reject `cwd` entirely. Fixes repo-relative path notes in `talent/heartbeat.md` so the agent runs correctly under journal cwd. Known gap: Google provider's `--sandbox=none` is unchanged and deferred to a follow-up lode.
Without drift refresh, existing journals keep stale AGENTS.md content forever whenever docs/JOURNAL.md changes upstream.
Decision log:
- Drift-detection mechanism: direct content compare between the journal's AGENTS.md and docs/JOURNAL.md. No sidecar, no marker file, no hash.
- Hand-edit policy: overwrite on mismatch with a stderr warning noting that prior content is recoverable via git history. AGENTS.md is a derived artifact, not a place for hand edits; per-journal customization belongs in sol/*.md.
- No-op when content already matches, so mtime is preserved.
The species prose was being copied verbatim into every journal's AGENTS.md via the seeding maint task, recreating the static-identity-in-a-doc problem the earlier identity retreat was meant to solve.
Move that prose to an authoritative Python constant in think/tools/sol.py, prepend it as a # species section in sol call identity hydrate output, and strip duplicate # {stem} headings from section bodies during hydrate composition. Individual subcommand output and on-disk sol/*.md files remain unchanged.
The heading strip is render-time only and case-insensitively matches an exact-stem first heading, so custom first-line headings in user files are preserved.
- remove the leftover talent/coding directories from the worktree\n- update chat.md to reference sol call identity instead of removed injected blocks\n- handle missing docs/JOURNAL.md through the maint task's existing one-line error path\n- refresh the sol preview API baseline for the prompt text change
Per-owner identity files (self.md, agency.md, partner.md, awareness.md,
pulse.md, pulse_output.md, identity_pulse.md, history.jsonl, news/*) were
being written into a repo-tracked sol/ directory and committed/pushed to
the public AGPL repo by talent/heartbeat.md. This is an active privacy
leak that violates sol pbc Article IV.
Five coupled moves in one pass:
1. git rm -r sol/ at repo root; /sol/ added to .gitignore.
2. {journal}/sol/ → {journal}/identity/ everywhere (code, tests, fixtures,
docs); ensure_sol_directory → ensure_identity_directory.
3. New write_identity() helper in think/identity.py — single write path
with per-directory fcntl.LOCK_EX, atomic tmpfile + os.replace, 0o600
perms, hash-based history.jsonl audit log (no diffs).
4. talent/heartbeat.md: deleted "Path notes" block; Step 6 is now a no-op
close. Other talents (awareness_tender, pulse, naming) updated to use
identity/ paths and stripped of any commit/push instructions.
5. think/prompts.py: removed SOL_DIR and the repo-root read branch;
template vars are now $identity_* loaded only from {journal}/identity/.
Acceptance greps all return 0. New tests cover write_identity()
atomicity, lock serialization, history schema, 0o600 mode, and missing-
file first writes. CLI surface (sol call identity ...) unchanged.
No data migration code (Jer is sole user, resets identity on deploy).
No backward-compat shims. Clean break.
Sprint 4 cleanup 1 (req_myu5gwm7 Lode 4xvyyikt). Previously
merged via hopper but blocked by known refine-stage stuck-lode
false-positive; applied manually from the lode worktree.
Path A — talent/sense.md: contamination guard now explicitly ties
role: attendee to meeting_detected: true for the same segment.
When meeting_detected: false, every Person must be role: mentioned
even if they spoke, were quoted, or referenced in transcript
(closes podcast / press conference / lecture leak).
Path B — talent/participation.py: post-processor clamps attendee
to mentioned when no contributing sense segment had
meeting_detected: true. Defensive net catching any Path A prompt
leakage. Logs at warning level with count + activity id for
observability.
Tests: 3 new cases in test_sense_contamination_guard.py covering
clamp positive (podcast with speakers → all mentioned), negative
pass-through (real meeting unchanged), idempotence + logging.
make test: 3254 passed, 1 skipped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Post-Sprint-4 trivials bundled:
- remove dead occurrence/has_extraction detection in settings routes
plus the now-unreachable Extract toggle UI branch in workspace.html,
and drop the corresponding extract/has_extraction keys from the
generators.json baseline; refresh adjacent docstring/copy that
referenced the removed field
- normalize the schedule generator description to the post-Sprint-3
"anticipated activity records" phrasing in talent/schedule.md and
the three API baselines that mirror it
- fix stale "calendar" in the talent/journal/references/config.md
apps.order example (now "activities")
- replace retired "occurrence" hook value with live "schedule" in
the _format_tags test data
- document the sense.json/sense.md dual-write coupling to
think/cluster.py's per-segment talents/**/*.md glob
The activity-anticipation dispatcher only loaded the current local day's
records, so a pre-alert for an early-morning D+1 activity was silently
missed when local_now was late on day D (and symmetric on the other side).
Scan yesterday/today/tomorrow gated by _ACTIVITY_ANTICIPATION_CROSSDAY_WINDOW_MINUTES
(default 120), and build start_dt from each record's own day.
Co-Authored-By: Codex <noreply@openai.com>
Sprint 4 added dict cadences ({"type": "activity-anticipation", "offset_minutes": -30}) and wired the supervisor dispatcher to handle them, but the CLI validator _validate_routine_cadence was left behind and hard-rejected every non-string cadence. That broke `sol call routines create --template meeting-prep`, forcing manual routines.json edits.
Extend the validator to accept dict cadences with a known `type`, mirroring the dispatcher's int coercion for `offset_minutes`. Add reciprocal sync comments in both the validator and the dispatcher so future cadence types get updated in both places. New tests cover the meeting-prep template, a string override of a dict default, and the missing-type / bad-offset rejection paths.
Co-Authored-By: OpenAI Codex <codex@openai.com>
The cogitate `activities_review` talent prompt has been instructing the
LLM to emit `participation[]` on the stdin payload, but the CLI's
`create` handler only read `title`/`activity`/`description`/`details` —
silently dropping the participation field on every new activity.
Extend `apps/activities/call.py::create_record` to:
- Validate an optional `participation[]` per Sprint 1's schema (role ∈
{attendee, mentioned}, source ∈ {voice, speaker_label, transcript,
screen, other}, string name required). Fail fast on the first bad
entry with an indexed stderr error and exit 1 before any record is
written.
- Resolve each entry's `entity_id` read-only via `load_entities` +
`find_matching_entity`, mirroring `talent/participation.py::post_process`.
Unmatched names store `entity_id: null`. Never writes to any entity
file.
- Persist `participation` on the new record only when the caller
provided the key (absent stays absent; empty `[]` is stored as `[]`).
- Append `"participation"` to `edits[0].fields` when the caller opted in.
Eight new CLI-level tests cover the happy path, absent vs empty, every
validation branch, the read-only resolver invariant (st_size +
st_mtime_ns snapshot), and the no-write-on-rejection contract.
No change to `update`, the cogitate prompt, `think/activities.py`, or
`talent/participation.py`.
Sprint 4 Lode D. Surgical scrub of stale references left over from Lodes A-C3c.
Runtime was already migrated; this lode removes dead-pointer debris so readers
and prompts do not follow commands, files, or features that no longer exist.
Docs:
- APPS.md: drop occurrence-hook as a live option; mark the V6 audit retired; reference implementations now cite only `talent/schedule.py`.
- THINK.md, CORTEX.md: align the hook example and Active Facet Detection description with live behavior (`think/facets.py::get_active_facets`).
- talent/journal/references/{captures,cli,config,facets,storage}.md: frame events as historical; delete the `events` subcommand section and dead `extract` config knob; add activities alongside events; swap `flow` examples for live talent names.
Talent prompts / skill (surgical swap-or-drop, no style rewrites):
- skills/solstone/SKILL.md, triage.md, joke_bot.md, decisionalizer.md, think/planner.md, apps/entities/talent/{entity_assist,entities}.md, apps/todos/talent/{daily,weekly}.md: replace `sol call journal events` with either `sol call activities list --source anticipated` (future-facing) or `sol call journal search -a meetings` (historical); drop it where no literal equivalent applies.
- think/planner.md: drop `get_events` from the tool list and replace examples with `search_journal` agent filters.
Code:
- think/tools/search.py: drop `flow` from live-agent examples in the docstring; swap `agent="flow"` to `agent="meetings"`; keep `agent="event"` as the historical example.
- convey/templates/app.html: simplify the command-hint branch to check only `sol call activities list`.
Tests + fixtures:
- tests/baselines/api/activities/day-events.json: delete the orphan baseline (endpoint removed in Lode C1).
- make update-api-baselines is zero-delta; live baselines were regenerated in `9a5d800b`. Historical `flow` / `knowledge_graph` entries in `tokens/usage.json` come from fixture talent logs and remain out of scope.
- tests/fixtures/journal/sol/briefing.md: align with the morning briefing template (`calendar_events` to `anticipated_activities`).
Keep-list invariants intact:
- `think/formatters.py:142` `facets/*/events/*.jsonl` registry entry.
- `think/event_formatter.py::format_events`.
- `think/indexer/journal.py:~250` glob.
- `tests/test_formatters.py` and `tests/test_journal_index.py` event/flow fixture coverage.
Follow-ups (flagged, not fixed):
- `apps/settings/routes.py:435-441, 931-935`: dead `hook == "occurrence"` branches and orphan `extract` field handling.
- `talent/schedule.md:5`: schedule output still says "structured anticipation records"; this leaks into three baselines but was outside Lode D scope.
3238 passed, 1 skipped, 1 warning.
- Wire {"type": "activity-anticipation", "offset_minutes": N}
cadences into think.routines.check() so routines like
meeting-prep fire at start + offset (±60s) for visible
anticipated activities across all facets on the current
local day.
- _run_routine now accepts an optional
trigger_context={"activity": ..., "facet": ...} and injects
an ## Upcoming Activity block (title, type, facet,
start/end, description, details, attendee names) before the
"Execute this routine now." line; other callers are unchanged.
- Dedup is in-memory only (_fired_triggers, pruned at 2 days
per check() tick). State does not survive supervisor restarts
— acceptable since re-firing a past trigger is worse than
missing a restart-window fire.
- Unknown dict cadence types (e.g. legacy {"type": "event"})
are silently skipped with one INFO log per routine_id via
_logged_unknown_cadence; string-cron behavior is untouched.
- Known limitations: (1) anticipated activities carry no
timezone field — the routine's timezone is assumed. (2)
negative offsets that cross midnight (e.g. a 00:15 activity
with offset_minutes=-30) will miss their trigger because the
dispatcher only scans the activity-day file. (3) dedup is
written before dispatch, so a _run_routine failure will not
auto-retry.
- Follow-up: think/tools/routines.py::_validate_routine_cadence
still rejects non-string cadences, so sol call routines
create --template meeting-prep fails until that validation is
extended to accept dict cadences (and the manual routines run
path updated to supply trigger context).
Sprint 4 Lode C3 partial — surgical rename pass for the two template-level
terminology updates that fit under the VPE playbook's "targeted surgical
fix" carve-out (pure text replacements, no design judgment, smaller than
the scoping overhead).
talent/morning_briefing.md:
- "calendar events" → "anticipated activities" across instructions, gap
examples, and the coverage-preamble example.
- YAML frontmatter source counter renamed
`sources.calendar_events` → `sources.anticipated_activities`.
- Prose references to "calendar event title" + "calendar events are
self-evident" updated to the activity vocabulary. Sprint 3 already
switched the data source to `sol call activities list --source anticipated`;
this commit aligns the prompt text with the actual data flow.
routines/templates/meeting-prep.md:
- default_cadence switched from the legacy
`{"type": "event", "trigger": "calendar", "offset_minutes": -30}`
(dead spec — never dispatched) to
`{"type": "activity-anticipation", "offset_minutes": -30}`.
- Introductory prose updated from "Upcoming Event" → "Upcoming Activity".
The activity-anticipation cadence implementation in think/routines.py is
NOT in this commit — it is a substantive feature requiring a hopper lode.
The original C3 hop (5sh7y7va) got stuck in the known hopper gated-state
false-positive (see extro pulse.md: "Hopper live server bounce needed")
and was killed without commits. Until the cadence dispatcher is
implemented, meeting-prep with this new cadence will stay dormant — same
end state as today, where the old event/calendar cadence never fired.
Follow-up: re-scope the cadence implementation as a fresh hop lode after
the hopper server is bounced (commit 2030d96 activates the false-positive
fix).
Sprint 4 status after this commit:
Lode A (9a5d800b) — shipped
Lode B (930f1d36) — shipped
Lode C1 (53762ba9) — shipped
Lode C2 (cb034248) — shipped
Lode C3 (this commit, partial) — templates updated directly; cadence
implementation deferred
Lode D — pending (docs + baselines scrub)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Bump SCHEMA_VERSION from 3 to 4 so per-day {day}/stats.json caches are invalidated on the next `sol think` and legacy events-agent labels (`meetings`/`decisions`/`followups`) do not linger in cached bars.
- Switch the dashboard pipeline to read `facets/*/activities/{day}.jsonl` via `load_activity_records`, `estimate_duration_minutes`, and `segment_parse`; bar categories now reflect activity types (`meeting`, `coding`, ...) while the per-day cache key `agent_data` is retained for cache-format compatibility and is now keyed by activity type.
- FOUNDER-REVIEW GATE: Do NOT deploy to /data/solstone or the Mac Mini until founder has pulled this branch, run `sol think --day <recent>`, and visually confirmed the rebuilt Activities chart and heatmap render correctly with activity-type labels.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint 4 Lode C1. Completes retirement of legacy reader surfaces started
in 9a5d800b (A) and 930f1d36 (B); last-known-good for revert: 930f1d36.
- apps/home: rename `_collect_events` → `_collect_anticipated_activities`;
drop `get_events` call; rename pulse context key `events` →
`anticipated_activities`; update template iteration + empty-state copy.
- apps/activities: delete `/api/day/<day>/events` route + `activities_day_events`;
replace `activities_stats` with activities-sourced stub returning
`{YYYYMMDD: {facet: count}}` via `calendar.monthrange` + `load_activity_records`.
- apps/activities/_day.html: template surgery — replace `#eventsView` with
`#timelineView`, extract timeline rendering into new `renderTimeline()`,
delete event modal + `openModal`, delete all-day event block, delete
processedEvents/column-layout/legend code, drop `/api/day/<day>/events`
fetch. File: 1470 → 974 LOC.
- convey/static/month-picker.js: tooltip wording `events` → `items`.
- think/journal_stats.py: drop `facets/*/events/*.jsonl` mtime block from
`_get_day_mtime`.
- think/indexer/journal.py: delete `get_events`. `_find_signal_files`
event glob retained — historical event files remain searchable via
`sol call journal search`.
- think/event_formatter.py: delete `get_month_event_counts`; `format_events`
remains as the only public export. Registry entry at
`think/formatters.py:142` unchanged.
- tests: delete `tests/test_events.py` (5 tests); delete
`test_get_events*` from `test_journal_index.py` (2 tests); delete
`test_facet_event_mtime_invalidates_cache` from `test_journal_stats.py`
(1 test, covered deleted functionality); rewrite Pulse collector test
in `test_home_yesterdays_processing.py` (anticipated-only assertion);
update monkeypatches in `test_home_skills.py` + `test_home_routines.py`;
add one route-level stats stub test in `tests/test_app_activities.py`;
remove `day-events` entry from `tests/verify_api.py`; regenerate
`tests/baselines/api/activities/stats-month.json` for the new shape.
Keep-list invariants intact:
- `think/formatters.py:142` event formatter registry entry
- `think/event_formatter.py::format_events`
- `think/indexer/journal.py::_find_signal_files` event glob (line ~250)
- `tests/test_formatters.py`
Deferred to Lode D: `think/planner.md` references to `get_events` at
lines 19, 46, 82 (live prompt text, but no runtime tool-wrapper exists —
misleading but non-breaking); wider docs/baselines scrub.
Test count: 3234 passed, 1 skipped (net −7 tests from the migration:
−5 `test_events.py`, −2 `test_journal_index.py::test_get_events*`,
−1 `test_journal_stats.py::test_facet_event_mtime_invalidates_cache`,
−1 rewritten-in-place in `test_home_yesterdays_processing.py`,
+1 new `test_app_activities.py` stats route test, +1 existing
`test_home_yesterdays_processing.py` rewrite counted above).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Continues sprint 4 events retirement (parent req_ouq77ho6) after lode A
(commit 9a5d800b). Narrows the events reader surface area:
- git-mv think/events.py -> think/event_formatter.py (preserves history)
- update formatter registry tuple to point at think.event_formatter
- update apps/activities/routes.py:105 (sole remaining live consumer of
get_month_event_counts) to import from think.event_formatter
- remove `sol call journal events` subcommand from think/tools/call.py
- remove get_events() LLM tool from think/tools/search.py
- drop get_events from think/indexer/__init__.py public re-exports
- update 16 test imports across tests/test_formatters.py and tests/test_events.py
- delete 7 stale tests/test_call.py tests that exercised the removed CLI subcommand
Deliberately preserved (still has live consumers):
- think/indexer/journal.py::get_events (called by apps/home + apps/activities)
- think/event_formatter.py::get_month_event_counts (called by calendar_stats)
- think/indexer/journal.py glob facets/*/events/*.jsonl (historical search)
- tests/test_events.py body and tests/test_journal_index.py::test_get_events*
Test baseline shifts from 3249/1 (lode A) to 3242/1 — the seven removed
tests directly exercised the deleted CLI handler and have no replacement
target. make ci passes at 3242 passed / 1 skipped.
Out-of-scope followups:
- Lode C: migrate apps/home and apps/activities consumers off get_events /
get_month_event_counts; then delete both functions and their tests.
- Lode D: scrub stale references in think/planner.md, talent/*.md,
skills/solstone/SKILL.md, convey/templates/app.html, and API baselines.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Three independent test-infra hardening changes bundled into one pass, all
surfaced by the install-service load diagnosis (req_4655ikra):
- Delete the order-dependent soundfile stub in tests/conftest.py. The real
library is already a top-level dep; no test relied on the fake b"fLaCfake"
payload.
- Hoist `from faster_whisper.vad import VadOptions, get_speech_timestamps`
to module scope in observe/vad.py, eliminating the partial-module cascade
risk under timeout. Retarget the 8 @patch decorators in tests/test_vad.py
to observe.vad.get_speech_timestamps accordingly. sol --help is unaffected
(transcribe is lazy-loaded via the COMMANDS registry).
- Fix 5 tests in tests/test_sense.py that leaked into tests/fixtures/journal
via observe/sense.day_path() and post-test subprocess races. Each test now
overrides _SOLSTONE_JOURNAL_OVERRIDE to tmp_path via monkeypatch (covers
both think.runner._get_journal_path and observe.sense.day_path code paths).
- Add submit_mock to test_supervisor_schedule.py::test_excludes_today to
prevent it from spawning a real background `sol think` task that keeps
writing to the fixture journal after the test returns.
Validated: two concurrent `pytest tests/ --ignore=tests/integration` runs
complete green with no FileNotFoundError races; `make test` leaves
tests/fixtures/journal/ byte-for-byte unchanged (empty find-snapshot diff).
Sprint 4 Lode A for parent req_ouq77ho6.
Retire the occurrence/event producer side by deleting talent/timeline.md,
talent/flow.md, talent/knowledge_graph.md, talent/occurrence.md, and
talent/occurrence.py.
Scrub the occurrence-producing frontmatter from talent/meetings.md,
talent/decisions.md, talent/followups.md, and talent/messaging.md by
removing the occurrences metadata and hook.post=occurrence contract.
Create think/_extraction_utils.py with only log_extraction_failure,
move think/talents.py to import it from there, and delete think/hooks.py.
Trim tests/test_output_hooks.py per triage:
- Kept the generic post-hook, pre-hook, and template-var coverage
(~lines 50-306 and ~594-end).
- Deleted the 3 occurrence post-process tests
(drop-26-participants / keep-25 / keep-non-meeting-large).
- Deleted the 5 write_events_jsonl tests because they depended on the
deleted talent/occurrence.py or think/hooks.write_events_jsonl.
Remove the stale think/hooks.py stealth-writes row from
docs/coding-standards.md.
Refresh the API baselines and 5 collateral tests to reflect the deleted
talents and the scrubbed frontmatter on the surviving activity talents.
talent/schedule.md and talent/schedule.py were verified untouched,
correcting the CPO spec error called out in section 3.
make ci now passes at 3249 passed / 1 skipped, which is -8 from the
Sprint 3 baseline of 3257 tests.
Out-of-scope followups for the remaining lodes:
- Lode B: think/events.py rename and formatters registry cleanup.
- Lode C: read-side cleanup in apps/, stats, and briefing surfaces.
- Lode D: remaining docs/baselines scrub.
Replaces the schedule 2-pass flow (markdown → anticipation post-hook re-extracts
JSON) with a single-pass JSON-output talent processed by a new schedule post-hook
that writes activity records with source: "anticipated" directly to
facets/{facet}/activities/{target_date}.jsonl.
Key changes:
- New talent/schedule.py post-hook parses JSON result, validates per-item, and
writes via append_activity_record (atomic fcntl-locked dedup by stable ID).
- New dedup_anticipation helper + ANTICIPATION_FUZZY_THRESHOLD (0.85) in
think/activities.py supersedes pending rows whose title drifted between runs.
- Cancelled calendar events captured with cancelled: true and hidden: true.
- Home today-card merges anticipated activities into events and suppresses them
from the recent-activity strip to avoid double display.
- activities list gains --source {anticipated,user,cogitate} filter.
- Sweep replaces 16 stale sol call calendar references across talents, skills,
and routine templates with sol call activities list equivalents.
- Deletes talent/anticipation.{md,py}; removes anticipation from settings
extraction-exposure tuples; updates fixtures, docs, and API baselines.
Closes Sprint 3 of the schedule→activities refactor (req_hfwtzduq). Parallel
investigation of the schedule dup-bomb (req_b46jht3a) is closed by the
structural fix (atomic ID-dedup under fcntl lock).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint 2 Part B (req_izc53wlf, founder-approved 2026-04-18). Adds the daily
cogitate talent that reads per-span narrative .md files and creates missed
activity records (in-person meetings, phone calls, brief interactions).
Wires two new template vars into _build_prompt_context:
- $facet: bare facet name (e.g., \"work\")
- $activity_md_dir: {journal}/facets/{facet}/activities/{day}/
Both vars are populated only when a facet is dispatched with a day, matching
production semantics. The existing $facets (plural rich markdown) variable is
unchanged.
The talent slots in at priority 30, between events agents (priority 10) and
entity analysis (priority 40+). multi_facet: true fans out per active facet.
Note: the founder-approved prompt instructs the talent to include a
`participation` field on `sol call activities create` payloads, but the CLI
does not currently ingest that field — it falls through silently to
`active_entities: []`. Tracked as a follow-up; prompt body intentionally
matches the approved source verbatim.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Split `make ci` into `make install-checks` (fast deterministic gates —
format, ruff, rename gate, layer-hygiene, mypy) and `make verify`
(install-checks + tests). `make ci` still exists and runs the full
sequence; `install-service` upgrade now gates on `install-checks`
instead of the full `ci` suite, so tests can't flake under real
service load and abort the upgrade.
Raise global pytest timeout from 5s to 15s and add
@pytest.mark.timeout(30) to test_make_skills_idempotent, which is
subprocess-heavy and occasionally slow on a quiet box.
Co-authored-by: Codex <codex@openai.com>
Introduce a per-span participation talent that produces structured
participation records (role, source, confidence, context, entity_id)
for each activity, consolidating per-segment Sense entity drafts.
Three coordinated changes:
1. Extend talent/sense.md entities schema with role (attendee|mentioned) and source (voice|speaker_label|transcript|screen|other), plus a prompt-level contamination guard for screen-only names and tool/app UI.
2. Add talent/participation.md (quality-judge, activity-scheduled,
tier 3) and talent/participation.py post-hook. The hook parses
the LLM JSON, resolves entities read-only via
think.entities.matching.find_matching_entity, and atomically
merges participation + participation_confidence onto the
activity record.
3. Generalize think/activities.update_record_description into
update_record_fields, preserving the atomic tempfile+rename
write. update_record_description becomes a thin wrapper.
Sense drafts reach participation via think/sense_splitter.py, which
now also writes a sense.md summary alongside sense.json. This was
the only viable load path because think/cluster.py only discovers
talents/**/*.md — sense.json is invisible to the existing load
mechanism. The alternative (widening cluster) was rejected as out
of scope.
active_entities on activity records is preserved unchanged for
backward compatibility. The state-machine flatten in
think/activity_state_machine.py is untouched.
The entity resolver is read-only: it calls find_matching_entity
against already-loaded entities and injects entity_id (or null) per
participation entry. It never writes under facets/{facet}/entities/.
Tests cover: Sense schema, contamination guard prose, participation
frontmatter/prompt, resolver file-count invariance, contamination
regression at the plumbing layer, and atomic activity-record merge
including malformed-JSON early return.
Co-authored-by: Codex <codex@openai.com>
Closes the gap left by the dream→think rename: existing journals' config/schedules.json
still has stale `cmd: ["sol","dream","--weekly","-v"]` entries that now fail with
"Unknown command: dream". This one-shot maint task, run by the supervisor's
run_pending_tasks() before Callosum and scheduler start, rewrites cmd[1] from
"dream" to "think" for any schedule entry whose cmd starts with ["sol","dream"].
- Matches only dict entries with a list-typed cmd of length ≥ 2 starting with
["sol","dream"]; preserves all other fields and top-level scalars.
- Atomic write via tempfile.mkstemp + Path.replace, mirroring
think/scheduler.py:register_defaults(). tmp file cleaned up on BaseException.
- No-op on missing file, empty file, malformed JSON, or already-clean config
(exits 0 with an informative skipped_reason summary).
- --dry-run reports the planned rewrite without writing.
Clean-break rename of the background-analysis pipeline. No aliases, no
compat shims: `sol dream` is gone, `think.dream` is unimportable.
- Subcommand `sol dream` → `sol think`
- Module `think/dream.py` → `think/thinking.py` (asymmetric by design:
verb is `think`, module noun is `thinking`, class is
`ThinkingJSONLWriter`)
- Callosum tract `"dream"` → `"think"`
- Health sidecar filenames drop `_dream`: `{ref}_{mode}.jsonl` and
`{ref}_{mode}.log` (no `_think` replacement)
- `apps/entities/events.py` dead `dream.generators_completed` listener
removed (handler subscribed to an event the pipeline never emitted)
Pre-rename journal logs under `journal/**/*_dream.{log,jsonl}` are left
untouched as historical artifacts (accepted history blackout).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lifts the deep-merge algorithm from apps/speakers/bootstrap.py:merge_names
into think/entities/merge.py as a slug-keyed primitive merge_entity(...),
exposed via sol call entities merge SOURCE_SLUG TARGET_SLUG. Default-safe
(--no-commit); --commit required to mutate. Reduces sol call speakers
merge-names to a name-resolving shim that preserves its existing contract
including the any-principal rejection.
Policy deltas vs. the original merge_names:
- source-principal + target-not: transfers principal to target
- both-principal: error, no mutation
- --no-keep-source-as-aka: skips adding source display name to target.aka
while still merging source's own aka list
- rejects the merge if any other entity's aka list references the source
slug or display name, listing every offending entity id
Lifts _dedupe_akas / _dedupe_emails / _dedupe_observations from
think/merge.py:_merge_entities into think/entities/merge.py so merge_journals
and merge_entity share one implementation.
Extracts voiceprint I/O (load_existing_voiceprint_keys,
save_voiceprints_batch, save_voiceprints_safely, normalize_embedding,
load_entity_voiceprints_file) into think/entities/voiceprints.py so the
entity merge primitive does not reach up into apps/speakers.
Audit-log line appended to journal/logs/entity-merges.jsonl on successful
commit only; dry-run prints a plan JSON with would_* counts and zero disk
mutation.
Updates the L2 write-owner table in docs/coding-standards.md.
The talents-rename lode (commit 0050be8c) left a read-side shim
accepting legacy agent.* chronicle event names through a stated
2026-05-01 sunset. That sunset assumed chronicle JSONL would be
purged by think/retention.py before the cutoff — not true.
Chronicle events are retained indefinitely, so the shim would
have lived forever as-is. Removing it now as a clean break
rather than carrying dead code.
think/pipeline_health.py and apps/home/routes.py drop the
dual-value set-membership checks against {"agent.X", "talent.X"}
in favor of single-value == comparisons on talent.* — idiomatic
with the surrounding branches in each file.
scripts/gate_agents_rename.py drops its SHIM_FILES / SHIM_WINDOW
suppression for those two files; iter_lines() collapses to a
plain enumerate. The gate now scans them as ordinary production
code with zero new allowlist entries.
Three tests that existed solely to enforce the shim (and whose
names announced as much) are deleted:
- test_newsletter_attempts_accept_legacy_agent_fail
- test_failure_reader_accepts_legacy_and_new_names
- test_reader_accepts_legacy_and_new_event_names
Pre-rename agent.* chronicle events on disk become ignored —
acceptable per founder decision.
Co-authored-by: Codex <codex@openai.com>
Two unrelated-to-shim-removal pre-existing flakes surfaced during
baseline `make ci` verification and blocked the shim-removal lode.
Both are test-only fixes.
test_entity_intelligence was depending on a gitignored, shared
tests/fixtures/journal/indexer/journal.sqlite whose contents were
only populated if an earlier test happened to run scan_journal
against that fixture first. Replaced the autouse fixture with the
repo-standard journal_copy + scan_journal(..., full=True) pattern
so each test builds its own deterministic index.
test_cortex_client::test_wait_for_agents_missed_event_recovery had
a race between unlinking {use_id}_active.jsonl and writing
{use_id}.jsonl. Under make ci's coverage overhead, wait_for_uses()
could observe the gap and return empty completed. Reordered the
two ops so the terminal file exists before the active marker is
removed — matches the real production invariant relied on by
_find_use_file().
Co-authored-by: Codex <codex@openai.com>
Swap "run with no flags" from destructive to preview across four CLI
surfaces, so defaults are safe per docs/coding-standards.md §L4/§L5.
- apps/speakers/call.py: bootstrap, resolve-names, attribute-segment,
backfill, seed-from-imports now take --commit (default False) instead
of --dry-run. Wrappers translate to dry_run=not commit. For
attribute-segment, --save/--no-save and --accumulate/--no-accumulate
only fire when --commit is also passed.
- apps/settings/call.py: keys validate is read-only by default; pass
--cache-result to persist providers.key_validation. Stdout shape is
unchanged.
- apps/todos/call.py: remove check-nudges; add list-nudges-due (pure
read, supports --json) and dispatch-nudges (old write path intact).
Shared _due_nudges() enumerates due-and-unnotified items.
- apps/import/call.py: rename resolve-facet -> resolve-staged-facet,
replace positional action with mutually-exclusive --apply / --skip.
Python fn renamed to apply_staged_facet (no read-verb segments).
Layer-hygiene ALLOWLIST is now empty; the import-resolve-facet entry
and its TODO comment are gone. docs/SOLCLI.md and talent docs
(review.md, review_facets.md) updated to new command names and flags.
Python API signatures unchanged. No shims/aliases. No changes to
confirm-owner, talent/speaker_attribution.py, apps/settings/routes.py,
keys_set/keys_clear, or resolve-entity/config/config-all.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mechanical relocation out of think/importers/shared.py into a new
think/entities/seeding.py module. No behavior change — function body
is byte-for-byte identical.
- Five importer modules now import seed_entities from
think.entities.seeding: kindle, documents, granola, ics, obsidian.
- Three function-scoped test imports in tests/test_importer_granola.py
updated to the new path.
- Mock/patch targets in test_importer_obsidian_sync.py and
test_importer_documents.py intentionally unchanged: they patch the
caller's namespace, which is still valid after the move.
- Removed the "think/importers/shared.py": "V2" allowlist entry in
scripts/check_layer_hygiene.py (the file no longer contains a
domain-write function). Kept the generic allowlist framing.
Restores the L2 invariant that only think/entities/saving.py and
apps/entities/call.py own entity-write code paths by making the
write-through-a-domain-call visible at every importer callsite.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract `consolidate_segment_entities` out of `think/indexer/journal.py`
into `think/entities/consolidation.py` as `consolidate_detected_entities`.
Insert a `find_matching_entity` gate (threshold 85) before any new slug
is written, and route all writes through `save_journal_entity` instead
of a direct `atomic_write`. Remove the implicit call from `scan_journal`
so it is pure with respect to `journal/entities/` state. Expose the
operation as `sol call entities consolidate [--full]`.
Also relocates `is_noise_entity` + `_NOISE_ENTITY_RE` to
`think/entities/core.py` so the new module does not cross into
`think.indexer`, and updates `apps/graph/routes.py` to follow.
This closes V1 in the layer-hygiene allowlist. Historical duplicates
(8 Jeremie-variant entities on the live journal) are NOT cleaned up
here — a separate merge pass will handle them.
Regression coverage: `test_scan_journal_is_pure_wrt_entity_state` in
`tests/test_journal_index.py` seeds a detection file and snapshots
`journal/entities/` across two full scans. Five unit/CLI tests in
`tests/test_entities_consolidation.py` cover create, fuzzy-skip,
unrelated-create, idempotence, and CLI dispatch.
Parent plan: vpe/workspace/plan-bundle-a-entity-write-ownership.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Delete `get_or_create_journal_entity` and replace with explicit
`create_journal_entity` alongside the existing reader `load_journal_entity`.
Every caller now composes the two at the call site so writes are visible
in the caller's own source:
load_journal_entity(id) or create_journal_entity(id, ...)
This is the L2 foundation of the entity-write-ownership refactor.
Rewritten call sites:
- think/importers/shared.py (seed_entities)
- apps/speakers/bootstrap.py (bootstrap_voiceprints)
- apps/speakers/discovery.py (identify_cluster — reuses existing load)
- apps/speakers/owner.py (confirm_owner_candidate)
- apps/entities/call.py (attach_entity)
- think/entities/saving.py (_save_entities_attached)
Route both observer metadata writers (save_observer, increment_stat)
through atomic_write so a crash between truncate and rename cannot leave
the observer JSON empty or partial. The observer JSON holds the remote
pairing auth key; truncation there bricks sync until the remote device is
re-enrolled. Final-path 0o600 permissions and the existing return /
exception contracts are preserved.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Codify the layer-hygiene invariants from the April 2026 solstone
layer-violations audit as a new "Layer Hygiene" section in
docs/coding-standards.md: read paths pure, domain writes owned by one
module per domain, infrastructure modules never mutate cross-cutting
state, CLI read-verbs stay read-only.
Add scripts/check_layer_hygiene.py — low-bar grep lint that warns when
code in think/{indexer,importers,search,graph}/ or a read-verb handler
in apps/*/call.py performs a direct write against journal/entities,
journal/facets, or journal/observations. Known audit-flagged files
(V1 think/indexer/journal.py, V2 think/importers/shared.py) are
allowlisted with TODOs that disappear as Bundle A lands. First run
surfaced apps/import/call.py's resolve-facet command as a new
read-verb-name / write-body conflict — allowlisted for CPO/VPE
disposition.
Wired into make ci between gate-agents-rename and mypy.
make install is now repo-local dev env only and no longer writes to
user-level aliases or skills. make install-service becomes the single
smart install-or-upgrade path that owns ~/.local/bin/sol, the global
solstone skill, and the background service.
add think.install_guard to enforce alias ownership before any
user-level mutation. cross-repo, dangling, not-a-symlink, and worktree
states hard-fail without a bypass. remove the upgrade target, make
uninstall an ambiguity error, and align the docs with the new install
split.
Commit the journal-side agent stub and permanent skill symlinks so journal
checkouts can resolve the shared journal skill without relying on a maint task.
Update ignore rules to keep the template files tracked while leaving runtime
journal data ignored.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove --basetemp=/var/tmp/pytest-solstone from pyproject.toml addopts
and add TMPDIR=/var/tmp to the Makefile instead.
The --basetemp flag pinned every pytest run to one absolute path; pytest
wipes that path on startup (documented behavior), so concurrent runs
(two hop lodes, or a lode + local make ci) destroyed each other's tmp
state mid-test. Symptom was mysterious vanishing-file errors and the
need for manual `rm -rf /var/tmp/pytest-solstone` rituals.
TMPDIR=/var/tmp gets tests onto disk (preserving the original intent of
fb0bba28 — /tmp is tmpfs and filled RAM repeatedly) while pytest's
default per-run isolation (/var/tmp/pytest-of-jer/pytest-N/) handles
concurrency correctly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Land the pre-release agents -> talents vocabulary rename in one coordinated
commit while the surface area is still cheap to change and there are no
external consumers to preserve. This keeps the product language coherent
before release and avoids shipping a half-renamed runtime, API, and journal
layout.
Key changes:
- Split the old `think/agents.py` surface into `think/talents.py` for runtime
execution and `think/providers_cli.py` for the standalone `sol providers
check` health-check CLI.
- Remove `sol agents`; add `sol providers check` for provider health, and have
Cortex spawn talents via `[sys.executable, "-m", TALENT_EXECUTION_MODULE]`
instead of shelling out through `sol`.
- Apply the vocabulary split consistently: `talent` means config/prompt
definition, `use` means one invocation (`agent_id` -> `use_id`,
`running_agents` -> `running_uses`, `agent_requests` -> `use_requests`,
and related helpers/attrs).
- Rename wire/runtime events: dream chronicle `agent.{fail,dispatch,complete,skip}`
-> `talent.*`, Callosum `agent_{started,completed}` -> `talent_*`, and
`agent_updated` -> `talent_updated`.
- Rename the day route from `/api/agents/<day>` to `/api/talents/<day>` and
return both `uses` (invocations) and `talents` (config metadata) in the
payload.
- Rename the journal layout from `<journal>/agents/` to `<journal>/talents/`
across root/day/segment/health locations and add
`apps/sol/maint/004_rename_agents_to_talents.py` for automatic live-journal
migration on supervisor startup.
- Bump the stats schema from v2 to v3 and require top-level `"talents"`.
- Keep the only allowed compatibility shim in `think/pipeline_health.py` and
`apps/home/routes.py`, where historical dream chronicle readers accept both
legacy `agent.*` and new `talent.*` event names through 2026-05-01.
- Add `scripts/gate_agents_rename.py` and wire it into `make ci` to prevent
reintroduction of legacy rename patterns.
Out-of-scope followups:
- Decompose `think/talents.py` (1,300+ lines) into submodules.
- Restructure `think/talent.py` + `think/talent_cli.py` into a `think/talent/`
package.
- Remove the historical read-side shims after the 2026-05-01 sunset.
Operator verification after supervisor restart on a live journal:
- Migration script runs at startup and renames pre-existing `<journal>/agents/`
dirs to `<journal>/talents/`.
- Cortex spawn logs show `[sys.executable, -m, think.talents]`.
- `sol providers check` reports all providers (Anthropic, OpenAI, Google,
Ollama).
- `sol agents` fails with unknown command.
- Home yesterdays-card renders on a journal that has both pre- and post-rename
chronicle events.
- `make ci` is green and the rename gate passes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a facet-registry gate in hooks.py that consults get_facets() once per write call and skips events with unknown or non-canonical facets with a WARNING log.
Add a megameeting guard in occurrence.py that rejects meeting events with more than 25 participants with a WARNING log.
Tighten the meetings.md and timeline.md prompts so orgs, projects, tools, and topics are not lifted into structured participants.
Document the existing muted-facet silent-disable behavior in docs/JOURNAL.md for agents such as entity_observer.
Add 8 focused tests in tests/test_output_hooks.py and prepend the worktree root in tests/conftest.py so pytest exercises local code.
Defensive guards against pollution patterns surfaced in the CPO journal-data-quality investigation.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Adds an optional `cwd` field on talent frontmatter with two legal values (`"journal"` default, `"repo"` escape hatch) for `type: cogitate` prompts. Threads the resolved absolute path through `prepare_config` and each provider's `CLIRunner`, and also through the cortex-side `Popen` for `sol agents`, so every cogitate subprocess lands in the correct working directory. Validation is centralized in `_validate_cwd` (`think/talent.py`) and runs at both `get_talent_configs` and `get_agent`. `talent/coder.md` opts into `cwd: "repo"`; all other cogitate talents default to `"journal"`. Generators reject `cwd` entirely. Fixes repo-relative path notes in `talent/heartbeat.md` so the agent runs correctly under journal cwd. Known gap: Google provider's `--sandbox=none` is unchanged and deferred to a follow-up lode.
Without drift refresh, existing journals keep stale AGENTS.md content forever whenever docs/JOURNAL.md changes upstream.
Decision log:
- Drift-detection mechanism: direct content compare between the journal's AGENTS.md and docs/JOURNAL.md. No sidecar, no marker file, no hash.
- Hand-edit policy: overwrite on mismatch with a stderr warning noting that prior content is recoverable via git history. AGENTS.md is a derived artifact, not a place for hand edits; per-journal customization belongs in sol/*.md.
- No-op when content already matches, so mtime is preserved.
The species prose was being copied verbatim into every journal's AGENTS.md via the seeding maint task, recreating the static-identity-in-a-doc problem the earlier identity retreat was meant to solve.
Move that prose to an authoritative Python constant in think/tools/sol.py, prepend it as a # species section in sol call identity hydrate output, and strip duplicate # {stem} headings from section bodies during hydrate composition. Individual subcommand output and on-disk sol/*.md files remain unchanged.
The heading strip is render-time only and case-insensitively matches an exact-stem first heading, so custom first-line headings in user files are preserved.
- remove the leftover talent/coding directories from the worktree\n- update chat.md to reference sol call identity instead of removed injected blocks\n- handle missing docs/JOURNAL.md through the maint task's existing one-line error path\n- refresh the sol preview API baseline for the prompt text change