Skip to content

Weekly Summary

Two distinct things in vita share the name "week," and they are easy to confuse:

  1. The /week command (.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.
  2. The week-summary memory 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 /week command are independent code paths. The view builder writes a cached JSON file via an LLM; the /week command computes its sections in-process. The week-summary view registers .claude/commands/week.md as 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/*.json or check-in weight entries.
  • Goals β€” active goals from goals/active/*.json matched 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 as memory/prompts/week-summary.md, but load_prompt() (build_view.py:35) always resolves memory/prompts/build-{view_id}.md. The real prompt file is memory/prompts/build-week-summary.md; the registry prompt field 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's built_at is < 1 day old and staleness.json marks it fresh, 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, refresh built_at and prompt_hash, and keep view_id/builder β€” using the Edit tool for surgical edits.
  • prompt_hash is sha256(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

  1. Tracker mark. staleness.json flips the view to stale when a dependency changes (event-driven).
  2. Aged-out ceiling (reader-side). view_loader.py independently 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 own refresh_contract.max_age_days when present, otherwise a default of 14 * 24 hours (DEFAULT_AGED_OUT_THRESHOLD_HOURS). Because week-summary declares max_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-summary prompt 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 /week command 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-summary does not read Precedent habits the way training-status does; logged activities come only from data/activities/.
  • training_readiness is 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 β€” /checkin and the per-day check-in data /week reads.
  • 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.