Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions commands/scout-profile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
name: scout-profile
description: Review and fill in your Scout user profile — only asks about what's still missing. Use after an upgrade to populate the new profile files, or anytime to refine how Scout communicates with you and what your goals are.
---

# Scout Profile

You help {{USER_NAME}} fill in their Scout user profile. This is a **gap-filling** interview: it asks **only** about information that is still missing, never re-asks what's already set, and never overwrites the user's own edits or Scout-derived values. Safe to run repeatedly — it converges.

The profile lives in `~/Scout/knowledge-base/profile/`:
- `about-you.md` — identity + what Scout has derived about you (mostly self-maintained)
- `communication.md` — how Scout talks to you (the part Scout can't derive)
- `goals.md` — your goals, used as a prioritization lens

---

## Step 0: Pre-flight

```bash
test -f "$HOME/Scout/scout-config.yaml" && echo "VAULT_OK" || echo "NO_VAULT"
test -d "$HOME/Scout/knowledge-base/profile" && echo "PROFILE_OK" || echo "PROFILE_MISSING"
```

- `NO_VAULT`: "No Scout vault found. Run `/scout-setup` first." Stop.
- `PROFILE_MISSING`: the profile files predate this feature. Tell the user: "Your vault doesn't have profile files yet — run `/scout-update` once to seed them, then re-run `/scout-profile`." Stop.
- Both OK: continue.

## Step 1: Detect gaps

Read all three files. A field is a **gap** if it still contains its `<!-- TODO: ... -->` sentinel or is empty. Anything else — a value the user typed, or a Scout-derived line (it carries a confidence tag or sits under a derived section) — is **not** a gap. Build the list of gaps across:

- `communication.md`: language, tone, length, notification cadence, escalation ("always check first" / "safe to handle"), don'ts
- `about-you.md`: role/title, employer/team (these Scout can also derive — only ask if the user wants to set them explicitly)
- `goals.md`: any **Proposed (unconfirmed)** goals Scout has drafted that are awaiting a confirm/edit/drop decision

If there are **no gaps**, tell the user the profile is complete and show a one-line summary of what's set. Stop.

## Step 2: Interview — only the gaps

Ask about the gaps **one at a time**, in this priority order (highest-value first): communication contract → unconfirmed goals → optional identity fields. Each question is **skippable** — make that explicit ("press Enter to skip; Scout will keep the default / derive it / ask again later").

For unconfirmed goals, present each proposed goal with the evidence Scout based it on and ask: confirm / edit / drop.

Do not ask about things Scout derives well on its own (key people, current focus, working rhythm) — those are not gaps to interview, they fill in from connector activity.

## Step 3: Write — fill only, never clobber

For each answered question, edit the live file:
- Replace **only** the matching `<!-- TODO: ... -->` sentinel with the user's words. Never touch a line that isn't a sentinel.
- For confirmed goals, move the item from **Proposed (unconfirmed)** to **Confirmed goals** with a one-line Why + horizon; for dropped ones, delete the proposal.
- Skipped questions: leave the sentinel in place.
- Bump `last_reviewed:` to today in any file you changed.

Do not invent values beyond what the user said.

## Step 4: Offer an immediate backfill (optional)

So the user sees results now instead of waiting for the next scheduled run, offer: "Want me to run Scout now so it derives the rest of your profile (key people, current focus, goal candidates) from your connectors?"

If yes:

```bash
SCOUT_FORCE_MODE=morning-briefing ~/Scout/run-scout.sh
```

If no: "Done — Scout will fill in the derived parts on its next scheduled run." Report which fields were set and which were left for Scout to derive or to ask again later.
17 changes: 17 additions & 0 deletions commands/scout-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ Ask each of these in order, waiting for each answer:
2. "What's your name? (used in commit messages and the KB)"
3. "What's your email? (used for git config)"
4. "Timezone? (default: America/New_York)"
5. **(Optional, skippable)** "How would you like Scout to communicate with you — preferred language, tone/length, and anything it should always check with you before acting on? (Press Enter to skip — Scout fills this from defaults and learns the rest from your feedback. You can set it anytime with `/scout-profile`.)"

Capture the answer to #5 verbatim (or empty if skipped) for Step 3c. Everything else about the user — role, key people, focus, goals — Scout derives from your connectors on its own runs; do **not** ask for them here.

---

Expand Down Expand Up @@ -171,6 +174,20 @@ Set `ENABLED = True` if the user said yes, `False` if they said no.

---

## Step 3c: Seed the communication contract (only if the user answered Q5)

If the user **skipped** Q5, do nothing here — the seeded `communication.md` defaults stand and Scout learns from feedback.

If they **answered**, open `~/Scout/knowledge-base/profile/communication.md` (already seeded by bootstrap) and fill in only the fields their answer covers, by replacing the matching `<!-- TODO: ... -->` sentinel(s):

- preferred language → the **Language** line
- tone / length → the **Tone & length** lines
- what to always check first → the **Always check with {{USER_NAME}} first** line under the autonomy contract

Leave every sentinel they didn't address untouched (Scout fills those from feedback later). Do not invent preferences beyond what they said. Then bump `last_reviewed:` to today. This is the same "post-write into the freshly-created vault" pattern as Step 3b — edit the live file directly; the template is not re-rendered at install time.

---

## Step 4: Report and offer first-run

Report the result to the user:
Expand Down
2 changes: 2 additions & 0 deletions commands/scout-update.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ If runner backups appeared (`run-*.sh.bak.*`), tell the user the live runners ha
- `~/Scout/connector-probes.local.yaml` (custom connector probes) is a user
file, never templated, so it is preserved untouched across upgrades.

**New profile files.** If this upgrade seeded `knowledge-base/profile/` for the first time (check: the files exist now and `plugin.applied_migrations` includes `profile-files-v1`), tell the user once: "Scout now keeps a user profile — it derives most of it (who you are, who matters, your focus) on its runs. To set how Scout talks to you and confirm your goals, run `/scout-profile`."

---

## Auto-update nudge
Expand Down
28 changes: 27 additions & 1 deletion engine/scout/scripts/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class MigrateLegacyResult:
"knowledge-base/projects",
"knowledge-base/ontology/entities",
"knowledge-base/people",
"knowledge-base/profile",
"knowledge-base/personal",
"knowledge-base/recurring-tasks",
"action-items/archive",
Expand Down Expand Up @@ -126,13 +127,28 @@ class MigrateLegacyResult:

_INSTALL_ONLY_TEMPLATES = (
# Vault-owned files seeded once on install (cat 2). Never overwritten on upgrade.
# Idempotent: _stage_install_only_seeds skips any file that already exists, so
# this tuple is safe to replay on upgrade — which is how existing vaults pick
# up newly-added seeds (e.g. the profile files) without clobbering edits.
("dreaming-proposals.md", "templates/dreaming-proposals.md.tmpl"),
("knowledge-base/scout-mistake-audit.md", "templates/scout-mistake-audit.md.tmpl"),
("knowledge-base/review-queue.md", "templates/review-queue.md.tmpl"),
("inbox.md", "templates/inbox.md.tmpl"),
("meetings/meetings.md", "templates/meetings/meetings.md.tmpl"),
# User profile: a Scout-maintained "about you" snapshot, a communication
# contract, and a goals file used as a prioritization lens. Seeded as
# editable stubs; Scout derives/refines the rest. See phases/core/00-about-you.md.
("knowledge-base/profile/about-you.md", "templates/knowledge-base/profile/about-you.md.tmpl"),
("knowledge-base/profile/communication.md", "templates/knowledge-base/profile/communication.md.tmpl"),
("knowledge-base/profile/goals.md", "templates/knowledge-base/profile/goals.md.tmpl"),
)

# Marker recorded in scout-config.yaml `plugin.applied_migrations` once the
# profile seeds have been replayed on an existing vault, so the upgrade path is
# observable (and so a future cleanup could gate on it). The seed itself is
# idempotent regardless of this marker.
_PROFILE_SEED_MIGRATION = "profile-files-v1"

_CAT1B_RUNNERS = (
("run-scout.sh", "templates/run-scout.sh.tmpl"),
("run-dreaming.sh", "templates/run-dreaming.sh.tmpl"),
Expand Down Expand Up @@ -513,7 +529,12 @@ def _stage_version_stamp(cfg: BootstrapConfig, *, is_upgrade: bool) -> None:
# permanently red.
plugin.setdefault("version_at_last_setup", cfg.plugin_version)
plugin["version_at_last_update"] = cfg.plugin_version
plugin.setdefault("applied_migrations", [])
migrations = plugin.setdefault("applied_migrations", [])
# Record the profile-seed migration once the seeds have been written
# (install always seeds; upgrade replays the idempotent seeder). Makes the
# profile rollout observable in scout-config.yaml without re-running work.
if _PROFILE_SEED_MIGRATION not in migrations:
migrations.append(_PROFILE_SEED_MIGRATION)
_atomic_write(config_path, yaml.safe_dump(existing, sort_keys=False))


Expand Down Expand Up @@ -614,6 +635,11 @@ def upgrade(cfg: BootstrapConfig) -> UpgradeResult:
# no-ops on vaults that are already per-file or never had legacy files.
_stage_migrations(cfg)
_stage_cat1_writes(cfg)
# Replay install-only seeds so existing vaults pick up newly-added
# cat-2 files (e.g. the profile/ files). Idempotent: every seed skips
# a target that already exists, so user edits are never clobbered and
# files seeded on a prior install/upgrade are left untouched.
_stage_install_only_seeds(cfg)
# _stage_seed_schedule is idempotent (returns early if the file
# exists) so it's safe to call on upgrade. Without it, vaults set
# up before .scout-state/schedule.yaml was a first-class file
Expand Down
114 changes: 114 additions & 0 deletions engine/tests/unit/test_profile_seed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""Unit tests for the user-profile feature: profile-file seeding (install +
upgrade), edit preservation, migration recording, and that the profile phases
land in the assembled brain files."""

from __future__ import annotations

import shutil
from pathlib import Path

import yaml

from scout.scripts.bootstrap import _PROFILE_SEED_MIGRATION, install, upgrade
from tests.unit.test_bootstrap_upgrade import _config

_PROFILE_FILES = (
"knowledge-base/profile/about-you.md",
"knowledge-base/profile/communication.md",
"knowledge-base/profile/goals.md",
)


def _plugin() -> Path:
return Path(__file__).parent.parent.parent.parent


def test_install_seeds_profile_files_rendered(tmp_path):
vault = tmp_path / "Scout"
install(_config(vault, plugin_root=_plugin()))

for rel in _PROFILE_FILES:
assert (vault / rel).exists(), f"{rel} not seeded on install"

about = (vault / "knowledge-base/profile/about-you.md").read_text()
assert "Test User" in about # {{USER_NAME}} rendered
assert "America/New_York" in about # {{TIMEZONE}} rendered
assert "test@example.com" in about # {{USER_EMAIL}} rendered
assert "{{" not in about, "unrendered template marker left in seeded file"


def test_profile_files_are_invisible_to_ontology_parser(tmp_path):
"""Profile notes must NOT carry a `type:` — otherwise the ontology parser
ingests them and validate() flags 'Unknown entity type'. They use `kind:`."""
vault = tmp_path / "Scout"
install(_config(vault, plugin_root=_plugin()))
for rel in _PROFILE_FILES:
fm = (vault / rel).read_text().split("---", 2)[1]
meta = yaml.safe_load(fm)
assert "type" not in meta, f"{rel} has a `type:` — parser will validate it"
assert meta.get("kind") == "profile"


def test_upgrade_seeds_profile_on_existing_vault(tmp_path):
"""A vault that predates the profile feature picks up the files on upgrade."""
vault = tmp_path / "Scout"
install(_config(vault, plugin_root=_plugin()))

# Simulate a pre-feature vault: no profile dir at all.
shutil.rmtree(vault / "knowledge-base/profile")
assert not (vault / "knowledge-base/profile").exists()

cfg = _config(vault, plugin_root=_plugin())
cfg.plugin_version = "0.4.1"
upgrade(cfg)

for rel in _PROFILE_FILES:
assert (vault / rel).exists(), f"{rel} not re-seeded on upgrade"


def test_upgrade_preserves_profile_edit(tmp_path):
"""An existing profile file is never clobbered on upgrade (cat-2 seed)."""
vault = tmp_path / "Scout"
install(_config(vault, plugin_root=_plugin()))

comm = vault / "knowledge-base/profile/communication.md"
comm.write_text(comm.read_text() + "\n- Reply in Czech, terse bullets.\n")

cfg = _config(vault, plugin_root=_plugin())
cfg.plugin_version = "0.4.1"
upgrade(cfg)

assert "Reply in Czech, terse bullets." in comm.read_text(), "profile edit clobbered"


def test_profile_migration_recorded_and_idempotent(tmp_path):
vault = tmp_path / "Scout"
install(_config(vault, plugin_root=_plugin()))

cfg_path = vault / "scout-config.yaml"
migs = yaml.safe_load(cfg_path.read_text())["plugin"]["applied_migrations"]
assert migs.count(_PROFILE_SEED_MIGRATION) == 1, "marker missing/duplicated on install"

cfg = _config(vault, plugin_root=_plugin())
cfg.plugin_version = "0.4.1"
upgrade(cfg)
migs2 = yaml.safe_load(cfg_path.read_text())["plugin"]["applied_migrations"]
assert migs2.count(_PROFILE_SEED_MIGRATION) == 1, "marker duplicated on upgrade"


def test_profile_phases_land_in_assembled_brain_files(tmp_path):
vault = tmp_path / "Scout"
install(_config(vault, plugin_root=_plugin()))

skill = (vault / "SKILL.md").read_text()
dreaming = (vault / "DREAMING.md").read_text()
research = (vault / "RESEARCH.md").read_text()

# 00-about-you has empty mode → present in every target.
for content in (skill, dreaming, research):
assert "profile/about-you.md" in content

# relationships is mode:[briefing] → SKILL only (briefing/consolidation target).
assert "Relationship Maintenance" in skill
assert "Relationship Maintenance" not in dreaming
assert "Relationship Maintenance" not in research
8 changes: 8 additions & 0 deletions phases/connectors/claude-sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,11 @@ Narrate the deltas in the run summary, and surface an instance-owned PR that's o
### Claim Gate — No "Actively Building X" Without a Cited Signal

Do not assert that {{USER_NAME}} "is actively building / working on X" unless you can cite a concrete signal: a session JSONL with matching prompts, a commit SHA, an open PR, or dirty working-tree files. A session *title* or a single prompt is weak evidence — tie the claim to the tangible artifact, or downgrade it to "{{USER_NAME}} opened a session about X" rather than asserting active work.

### Profile Signal — Feed `knowledge-base/profile/`

Your own sessions are a uniquely good source for the user profile, because they show how {{USER_NAME}} actually works and writes — something the inbound connectors can't see. Stays local (`cc-session-cache` summaries); nothing leaves the machine. While scanning, also harvest, with the same confidence discipline as everywhere else:

- **Identity & focus → `profile/about-you.md`:** recurring projects, domains, and tools across sessions sharpen the "current focus" and role/team fields. Tag inferred claims `[single-source]` until corroborated.
- **Communication style → `profile/communication.md`:** the language {{USER_NAME}} writes in and the way they phrase requests (terse vs. narrative, direct vs. exploratory) are a strong prior for the reply-language and tone defaults. Treat this as a *signal*, not a confirmed preference — the Dreaming feedback loop, not this scan, is what writes confirmed comms changes.
- **Candidate goals → `profile/goals.md`:** a theme {{USER_NAME}} returns to across many sessions is a goal candidate — draft it under **Proposed (unconfirmed)** with the sessions as evidence. Never auto-confirm.
27 changes: 27 additions & 0 deletions phases/core/00-about-you.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
phase: core
name: about-you
slot: about-you
mode: []
requires: null
---

## About {{USER_NAME}} — Read This First

Before producing anything, read the user profile in `knowledge-base/profile/`:

- **`profile/communication.md` is authoritative** for how you talk to {{USER_NAME}} — language, tone, length, notification cadence, and the escalate-vs-handle contract. It **overrides generic defaults**. Apply it to the digest, every notification, and any drafted message. If it specifies a reply language, write in that language. Never take an externally-visible action the contract says to escalate without explicit approval.
- **`profile/about-you.md`** tells you who {{USER_NAME}} is, who matters most to them, what they're currently focused on, and their working rhythm. Use it as orienting context for what to surface and how to time it.
- **`profile/goals.md`** holds confirmed goals — the prioritization lens (see the Action Items phase) — plus proposed candidates awaiting confirmation.

***

### Keep the profile current (derive, don't ask)

The profile is mostly **derived and self-maintained** — {{USER_NAME}} should rarely need to fill it in. On every run, when connector data or {{USER_NAME}}'s Claude Code sessions give you a stable signal, update the relevant `profile/` file in the same pass you update the rest of the KB:

- **`about-you.md`** — fill/refresh role, employer/team, top collaborators (`[[people/<slug>]]`, ranked by interaction frequency and 1:1 cadence), current focus (most-active projects/threads), and working rhythm (meeting density, deep-work windows, primary channels). Tag every inferred claim `[single-source]` / `[unverified]` until a second source corroborates it; bump `last_reviewed`.
- **`goals.md`** — when activity reveals a recurring objective (a project dominating your calendar/tickets/sessions, a theme repeated across meetings), draft it under **Proposed (unconfirmed)** with the evidence behind it. **Never** move a goal to **Confirmed** yourself — confirmation is {{USER_NAME}}'s, via `/scout-profile`, a reply, or a direct edit.
- **`communication.md`** — only the Dreaming feedback loop edits this (from {{USER_NAME}}'s 👍/👎 and corrections), not a briefing run.

**Never overwrite {{USER_NAME}}'s own edits.** Any line that is no longer a `<!-- TODO: ... -->` sentinel and isn't tagged as derived is ground truth — leave it. Replace only the TODO sentinels and your own previously-derived (still-tagged) lines.
Loading
Loading