diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f52a3e..5a73b07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to iContext will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [0.4.0] - 2026-05-04 + +### Added +- **`icontext-write-fact` skill.** Explicit decision tree that routes content to the correct vault location before writing. Covers six categories: legal/entity facts (`vault/legal/`), project facts (`vault/projects/`), team/people (`vault/team/`), strategy (`vault/strategy/`), secretarial activity (`vault/secretary/logs/`), and credentials. Includes a full top-level vault directory reference so agents can match against the real structure without guessing. Eliminates the recurring "agent dumps legal facts in log files" failure mode. +- `icontext-populate-profile` now references `icontext-write-fact` for any durable facts surfaced during profile synthesis (e.g. company registrations, attorney contacts, funding dates). +- `icontext-write-fact` ships with `icontext init` — installed to `~/.claude/skills/` and `~/.cursor/rules/` alongside the existing three skills. +- CLAUDE.md snippet updated to list all four skills. +- Tests for the new skill: frontmatter, decision tree category coverage, init installation, Cursor rule, skills-list count, and CLAUDE.md reference. + +### Changed +- Bumped version to 0.4.0. + ## [0.3.0] - 2026-05-02 ### Added diff --git a/cli.py b/cli.py index 5aa9c40..7a70fd9 100755 --- a/cli.py +++ b/cli.py @@ -9,7 +9,7 @@ import sys from pathlib import Path -__version__ = "0.3.0" +__version__ = "0.4.0" # --------------------------------------------------------------------------- @@ -398,7 +398,12 @@ def _install_skills() -> tuple[int, list[str]]: msgs.append(_warn("skills/ source dir not found — skipping skill install")) return 0, msgs - skill_names = ["icontext-populate-profile", "icontext-refresh-profile", "icontext-share-card"] + skill_names = [ + "icontext-populate-profile", + "icontext-refresh-profile", + "icontext-share-card", + "icontext-write-fact", + ] claude_skills_dir = Path("~/.claude/skills").expanduser() cursor_rules_dir = Path("~/.cursor/rules").expanduser() claude_skills_dir.mkdir(parents=True, exist_ok=True) @@ -450,7 +455,8 @@ def _install_claude_md_snippet(vault: Path) -> None: "Available skills:\n" "- icontext-populate-profile (build profile from Gmail/LinkedIn/chat)\n" "- icontext-refresh-profile (update stale profile)\n" - "- icontext-share-card (regenerate shareable summary)\n\n" + "- icontext-share-card (regenerate shareable summary)\n" + "- icontext-write-fact (route a fact to the correct vault location)\n\n" "Multi-device sync: at session start, run `icontext pull` to fetch any updates\n" "from other machines. The user-prompt-submit hook does this automatically if a\n" "remote is configured.\n" @@ -518,7 +524,12 @@ def cmd_skills(args: argparse.Namespace) -> int: action = getattr(args, "skills_action", None) or "list" claude_skills_dir = Path("~/.claude/skills").expanduser() cursor_rules_dir = Path("~/.cursor/rules").expanduser() - skill_names = ["icontext-populate-profile", "icontext-refresh-profile", "icontext-share-card"] + skill_names = [ + "icontext-populate-profile", + "icontext-refresh-profile", + "icontext-share-card", + "icontext-write-fact", + ] if action == "list": _header("skills") @@ -1023,7 +1034,7 @@ def main() -> int: " icontext connect linkedin --pdf ~/Downloads/Profile.pdf\n" " icontext sync\n" "\n" - "docs: https://icontext.dev\n" + "docs: https://icontext.floom.dev\n" "issues: https://github.com/floomhq/icontext/issues" ), ) diff --git a/get.sh b/get.sh index f28d891..1ea4ede 100755 --- a/get.sh +++ b/get.sh @@ -37,7 +37,7 @@ if [ "${#MISSING_DEPS[@]}" -gt 0 ]; then echo " sudo apt install git python3" fi echo "" - echo "Then re-run: curl -fsSL https://icontext.dev/install | bash" + echo "Then re-run: curl -fsSL https://icontext.floom.dev/install | bash" exit 1 fi # ----------------------------------------------------------------------------- diff --git a/launch/COPY.md b/launch/COPY.md index 6bb801f..95ba83c 100644 --- a/launch/COPY.md +++ b/launch/COPY.md @@ -25,7 +25,7 @@ Gemini-based headless sync is also available (`pip install icontext[sync]`) for Install: ``` -curl -fsSL icontext.dev/install | bash +curl -fsSL icontext.floom.dev/install | bash icontext init ``` Then: open Claude Code and say "Populate my icontext profile." @@ -85,7 +85,7 @@ icontext is a folder and a set of skills. Two commands: ```bash -curl -fsSL icontext.dev/install | bash +curl -fsSL icontext.floom.dev/install | bash icontext init ``` @@ -186,7 +186,7 @@ The whole thing runs locally. No server, no cloud sync. The context folder is sp Works with Claude Code, Cursor, and Codex. Two install commands: -curl -fsSL icontext.dev/install | bash +curl -fsSL icontext.floom.dev/install | bash icontext init Open source, on GitHub: https://github.com/floomhq/icontext @@ -218,7 +218,7 @@ Also works with Cursor (installs into `.cursor/rules/`) and Codex (via optional Install: ``` -curl -fsSL icontext.dev/install | bash +curl -fsSL icontext.floom.dev/install | bash icontext init ``` diff --git a/launch/video-script-v2.txt b/launch/video-script-v2.txt index 0660413..ff63a63 100644 --- a/launch/video-script-v2.txt +++ b/launch/video-script-v2.txt @@ -59,13 +59,13 @@ Three quick visual cards or screen recordings. Each one beat. [1:05 — 1:15] CTA -Visual: Install command big and clean. icontext.dev URL. +Visual: Install command big and clean. icontext.floom.dev URL. "icontext. Open source. Two commands. No API key. Your AI never forgets you again. - icontext.dev" + icontext.floom.dev" (end on the URL on screen, hold 2 seconds) diff --git a/pyproject.toml b/pyproject.toml index 259f27e..2b80b1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.backends.legacy:build" [project] name = "icontext" -version = "0.3.0" +version = "0.4.0" description = "Persistent memory for AI coding tools — local, encrypted, built from your real data." readme = "README.md" license = {text = "MIT"} diff --git a/skills/fbrain-enrich/SKILL.md b/skills/fbrain-enrich/SKILL.md new file mode 100644 index 0000000..114cddc --- /dev/null +++ b/skills/fbrain-enrich/SKILL.md @@ -0,0 +1,58 @@ +--- +name: fbrain-enrich +description: > + Update a single person or project entry in the fbrain vault from the current + conversation. Use when the user says "I just spoke to X", "update X in my + brain", "enrich X", or "add note about X". +--- + +# fbrain: Enrich One Entry + +Update one existing person or project entry without rerunning populate or refresh. + +## Trigger + +Use this skill when the user asks to enrich, update, or add a note about one +person or project. + +## Target + +1. Identify the target name from the user's message. +2. Read `~/context/internal/profile/relationships.md` and + `~/context/internal/profile/projects.md`. +3. Match the target against exactly one existing person or project entry. +4. If there is no clear match, ask one short clarification question. + +## Source + +Use only the current conversation context: + +- the user's message +- recent tool outputs in this session +- facts the user just confirmed + +Do not call external APIs, search the web, read Gmail, browse LinkedIn, or run +the full populate or refresh pipeline. + +## Write + +Update only the matched entry in place: + +- For a person, edit just that row in `relationships.md`. +- For a project, edit just that project line or paragraph in `projects.md`. +- Preserve table formatting, headings, ordering, and every unrelated entry. +- Keep the change short and evidence-grounded. + +## Log + +Append one line to `~/context/internal/changes.md`: + +```text +YYYY-MM-DD: enriched +``` + +Create the file if it does not exist. + +## Output + +Confirm what changed in one sentence. diff --git a/skills/icontext-populate-profile/SKILL.md b/skills/icontext-populate-profile/SKILL.md index 440184f..3a364a9 100644 --- a/skills/icontext-populate-profile/SKILL.md +++ b/skills/icontext-populate-profile/SKILL.md @@ -145,6 +145,13 @@ generated: YYYY-MM-DD Strict 200-word max. No corporate filler. No "passionate about", no "pioneering". Plain second person. The user's actual voice from their actual data. +## Changelog + +After writing all four profile files, append one line to `~/context/internal/changes.md` (create the file if it does not exist): +``` +YYYY-MM-DD: populate — source: , relationships, projects +``` + ## Voice rules - Plain second person ("you are", "you work on") @@ -163,6 +170,14 @@ If `internal/profile/user.md` exists: - If 7-30 days old: ask the user "your profile is N days old, refresh?" - If >30 days old: refresh automatically. +## Saving facts that surface during synthesis + +If during synthesis you encounter a durable fact that doesn't belong in the +profile (e.g. the user mentions their company's Delaware registration, an +attorney contact, a funding round closing date), use the `icontext-write-fact` +skill to route it to the correct vault location. Do not dump it into the +profile files or a log file. + ## Output After writing, summarize for the user: @@ -171,3 +186,12 @@ After writing, summarize for the user: - Number of relationships, projects, topics extracted - File paths written - Suggested next: "Open a new Claude Code session and ask me what I know about you." + +## Injecting into CLAUDE.md + +After running this skill, paste the following block into your CLAUDE.md to enable brain-first lookups: + +``` +## fbrain: brain-first lookup +Before searching the web or calling external APIs for information about a person, company, or project, check ~/context/internal/profile/relationships.md and ~/context/internal/profile/projects.md first. If a relevant entry exists, use it as the primary source and supplement from external sources only if the entry is stale or incomplete. +``` diff --git a/skills/icontext-refresh-profile/SKILL.md b/skills/icontext-refresh-profile/SKILL.md index db597c4..5ca0cc1 100644 --- a/skills/icontext-refresh-profile/SKILL.md +++ b/skills/icontext-refresh-profile/SKILL.md @@ -70,6 +70,13 @@ previous_generated: {prior_date_from_old_file} This gives future refresh runs a trail of when the profile last meaningfully changed. +## Changelog + +After a write (skip if no meaningful changes), append one line to `~/context/internal/changes.md`: +``` +YYYY-MM-DD: refresh — , source: +``` + ## Output Summarize for the user: diff --git a/skills/icontext-write-fact/SKILL.md b/skills/icontext-write-fact/SKILL.md new file mode 100644 index 0000000..2c63ae6 --- /dev/null +++ b/skills/icontext-write-fact/SKILL.md @@ -0,0 +1,144 @@ +--- +name: icontext-write-fact +description: > + Decide WHERE to write a fact, document, or piece of context in the iContext + vault. Use whenever you need to persist a fact (legal entity info, project + status, contact details, decision rationale) that doesn't fit neatly into + the synthesized profile. Prevents agents from dumping everything into + log files. + Triggers: "save to vault", "store this in icontext", "persist this", "add to + my context", "remember this", "write this down", "log this". +--- + +# iContext: Write a Fact to the Vault + +When asked to save something to the vault, use this decision tree FIRST. Do NOT default to log files. + +## Decision tree + +### Is it a legal/entity/compliance fact? + +Examples: company registration, EIN, VAT ID, articles of incorporation, jurisdiction, registered agent, attorney contact, contracts, NDAs, cap table entries. + +Route to: `vault/legal/.md` (e.g. `vault/legal/floom-inc.md`, `vault/legal/scaile-gmbh.md`) + +- For ongoing matters or disputes: `vault/legal/incidents//` +- For contracts with a specific party: `vault/legal/contracts/.md` + +### Is it about a specific project? + +Examples: launch plans, user research, technical decisions, project status, KPIs, GTM specifics, workplans, feature flags, error logs tied to a project. + +Route to: `vault/projects//.md` + +- For sub-streams: `vault/projects///.md` +- Known project slugs: `floom`, `rocketlist`, `openpaper`, `signaldash`, `cheers`, `agora`, `hyperniche` + +### Is it about a person on your team or in your network? + +Examples: a team member's role and scope, an investor's interest area, a recurring collaborator's background, hiring notes. + +Route to: + +- Your own team: `vault/team//.md` (e.g. `vault/team/floom/cedrik.md`) +- External relationships you actively track: `vault/network/.md` + +### Is it strategy or roadmap? + +Examples: 90-day plan, market positioning, fundraising strategy, OKRs, go-to-market thesis, competitive analysis. + +Route to: `vault/strategy/.md` or `vault/strategy//.md` + +### Is it a one-off note or in-progress thought? + +Examples: "I should remember to ask X about Y", "interesting thing I noticed", informal to-do. + +Route to: `vault/notes/.md` or `internal/scratch/.md` + +Do NOT put this in logs. Logs are time-stamped activity records. + +### Is it secretarial activity? + +Examples: "Sent legal letter 16 to Finom on 2026-04-30", "Meeting with Garima at Founders Inc 2026-05-02", "Completed task X". + +Route to: `vault/secretary/logs/YYYY-MM.md` (one file per month, append to it) + +This is the ONLY category that belongs in log files. + +### Is it credentials or secrets? + +Examples: API keys, OAuth tokens, app passwords, certificates. + +Do NOT write to vault. Use OS keychain (`keyring` library) or password manager. If absolutely necessary and the vault has git-crypt active: `vault/credentials/.md` with a clear warning header. + +### Does it fit an existing top-level vault directory? + +Before creating a new top-level directory, check whether content fits one of the established categories: + +- `vault/applications/` — job or program applications +- `vault/brand/` — logos, brand assets, visual guidelines +- `vault/content/` — LinkedIn posts, blog drafts, social copy, transcripts +- `vault/cv/` — CV and resume versions +- `vault/documents/` — official documents, certificates, diplomas +- `vault/infra/` — server setup, config, tools, scripts +- `vault/legal/` — legal entities, contracts, incidents +- `vault/partnerships/` — active partnership threads +- `vault/pitches/` — investor decks, pitch materials +- `vault/projects/` — active or past product projects +- `vault/research/` — market research, user research, analysis +- `vault/secretary/` — admin, logs, state, scheduling +- `vault/skills/` — custom agent skills +- `vault/strategy/` — plans, positioning, OKRs +- `vault/taxes/` — tax filings and records by entity +- `vault/team/` — team members by entity +- `vault/travel/` — travel plans, attachments + +If none fit: pick the closest, add a sub-directory, and do not invent a new top-level category without asking. + +## How to actually save + +After picking the target path: + +1. **Check if the file already exists.** If yes, append a new section (with a date or topic header) instead of overwriting. + +2. **Use appropriate structure.** For `vault/legal/.md`: + + ```markdown + # + + ## Registration + - **Type**: + - **Jurisdiction**: + - **Incorporated**: + - **EIN / VAT / Tax ID**: + - **Registered agent**: + + ## Officers / Directors + ... + + ## Founding documents + ... + ``` + +3. **Commit with a descriptive message:** + + ```bash + git add vault/legal/floom-inc.md + git commit -m "vault: add Floom Inc Delaware registration" + ``` + + git-crypt encrypts on stage automatically if the machine has git-crypt configured. + +4. **NEVER append legal/entity/durable facts to log files.** Logs are time-stamped activity. Facts are searchable references that must stay in the correct category directory. + +## Anti-patterns + +- Storing company registration in `vault/secretary/logs/icontext.md` — this is the bug this skill exists to prevent +- Storing meeting notes in `vault/legal/` +- Storing contact details for a team member in `vault/strategy/` +- Creating a new top-level vault directory when an existing one fits +- Overwriting an existing file instead of appending a new section + +## When uncertain + +If a fact doesn't fit any category cleanly, ask the user where they want it before guessing. One specific question is cheaper than burying a durable fact in a log the user has to dig out later. diff --git a/tests/test_skill_routing.py b/tests/test_skill_routing.py new file mode 100644 index 0000000..68aaabd --- /dev/null +++ b/tests/test_skill_routing.py @@ -0,0 +1,192 @@ +"""Tests for the icontext-write-fact skill routing.""" +from __future__ import annotations + +import unittest +from pathlib import Path + +SKILLS_ROOT = Path(__file__).resolve().parents[1] / "skills" +WRITE_FACT_SKILL = SKILLS_ROOT / "icontext-write-fact" / "SKILL.md" + + +class TestWriteFactSkillExists(unittest.TestCase): + + def test_skill_file_exists(self): + self.assertTrue( + WRITE_FACT_SKILL.exists(), + f"icontext-write-fact SKILL.md missing at {WRITE_FACT_SKILL}" + ) + + def test_skill_has_frontmatter(self): + content = WRITE_FACT_SKILL.read_text() + self.assertTrue( + content.startswith("---"), + "SKILL.md must start with YAML frontmatter (---)" + ) + self.assertIn("---", content[3:], "SKILL.md frontmatter must be closed with ---") + + def test_skill_frontmatter_has_name(self): + content = WRITE_FACT_SKILL.read_text() + self.assertIn("name: icontext-write-fact", content, + "frontmatter must declare name: icontext-write-fact") + + def test_skill_frontmatter_has_description(self): + content = WRITE_FACT_SKILL.read_text() + self.assertIn("description:", content, + "frontmatter must include a description field") + + def test_skill_frontmatter_has_triggers(self): + content = WRITE_FACT_SKILL.read_text() + self.assertIn("save to vault", content, + "skill description must include 'save to vault' trigger phrase") + + +class TestWriteFactDecisionTree(unittest.TestCase): + """Verify the six routing categories are present in the decision tree.""" + + def setUp(self): + self.content = WRITE_FACT_SKILL.read_text() + + def test_legal_entity_category(self): + self.assertIn("vault/legal/", self.content, + "decision tree must route legal/entity facts to vault/legal/") + + def test_project_category(self): + self.assertIn("vault/projects/", self.content, + "decision tree must route project facts to vault/projects/") + + def test_team_category(self): + self.assertIn("vault/team/", self.content, + "decision tree must route team/person facts to vault/team/") + + def test_strategy_category(self): + self.assertIn("vault/strategy/", self.content, + "decision tree must route strategy facts to vault/strategy/") + + def test_secretary_logs_category(self): + self.assertIn("vault/secretary/logs/", self.content, + "decision tree must route secretarial activity to vault/secretary/logs/") + + def test_credentials_category(self): + self.assertIn("credentials", self.content, + "decision tree must address credentials/secrets routing") + + def test_antipattern_log_file_mentioned(self): + # The specific bug that triggered this skill + self.assertIn("vault/secretary/logs/icontext.md", self.content, + "skill must explicitly name the anti-pattern that caused the bug") + + +class TestWriteFactInstalledByInit(unittest.TestCase): + """Verify the skill ships via icontext init.""" + + def test_init_installs_write_fact_skill(self): + import os + import subprocess + import sys + import tempfile + + cli = str(Path(__file__).resolve().parents[1] / "cli.py") + with tempfile.TemporaryDirectory() as tmp: + home = Path(tmp) / "home" + home.mkdir() + vault = Path(tmp) / "vault" + env = os.environ.copy() + env["HOME"] = str(home) + env.pop("ICONTEXT_VAULT", None) + subprocess.run( + [sys.executable, cli, "init", "--vault", str(vault)], + capture_output=True, text=True, env=env, + ) + skill_path = home / ".claude" / "skills" / "icontext-write-fact" / "SKILL.md" + self.assertTrue( + skill_path.exists(), + f"icontext-write-fact not installed by init. Expected at {skill_path}" + ) + + def test_init_installs_cursor_rule_for_write_fact(self): + import os + import subprocess + import sys + import tempfile + + cli = str(Path(__file__).resolve().parents[1] / "cli.py") + with tempfile.TemporaryDirectory() as tmp: + home = Path(tmp) / "home" + home.mkdir() + vault = Path(tmp) / "vault" + env = os.environ.copy() + env["HOME"] = str(home) + env.pop("ICONTEXT_VAULT", None) + subprocess.run( + [sys.executable, cli, "init", "--vault", str(vault)], + capture_output=True, text=True, env=env, + ) + cursor_path = home / ".cursor" / "rules" / "icontext-write-fact.mdc" + self.assertTrue( + cursor_path.exists(), + f"icontext-write-fact cursor rule not installed. Expected at {cursor_path}" + ) + + def test_skills_list_shows_four_skills(self): + import os + import subprocess + import sys + import tempfile + + cli = str(Path(__file__).resolve().parents[1] / "cli.py") + with tempfile.TemporaryDirectory() as tmp: + home = Path(tmp) / "home" + home.mkdir() + vault = Path(tmp) / "vault" + env = os.environ.copy() + env["HOME"] = str(home) + env.pop("ICONTEXT_VAULT", None) + subprocess.run( + [sys.executable, cli, "init", "--vault", str(vault)], + capture_output=True, text=True, env=env, + ) + result = subprocess.run( + [sys.executable, cli, "skills", "list"], + capture_output=True, text=True, env=env, + ) + output = result.stdout + result.stderr + for name in ( + "icontext-populate-profile", + "icontext-refresh-profile", + "icontext-share-card", + "icontext-write-fact", + ): + self.assertIn(name, output, + f"skills list missing {name} after init") + + +class TestClaudeMdSnippetReferencesWriteFact(unittest.TestCase): + """CLAUDE.md snippet written by init must mention the new skill.""" + + def test_claude_md_snippet_includes_write_fact(self): + import os + import subprocess + import sys + import tempfile + + cli = str(Path(__file__).resolve().parents[1] / "cli.py") + with tempfile.TemporaryDirectory() as tmp: + home = Path(tmp) / "home" + home.mkdir() + vault = Path(tmp) / "vault" + env = os.environ.copy() + env["HOME"] = str(home) + env.pop("ICONTEXT_VAULT", None) + subprocess.run( + [sys.executable, cli, "init", "--vault", str(vault)], + capture_output=True, text=True, env=env, + ) + claude_md = home / ".claude" / "CLAUDE.md" + self.assertTrue(claude_md.exists(), "CLAUDE.md not written by init") + content = claude_md.read_text() + self.assertIn("icontext-write-fact", content, + "CLAUDE.md snippet must reference icontext-write-fact") + + +if __name__ == "__main__": + unittest.main()