Weekly Summary
Two distinct things in vita share the name "week," and they are easy to confuse:
- The
/weekcommand (.claude/commands/week.md) β an interactive, code-driven weekly review that gathers the past 7 days directly from per-day data files and the objectives analytics module, then formats a human-readable digest. It does not call the LLM view builder. - The
week-summarymemory view β one of seven LLM-synthesized, cached JSON views maintained by the view builder (scripts/memory/build_view.py). It is a 7-day health digest that other parts of the system read as a precomputed source.
This page documents both, plus the shared memory-view infrastructure (build flow, staleness, dependency-ordered rebuilds) that the week-summary view rides on.
Note: The view builder and the
/weekcommand are independent code paths. The view builder writes a cached JSON file via an LLM; the/weekcommand computes its sections in-process. Theweek-summaryview registers.claude/commands/week.mdas its "consumer," but that is aspirational β the command reads raw per-day files, not the view.
The /week command
/week is a slash command (markdown procedure executed by the agent, not a Python entry point). It synthesizes the past 7 days across activities, sleep, weight, check-ins, goals, objectives, and identity-framed coaching.
Control flow
| Step | Action |
|---|---|
| 1. Gather data | Load 7 days of data/activities/, data/checkins/, data/garmin/ via safe_read_json(...)["entries"], defaulting to {"entries": []} for missing files. |
| 2. Build sections | Compute and format Activity, Sleep, Check-in, Weight, Goal, Objectives, Coaching, and Week-over-Week sections. |
| 3. Format output | Emit the full review (a fixed-width "Week in Review" panel) or a compact variant. |
| 4. Edge cases | Degrade gracefully on missing data (see table below). |
| 5. Save snapshot (optional) | On request, write data/weekly/YYYY-WXX.json. |
The data-gathering preamble is plain Python run by the agent:
import sys; sys.path.insert(0, 'scripts')
from utils import safe_read_json
from datetime import datetime, timedelta
today = datetime.now()
dates = [(today - timedelta(days=i)).strftime("%Y-%m-%d") for i in range(7)]
activities, checkins, garmin_data = [], [], []
for date in dates:
activities.extend(safe_read_json(f"data/activities/{date}.json", {"entries": []})["entries"])
checkins.extend(safe_read_json(f"data/checkins/{date}.json", {"entries": []})["entries"])
garmin_data.extend(safe_read_json(f"data/garmin/{date}.json", {"entries": []})["entries"])
Sections it produces
- Activities β total count and duration, breakdown by
activity_type, intensity distribution. - Sleep β average / best / worst from check-in responses and Garmin sleep data, with a vs-last-week delta.
- Check-ins β completion rate (days completed / 7), average energy, recurring themes.
- Weight β start/end/change from
data/weight/*.jsonor check-in weight entries. - Goals β active goals from
goals/active/*.jsonmatched against logged activity/check-in data. - Objectives β see below; the richest computed section.
- Coaching Insights β see below; identity-framed reflection.
- Week-over-Week β comparison shown when previous-week data exists.
Objectives integration (scripts.objectives)
The objectives section is real, computed analytics β not LLM narration. All functions below are importable from scripts.objectives:
| Function | Source | Returns |
|---|---|---|
get_weekly(week=None) |
scripts/objectives/storage.py |
This week's weekly objectives (with per-objective touch counts). |
get_daily_range(start, end) |
scripts/objectives/storage.py |
List of daily-objective sets across a date range. |
get_completion_rate(days=7) |
scripts/objectives/analytics.py |
Fraction of daily objectives completed over the window. |
get_day_of_week_stats(days=7) |
scripts/objectives/analytics.py |
Per-weekday completion stats (best/worst day). |
get_deferred_patterns(days=7) |
scripts/objectives/analytics.py |
Which objectives get deferred most often. |
The command renders weekly objectives with status + touch count, a daily completion rate, best/worst day, the most-deferred objective, and optionally a day-by-day icon grid. See Objectives for the underlying data model.
Coaching integration (scripts.coaching)
After goal progress, /week appends 2-3 sentences of identity-framed reflection per goal. For each goal it calls:
from scripts.coaching import (
get_or_create_mapping, determine_context, generate_coaching_message,
)
context, ctx_vars = determine_context(goal, progress_data) # celebration | progress | stagnation | ...
if context in ("celebration", "progress", "stagnation"):
mapping = get_or_create_mapping(goal_id, goal) # observations + quotes for this goal
msg = generate_coaching_message(
goal, context, ctx_vars, mapping["observations"], mapping["quotes"],
)
determine_context classifies the goal's state; celebration/progress produce a brief win with an identity tie-in, stagnation produces a single Socratic question. The composer can reference the person's own observations and quotes β see Coaching Engine. Output is personal by design, so any example here is a sanitized placeholder:
Coaching Insights
-----------------
<Goal A>: <N> sessions this week, exceeding target. Tied back to a stated
value/observation about <theme>.
<Goal B>: stalled at <N> sessions. One Socratic question on whether it's
still chosen.
Edge cases
| Situation | Handling |
|---|---|
| No activities | "No activities logged this week" |
| Partial week | Compute with available days, note "X days of data" |
| No check-ins | Skip the check-in section |
| No previous week | Skip the comparison section |
| Missing Garmin | Use manually-logged data only |
| No objectives | "No objectives set this week" |
| No weekly objectives | Skip the weekly-objectives section |
Optional weekly snapshot
On explicit request, /week can persist a compact snapshot to data/weekly/YYYY-WXX.json (ISO week number):
{
"week_start": "<oldest date>",
"week_end": "<today>",
"activities": { "count": 0, "duration_minutes": 0 },
"sleep": { "average_hours": 0.0 },
"weight": { "start": 0.0, "end": 0.0 },
"checkins": { "completed": 0, "total": 7 }
}
Note:
data/weekly/does not currently exist in the repo, so this snapshot step has apparently never been exercised. A reimplementer should create the directory before writing.
The week-summary memory view
week-summary is one of seven active views in the memory-view system. Each view is an LLM-synthesized JSON file gathered from source data, cached, and tracked for staleness.
View registry
The canonical list lives in memory/_meta/view-registry.json. The seven active views:
| View | Description | Staleness | Priority | Key consumers |
|---|---|---|---|---|
health-snapshot |
Body composition, cardiovascular, sleep, hormone summary | 1 day | high | morning message builder |
training-status |
Training readiness, recovery, recent workouts | 1 day | medium | /now, proactive builders |
goal-progress |
All active goals with frequency tracking | 1 day | medium | /now |
week-summary |
7-day sleep / activity / health trends | 1 day | medium | .claude/commands/week.md |
identity-snapshot |
Identity facts + communication preferences | 7 days | low | session start |
ceo-scorecard |
Weekly time-allocation / accountability scorecard | 7 days | high | web dashboard, proactive, /sleep |
current-status |
Quick-scan MOTD: headline, metrics, attention items | 0.5 days | high | web dashboard, /now, proactive |
Each registry entry carries path, description, prompt, dependencies (glob list of source files), staleness_days, priority, and consumers. week-summary declares dependencies on data/garmin/*.json, data/checkins/*.json, and data/activities/*.json.
Warning (registry inconsistency a reimplementer must know): the registry lists
week-summary's prompt asmemory/prompts/week-summary.md, butload_prompt()(build_view.py:35) always resolvesmemory/prompts/build-{view_id}.md. The real prompt file ismemory/prompts/build-week-summary.md; the registrypromptfield is effectively unused dead metadata. Do not rely on it.
Build flow
build_view(view_id, force=False) drives a single view (build_view.py:820):
load_prompt(view_id) # memory/prompts/build-{view_id}.md
-> gather_sources(view_id) # view-specific gatherer
-> fill {placeholders} in prompt
-> ensure skeleton view file exists
-> run_memory_builder(edit_prompt) # Claude with Read/Edit/Write + acceptEdits
-> Claude edits memory/views/{view_id}.json in place
-> validate_view() + log_build() # staleness + build-log.jsonl
Key details:
- Staleness short-circuit. Unless
force=True, if the existing view'sbuilt_atis < 1 day old andstaleness.jsonmarks itfresh, the cached view is returned without rebuilding (build_view.py:824-844). - Skeleton. If no view file exists yet, a skeleton is written with
builder: "claude --print --tools"before the LLM runs (build_view.py:869-878). - The builder is an LLM editing the file, not a function computing JSON. The edit prompt instructs Claude to read the current view, update
content/citations, refreshbuilt_atandprompt_hash, and keepview_id/builderβ using the Edit tool for surgical edits. prompt_hashissha256(prompt_template)[:16]β a change-detection hash of the prompt template (build_view.py:867).
The memory builder agent
run_memory_builder() resolves the memory.builder agent config in scripts/claude/agents.py:
"memory.builder": AgentConfig(
allowed_tools=["Read", "Edit", "Write"],
permission_mode="acceptEdits",
),
It has no explicit timeout (timeout defaults to None β harness default). There is no 180-second build timeout anywhere in the code; any prior claim of one was invented.
Source gathering for week-summary
gather_sources_week_summary() (build_view.py:274) builds three placeholder strings, each by iterating 7 days backward from today:
def gather_sources_week_summary() -> dict:
today = date.today()
garmin_data = []
for i in range(7):
day = today - timedelta(days=i)
garmin_file = DATA_ROOT / "garmin" / f"{day.isoformat()}.json"
if garmin_file.exists():
try:
data = json.loads(garmin_file.read_text())
data["_date"] = day.isoformat()
garmin_data.append(data)
except json.JSONDecodeError:
continue
sources["garmin_data"] = json.dumps(garmin_data, indent=2) if garmin_data else "(No Garmin data)"
# ...identical loops for checkins -> "(No check-in data)" and activities -> "(No activity data)"
return sources
| Placeholder | Source dir | Empty fallback |
|---|---|---|
{garmin_data} |
data/garmin/ |
"(No Garmin data)" |
{checkins_data} |
data/checkins/ |
"(No check-in data)" |
{activities_data} |
data/activities/ |
"(No activity data)" |
Behavior: iterates backward from today; stamps each record with a _date field; missing files are skipped; invalid-JSON files are swallowed via except json.JSONDecodeError: continue (graceful degradation). When a whole source has no usable days, the placeholder becomes the bracketed fallback string rather than an empty list.
Build prompt
The full template at memory/prompts/build-week-summary.md:
# Build Week Summary View
Create a summary of the past 7 days of health and activity data.
## Garmin Data (Last 7 Days)
{garmin_data}
## Check-ins (Last 7 Days)
{checkins_data}
## Activities Logged
{activities_data}
## Instructions
1. Summarize key metrics averages (sleep, steps, HRV)
2. Identify trends (improving, declining, stable)
3. Highlight notable events or outliers
4. Compare to previous week if data suggests a pattern
## Output Format
Return ONLY valid JSON (no markdown code fences):
{ "content": { "summary", "period", "metrics", "activities", "highlights", "concerns" },
"citations": [...] }
Important:
- Calculate actual averages from the provided data
- Only include sections where data is available
- Keep highlights and concerns to 2-3 items each
- Focus on actionable insights
Output schema
The view written to memory/views/week-summary.json. Values below are illustrative placeholders β do not treat them as real readings:
{
"content": {
"summary": "2-3 sentence narrative of the week.",
"period": { "start": "2026-01-01", "end": "2026-01-07" },
"metrics": {
"sleep": { "avg_hours": 0.0, "best_night": "2026-01-03",
"worst_night": "2026-01-05", "trend": "stable" },
"steps": { "avg_daily": 0, "total": 0, "most_active_day": "2026-01-02" },
"hrv": { "avg_ms": 0, "trend": "stable" },
"training_readiness": { "avg_score": 0, "trend": "stable" }
},
"activities": [ { "date": "2026-01-02", "type": "strength", "note": "..." } ],
"highlights": [ "...", "..." ],
"concerns": [ "..." ]
},
"citations": [ { "claim": "...", "source": "data/garmin/2026-01-*.json" } ],
"view_id": "week-summary",
"built_at": "2026-01-07T22:00:00Z",
"builder": "claude --print --tools",
"prompt_hash": "0123456789abcdef",
"refresh_contract": {
"max_age_days": 7,
"owner": "scripts/memory/build_view.py",
"rebuild_trigger": "/sleep nightly (Monday) | manual",
"input_sources": [ "data/garmin/*.json", "data/checkins/*.json", "data/activities/*.json" ]
}
}
| Field | Type | Notes |
|---|---|---|
content.summary |
string | Narrative summary. |
content.period.{start,end} |
date | 7-day window. |
content.metrics.sleep.{avg_hours,best_night,worst_night,trend} |
mixed | trend β stable/improving/declining. |
content.metrics.steps.{avg_daily,total,most_active_day} |
mixed | |
content.metrics.hrv.{avg_ms,trend} |
mixed | |
content.metrics.training_readiness.{avg_score,trend} |
mixed | In the prompt schema; populated only when present in source. |
content.activities[] |
array | Notable activities. |
content.highlights[] / content.concerns[] |
array | 2-3 items each. |
citations[] |
array | LLM-generated, not validated against files. |
view_id |
string | Always week-summary. |
built_at |
ISO8601 | Last build. |
builder |
string | Always claude --print --tools. |
prompt_hash |
string | sha256(prompt)[:16]. |
refresh_contract |
object | Declares freshness contract (see below). |
Note: the live view file embeds real body/health readings. Never copy them into docs β the schema above uses zeroed placeholders deliberately.
The refresh contract
Each maintained view carries a top-level refresh_contract object. For week-summary:
| Field | Value |
|---|---|
max_age_days |
7 |
owner |
scripts/memory/build_view.py |
rebuild_trigger |
/sleep nightly (Monday) \| manual |
input_sources |
the source globs |
This contract drives the reader-side staleness ceiling (below).
Staleness, freshness, and rebuilds
State lives in three files under memory/_meta/:
| File | Purpose |
|---|---|
staleness.json |
Per-view status (fresh/stale), stale_at, reason, build timestamps. Event-driven: flips to stale when an upstream dependency changes. |
build-log.jsonl |
Append-only log of build attempts (success/failure + error). |
view-access.jsonl |
Append-only access log: {ts, view_id, source} via log_view_access(view_id, source) (build_view.py:996). source is e.g. vdb/now/telegram/proactive. |
Two ways a view becomes stale
- Tracker mark.
staleness.jsonflips the view tostalewhen a dependency changes (event-driven). - Aged-out ceiling (reader-side).
view_loader.pyindependently declares a view stale if it ages past a ceiling, even when the tracker never marked it β this catches views no upstream change ever fired for. The ceiling is the view's ownrefresh_contract.max_age_dayswhen present, otherwise a default of14 * 24hours (DEFAULT_AGED_OUT_THRESHOLD_HOURS). Becauseweek-summarydeclaresmax_age_days: 7, its effective ceiling is 7 days, not 14; the 14-day default only applies to views lacking a refresh contract (view_loader.py:181-202).
contract_max_age_days = content.get("refresh_contract", {}).get("max_age_days")
aged_out_hours = (contract_max_age_days * 24) if contract_max_age_days is not None \
else DEFAULT_AGED_OUT_THRESHOLD_HOURS # 14 * 24
if status != "stale" and age_since_build is not None and age_since_build > aged_out_hours:
status = "stale"
stale_reason = f"aged_out_{contract_max_age_days}d" if contract_max_age_days else "aged_out"
This dual mechanism exists because the tracker only flips on upstream marks β a view could otherwise sit "fresh" for weeks if no dependency event fired. Reading via view_loader always re-checks age against the contract.
Dependency-ordered rebuild
rebuild_stale_views(force=False) (build_view.py:1037) rebuilds in a fixed dependency order so dependent views build last:
build_order = [
"health-snapshot", # no deps
"training-status", # garmin
"goal-progress", # goals
"week-summary", # garmin, checkins, activities
"identity-snapshot",
"ceo-scorecard", # goals, heartbeat, commitments
"current-status", # depends on training-status + goal-progress -> MUST be last
]
For each view it builds if it is tracker-stale, loader-aged-out (list_all_views() verdict), or force=True. It returns {"rebuilt": [...], "errors": {view_id: msg}}.
Failed-build handling (no silent masking)
When a build raises, rebuild_stale_views does not leave the view marked fresh. It writes a build_failing entry into staleness.json (build_view.py:1089-1110):
{
"status": "stale",
"stale_at": "<first stale time>",
"reason": "build_failing: <error head>",
"last_built": "<prior success>",
"last_build_attempt": "<now>",
"last_build_error": "<error, truncated 500 chars>"
}
This was added after a 2026-05-02 audit where a view sat at 28 days old while the tracker still said fresh (the tracker only flipped on dependency marks, so failed builds were invisible). A single failure does not stop the loop β it continues to the next view.
Inside build_view itself, if the LLM build raises but a view file with real content already exists, the stale view is served as a fallback rather than erroring (build_view.py:918-939).
Entry points
| Trigger | Mechanism |
|---|---|
/week command |
Reads raw per-day files directly (does not build the view). |
python scripts/memory/build_view.py week-summary |
Build one view (add --force to bypass staleness). |
python scripts/memory/build_view.py --rebuild-stale |
Rebuild all stale views in dependency order; prints the result dict as JSON; exits non-zero if any errored. |
--all |
build_all_stale() (simpler loop, no dependency ordering or failure-tracking). |
/sleep nightly |
Triggers rebuilds (per refresh_contract.rebuild_trigger). |
| Post-sync (API) | Rebuilds are kicked after sync operations so views reflect new data. |
Known limitations (reimplementer notes)
- Week-over-week is prompt-based, not computed. The
week-summaryprompt asks the LLM to "compare to previous week if data suggests a pattern," but only the current 7 days are loaded β there is no{prev_week_data}placeholder. The/weekcommand shows a comparison only when previous-week data already exists in its sources. - Citations are LLM-generated and not validated against the actual source files.
- Fixed 7-day window. Neither path supports custom ranges (e.g. "last 2 weeks").
week-summarydoes not read Precedent habits the waytraining-statusdoes; logged activities come only fromdata/activities/.training_readinessis in the prompt schema but inconsistently populated β it appears only when present in source data.
File Locations
| Purpose | Path |
|---|---|
/week command |
.claude/commands/week.md |
| View builder | scripts/memory/build_view.py |
| Reader / freshness logic | scripts/memory/view_loader.py |
| Builder agent config | scripts/claude/agents.py (memory.builder) |
| Build prompt | memory/prompts/build-week-summary.md |
| View registry | memory/_meta/view-registry.json |
| Output view | memory/views/week-summary.json |
| Staleness tracker | memory/_meta/staleness.json |
| Build log | memory/_meta/build-log.jsonl |
| View access log | memory/_meta/view-access.jsonl |
| Objectives analytics | scripts/objectives/analytics.py, scripts/objectives/storage.py |
| Coaching | scripts/coaching/ |
| Source: Garmin / check-ins / activities | data/garmin/, data/checkins/, data/activities/ |
| Optional snapshot (not yet created) | data/weekly/YYYY-WXX.json |
Where to go next
- Objectives β the daily/weekly objective model behind
/week's objectives section. - Coaching Engine β context classification and identity-framed messages.
- Check-in System β
/checkinand the per-day check-in data/weekreads. - Garmin Integration β the per-day Garmin files that feed both paths.
- Goals β active goals and frequency tracking.
- Insights β deeper pattern analysis across longer timeframes than a single week.